mainTypes = new ArrayList<>();
+ IPackageFragment[] packages = javaProject.getPackageFragments();
+ for (IPackageFragment pkg : packages) {
+ if (pkg.getKind() == IPackageFragmentRoot.K_SOURCE) {
+ ICompilationUnit[] units = pkg.getCompilationUnits();
+ for (ICompilationUnit unit : units) {
+ IType[] types = unit.getTypes();
+ for (IType type : types) {
+ if (hasMainMethod(type)) {
+ // If it's named Application or contains Application, prefer it
+ if (type.getElementName().contains("Application")) {
+ return type;
+ }
+ mainTypes.add(type);
+ }
+ }
+ }
+ }
+ }
+
+ if (mainTypes.size() == 1) {
+ return mainTypes.get(0);
+ } else if (mainTypes.size() > 1) {
+ // Let user choose
+ return chooseMainType(mainTypes);
+ }
+
+ return null;
+ }
+
+ /**
+ * Find a Spring Boot application class in the project.
+ *
+ * @param javaProject
+ * The Java project to search
+ * @return The Spring Boot application type, or null if not found
+ */
+ private IType findSpringBootApplication(IJavaProject javaProject) throws JavaModelException {
+ IPackageFragment[] packages = javaProject.getPackageFragments();
+ for (IPackageFragment pkg : packages) {
+ if (pkg.getKind() == IPackageFragmentRoot.K_SOURCE) {
+ ICompilationUnit[] units = pkg.getCompilationUnits();
+ for (ICompilationUnit unit : units) {
+ IType[] types = unit.getTypes();
+ for (IType type : types) {
+ // Check for @SpringBootApplication annotation
+ IAnnotation[] annotations = type.getAnnotations();
+ for (IAnnotation annotation : annotations) {
+ String annotationName = annotation.getElementName();
+ if ("SpringBootApplication".equals(annotationName) ||
+ "org.springframework.boot.autoconfigure.SpringBootApplication".equals(annotationName)) {
+ if (hasMainMethod(type)) {
+ return type;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the active shell.
+ *
+ * @return The active shell
+ */
+ private Shell getShell() {
+ return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
+ }
+
+ @Override
+ public ILaunchConfiguration[] getLaunchConfigurations(ISelection selection) {
+ // Not used, but required by interface
+ return null;
+ }
+
+ @Override
+ public ILaunchConfiguration[] getLaunchConfigurations(IEditorPart editorpart) {
+ // Not used, but required by interface
+ return null;
+ }
+
+ @Override
+ public IResource getLaunchableResource(ISelection selection) {
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection ss = (IStructuredSelection) selection;
+ Object element = ss.getFirstElement();
+ if (element instanceof IAdaptable) {
+ return ((IAdaptable) element).getAdapter(IResource.class);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public IResource getLaunchableResource(IEditorPart editorpart) {
+ return editorpart.getEditorInput().getAdapter(IResource.class);
+ }
+}
diff --git a/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/hotswap/JetBrainsRuntimeManager.java b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/hotswap/JetBrainsRuntimeManager.java
new file mode 100644
index 0000000..eef3cde
--- /dev/null
+++ b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/hotswap/JetBrainsRuntimeManager.java
@@ -0,0 +1,477 @@
+package com.vaadin.plugin.hotswap;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.launching.IVMInstall;
+import org.eclipse.jdt.launching.IVMInstallType;
+import org.eclipse.jdt.launching.JavaRuntime;
+import org.eclipse.jdt.launching.VMStandin;
+
+/**
+ * Manages JetBrains Runtime (JBR) installation and configuration. JBR is required for enhanced class redefinition
+ * support with Hotswap Agent.
+ */
+public class JetBrainsRuntimeManager {
+
+ private static final String JBR_VENDOR = "JetBrains";
+ private static final String JBR_NAME_PREFIX = "JetBrains Runtime";
+ private static final String VAADIN_HOME = ".vaadin";
+ private static final String ECLIPSE_PLUGIN_DIR = "eclipse-plugin";
+ private static final String JBR_DIR = "jbr";
+
+ // Known broken JBR version
+ private static final String BROKEN_JBR_VERSION = "21.0.4+13-b509.17";
+
+ private static JetBrainsRuntimeManager instance;
+
+ private Path vaadinHomePath;
+ private Path jbrInstallPath;
+
+ public static JetBrainsRuntimeManager getInstance() {
+ if (instance == null) {
+ instance = new JetBrainsRuntimeManager();
+ }
+ return instance;
+ }
+
+ private JetBrainsRuntimeManager() {
+ initializePaths();
+ }
+
+ private void initializePaths() {
+ String userHome = System.getProperty("user.home");
+ vaadinHomePath = Paths.get(userHome, VAADIN_HOME, ECLIPSE_PLUGIN_DIR);
+ jbrInstallPath = vaadinHomePath.resolve(JBR_DIR);
+
+ // Create directories if they don't exist
+ try {
+ Files.createDirectories(jbrInstallPath);
+ } catch (IOException e) {
+ System.err.println("Failed to create JBR directory: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Check if a JVM is JetBrains Runtime.
+ *
+ * @param vmInstall
+ * The JVM installation to check
+ * @return true if it's JBR
+ */
+ public boolean isJetBrainsRuntime(IVMInstall vmInstall) {
+ if (vmInstall == null) {
+ return false;
+ }
+
+ String name = vmInstall.getName();
+ if (name != null && name.contains("JetBrains")) {
+ return true;
+ }
+
+ // Check by running java -version
+ File javaExecutable = getJavaExecutable(vmInstall);
+ if (javaExecutable != null && javaExecutable.exists()) {
+ try {
+ ProcessBuilder pb = new ProcessBuilder(javaExecutable.getAbsolutePath(), "-version");
+ pb.redirectErrorStream(true);
+ Process process = pb.start();
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.contains("JBR") || line.contains("JetBrains")) {
+ return true;
+ }
+ }
+ }
+
+ process.waitFor();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if a JBR version is the known broken version.
+ *
+ * @param vmInstall
+ * The JVM installation to check
+ * @return true if it's the broken version
+ */
+ public boolean isBrokenJBR(IVMInstall vmInstall) {
+ if (!isJetBrainsRuntime(vmInstall)) {
+ return false;
+ }
+
+ String version = getJavaVersion(vmInstall);
+ return BROKEN_JBR_VERSION.equals(version);
+ }
+
+ /**
+ * Find an installed JetBrains Runtime.
+ *
+ * @return The JBR installation, or null if not found
+ */
+ public IVMInstall findInstalledJBR() {
+ IVMInstallType[] vmTypes = JavaRuntime.getVMInstallTypes();
+
+ for (IVMInstallType vmType : vmTypes) {
+ IVMInstall[] vms = vmType.getVMInstalls();
+ for (IVMInstall vm : vms) {
+ if (isJetBrainsRuntime(vm) && !isBrokenJBR(vm)) {
+ return vm;
+ }
+ }
+ }
+
+ // Check if JBR is installed in our directory
+ File[] jbrDirs = jbrInstallPath.toFile().listFiles(File::isDirectory);
+ if (jbrDirs != null) {
+ for (File jbrDir : jbrDirs) {
+ File javaHome = findJavaHome(jbrDir);
+ if (javaHome != null && isValidJavaHome(javaHome)) {
+ // Register this JBR with Eclipse
+ IVMInstall jbr = registerJBR(javaHome);
+ if (jbr != null && !isBrokenJBR(jbr)) {
+ return jbr;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get a compatible JetBrains Runtime for the given Java version.
+ *
+ * @param requiredJavaVersion
+ * The required Java version (e.g., "17", "21")
+ * @return The compatible JBR, or null if none found
+ */
+ public IVMInstall getCompatibleJBR(String requiredJavaVersion) {
+ IVMInstall jbr = findInstalledJBR();
+
+ if (jbr != null) {
+ String jbrVersion = getJavaMajorVersion(jbr);
+ if (jbrVersion != null && jbrVersion.equals(requiredJavaVersion)) {
+ return jbr;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Download and install JetBrains Runtime. This should be called in a background job.
+ *
+ * @param javaVersion
+ * The Java version to download (e.g., "17", "21")
+ * @param monitor
+ * Progress monitor
+ * @return The installed JBR, or null if installation failed
+ */
+ public IVMInstall downloadAndInstallJBR(String javaVersion, IProgressMonitor monitor) {
+ try {
+ monitor.beginTask("Downloading JetBrains Runtime " + javaVersion, 100);
+
+ // Determine platform
+ String os = System.getProperty("os.name").toLowerCase();
+ String arch = System.getProperty("os.arch");
+ String platform = getPlatformString(os, arch);
+
+ // Construct download URL (this is a simplified version)
+ // In reality, you'd need to fetch the actual download URL from JetBrains
+ String downloadUrl = getJBRDownloadUrl(javaVersion, platform);
+
+ if (downloadUrl == null) {
+ throw new IOException("Could not determine JBR download URL");
+ }
+
+ monitor.subTask("Downloading JBR...");
+ // Download logic would go here
+ // For now, we'll just print a message
+ System.out.println("Would download JBR from: " + downloadUrl);
+
+ monitor.worked(50);
+
+ monitor.subTask("Extracting JBR...");
+ // Extraction logic would go here
+
+ monitor.worked(40);
+
+ monitor.subTask("Registering JBR with Eclipse...");
+ // Registration logic
+
+ monitor.worked(10);
+
+ return null; // Would return the installed JBR
+
+ } catch (Exception e) {
+ System.err.println("Failed to download JBR: " + e.getMessage());
+ e.printStackTrace();
+ return null;
+ } finally {
+ monitor.done();
+ }
+ }
+
+ /**
+ * Register a JBR installation with Eclipse.
+ *
+ * @param javaHome
+ * The Java home directory
+ * @return The registered JVM installation
+ */
+ private IVMInstall registerJBR(File javaHome) {
+ try {
+ IVMInstallType vmType = JavaRuntime
+ .getVMInstallType("org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType");
+ if (vmType == null) {
+ return null;
+ }
+
+ // Generate a unique ID
+ String id = "jbr_" + System.currentTimeMillis();
+
+ // Create VM standin
+ VMStandin standin = new VMStandin(vmType, id);
+ standin.setName(JBR_NAME_PREFIX + " " + getJavaVersion(javaHome));
+ standin.setInstallLocation(javaHome);
+
+ // Convert standin to real VM
+ IVMInstall vm = standin.convertToRealVM();
+
+ // Save the VM configuration
+ JavaRuntime.saveVMConfiguration();
+
+ return vm;
+
+ } catch (Exception e) {
+ System.err.println("Failed to register JBR: " + e.getMessage());
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Get the Java executable for a VM installation.
+ *
+ * @param vmInstall
+ * The VM installation
+ * @return The Java executable file
+ */
+ private File getJavaExecutable(IVMInstall vmInstall) {
+ if (vmInstall == null) {
+ return null;
+ }
+
+ File installLocation = vmInstall.getInstallLocation();
+ if (installLocation == null) {
+ return null;
+ }
+
+ // Try standard locations
+ File javaExe = new File(installLocation, "bin/java");
+ if (!javaExe.exists()) {
+ javaExe = new File(installLocation, "bin/java.exe");
+ }
+
+ return javaExe.exists() ? javaExe : null;
+ }
+
+ /**
+ * Get the Java version string for a VM installation.
+ *
+ * @param vmInstall
+ * The VM installation
+ * @return The version string
+ */
+ private String getJavaVersion(IVMInstall vmInstall) {
+ File javaExe = getJavaExecutable(vmInstall);
+ if (javaExe == null) {
+ return null;
+ }
+
+ return getJavaVersion(javaExe.getParentFile().getParentFile());
+ }
+
+ /**
+ * Get the Java version from a Java home directory.
+ *
+ * @param javaHome
+ * The Java home directory
+ * @return The version string
+ */
+ private String getJavaVersion(File javaHome) {
+ try {
+ File javaExe = new File(javaHome, "bin/java");
+ if (!javaExe.exists()) {
+ javaExe = new File(javaHome, "bin/java.exe");
+ }
+
+ if (!javaExe.exists()) {
+ return null;
+ }
+
+ ProcessBuilder pb = new ProcessBuilder(javaExe.getAbsolutePath(), "-version");
+ pb.redirectErrorStream(true);
+ Process process = pb.start();
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ // Parse version from output like: openjdk version "21.0.1" 2023-10-17 LTS
+ Pattern pattern = Pattern.compile("version \"([^\"]+)\"");
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ }
+ }
+
+ process.waitFor();
+ } catch (Exception e) {
+ // Ignore
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the major Java version (e.g., "17" from "17.0.1").
+ *
+ * @param vmInstall
+ * The VM installation
+ * @return The major version string
+ */
+ private String getJavaMajorVersion(IVMInstall vmInstall) {
+ String fullVersion = getJavaVersion(vmInstall);
+ if (fullVersion == null) {
+ return null;
+ }
+
+ // Extract major version
+ String[] parts = fullVersion.split("\\.");
+ if (parts.length > 0) {
+ // Handle both "1.8.0" and "17.0.1" formats
+ if (parts[0].equals("1") && parts.length > 1) {
+ return parts[1]; // Java 8 or earlier
+ } else {
+ return parts[0]; // Java 9+
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Find the Java home directory within a JBR installation.
+ *
+ * @param jbrDir
+ * The JBR installation directory
+ * @return The Java home directory, or null if not found
+ */
+ private File findJavaHome(File jbrDir) {
+ // Check if it's already a Java home
+ if (isValidJavaHome(jbrDir)) {
+ return jbrDir;
+ }
+
+ // Check Contents/Home on macOS
+ File contentsHome = new File(jbrDir, "Contents/Home");
+ if (contentsHome.exists() && isValidJavaHome(contentsHome)) {
+ return contentsHome;
+ }
+
+ // Check jbr subdirectory
+ File jbrSubdir = new File(jbrDir, "jbr");
+ if (jbrSubdir.exists() && isValidJavaHome(jbrSubdir)) {
+ return jbrSubdir;
+ }
+
+ return null;
+ }
+
+ /**
+ * Check if a directory is a valid Java home.
+ *
+ * @param dir
+ * The directory to check
+ * @return true if it's a valid Java home
+ */
+ private boolean isValidJavaHome(File dir) {
+ if (!dir.exists() || !dir.isDirectory()) {
+ return false;
+ }
+
+ File binDir = new File(dir, "bin");
+ File javaExe = new File(binDir, "java");
+ if (!javaExe.exists()) {
+ javaExe = new File(binDir, "java.exe");
+ }
+
+ return javaExe.exists();
+ }
+
+ /**
+ * Get the platform string for downloading JBR.
+ *
+ * @param os
+ * Operating system name
+ * @param arch
+ * Architecture
+ * @return The platform string
+ */
+ private String getPlatformString(String os, String arch) {
+ String platform = "";
+
+ if (os.contains("win")) {
+ platform = "windows";
+ } else if (os.contains("mac")) {
+ platform = "osx";
+ } else if (os.contains("linux")) {
+ platform = "linux";
+ } else {
+ return null;
+ }
+
+ if (arch.contains("64")) {
+ platform += "-x64";
+ } else if (arch.contains("aarch64") || arch.contains("arm64")) {
+ platform += "-aarch64";
+ } else {
+ platform += "-x86";
+ }
+
+ return platform;
+ }
+
+ /**
+ * Get the JBR download URL for a specific version and platform. This is a placeholder - actual implementation would
+ * need to fetch the real URL from JetBrains or use a hardcoded mapping.
+ *
+ * @param javaVersion
+ * The Java version
+ * @param platform
+ * The platform string
+ * @return The download URL
+ */
+ private String getJBRDownloadUrl(String javaVersion, String platform) {
+ // This would need to be implemented with actual JBR download URLs
+ // For now, return a placeholder
+ return "https://github.com/JetBrains/JetBrainsRuntime/releases/download/...";
+ }
+}
diff --git a/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/launch/ServerLaunchListener.java b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/launch/ServerLaunchListener.java
new file mode 100644
index 0000000..c29e01d
--- /dev/null
+++ b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/launch/ServerLaunchListener.java
@@ -0,0 +1,62 @@
+package com.vaadin.plugin.launch;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchListener;
+import org.eclipse.wst.server.core.IModule;
+import org.eclipse.wst.server.core.IServer;
+import org.eclipse.wst.server.core.ServerUtil;
+
+/**
+ * Listener that hooks into server launch events to trigger a build for Vaadin projects. The Vaadin builder will
+ * automatically generate necessary files if Vaadin dependencies are detected.
+ */
+public class ServerLaunchListener implements ILaunchListener {
+
+ @Override
+ public void launchAdded(ILaunch launch) {
+ try {
+ ILaunchConfiguration config = launch.getLaunchConfiguration();
+ if (config == null) {
+ return;
+ }
+
+ // Check if this is a server launch
+ IServer server = ServerUtil.getServer(config);
+ if (server == null) {
+ return;
+ }
+
+ // Get the modules being deployed
+ IModule[] modules = server.getModules();
+ if (modules == null || modules.length == 0) {
+ return;
+ }
+
+ for (IModule module : modules) {
+ IProject project = module.getProject();
+ if (project != null) {
+ // Trigger a build to ensure hello.txt is generated if this is a Vaadin project
+ // The builder will check for Vaadin dependencies internally
+ project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, null);
+ }
+ }
+
+ } catch (Exception e) {
+ // Log but don't fail the launch
+ System.err.println("Failed to trigger build: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void launchRemoved(ILaunch launch) {
+ // Nothing to clean up
+ }
+
+ @Override
+ public void launchChanged(ILaunch launch) {
+ // Not needed for this implementation
+ }
+}
diff --git a/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/launch/VaadinModuleArtifactAdapter.java b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/launch/VaadinModuleArtifactAdapter.java
new file mode 100644
index 0000000..b12520e
--- /dev/null
+++ b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/launch/VaadinModuleArtifactAdapter.java
@@ -0,0 +1,115 @@
+package com.vaadin.plugin.launch;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.wst.server.core.IModule;
+import org.eclipse.wst.server.core.model.IModuleFile;
+import org.eclipse.wst.server.core.model.IModuleResource;
+import org.eclipse.wst.server.core.model.ModuleDelegate;
+
+/**
+ * Adapter that adds additional resources to a web module during deployment.
+ */
+public class VaadinModuleArtifactAdapter extends ModuleDelegate {
+
+ private static final String HELLO_FILE_NAME = "hello.txt";
+ private IModule module;
+ private IProject project;
+
+ public VaadinModuleArtifactAdapter(IModule module) {
+ this.module = module;
+ this.project = module.getProject();
+ }
+
+ @Override
+ public IStatus validate() {
+ return Status.OK_STATUS;
+ }
+
+ @Override
+ public IModuleResource[] members() throws CoreException {
+ // Get the original module resources
+ ModuleDelegate originalDelegate = (ModuleDelegate) module.loadAdapter(ModuleDelegate.class, null);
+ IModuleResource[] originalResources = originalDelegate != null
+ ? originalDelegate.members()
+ : new IModuleResource[0];
+
+ // Add our custom file
+ IModuleResource[] newResources = new IModuleResource[originalResources.length + 1];
+ System.arraycopy(originalResources, 0, newResources, 0, originalResources.length);
+
+ // Create the hello.txt file resource
+ newResources[originalResources.length] = createHelloFileResource();
+
+ return newResources;
+ }
+
+ private IModuleResource createHelloFileResource() {
+ String projectPath = project.getLocation().toOSString();
+ byte[] content = projectPath.getBytes();
+
+ // Place the file in WEB-INF/classes so it's available as a classpath resource
+ IPath classesPath = new Path("WEB-INF").append("classes");
+ return new VirtualModuleFile(HELLO_FILE_NAME, classesPath, content);
+ }
+
+ /**
+ * A virtual file that exists only during deployment.
+ */
+ private static class VirtualModuleFile implements IModuleFile {
+ private final String name;
+ private final IPath path;
+ private final byte[] content;
+ private final long timestamp;
+
+ public VirtualModuleFile(String name, IPath path, byte[] content) {
+ this.name = name;
+ this.path = path;
+ this.content = content;
+ this.timestamp = System.currentTimeMillis();
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public IPath getModuleRelativePath() {
+ return path;
+ }
+
+ @Override
+ public Object getAdapter(Class adapter) {
+ if (adapter == InputStream.class) {
+ return new ByteArrayInputStream(content);
+ }
+ if (adapter == IFile.class) {
+ // Return null as this is a virtual file
+ return null;
+ }
+ return null;
+ }
+
+ public long getModificationStamp() {
+ return timestamp;
+ }
+
+ public byte[] getContent() {
+ return content;
+ }
+ }
+
+ @Override
+ public IModule[] getChildModules() {
+ return new IModule[0];
+ }
+}
diff --git a/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/util/ResourceReader.java b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/util/ResourceReader.java
new file mode 100644
index 0000000..4ae95e9
--- /dev/null
+++ b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/util/ResourceReader.java
@@ -0,0 +1,57 @@
+package com.vaadin.plugin.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.stream.Collectors;
+
+/**
+ * Utility class to demonstrate how deployed applications can read the flow-build-info.json resource from the classpath.
+ *
+ * The flow-build-info.json file is automatically updated in META-INF/VAADIN/config during build, making it available as
+ * a classpath resource.
+ */
+public class ResourceReader {
+
+ private static final String FLOW_BUILD_INFO_RESOURCE = "/META-INF/VAADIN/config/flow-build-info.json";
+
+ /**
+ * Reads the content of flow-build-info.json from the classpath.
+ *
+ * @return The content of flow-build-info.json, or null if not found
+ */
+ public static String readFlowBuildInfo() {
+ try (InputStream is = ResourceReader.class.getResourceAsStream(FLOW_BUILD_INFO_RESOURCE)) {
+ if (is != null) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ return reader.lines().collect(Collectors.joining("\n"));
+ }
+ }
+ } catch (IOException e) {
+ System.err.println("Failed to read flow-build-info.json resource: " + e.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * Example usage from within a deployed application:
+ *
+ *
+ * String flowBuildInfo = ResourceReader.readFlowBuildInfo();
+ * if (flowBuildInfo != null) {
+ * System.out.println("Flow build info: " + flowBuildInfo);
+ * // Parse JSON to get npmFolder value
+ * }
+ *
+ */
+ public static void exampleUsage() {
+ String content = readFlowBuildInfo();
+ if (content != null) {
+ System.out.println("flow-build-info.json content: " + content);
+ } else {
+ System.out.println("flow-build-info.json resource not found in classpath");
+ }
+ }
+}
diff --git a/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/wizards/NewVaadinProjectWizard.java b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/wizards/NewVaadinProjectWizard.java
new file mode 100644
index 0000000..b9aa9c6
--- /dev/null
+++ b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/wizards/NewVaadinProjectWizard.java
@@ -0,0 +1,417 @@
+package com.vaadin.plugin.wizards;
+
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.ui.INewWizard;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.ide.IDE;
+
+/**
+ * New Vaadin Project creation wizard.
+ */
+public class NewVaadinProjectWizard extends Wizard implements INewWizard {
+
+ private VaadinProjectWizardPage mainPage;
+ private IWorkbench workbench;
+
+ public NewVaadinProjectWizard() {
+ super();
+ setNeedsProgressMonitor(true);
+ setWindowTitle("Vaadin");
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ this.workbench = workbench;
+ }
+
+ @Override
+ public void addPages() {
+ mainPage = new VaadinProjectWizardPage();
+ addPage(mainPage);
+ }
+
+ @Override
+ public boolean performFinish() {
+ final ProjectModel model = mainPage.getProjectModel();
+
+ IRunnableWithProgress op = new IRunnableWithProgress() {
+ @Override
+ public void run(IProgressMonitor monitor) throws InvocationTargetException {
+ try {
+ doFinish(model, monitor);
+ } catch (Exception e) {
+ throw new InvocationTargetException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+ };
+
+ try {
+ getContainer().run(true, false, op);
+ } catch (InterruptedException e) {
+ return false;
+ } catch (InvocationTargetException e) {
+ Throwable realException = e.getTargetException();
+ MessageDialog.openError(getShell(), "Error", "Project creation failed: " + realException.getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ private void doFinish(ProjectModel model, IProgressMonitor monitor)
+ throws IOException, CoreException, InterruptedException {
+ SubMonitor subMonitor = SubMonitor.convert(monitor, "Creating Vaadin project...", 100);
+
+ // Step 1: Download project ZIP
+ subMonitor.subTask("Downloading project template...");
+ Path tempZip = downloadProject(model, subMonitor.split(40));
+
+ // Step 2: Extract to workspace
+ subMonitor.subTask("Extracting project...");
+ Path projectPath = extractProject(tempZip, model.getProjectName(), subMonitor.split(30));
+
+ // Step 3: Import project based on type
+ subMonitor.subTask("Importing project...");
+ IProject project = null;
+
+ if (Files.exists(projectPath.resolve("pom.xml"))) {
+ // Import as Maven project directly
+ project = importMavenProject(projectPath, model.getProjectName(), subMonitor.split(25));
+ } else if (Files.exists(projectPath.resolve("build.gradle"))
+ || Files.exists(projectPath.resolve("build.gradle.kts"))) {
+ // Import as Gradle project
+ project = importGradleProject(projectPath, model.getProjectName(), subMonitor.split(25));
+ } else {
+ // Import as generic Eclipse project
+ project = importProject(projectPath, model.getProjectName(), subMonitor.split(25));
+ }
+
+ // Step 4: Open README
+ subMonitor.subTask("Opening README...");
+ openReadme(project, subMonitor.split(5));
+
+ // Clean up
+ Files.deleteIfExists(tempZip);
+ }
+
+ private Path downloadProject(ProjectModel model, IProgressMonitor monitor)
+ throws IOException, InterruptedException {
+ String downloadUrl = model.getDownloadUrl();
+ Path tempFile = Files.createTempFile("vaadin-project", ".zip");
+
+ HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
+
+ HttpRequest request = HttpRequest.newBuilder().uri(URI.create(downloadUrl)).GET().build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofFile(tempFile));
+
+ if (response.statusCode() != 200) {
+ throw new IOException("Failed to download project: HTTP " + response.statusCode());
+ }
+
+ return tempFile;
+ }
+
+ private Path extractProject(Path zipFile, String projectName, IProgressMonitor monitor) throws IOException {
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ Path workspacePath = Paths.get(root.getLocation().toString());
+ Path finalProjectPath = workspacePath.resolve(projectName);
+
+ // If project directory already exists, delete it
+ if (Files.exists(finalProjectPath)) {
+ deleteDirectory(finalProjectPath);
+ }
+
+ // Create the project directory
+ Files.createDirectories(finalProjectPath);
+
+ try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(zipFile.toFile())))) {
+ ZipEntry entry;
+ byte[] buffer = new byte[4096];
+ String rootFolder = null;
+
+ while ((entry = zis.getNextEntry()) != null) {
+ String entryName = entry.getName();
+
+ // Identify the root folder in the ZIP (if any)
+ if (rootFolder == null && entryName.contains("/")) {
+ int firstSlash = entryName.indexOf("/");
+ rootFolder = entryName.substring(0, firstSlash + 1);
+ }
+
+ // Skip the root folder itself and strip it from the path
+ String targetName = entryName;
+ if (rootFolder != null && entryName.startsWith(rootFolder)) {
+ targetName = entryName.substring(rootFolder.length());
+ // Skip if it's just the root folder entry itself
+ if (targetName.isEmpty()) {
+ zis.closeEntry();
+ continue;
+ }
+ }
+
+ Path targetPath = finalProjectPath.resolve(targetName);
+
+ if (entry.isDirectory()) {
+ Files.createDirectories(targetPath);
+ } else {
+ Files.createDirectories(targetPath.getParent());
+ try (FileOutputStream fos = new FileOutputStream(targetPath.toFile())) {
+ int len;
+ while ((len = zis.read(buffer)) > 0) {
+ fos.write(buffer, 0, len);
+ }
+ }
+ }
+ zis.closeEntry();
+ }
+
+ return finalProjectPath;
+ }
+ }
+
+ private void deleteDirectory(Path path) throws IOException {
+ if (Files.exists(path)) {
+ Files.walk(path).sorted(java.util.Comparator.reverseOrder()).map(Path::toFile)
+ .forEach(java.io.File::delete);
+ }
+ }
+
+ private IProject importMavenProject(Path projectPath, String projectName, IProgressMonitor monitor)
+ throws CoreException {
+ // Use the regular import and then configure as Maven
+ System.out.println("=== Creating project and configuring Maven ===");
+
+ // First create the project normally
+ IProject project = importProject(projectPath, projectName, monitor);
+
+ try {
+ // Then configure it as a Maven project
+ org.eclipse.m2e.core.project.IProjectConfigurationManager configManager = org.eclipse.m2e.core.MavenPlugin
+ .getProjectConfigurationManager();
+
+ // Create resolver configuration
+ org.eclipse.m2e.core.project.ResolverConfiguration resolverConfig = new org.eclipse.m2e.core.project.ResolverConfiguration();
+ resolverConfig.setResolveWorkspaceProjects(true);
+
+ // Enable Maven nature on the project
+ configManager.enableMavenNature(project, resolverConfig, monitor);
+
+ // Force update project configuration - this is important for Kotlin projects
+ // and ensures all dependencies are downloaded and configured
+ org.eclipse.m2e.core.project.MavenUpdateRequest updateRequest = new org.eclipse.m2e.core.project.MavenUpdateRequest(
+ java.util.Collections.singletonList(project), // projects to update
+ false, // offline
+ true // force update snapshots
+ );
+
+ configManager.updateProjectConfiguration(updateRequest, monitor);
+
+ // Additional refresh to ensure all resources are visible
+ project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+
+ System.out.println("Maven nature enabled and project configured with forced update");
+ System.out.println("Has Maven nature: " + project.hasNature("org.eclipse.m2e.core.maven2Nature"));
+
+ } catch (Exception e) {
+ System.err.println("Failed to configure Maven nature: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ return project;
+ }
+
+ private IProject importGradleProject(Path projectPath, String projectName, IProgressMonitor monitor)
+ throws CoreException {
+ System.out.println("=== Importing Gradle project ===");
+ System.out.println("Project path: " + projectPath);
+ System.out.println("Project name: " + projectName);
+
+ IProject project = null;
+
+ // Try to use Buildship's import mechanism if available
+ try {
+ // This will throw NoClassDefFoundError if Buildship is not available
+ project = importGradleProjectWithBuildship(projectPath, projectName, monitor);
+ if (project != null) {
+ System.out.println("Gradle project imported with Buildship successfully");
+ return project;
+ }
+ } catch (NoClassDefFoundError | ClassNotFoundException e) {
+ System.out.println("Buildship not available, using basic Gradle configuration");
+ } catch (Exception e) {
+ System.err.println("Failed to import Gradle project with Buildship: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ // Fall back to basic import
+ project = importProject(projectPath, projectName, monitor);
+ configureBasicGradleProject(project, monitor);
+
+ return project;
+ }
+
+ /**
+ * Import a Gradle project using Buildship API directly.
+ * This method will fail with NoClassDefFoundError if Buildship is not available,
+ * which is caught by the caller.
+ */
+ private IProject importGradleProjectWithBuildship(Path projectPath, String projectName, IProgressMonitor monitor)
+ throws Exception {
+ // Direct API calls - will fail if Buildship is not available
+ org.eclipse.buildship.core.GradleWorkspace workspace = org.eclipse.buildship.core.GradleCore.getWorkspace();
+
+ // Create build configuration
+ org.eclipse.buildship.core.BuildConfiguration buildConfig = org.eclipse.buildship.core.BuildConfiguration
+ .forRootProjectDirectory(projectPath.toFile())
+ .overrideWorkspaceConfiguration(true)
+ .build();
+
+ // Create a new Gradle build for this configuration
+ org.eclipse.buildship.core.GradleBuild gradleBuild = workspace.createBuild(buildConfig);
+
+ // Synchronize the project - this will import it and set up everything
+ gradleBuild.synchronize(monitor);
+
+ // The project should now exist in the workspace
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ IProject project = root.getProject(projectName);
+
+ // Ensure the project is open and refreshed
+ if (project != null && project.exists()) {
+ if (!project.isOpen()) {
+ project.open(monitor);
+ }
+ project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+
+ // Give Buildship a moment to finish background tasks
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+
+ // One more refresh to be sure
+ project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+ }
+
+ return project;
+ }
+
+ private void configureBasicGradleProject(IProject project, IProgressMonitor monitor) throws CoreException {
+ // Add Gradle nature and Java nature if not already present
+ IProjectDescription description = project.getDescription();
+ String[] natures = description.getNatureIds();
+
+ boolean hasJavaNature = false;
+ boolean hasGradleNature = false;
+
+ for (String nature : natures) {
+ if ("org.eclipse.jdt.core.javanature".equals(nature)) {
+ hasJavaNature = true;
+ }
+ if ("org.eclipse.buildship.core.gradleprojectnature".equals(nature)) {
+ hasGradleNature = true;
+ }
+ }
+
+ java.util.List newNatures = new java.util.ArrayList<>(java.util.Arrays.asList(natures));
+ if (!hasJavaNature) {
+ newNatures.add("org.eclipse.jdt.core.javanature");
+ }
+ if (!hasGradleNature) {
+ newNatures.add("org.eclipse.buildship.core.gradleprojectnature");
+ }
+
+ if (!hasJavaNature || !hasGradleNature) {
+ description.setNatureIds(newNatures.toArray(new String[0]));
+
+ // Add builders
+ org.eclipse.core.resources.ICommand javaBuilder = description.newCommand();
+ javaBuilder.setBuilderName("org.eclipse.jdt.core.javabuilder");
+
+ org.eclipse.core.resources.ICommand gradleBuilder = description.newCommand();
+ gradleBuilder.setBuilderName("org.eclipse.buildship.core.gradleprojectbuilder");
+
+ description.setBuildSpec(new org.eclipse.core.resources.ICommand[] { javaBuilder, gradleBuilder });
+
+ project.setDescription(description, monitor);
+ }
+
+ project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+ }
+
+ private IProject importProject(Path projectPath, String projectName, IProgressMonitor monitor)
+ throws CoreException {
+ System.out.println("=== Using regular Eclipse project import ===");
+ System.out.println("Project path: " + projectPath);
+ System.out.println("Project name: " + projectName);
+
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ IProject project = root.getProject(projectName);
+
+ if (!project.exists()) {
+ // Create project description
+ IProjectDescription description = ResourcesPlugin.getWorkspace().newProjectDescription(projectName);
+ description.setLocation(null); // Use default location
+
+ // Create and open project
+ project.create(description, monitor);
+ project.open(monitor);
+
+ // Refresh to pick up extracted files
+ project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+ }
+
+ return project;
+ }
+
+ private void openReadme(IProject project, IProgressMonitor monitor) {
+ PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
+ try {
+ IResource readme = project.findMember("README.md");
+ if (readme == null) {
+ readme = project.findMember("readme.md");
+ }
+
+ if (readme != null && readme.exists()) {
+ IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
+ IDE.openEditor(page, (org.eclipse.core.resources.IFile) readme);
+ }
+ } catch (PartInitException e) {
+ // Ignore - README opening is not critical
+ }
+ });
+ }
+}
diff --git a/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/wizards/ProjectModel.java b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/wizards/ProjectModel.java
new file mode 100644
index 0000000..69d3d3b
--- /dev/null
+++ b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/wizards/ProjectModel.java
@@ -0,0 +1,181 @@
+package com.vaadin.plugin.wizards;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Model for Vaadin project creation options.
+ */
+public class ProjectModel {
+
+ public enum ProjectType {
+ STARTER, HELLO_WORLD
+ }
+
+ private ProjectType projectType = ProjectType.STARTER;
+ private String projectName;
+ private String location;
+
+ // Starter project options
+ private boolean prerelease = false;
+ private boolean includeFlow = true;
+ private boolean includeHilla = false;
+
+ // Hello World options
+ private String framework = "flow"; // flow or hilla
+ private String language = "java"; // java or kotlin
+ private String buildTool = "maven"; // maven or gradle
+ private String architecture = "springboot"; // springboot, quarkus, jakartaee, servlet
+
+ public ProjectModel() {
+ }
+
+ public String getDownloadUrl() {
+ if (projectType == ProjectType.STARTER) {
+ return buildStarterUrl();
+ } else {
+ return buildHelloWorldUrl();
+ }
+ }
+
+ private String buildStarterUrl() {
+ StringBuilder url = new StringBuilder("https://start.vaadin.com/skeleton?");
+
+ // Add project name as group and artifact ID
+ String artifactId = toArtifactId(projectName);
+ url.append("artifactId=").append(encode(artifactId));
+
+ // Add framework selection using the 'frameworks' parameter
+ if (includeFlow && includeHilla) {
+ url.append("&frameworks=flow,hilla");
+ } else if (includeHilla) {
+ url.append("&frameworks=hilla");
+ } else {
+ url.append("&frameworks=flow");
+ }
+
+ // Add version selection
+ if (prerelease) {
+ url.append("&platformVersion=pre");
+ } else {
+ url.append("&platformVersion=latest");
+ }
+
+ // Add reference for tracking
+ url.append("&ref=eclipse-plugin");
+
+ return url.toString();
+ }
+
+ private String buildHelloWorldUrl() {
+ StringBuilder url = new StringBuilder("https://start.vaadin.com/helloworld?");
+
+ // Add framework
+ url.append("framework=").append(framework);
+
+ // Add language
+ url.append("&language=").append(language);
+
+ // Add build tool (note: parameter name is 'buildtool' not 'buildTool')
+ url.append("&buildtool=").append(buildTool);
+
+ // Add architecture (note: parameter name is 'stack' not 'architecture')
+ url.append("&stack=").append(architecture);
+
+ // Add reference for tracking
+ url.append("&ref=eclipse-plugin");
+
+ return url.toString();
+ }
+
+ private String toArtifactId(String projectName) {
+ // Convert project name to valid Maven artifact ID
+ return projectName.toLowerCase().replaceAll("[^a-z0-9-]", "-").replaceAll("-+", "-").replaceAll("^-|-$", "");
+ }
+
+ private String encode(String value) {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8);
+ }
+
+ // Getters and setters
+
+ public ProjectType getProjectType() {
+ return projectType;
+ }
+
+ public void setProjectType(ProjectType projectType) {
+ this.projectType = projectType;
+ }
+
+ public String getProjectName() {
+ return projectName;
+ }
+
+ public void setProjectName(String projectName) {
+ this.projectName = projectName;
+ }
+
+ public String getLocation() {
+ return location;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public boolean isPrerelease() {
+ return prerelease;
+ }
+
+ public void setPrerelease(boolean prerelease) {
+ this.prerelease = prerelease;
+ }
+
+ public boolean isIncludeFlow() {
+ return includeFlow;
+ }
+
+ public void setIncludeFlow(boolean includeFlow) {
+ this.includeFlow = includeFlow;
+ }
+
+ public boolean isIncludeHilla() {
+ return includeHilla;
+ }
+
+ public void setIncludeHilla(boolean includeHilla) {
+ this.includeHilla = includeHilla;
+ }
+
+ public String getFramework() {
+ return framework;
+ }
+
+ public void setFramework(String framework) {
+ this.framework = framework;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public String getBuildTool() {
+ return buildTool;
+ }
+
+ public void setBuildTool(String buildTool) {
+ this.buildTool = buildTool;
+ }
+
+ public String getArchitecture() {
+ return architecture;
+ }
+
+ public void setArchitecture(String architecture) {
+ this.architecture = architecture;
+ }
+}
diff --git a/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/wizards/VaadinProjectWizardPage.java b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/wizards/VaadinProjectWizardPage.java
new file mode 100644
index 0000000..3fac852
--- /dev/null
+++ b/vaadin-eclipse-plugin-main/src/com/vaadin/plugin/wizards/VaadinProjectWizardPage.java
@@ -0,0 +1,484 @@
+package com.vaadin.plugin.wizards;
+
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.DirectoryDialog;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * The main page of the New Vaadin Project wizard.
+ */
+public class VaadinProjectWizardPage extends WizardPage {
+
+ private Text projectNameText;
+ private Text locationText;
+ private Button useDefaultLocationButton;
+
+ // Starter project options
+ private Button starterProjectRadio;
+ private Group starterGroup;
+ private Button flowCheckbox;
+ private Button hillaCheckbox;
+ private Combo vaadinVersionCombo;
+
+ // Hello World options
+ private Button helloWorldRadio;
+ private Group helloWorldGroup;
+ private Combo frameworkCombo;
+ private Combo languageCombo;
+ private Combo buildToolCombo;
+ private Combo architectureCombo;
+ private Label kotlinNote;
+
+ private ProjectModel model;
+
+ public VaadinProjectWizardPage() {
+ super("vaadinProjectPage");
+ setTitle("Vaadin");
+ setDescription("Create a new Vaadin project");
+ model = new ProjectModel();
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 3;
+ layout.verticalSpacing = 9;
+ container.setLayout(layout);
+
+ // Project name
+ Label label = new Label(container, SWT.NULL);
+ label.setText("&Project name:");
+
+ projectNameText = new Text(container, SWT.BORDER | SWT.SINGLE);
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ projectNameText.setLayoutData(gd);
+ projectNameText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ dialogChanged();
+ }
+ });
+
+ // Location
+ useDefaultLocationButton = new Button(container, SWT.CHECK);
+ useDefaultLocationButton.setText("Use default location");
+ useDefaultLocationButton.setSelection(true);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 3;
+ useDefaultLocationButton.setLayoutData(gd);
+
+ label = new Label(container, SWT.NULL);
+ label.setText("Location:");
+
+ locationText = new Text(container, SWT.BORDER | SWT.SINGLE);
+ locationText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ locationText.setEnabled(false);
+
+ Button browseButton = new Button(container, SWT.PUSH);
+ browseButton.setText("Browse...");
+ browseButton.setEnabled(false);
+
+ useDefaultLocationButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ boolean useDefault = useDefaultLocationButton.getSelection();
+ locationText.setEnabled(!useDefault);
+ browseButton.setEnabled(!useDefault);
+ if (useDefault) {
+ updateDefaultLocation();
+ }
+ }
+ });
+
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ DirectoryDialog dialog = new DirectoryDialog(getShell());
+ dialog.setMessage("Select project location");
+ String result = dialog.open();
+ if (result != null) {
+ locationText.setText(result);
+ }
+ }
+ });
+
+ // Project type selection
+ createProjectTypeSection(container);
+
+ // Add separator
+ Label separator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 3;
+ separator.setLayoutData(gd);
+
+ // Add help sections
+ createHelpSections(container);
+
+ // Initialize default location
+ updateDefaultLocation();
+
+ // Set default project name
+ projectNameText.setText(generateProjectName());
+
+ dialogChanged();
+ setControl(container);
+ }
+
+ private void createHelpSections(Composite parent) {
+ // Getting Started section
+ Label gettingStartedLabel = new Label(parent, SWT.NONE);
+ gettingStartedLabel.setText("Getting Started");
+ gettingStartedLabel.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ gettingStartedLabel.setLayoutData(gd);
+
+ Label gettingStartedText = new Label(parent, SWT.WRAP);
+ gettingStartedText
+ .setText("The Getting Started guide will quickly familiarize you with your new Walking Skeleton "
+ + "implementation. You'll learn how to set up your development environment, understand the project "
+ + "structure, and find resources to help you add muscles to your skeleton—transforming it into a "
+ + "fully-featured application.");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 3;
+ gd.widthHint = 500;
+ gettingStartedText.setLayoutData(gd);
+
+ // Flow and Hilla section
+ Label flowHillaLabel = new Label(parent, SWT.NONE);
+ flowHillaLabel.setText("Flow and Hilla");
+ flowHillaLabel.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
+ gd = new GridData();
+ gd.horizontalSpan = 3;
+ gd.verticalIndent = 10;
+ flowHillaLabel.setLayoutData(gd);
+
+ Label flowHillaText = new Label(parent, SWT.WRAP);
+ flowHillaText.setText("Flow framework is the most productive choice, allowing 100% of the user interface to be "
+ + "coded in server-side Java. Hilla framework, on the other hand, enables implementation of your user "
+ + "interface with React while automatically connecting it to your Java backend.");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 3;
+ gd.widthHint = 500;
+ flowHillaText.setLayoutData(gd);
+ }
+
+ private void createProjectTypeSection(Composite parent) {
+ // Project Type Selection - Radio buttons in same parent for mutual exclusivity
+ Composite radioContainer = new Composite(parent, SWT.NONE);
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 3;
+ radioContainer.setLayoutData(gd);
+ radioContainer.setLayout(new GridLayout(1, false));
+
+ Label projectTypeLabel = new Label(radioContainer, SWT.NONE);
+ projectTypeLabel.setText("Project Type:");
+ projectTypeLabel.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
+
+ starterProjectRadio = new Button(radioContainer, SWT.RADIO);
+ starterProjectRadio.setText("Starter Project - Full-featured application skeleton with user management and security");
+ starterProjectRadio.setSelection(true);
+
+ helloWorldRadio = new Button(radioContainer, SWT.RADIO);
+ helloWorldRadio.setText("Hello World Project - Minimal project to get started quickly");
+
+ // Starter Project Section
+ starterGroup = new Group(parent, SWT.NONE);
+ starterGroup.setText("Starter Project Options");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 3;
+ starterGroup.setLayoutData(gd);
+ starterGroup.setLayout(new GridLayout(2, false));
+
+ Label label = new Label(starterGroup, SWT.NONE);
+ label.setText("Vaadin Version:");
+
+ vaadinVersionCombo = new Combo(starterGroup, SWT.READ_ONLY);
+ vaadinVersionCombo.setItems("Stable", "Prerelease");
+ vaadinVersionCombo.select(0);
+ vaadinVersionCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // Include Walking Skeleton section
+ Label skeletonLabel = new Label(starterGroup, SWT.NONE);
+ skeletonLabel.setText("Include Walking Skeleton");
+ skeletonLabel.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
+ gd = new GridData();
+ gd.horizontalSpan = 2;
+ skeletonLabel.setLayoutData(gd);
+
+ Label descLabel = new Label(starterGroup, SWT.WRAP);
+ descLabel.setText("A walking skeleton is a minimal application that includes a fully-functional "
+ + "end-to-end workflow. All major building blocks are included, but it does not yet "
+ + "perform any meaningful tasks.");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ gd.widthHint = 400;
+ descLabel.setLayoutData(gd);
+
+ flowCheckbox = new Button(starterGroup, SWT.CHECK);
+ flowCheckbox.setText("Pure Java with Vaadin Flow");
+ flowCheckbox.setSelection(true);
+ gd = new GridData();
+ gd.horizontalSpan = 2;
+ flowCheckbox.setLayoutData(gd);
+
+ hillaCheckbox = new Button(starterGroup, SWT.CHECK);
+ hillaCheckbox.setText("Full-stack React with Vaadin Hilla");
+ hillaCheckbox.setSelection(false);
+ gd = new GridData();
+ gd.horizontalSpan = 2;
+ hillaCheckbox.setLayoutData(gd);
+
+ // Hello World Projects Section
+ helloWorldGroup = new Group(parent, SWT.NONE);
+ helloWorldGroup.setText("Hello World Project Options");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 3;
+ helloWorldGroup.setLayoutData(gd);
+ helloWorldGroup.setLayout(new GridLayout(2, false));
+
+ label = new Label(helloWorldGroup, SWT.NONE);
+ label.setText("Framework:");
+
+ frameworkCombo = new Combo(helloWorldGroup, SWT.READ_ONLY);
+ frameworkCombo.setItems("Flow / Java", "Hilla / React");
+ frameworkCombo.select(0);
+ frameworkCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ label = new Label(helloWorldGroup, SWT.NONE);
+ label.setText("Language:");
+
+ languageCombo = new Combo(helloWorldGroup, SWT.READ_ONLY);
+ languageCombo.setItems("Java", "Kotlin");
+ languageCombo.select(0);
+ languageCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ label = new Label(helloWorldGroup, SWT.NONE);
+ label.setText("Build tool:");
+
+ buildToolCombo = new Combo(helloWorldGroup, SWT.READ_ONLY);
+ buildToolCombo.setItems("Maven", "Gradle");
+ buildToolCombo.select(0);
+ buildToolCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ label = new Label(helloWorldGroup, SWT.NONE);
+ label.setText("Architecture:");
+
+ architectureCombo = new Combo(helloWorldGroup, SWT.READ_ONLY);
+ architectureCombo.setItems("Spring Boot", "Quarkus", "Jakarta EE", "Servlet");
+ architectureCombo.select(0);
+ architectureCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // Add note label for Kotlin (initially hidden)
+ kotlinNote = new Label(helloWorldGroup, SWT.WRAP | SWT.ITALIC);
+ kotlinNote.setText("Kotlin support uses a community add-on.");
+ kotlinNote.setVisible(false);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ kotlinNote.setLayoutData(gd);
+
+ // Add listeners to enable/disable sections
+ starterProjectRadio.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateProjectTypeEnablement();
+ dialogChanged();
+ }
+ });
+
+ helloWorldRadio.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateProjectTypeEnablement();
+ dialogChanged();
+ }
+ });
+
+ // Add listeners for validation
+ SelectionAdapter validationListener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ validateAndUpdateOptions();
+ dialogChanged();
+ }
+ };
+
+ frameworkCombo.addSelectionListener(validationListener);
+ languageCombo.addSelectionListener(validationListener);
+ buildToolCombo.addSelectionListener(validationListener);
+ architectureCombo.addSelectionListener(validationListener);
+ flowCheckbox.addSelectionListener(validationListener);
+ hillaCheckbox.addSelectionListener(validationListener);
+
+ // Initial enablement
+ updateProjectTypeEnablement();
+ }
+
+ private void updateProjectTypeEnablement() {
+ boolean isStarter = starterProjectRadio.getSelection();
+ boolean isHelloWorld = helloWorldRadio.getSelection();
+
+ // Show/hide entire groups
+ starterGroup.setVisible(isStarter);
+ ((GridData) starterGroup.getLayoutData()).exclude = !isStarter;
+
+ helloWorldGroup.setVisible(isHelloWorld);
+ ((GridData) helloWorldGroup.getLayoutData()).exclude = !isHelloWorld;
+
+ // Request layout update to adjust spacing
+ starterGroup.getParent().layout(true, true);
+ }
+
+ private void validateAndUpdateOptions() {
+ if (helloWorldRadio.getSelection()) {
+ // Apply validation rules based on IntelliJ plugin's StarterSupport
+ boolean isHilla = frameworkCombo.getSelectionIndex() == 1;
+ boolean isKotlin = languageCombo.getSelectionIndex() == 1;
+ boolean isGradle = buildToolCombo.getSelectionIndex() == 1;
+ String architecture = architectureCombo.getText();
+
+ // Show/hide Kotlin note
+ if (kotlinNote != null) {
+ kotlinNote.setVisible(isKotlin);
+ }
+
+ // Hilla only supports Spring Boot
+ if (isHilla && !architecture.equals("Spring Boot")) {
+ architectureCombo.select(0); // Spring Boot
+ }
+
+ // Kotlin only supports Maven + Spring Boot
+ if (isKotlin) {
+ if (isGradle) {
+ buildToolCombo.select(0); // Maven
+ }
+ if (!architecture.equals("Spring Boot")) {
+ architectureCombo.select(0); // Spring Boot
+ }
+ }
+
+ // Gradle only supports Spring Boot and Servlet
+ if (isGradle && !architecture.equals("Spring Boot") && !architecture.equals("Servlet")) {
+ architectureCombo.select(0); // Spring Boot
+ }
+
+ // Disable invalid combinations
+ architectureCombo.setEnabled(!isHilla); // Only Spring Boot for Hilla
+
+ if (isKotlin) {
+ buildToolCombo.setEnabled(false); // Only Maven for Kotlin
+ architectureCombo.setEnabled(false); // Only Spring Boot for Kotlin
+ } else {
+ buildToolCombo.setEnabled(true);
+ architectureCombo.setEnabled(!isHilla);
+ }
+ }
+ }
+
+ private void updateDefaultLocation() {
+ if (useDefaultLocationButton.getSelection()) {
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ IPath workspacePath = root.getLocation();
+ String projectName = projectNameText.getText();
+ if (projectName != null && !projectName.isEmpty()) {
+ locationText.setText(workspacePath.append(projectName).toString());
+ } else {
+ locationText.setText(workspacePath.toString());
+ }
+ }
+ }
+
+ private String generateProjectName() {
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ String baseName = "vaadin-project";
+ String projectName = baseName;
+ int counter = 1;
+
+ while (root.getProject(projectName).exists()) {
+ projectName = baseName + "-" + counter;
+ counter++;
+ }
+
+ return projectName;
+ }
+
+ private void dialogChanged() {
+ String projectName = projectNameText.getText();
+
+ // Update location if using default
+ if (useDefaultLocationButton.getSelection()) {
+ updateDefaultLocation();
+ }
+
+ // Validate project name
+ if (projectName.length() == 0) {
+ updateStatus("Project name must be specified");
+ return;
+ }
+
+ if (projectName.contains(" ")) {
+ updateStatus("Project name cannot contain spaces");
+ return;
+ }
+
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ if (root.getProject(projectName).exists()) {
+ updateStatus("A project with this name already exists");
+ return;
+ }
+
+ // Validate project type selection
+ if (starterProjectRadio.getSelection()) {
+ if (!flowCheckbox.getSelection() && !hillaCheckbox.getSelection()) {
+ updateStatus("Please select at least one framework (Flow or Hilla)");
+ return;
+ }
+ }
+
+ updateStatus(null);
+ }
+
+ private void updateStatus(String message) {
+ setErrorMessage(message);
+ setPageComplete(message == null);
+ }
+
+ public ProjectModel getProjectModel() {
+ model.setProjectName(projectNameText.getText());
+ model.setLocation(locationText.getText());
+
+ if (starterProjectRadio.getSelection()) {
+ model.setProjectType(ProjectModel.ProjectType.STARTER);
+ model.setPrerelease(vaadinVersionCombo.getSelectionIndex() == 1);
+ model.setIncludeFlow(flowCheckbox.getSelection());
+ model.setIncludeHilla(hillaCheckbox.getSelection());
+ } else {
+ model.setProjectType(ProjectModel.ProjectType.HELLO_WORLD);
+ model.setFramework(frameworkCombo.getSelectionIndex() == 0 ? "flow" : "hilla");
+ model.setLanguage(languageCombo.getSelectionIndex() == 0 ? "java" : "kotlin");
+ model.setBuildTool(buildToolCombo.getSelectionIndex() == 0 ? "maven" : "gradle");
+
+ String[] architectures = { "springboot", "quarkus", "jakartaee", "servlet" };
+ model.setArchitecture(architectures[architectureCombo.getSelectionIndex()]);
+ }
+
+ return model;
+ }
+}
diff --git a/vaadin-eclipse-plugin-site/category.xml b/vaadin-eclipse-plugin-site/category.xml
new file mode 100644
index 0000000..1ccf940
--- /dev/null
+++ b/vaadin-eclipse-plugin-site/category.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+ Vaadin tools and utilities for Eclipse IDE
+
+
+
\ No newline at end of file
diff --git a/vaadin-eclipse-plugin-site/pom.xml b/vaadin-eclipse-plugin-site/pom.xml
new file mode 100644
index 0000000..808f12f
--- /dev/null
+++ b/vaadin-eclipse-plugin-site/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+
+ com.vaadin
+ vaadin-eclipse-plugin-parent
+ 1.0.0-SNAPSHOT
+
+
+ vaadin-eclipse-plugin-site
+ eclipse-repository
+ Vaadin Eclipse Plugin Update Site
+
+
+
+
+ org.eclipse.tycho
+ tycho-p2-repository-plugin
+ 4.0.13
+
+ false
+ true
+ true
+
+
+
+
+
\ No newline at end of file
diff --git a/vaadin-eclipse-plugin.tests/.classpath b/vaadin-eclipse-plugin.tests/.classpath
new file mode 100644
index 0000000..5050774
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/.classpath
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vaadin-eclipse-plugin.tests/.project b/vaadin-eclipse-plugin.tests/.project
new file mode 100644
index 0000000..c8a8cee
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/.project
@@ -0,0 +1,34 @@
+
+
+ vaadin-eclipse-plugin.tests
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.pde.ManifestBuilder
+
+
+
+
+ org.eclipse.pde.SchemaBuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.pde.PluginNature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/vaadin-eclipse-plugin.tests/META-INF/MANIFEST.MF b/vaadin-eclipse-plugin.tests/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..cd261bb
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/META-INF/MANIFEST.MF
@@ -0,0 +1,16 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Vaadin Eclipse Plugin Tests
+Bundle-SymbolicName: vaadin-eclipse-plugin.tests
+Bundle-Version: 1.0.0.qualifier
+Bundle-Vendor: Vaadin
+Fragment-Host: vaadin-eclipse-plugin
+Bundle-RequiredExecutionEnvironment: JavaSE-17
+Require-Bundle: org.junit;bundle-version="4.0.0",
+ org.hamcrest.core;bundle-version="1.3.0",
+ org.eclipse.core.runtime,
+ org.eclipse.core.resources,
+ org.eclipse.ui,
+ org.eclipse.ui.ide
+Import-Package: com.sun.net.httpserver,
+ com.google.gson;version="2.8.0"
diff --git a/vaadin-eclipse-plugin.tests/build.properties b/vaadin-eclipse-plugin.tests/build.properties
new file mode 100644
index 0000000..5b359b5
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/build.properties
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+ .
\ No newline at end of file
diff --git a/vaadin-eclipse-plugin.tests/pom.xml b/vaadin-eclipse-plugin.tests/pom.xml
new file mode 100644
index 0000000..8b2d024
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+
+
+ com.vaadin
+ vaadin-eclipse-plugin-parent
+ 1.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ vaadin-eclipse-plugin.tests
+ eclipse-test-plugin
+
+ Vaadin Eclipse Plugin Tests
+
+
+ src
+
+
+ org.eclipse.tycho
+ tycho-surefire-plugin
+ ${tycho-version}
+
+ false
+
+ **/*Test.java
+ **/AllTests.java
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/AdvancedEndpointsTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/AdvancedEndpointsTest.java
new file mode 100644
index 0000000..97949cf
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/AdvancedEndpointsTest.java
@@ -0,0 +1,565 @@
+package com.vaadin.plugin.test;
+
+import static org.junit.Assert.*;
+
+import java.net.http.HttpResponse;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.vaadin.plugin.CopilotClient;
+import com.vaadin.plugin.CopilotRestService;
+
+/**
+ * Tests for advanced endpoint implementations including project analysis,
+ * compilation, and application management.
+ */
+public class AdvancedEndpointsTest extends BaseIntegrationTest {
+
+ private CopilotRestService restService;
+ private CopilotClient client;
+ private Gson gson = new Gson();
+
+ @Override
+ protected void doSetUp() throws CoreException {
+ restService = new CopilotRestService();
+
+ try {
+ restService.start();
+ String endpoint = restService.getEndpoint();
+ String projectPath = testProject.getLocation().toString();
+
+ client = new CopilotClient(endpoint, projectPath);
+
+ // Give the server a moment to fully start
+ Thread.sleep(100);
+
+ // Add Java nature for some tests
+ addJavaNature(testProject);
+
+ } catch (Exception e) {
+ fail("Failed to start REST service: " + e.getMessage());
+ }
+ }
+
+ @Override
+ protected void doTearDown() throws CoreException {
+ if (restService != null) {
+ restService.stop();
+ }
+
+ // Clean up any nested projects that might have been created
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ org.eclipse.core.resources.IProject moduleA = workspace.getRoot().getProject("module-a");
+ if (moduleA.exists()) {
+ moduleA.delete(true, null);
+ }
+ org.eclipse.core.resources.IProject moduleB = workspace.getRoot().getProject("module-b");
+ if (moduleB.exists()) {
+ moduleB.delete(true, null);
+ }
+ }
+
+ @Test
+ public void testGetModulePathsEndpoint() throws Exception {
+ // Add source folders to test project
+ IJavaProject javaProject = JavaCore.create(testProject);
+
+ IFolder srcMain = testProject.getFolder("src/main/java");
+ createFolderHierarchy(srcMain);
+ IFolder srcTest = testProject.getFolder("src/test/java");
+ createFolderHierarchy(srcTest);
+ IFolder resources = testProject.getFolder("src/main/resources");
+ createFolderHierarchy(resources);
+
+ // Add to classpath (include JRE container)
+ IClasspathEntry containerEntry = JavaCore.newContainerEntry(
+ org.eclipse.core.runtime.Path.fromPortableString("org.eclipse.jdt.launching.JRE_CONTAINER"));
+ IClasspathEntry[] entries = new IClasspathEntry[]{JavaCore.newSourceEntry(srcMain.getFullPath()),
+ JavaCore.newSourceEntry(srcTest.getFullPath()), JavaCore.newSourceEntry(resources.getFullPath()),
+ containerEntry};
+ javaProject.setRawClasspath(entries, null);
+
+ // Test the endpoint
+ JsonObject data = new JsonObject();
+ HttpResponse response = client.sendCommand("getModulePaths", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertTrue("Should have project key", responseObj.has("project"));
+
+ JsonObject project = responseObj.getAsJsonObject("project");
+ assertTrue("Should have basePath", project.has("basePath"));
+ assertTrue("Should have modules", project.has("modules"));
+
+ // Verify module structure
+ var modules = project.getAsJsonArray("modules");
+ assertNotNull("Modules should not be null", modules);
+ assertTrue("Should have at least one module", modules.size() > 0);
+
+ var module = modules.get(0).getAsJsonObject();
+ assertEquals("Module name should match", testProject.getName(), module.get("name").getAsString());
+ assertTrue("Should have javaSourcePaths", module.has("javaSourcePaths"));
+ assertTrue("Should have javaTestSourcePaths", module.has("javaTestSourcePaths"));
+ assertTrue("Should have resourcePaths", module.has("resourcePaths"));
+ }
+
+ @Test
+ public void testGetModulePathsWithMultiModuleMavenProject() throws Exception {
+ // Create a parent project structure
+ IFolder parentPom = testProject.getFolder("pom.xml");
+ String parentPomContent = "\n"
+ + "\n" + " 4.0.0\n"
+ + " com.example\n" + " parent-project\n"
+ + " 1.0.0\n" + " pom\n" + " \n"
+ + " module-a\n" + " module-b\n" + " \n"
+ + "";
+ createFile(testProject, "pom.xml", parentPomContent);
+
+ // Create module-a as a nested Eclipse project
+ String moduleALocation = testProject.getLocation().append("module-a").toString();
+ org.eclipse.core.resources.IProject moduleA = createNestedProject("module-a", moduleALocation);
+
+ // Add Java nature to module-a
+ addJavaNature(moduleA);
+ IJavaProject javaModuleA = JavaCore.create(moduleA);
+
+ // Create module-a structure
+ IFolder srcMainA = moduleA.getFolder("src/main/java");
+ createFolderHierarchy(srcMainA);
+ IFolder srcTestA = moduleA.getFolder("src/test/java");
+ createFolderHierarchy(srcTestA);
+ IFolder resourcesA = moduleA.getFolder("src/main/resources");
+ createFolderHierarchy(resourcesA);
+
+ // Set classpath for module-a
+ IClasspathEntry containerEntryA = JavaCore.newContainerEntry(
+ org.eclipse.core.runtime.Path.fromPortableString("org.eclipse.jdt.launching.JRE_CONTAINER"));
+ IClasspathEntry[] entriesA = new IClasspathEntry[]{JavaCore.newSourceEntry(srcMainA.getFullPath()),
+ JavaCore.newSourceEntry(srcTestA.getFullPath()), JavaCore.newSourceEntry(resourcesA.getFullPath()),
+ containerEntryA};
+ javaModuleA.setRawClasspath(entriesA, null);
+
+ // Create module-a pom.xml
+ String moduleAPomContent = "\n"
+ + "\n" + " 4.0.0\n"
+ + " \n" + " com.example\n"
+ + " parent-project\n" + " 1.0.0\n"
+ + " \n" + " module-a\n" + "";
+ createFile(moduleA, "pom.xml", moduleAPomContent);
+
+ // Create module-b as another nested Eclipse project
+ String moduleBLocation = testProject.getLocation().append("module-b").toString();
+ org.eclipse.core.resources.IProject moduleB = createNestedProject("module-b", moduleBLocation);
+
+ // Add Java nature to module-b
+ addJavaNature(moduleB);
+ IJavaProject javaModuleB = JavaCore.create(moduleB);
+
+ // Create module-b structure
+ IFolder srcMainB = moduleB.getFolder("src/main/java");
+ createFolderHierarchy(srcMainB);
+ IFolder srcTestB = moduleB.getFolder("src/test/java");
+ createFolderHierarchy(srcTestB);
+
+ // Set classpath for module-b (simpler, without resources)
+ IClasspathEntry containerEntryB = JavaCore.newContainerEntry(
+ org.eclipse.core.runtime.Path.fromPortableString("org.eclipse.jdt.launching.JRE_CONTAINER"));
+ IClasspathEntry[] entriesB = new IClasspathEntry[]{JavaCore.newSourceEntry(srcMainB.getFullPath()),
+ JavaCore.newSourceEntry(srcTestB.getFullPath()), containerEntryB};
+ javaModuleB.setRawClasspath(entriesB, null);
+
+ // Create module-b pom.xml
+ String moduleBPomContent = "\n"
+ + "\n" + " 4.0.0\n"
+ + " \n" + " com.example\n"
+ + " parent-project\n" + " 1.0.0\n"
+ + " \n" + " module-b\n" + "";
+ createFile(moduleB, "pom.xml", moduleBPomContent);
+
+ // Test the endpoint - request module paths for the parent project
+ JsonObject data = new JsonObject();
+ HttpResponse response = client.sendCommand("getModulePaths", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertTrue("Should have project key", responseObj.has("project"));
+
+ JsonObject project = responseObj.getAsJsonObject("project");
+ assertTrue("Should have basePath", project.has("basePath"));
+ assertTrue("Should have modules", project.has("modules"));
+
+ String basePath = project.get("basePath").getAsString();
+ assertEquals("Base path should be parent project", testProject.getLocation().toString(), basePath);
+
+ // Verify multi-module structure
+ var modules = project.getAsJsonArray("modules");
+ assertNotNull("Modules should not be null", modules);
+ assertEquals("Should have 3 modules (parent + 2 children)", 3, modules.size());
+
+ // Check parent module
+ var parentModule = modules.get(0).getAsJsonObject();
+ assertEquals("First module should be parent", testProject.getName(), parentModule.get("name").getAsString());
+
+ // Check module-a
+ boolean foundModuleA = false;
+ boolean foundModuleB = false;
+
+ for (int i = 1; i < modules.size(); i++) {
+ var module = modules.get(i).getAsJsonObject();
+ String moduleName = module.get("name").getAsString();
+
+ if ("module-a".equals(moduleName)) {
+ foundModuleA = true;
+ assertTrue("Module A should have contentRoots", module.has("contentRoots"));
+ var contentRoots = module.getAsJsonArray("contentRoots");
+ assertEquals("Module A should have one content root", 1, contentRoots.size());
+ String contentRoot = contentRoots.get(0).getAsString();
+ assertTrue("Module A content root should be nested in parent",
+ contentRoot.contains(testProject.getName() + "/module-a"));
+ } else if ("module-b".equals(moduleName)) {
+ foundModuleB = true;
+ assertTrue("Module B should have contentRoots", module.has("contentRoots"));
+ var contentRoots = module.getAsJsonArray("contentRoots");
+ assertEquals("Module B should have one content root", 1, contentRoots.size());
+ String contentRoot = contentRoots.get(0).getAsString();
+ assertTrue("Module B content root should be nested in parent",
+ contentRoot.contains(testProject.getName() + "/module-b"));
+ }
+ }
+
+ assertTrue("Should find module-a in response", foundModuleA);
+ assertTrue("Should find module-b in response", foundModuleB);
+
+ // Clean up nested projects
+ moduleA.delete(true, null);
+ moduleB.delete(true, null);
+ }
+
+ @Test
+ public void testGetVaadinVersionEndpoint() throws Exception {
+ JsonObject data = new JsonObject();
+ HttpResponse response = client.sendCommand("getVaadinVersion", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertTrue("Should have version key", responseObj.has("version"));
+
+ // Without Vaadin in classpath, should return N/A
+ String version = responseObj.get("version").getAsString();
+ assertEquals("Version should be N/A without Vaadin", "N/A", version);
+ }
+
+ @Test
+ public void testCompileFilesEndpoint() throws Exception {
+ // Create a Java file to compile
+ IFolder srcFolder = testProject.getFolder("src");
+ srcFolder.create(true, true, null);
+
+ IFile javaFile = srcFolder.getFile("Test.java");
+ String content = "public class Test { public static void main(String[] args) {} }";
+ javaFile.create(new java.io.ByteArrayInputStream(content.getBytes()), true, null);
+
+ // Test compile endpoint
+ JsonObject data = new JsonObject();
+ var filesArray = new com.google.gson.JsonArray();
+ filesArray.add(javaFile.getLocation().toString());
+ data.add("files", filesArray);
+
+ HttpResponse response = client.sendCommand("compileFiles", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertEquals("Should return ok status", "ok", responseObj.get("status").getAsString());
+ }
+
+ @Test
+ public void testReloadMavenModuleEndpoint() throws Exception {
+ // Test without module name (should refresh main project)
+ JsonObject data = new JsonObject();
+ HttpResponse response = client.sendCommand("reloadMavenModule", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertEquals("Should return ok status", "ok", responseObj.get("status").getAsString());
+ }
+
+ @Test
+ public void testReloadMavenModuleWithNameEndpoint() throws Exception {
+ // Test with specific module name
+ JsonObject data = new JsonObject();
+ data.addProperty("moduleName", testProject.getName());
+
+ HttpResponse response = client.sendCommand("reloadMavenModule", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertEquals("Should return ok status", "ok", responseObj.get("status").getAsString());
+ }
+
+ @Test
+ public void testGetVaadinRoutesEndpoint() throws Exception {
+ // Create a class with @Route annotation
+ createJavaClass("src", "com.example", "TestView",
+ "package com.example;\n" + "@Route(\"test\")\n" + "public class TestView {}\n");
+
+ JsonObject data = new JsonObject();
+ HttpResponse response = client.sendCommand("getVaadinRoutes", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertTrue("Should have routes key", responseObj.has("routes"));
+
+ var routes = responseObj.getAsJsonArray("routes");
+ assertNotNull("Routes should not be null", routes);
+ // Note: Will be empty without proper annotation scanning setup
+ }
+
+ @Test
+ public void testGetVaadinComponentsEndpoint() throws Exception {
+ JsonObject data = new JsonObject();
+ data.addProperty("includeMethods", true);
+
+ HttpResponse response = client.sendCommand("getVaadinComponents", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertTrue("Should have components key", responseObj.has("components"));
+
+ var components = responseObj.getAsJsonArray("components");
+ assertNotNull("Components should not be null", components);
+ // Will be empty without Vaadin in classpath
+ assertEquals("Components should be empty without Vaadin", 0, components.size());
+ }
+
+ @Test
+ public void testGetVaadinEntitiesEndpoint() throws Exception {
+ // Create an entity class
+ createJavaClass("src", "com.example.model", "TestEntity", "package com.example.model;\n" + "@Entity\n"
+ + "public class TestEntity {\n" + " private Long id;\n" + "}\n");
+
+ JsonObject data = new JsonObject();
+ data.addProperty("includeMethods", false);
+
+ HttpResponse response = client.sendCommand("getVaadinEntities", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertTrue("Should have entities key", responseObj.has("entities"));
+
+ var entities = responseObj.getAsJsonArray("entities");
+ assertNotNull("Entities should not be null", entities);
+ }
+
+ @Test
+ public void testGetVaadinSecurityEndpoint() throws Exception {
+ JsonObject data = new JsonObject();
+ HttpResponse response = client.sendCommand("getVaadinSecurity", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertTrue("Should have security key", responseObj.has("security"));
+ assertTrue("Should have userDetails key", responseObj.has("userDetails"));
+
+ var security = responseObj.getAsJsonArray("security");
+ var userDetails = responseObj.getAsJsonArray("userDetails");
+
+ assertNotNull("Security should not be null", security);
+ assertNotNull("UserDetails should not be null", userDetails);
+
+ // Will be empty without Spring Security in classpath
+ assertEquals("Security should be empty", 0, security.size());
+ assertEquals("UserDetails should be empty", 0, userDetails.size());
+ }
+
+ @Test
+ public void testRestartApplicationEndpoint() throws Exception {
+ // Test without main class
+ JsonObject data = new JsonObject();
+ HttpResponse response = client.sendCommand("restartApplication", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertEquals("Should return ok status", "ok", responseObj.get("status").getAsString());
+
+ // Without launch configurations, should return message
+ assertTrue("Should have message", responseObj.has("message"));
+ String message = responseObj.get("message").getAsString();
+ assertTrue("Message should indicate no config found", message.contains("No launch configuration"));
+ }
+
+ @Test
+ public void testRestartApplicationWithMainClassEndpoint() throws Exception {
+ // Test with specific main class
+ JsonObject data = new JsonObject();
+ data.addProperty("mainClass", "com.example.Main");
+
+ HttpResponse response = client.sendCommand("restartApplication", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertEquals("Should return ok status", "ok", responseObj.get("status").getAsString());
+ }
+
+ @Test
+ public void testUndoEndpoint() throws Exception {
+ // Create and modify a file first
+ IFile file = testProject.getFile("undo-endpoint-test.txt");
+ file.create(new java.io.ByteArrayInputStream("Original".getBytes()), true, null);
+
+ // Modify via write endpoint to record operation
+ JsonObject writeData = new JsonObject();
+ writeData.addProperty("file", file.getLocation().toString());
+ writeData.addProperty("content", "Modified");
+ writeData.addProperty("undoLabel", "Test modification");
+
+ client.sendCommand("write", writeData);
+
+ // Now test undo
+ JsonObject undoData = new JsonObject();
+ var filesArray = new com.google.gson.JsonArray();
+ filesArray.add(file.getLocation().toString());
+ undoData.add("files", filesArray);
+
+ HttpResponse response = client.sendCommand("undo", undoData);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertTrue("Should have performed key", responseObj.has("performed"));
+
+ // Note: Undo might not work in test environment due to operation history setup
+ boolean performed = responseObj.get("performed").getAsBoolean();
+ // Don't assert true - just verify structure
+ }
+
+ @Test
+ public void testRedoEndpoint() throws Exception {
+ JsonObject data = new JsonObject();
+ var filesArray = new com.google.gson.JsonArray();
+ filesArray.add("/test/file.txt");
+ data.add("files", filesArray);
+
+ HttpResponse response = client.sendCommand("redo", data);
+
+ assertEquals("Should return 200", 200, response.statusCode());
+
+ JsonObject responseObj = gson.fromJson(response.body(), JsonObject.class);
+ assertTrue("Should have performed key", responseObj.has("performed"));
+
+ // Should be false since no undo was performed
+ boolean performed = responseObj.get("performed").getAsBoolean();
+ assertFalse("Redo should not be performed without prior undo", performed);
+ }
+
+ /**
+ * Helper to create folder hierarchy.
+ */
+ /**
+ * Creates a file with the given content in the specified project.
+ */
+ private void createFile(org.eclipse.core.resources.IProject project, String fileName, String content)
+ throws CoreException {
+ IFile file = project.getFile(fileName);
+ if (!file.exists()) {
+ java.io.ByteArrayInputStream stream = new java.io.ByteArrayInputStream(
+ content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ file.create(stream, true, null);
+ }
+ }
+
+ /**
+ * Creates a nested Eclipse project at the specified location. This simulates a
+ * Maven multi-module project structure where child modules are Eclipse projects
+ * nested within the parent project's file system.
+ */
+ private org.eclipse.core.resources.IProject createNestedProject(String projectName, String location)
+ throws CoreException {
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ org.eclipse.core.resources.IProject nestedProject = workspace.getRoot().getProject(projectName);
+
+ if (!nestedProject.exists()) {
+ IProjectDescription description = workspace.newProjectDescription(projectName);
+ // Set the location to be nested inside the parent project
+ description.setLocation(new org.eclipse.core.runtime.Path(location));
+ nestedProject.create(description, null);
+ }
+
+ if (!nestedProject.isOpen()) {
+ nestedProject.open(null);
+ }
+
+ return nestedProject;
+ }
+
+ private void createFolderHierarchy(IFolder folder) throws CoreException {
+ if (!folder.exists()) {
+ org.eclipse.core.resources.IContainer parent = folder.getParent();
+ if (parent != null && !parent.exists() && parent.getType() == org.eclipse.core.resources.IResource.FOLDER) {
+ createFolderHierarchy((IFolder) parent);
+ }
+ folder.create(true, true, null);
+ }
+ }
+
+ /**
+ * Helper to create Java class.
+ */
+ private void createJavaClass(String sourceFolder, String packageName, String className, String content)
+ throws CoreException {
+ IFolder srcFolder = testProject.getFolder(sourceFolder);
+ if (!srcFolder.exists()) {
+ srcFolder.create(true, true, null);
+ }
+
+ IFolder packageFolder = srcFolder;
+ String[] packageParts = packageName.split("\\.");
+ for (String part : packageParts) {
+ packageFolder = packageFolder.getFolder(part);
+ if (!packageFolder.exists()) {
+ packageFolder.create(true, true, null);
+ }
+ }
+
+ IFile javaFile = packageFolder.getFile(className + ".java");
+ javaFile.create(new java.io.ByteArrayInputStream(content.getBytes()), true, null);
+ }
+
+ /**
+ * Helper to add Java nature.
+ */
+ private void addJavaNature(org.eclipse.core.resources.IProject project) throws CoreException {
+ if (!project.hasNature(JavaCore.NATURE_ID)) {
+ String[] prevNatures = project.getDescription().getNatureIds();
+ String[] newNatures = new String[prevNatures.length + 1];
+ System.arraycopy(prevNatures, 0, newNatures, 0, prevNatures.length);
+ newNatures[prevNatures.length] = JavaCore.NATURE_ID;
+
+ org.eclipse.core.resources.IProjectDescription description = project.getDescription();
+ description.setNatureIds(newNatures);
+ project.setDescription(description, null);
+ }
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/AllTests.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/AllTests.java
new file mode 100644
index 0000000..071d1db
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/AllTests.java
@@ -0,0 +1,15 @@
+package com.vaadin.plugin.test;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+/**
+ * Test suite for all Vaadin Eclipse Plugin tests.
+ */
+@RunWith(Suite.class)
+@SuiteClasses({CopilotRestServiceIntegrationTest.class, CopilotClientIntegrationTest.class, CopilotUtilTest.class,
+ VaadinProjectAnalyzerTest.class, CopilotUndoManagerTest.class, AdvancedEndpointsTest.class,
+ BinaryFileUndoRedoTest.class})
+public class AllTests {
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/BaseIntegrationTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/BaseIntegrationTest.java
new file mode 100644
index 0000000..d6d9ea6
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/BaseIntegrationTest.java
@@ -0,0 +1,63 @@
+package com.vaadin.plugin.test;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * Base class for integration tests that need a real Eclipse project.
+ */
+public abstract class BaseIntegrationTest {
+
+ protected static final String TEST_PROJECT_NAME = "vaadin-test-project";
+ protected IProject testProject;
+ protected IWorkspace workspace;
+
+ @Before
+ public void setUp() throws CoreException {
+ workspace = ResourcesPlugin.getWorkspace();
+
+ // Create a test project
+ testProject = workspace.getRoot().getProject(TEST_PROJECT_NAME);
+ if (!testProject.exists()) {
+ IProjectDescription description = workspace.newProjectDescription(TEST_PROJECT_NAME);
+ testProject.create(description, null);
+ }
+
+ if (!testProject.isOpen()) {
+ testProject.open(null);
+ }
+
+ // Additional setup in subclasses
+ doSetUp();
+ }
+
+ @After
+ public void tearDown() throws CoreException {
+ // Additional cleanup in subclasses
+ doTearDown();
+
+ // Clean up test project
+ if (testProject != null && testProject.exists()) {
+ testProject.delete(true, true, null);
+ }
+ }
+
+ /**
+ * Override in subclasses for additional setup.
+ */
+ protected void doSetUp() throws CoreException {
+ // Default implementation does nothing
+ }
+
+ /**
+ * Override in subclasses for additional cleanup.
+ */
+ protected void doTearDown() throws CoreException {
+ // Default implementation does nothing
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/BinaryFileUndoRedoTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/BinaryFileUndoRedoTest.java
new file mode 100644
index 0000000..60c2dd2
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/BinaryFileUndoRedoTest.java
@@ -0,0 +1,217 @@
+package com.vaadin.plugin.test;
+
+import static org.junit.Assert.*;
+
+import java.net.http.HttpResponse;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.vaadin.plugin.CopilotClient;
+import com.vaadin.plugin.CopilotRestService;
+
+/**
+ * Tests for binary file operations and undo/redo functionality.
+ */
+public class BinaryFileUndoRedoTest extends BaseIntegrationTest {
+
+ private CopilotRestService restService;
+ private CopilotClient client;
+ private Gson gson = new Gson();
+
+ @Override
+ protected void doSetUp() throws CoreException {
+ restService = new CopilotRestService();
+
+ try {
+ restService.start();
+ String endpoint = restService.getEndpoint();
+ String projectPath = testProject.getLocation().toString();
+
+ client = new CopilotClient(endpoint, projectPath);
+
+ // Give the server a moment to fully start
+ Thread.sleep(100);
+
+ } catch (Exception e) {
+ fail("Failed to start REST service: " + e.getMessage());
+ }
+ }
+
+ @Override
+ protected void doTearDown() throws CoreException {
+ if (restService != null) {
+ restService.stop();
+ }
+ }
+
+ @Test
+ public void testBinaryFileWriteAndRead() throws Exception {
+ // Create binary content (a simple PNG header)
+ byte[] binaryData = new byte[]{(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG header
+ 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52 // IHDR chunk start
+ };
+
+ String base64Content = java.util.Base64.getEncoder().encodeToString(binaryData);
+ Path filePath = Paths.get(testProject.getLocation().toString(), "test.png");
+
+ // Write binary file
+ HttpResponse writeResponse = client.writeBinary(filePath, base64Content);
+ assertEquals("Write should succeed", 200, writeResponse.statusCode());
+
+ // Verify file exists and has correct content
+ IFile file = testProject.getFile("test.png");
+ assertTrue("File should exist", file.exists());
+
+ byte[] readData = file.getContents().readAllBytes();
+ assertArrayEquals("Binary content should match", binaryData, readData);
+ }
+
+ @Test
+ public void testBinaryFileUndoRedo() throws Exception {
+ // Create initial binary content
+ byte[] originalData = new byte[]{0x01, 0x02, 0x03, 0x04};
+ byte[] modifiedData = new byte[]{0x05, 0x06, 0x07, 0x08, 0x09};
+
+ String originalBase64 = java.util.Base64.getEncoder().encodeToString(originalData);
+ String modifiedBase64 = java.util.Base64.getEncoder().encodeToString(modifiedData);
+ Path filePath = Paths.get(testProject.getLocation().toString(), "binary.dat");
+
+ // Write original binary file
+ HttpResponse response1 = client.writeBinary(filePath, originalBase64);
+ assertEquals("First write should succeed", 200, response1.statusCode());
+
+ // Modify the binary file
+ HttpResponse response2 = client.writeBinary(filePath, modifiedBase64);
+ assertEquals("Second write should succeed", 200, response2.statusCode());
+
+ // Verify modified content
+ IFile file = testProject.getFile("binary.dat");
+ byte[] currentData = file.getContents().readAllBytes();
+ assertArrayEquals("Should have modified data", modifiedData, currentData);
+
+ // Perform undo
+ HttpResponse undoResponse = client.undo(filePath);
+ assertEquals("Undo should succeed", 200, undoResponse.statusCode());
+
+ JsonObject undoResult = gson.fromJson(undoResponse.body(), JsonObject.class);
+ assertTrue("Undo should be performed", undoResult.get("performed").getAsBoolean());
+
+ // Verify content reverted to original
+ file.refreshLocal(0, null);
+ currentData = file.getContents().readAllBytes();
+ assertArrayEquals("Should have original data after undo", originalData, currentData);
+
+ // Perform redo
+ HttpResponse redoResponse = client.redo(filePath);
+ assertEquals("Redo should succeed", 200, redoResponse.statusCode());
+
+ JsonObject redoResult = gson.fromJson(redoResponse.body(), JsonObject.class);
+ assertTrue("Redo should be performed", redoResult.get("performed").getAsBoolean());
+
+ // Verify content is modified again
+ file.refreshLocal(0, null);
+ currentData = file.getContents().readAllBytes();
+ assertArrayEquals("Should have modified data after redo", modifiedData, currentData);
+ }
+
+ @Test
+ public void testLargeBinaryFileUndoRedo() throws Exception {
+ // Create a larger binary file (1KB of random-looking data)
+ byte[] largeData = new byte[1024];
+ for (int i = 0; i < largeData.length; i++) {
+ largeData[i] = (byte) (i % 256);
+ }
+
+ byte[] modifiedLargeData = new byte[1024];
+ for (int i = 0; i < modifiedLargeData.length; i++) {
+ modifiedLargeData[i] = (byte) ((i * 2) % 256);
+ }
+
+ String originalBase64 = java.util.Base64.getEncoder().encodeToString(largeData);
+ String modifiedBase64 = java.util.Base64.getEncoder().encodeToString(modifiedLargeData);
+ Path filePath = Paths.get(testProject.getLocation().toString(), "large.bin");
+
+ // Write original
+ client.writeBinary(filePath, originalBase64);
+
+ // Modify
+ client.writeBinary(filePath, modifiedBase64);
+
+ // Undo
+ HttpResponse undoResponse = client.undo(filePath);
+ JsonObject undoResult = gson.fromJson(undoResponse.body(), JsonObject.class);
+ assertTrue("Undo should be performed", undoResult.get("performed").getAsBoolean());
+
+ // Verify original content restored
+ IFile file = testProject.getFile("large.bin");
+ file.refreshLocal(0, null);
+ byte[] currentData = file.getContents().readAllBytes();
+ assertArrayEquals("Large file should be restored correctly", largeData, currentData);
+ }
+
+ @Test
+ public void testMixedTextAndBinaryUndo() throws Exception {
+ // Test that text and binary files can be undone independently
+ Path textPath = Paths.get(testProject.getLocation().toString(), "text.txt");
+ Path binaryPath = Paths.get(testProject.getLocation().toString(), "binary.dat");
+
+ // Create text file
+ client.write(textPath, "Original text");
+
+ // Create binary file
+ byte[] binaryContent = new byte[]{0x0A, 0x0B, 0x0C};
+ String base64Content = java.util.Base64.getEncoder().encodeToString(binaryContent);
+ client.writeBinary(binaryPath, base64Content);
+
+ // Modify both files
+ client.write(textPath, "Modified text");
+
+ byte[] modifiedBinary = new byte[]{0x1A, 0x1B, 0x1C, 0x1D};
+ client.writeBinary(binaryPath, java.util.Base64.getEncoder().encodeToString(modifiedBinary));
+
+ // Undo only the binary file
+ HttpResponse undoResponse = client.undo(binaryPath);
+ JsonObject undoResult = gson.fromJson(undoResponse.body(), JsonObject.class);
+ assertTrue("Binary undo should be performed", undoResult.get("performed").getAsBoolean());
+
+ // Verify binary reverted but text unchanged
+ IFile textIFile = testProject.getFile("text.txt");
+ String currentText = new String(textIFile.getContents().readAllBytes(), "UTF-8");
+ assertEquals("Text should still be modified", "Modified text", currentText);
+
+ IFile binaryIFile = testProject.getFile("binary.dat");
+ byte[] currentBinary = binaryIFile.getContents().readAllBytes();
+ assertArrayEquals("Binary should be reverted", binaryContent, currentBinary);
+ }
+
+ @Test
+ public void testEmptyBinaryFileUndo() throws Exception {
+ // Test handling of empty binary files
+ Path filePath = Paths.get(testProject.getLocation().toString(), "empty.bin");
+
+ // Create empty binary file
+ HttpResponse response = client.writeBinary(filePath, "");
+ assertEquals("Write should succeed", 200, response.statusCode());
+
+ // Add content
+ byte[] content = new byte[]{(byte) 0xFF, (byte) 0xFE};
+ client.writeBinary(filePath, java.util.Base64.getEncoder().encodeToString(content));
+
+ // Undo to empty state
+ HttpResponse undoResponse = client.undo(filePath);
+ JsonObject undoResult = gson.fromJson(undoResponse.body(), JsonObject.class);
+ assertTrue("Undo should be performed", undoResult.get("performed").getAsBoolean());
+
+ // Verify file is empty
+ IFile file = testProject.getFile("empty.bin");
+ file.refreshLocal(0, null);
+ byte[] currentData = file.getContents().readAllBytes();
+ assertEquals("File should be empty after undo", 0, currentData.length);
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotClientIntegrationTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotClientIntegrationTest.java
new file mode 100644
index 0000000..f48838c
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotClientIntegrationTest.java
@@ -0,0 +1,240 @@
+package com.vaadin.plugin.test;
+
+import static org.junit.Assert.*;
+
+import java.net.http.HttpResponse;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Base64;
+import java.util.Optional;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.junit.Test;
+
+import com.google.gson.JsonObject;
+import com.vaadin.plugin.CopilotClient;
+import com.vaadin.plugin.CopilotRestService;
+
+/**
+ * Integration tests for CopilotClient that test the client-side REST API calls
+ * against a real CopilotRestService instance.
+ */
+public class CopilotClientIntegrationTest extends BaseIntegrationTest {
+
+ private CopilotRestService restService;
+ private CopilotClient client;
+
+ @Override
+ protected void doSetUp() throws CoreException {
+ restService = new CopilotRestService();
+
+ try {
+ restService.start();
+ String endpoint = restService.getEndpoint();
+ String projectPath = testProject.getLocation().toString();
+
+ client = new CopilotClient(endpoint, projectPath);
+
+ // Give the server a moment to fully start
+ Thread.sleep(100);
+ } catch (Exception e) {
+ fail("Failed to start REST service: " + e.getMessage());
+ }
+ }
+
+ @Override
+ protected void doTearDown() throws CoreException {
+ if (restService != null) {
+ restService.stop();
+ }
+ }
+
+ @Test
+ public void testClientHeartbeat() throws Exception {
+ HttpResponse response = client.heartbeat();
+
+ assertEquals("HTTP status should be 200", 200, response.statusCode());
+ assertNotNull("Response body should not be null", response.body());
+ assertTrue("Response should contain status", response.body().contains("\"status\""));
+ assertTrue("Response should contain 'alive'", response.body().contains("alive"));
+ }
+
+ @Test
+ public void testClientWriteFile() throws Exception {
+ Path filePath = Paths.get(testProject.getLocation().toString(), "client-test.txt");
+ String content = "Content written by CopilotClient";
+
+ HttpResponse response = client.write(filePath, content);
+
+ assertEquals("HTTP status should be 200", 200, response.statusCode());
+ assertNotNull("Response body should not be null", response.body());
+ assertTrue("Response should indicate success", response.body().contains("\"status\":\"ok\""));
+
+ // Verify file was created
+ IFile file = testProject.getFile("client-test.txt");
+ assertTrue("File should exist after client write", file.exists());
+
+ // Verify content
+ try (java.io.InputStream is = file.getContents()) {
+ String actualContent = new String(is.readAllBytes(), "UTF-8");
+ assertEquals("File content should match", content, actualContent);
+ }
+ }
+
+ @Test
+ public void testClientWriteBinaryFile() throws Exception {
+ byte[] binaryData = "Binary data from client\u0000\u0001\u0002".getBytes("UTF-8");
+ String base64Content = Base64.getEncoder().encodeToString(binaryData);
+ Path filePath = Paths.get(testProject.getLocation().toString(), "client-binary.dat");
+
+ HttpResponse response = client.writeBinary(filePath, base64Content);
+
+ assertEquals("HTTP status should be 200", 200, response.statusCode());
+ assertNotNull("Response body should not be null", response.body());
+ assertTrue("Response should indicate success", response.body().contains("\"status\":\"ok\""));
+
+ // Verify file was created
+ IFile file = testProject.getFile("client-binary.dat");
+ assertTrue("Binary file should exist after client write", file.exists());
+
+ // Verify binary content
+ try (java.io.InputStream is = file.getContents()) {
+ byte[] actualContent = is.readAllBytes();
+ assertArrayEquals("Binary content should match", binaryData, actualContent);
+ }
+ }
+
+ @Test
+ public void testClientDeleteFile() throws Exception {
+ // First create a file to delete
+ IFile file = testProject.getFile("client-delete.txt");
+ file.create(new java.io.ByteArrayInputStream("Delete me via client".getBytes()), true, null);
+ assertTrue("File should exist before delete", file.exists());
+
+ Path filePath = Paths.get(file.getLocation().toString());
+ HttpResponse response = client.delete(filePath);
+
+ assertEquals("HTTP status should be 200", 200, response.statusCode());
+ assertNotNull("Response body should not be null", response.body());
+ assertTrue("Response should indicate success", response.body().contains("\"status\":\"ok\""));
+
+ // Verify file was deleted
+ assertFalse("File should not exist after client delete", file.exists());
+ }
+
+ @Test
+ public void testClientRefresh() throws Exception {
+ HttpResponse response = client.refresh();
+
+ assertEquals("HTTP status should be 200", 200, response.statusCode());
+ assertNotNull("Response body should not be null", response.body());
+ assertTrue("Response should indicate success", response.body().contains("\"status\":\"ok\""));
+ }
+
+ @Test
+ public void testClientShowInIde() throws Exception {
+ // Create a test file
+ IFile file = testProject.getFile("client-show.txt");
+ String content = "Line 1\nLine 2\nTarget line for client test\nLine 4";
+ file.create(new java.io.ByteArrayInputStream(content.getBytes()), true, null);
+
+ Path filePath = Paths.get(file.getLocation().toString());
+ HttpResponse response = client.showInIde(filePath, 3, 0);
+
+ assertEquals("HTTP status should be 200", 200, response.statusCode());
+ assertNotNull("Response body should not be null", response.body());
+ assertTrue("Response should indicate success", response.body().contains("\"status\":\"ok\""));
+ }
+
+ @Test
+ public void testClientUndoRedo() throws Exception {
+ Path filePath = Paths.get(testProject.getLocation().toString(), "undo-test.txt");
+
+ // Test undo (currently stubbed, but should not fail)
+ HttpResponse undoResponse = client.undo(filePath);
+ assertEquals("HTTP status should be 200", 200, undoResponse.statusCode());
+
+ // Test redo (currently stubbed, but should not fail)
+ HttpResponse redoResponse = client.redo(filePath);
+ assertEquals("HTTP status should be 200", 200, redoResponse.statusCode());
+ }
+
+ @Test
+ public void testClientRestartApplication() throws Exception {
+ HttpResponse response = client.restartApplication();
+
+ assertEquals("HTTP status should be 200", 200, response.statusCode());
+ assertNotNull("Response body should not be null", response.body());
+ // Currently stubbed, but should not fail
+ assertTrue("Response should indicate success", response.body().contains("\"status\":\"ok\""));
+ }
+
+ @Test
+ public void testClientGetVaadinRoutes() throws Exception {
+ Optional response = client.getVaadinRoutes();
+
+ assertTrue("Response should be present", response.isPresent());
+ JsonObject responseObj = response.get();
+ assertTrue("Response should contain routes", responseObj.has("routes"));
+ // Currently returns empty array, but structure should be correct
+ assertTrue("Routes should be an array", responseObj.get("routes").isJsonArray());
+ }
+
+ @Test
+ public void testClientGetVaadinVersion() throws Exception {
+ Optional response = client.getVaadinVersion();
+
+ assertTrue("Response should be present", response.isPresent());
+ JsonObject responseObj = response.get();
+ assertTrue("Response should contain version", responseObj.has("version"));
+ assertNotNull("Version should not be null", responseObj.get("version").getAsString());
+ }
+
+ @Test
+ public void testClientGetVaadinComponents() throws Exception {
+ Optional response = client.getVaadinComponents(true);
+
+ assertTrue("Response should be present", response.isPresent());
+ JsonObject responseObj = response.get();
+ assertTrue("Response should contain components", responseObj.has("components"));
+ assertTrue("Components should be an array", responseObj.get("components").isJsonArray());
+ }
+
+ @Test
+ public void testClientGetVaadinEntities() throws Exception {
+ Optional response = client.getVaadinEntities(false);
+
+ assertTrue("Response should be present", response.isPresent());
+ JsonObject responseObj = response.get();
+ assertTrue("Response should contain entities", responseObj.has("entities"));
+ assertTrue("Entities should be an array", responseObj.get("entities").isJsonArray());
+ }
+
+ @Test
+ public void testClientGetVaadinSecurity() throws Exception {
+ Optional response = client.getVaadinSecurity();
+
+ assertTrue("Response should be present", response.isPresent());
+ JsonObject responseObj = response.get();
+ assertTrue("Response should contain security", responseObj.has("security"));
+ assertTrue("Security should be an array", responseObj.get("security").isJsonArray());
+ }
+
+ @Test
+ public void testClientErrorHandling() throws Exception {
+ // Test with invalid project path
+ CopilotClient invalidClient = new CopilotClient(restService.getEndpoint(), "/invalid/project/path");
+ Path filePath = Paths.get("/invalid/path/file.txt");
+
+ try {
+ HttpResponse response = invalidClient.write(filePath, "content");
+ // Should get a response but with error status
+ assertEquals("HTTP status should be 200 (error in response body)", 200, response.statusCode());
+ assertTrue("Response should contain error", response.body().contains("error"));
+ } catch (Exception e) {
+ // Network errors are also acceptable for invalid requests
+ assertNotNull("Exception should have a message", e.getMessage());
+ }
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotRestServiceIntegrationTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotRestServiceIntegrationTest.java
new file mode 100644
index 0000000..19a1896
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotRestServiceIntegrationTest.java
@@ -0,0 +1,347 @@
+package com.vaadin.plugin.test;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Base64;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.runtime.CoreException;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.vaadin.plugin.CopilotRestService;
+import com.vaadin.plugin.Message;
+
+/**
+ * Integration tests for CopilotRestService that test the full REST API chain
+ * with real file operations in an Eclipse workspace.
+ */
+public class CopilotRestServiceIntegrationTest extends BaseIntegrationTest {
+
+ private CopilotRestService restService;
+ private String baseEndpoint;
+ private HttpClient httpClient;
+ private Gson gson;
+
+ @Override
+ protected void doSetUp() throws CoreException {
+ restService = new CopilotRestService();
+ httpClient = HttpClient.newHttpClient();
+ gson = new Gson();
+
+ try {
+ restService.start();
+ baseEndpoint = restService.getEndpoint();
+ assertNotNull("REST service endpoint should not be null", baseEndpoint);
+
+ // Give the server a moment to fully start
+ Thread.sleep(100);
+ } catch (Exception e) {
+ fail("Failed to start REST service: " + e.getMessage());
+ }
+ }
+
+ @Override
+ protected void doTearDown() throws CoreException {
+ if (restService != null) {
+ restService.stop();
+ }
+ }
+
+ @Test
+ public void testHeartbeat() throws Exception {
+ // Test the heartbeat endpoint to ensure service is running
+ String response = sendRestRequest("heartbeat", new Message.HeartbeatMessage());
+
+ assertNotNull("Response should not be null", response);
+ JsonObject responseObj = gson.fromJson(response, JsonObject.class);
+ assertEquals("alive", responseObj.get("status").getAsString());
+ assertEquals("eclipse", responseObj.get("ide").getAsString());
+ }
+
+ @Test
+ public void testWriteFileEndpoint() throws Exception {
+ // Test writing a new file
+ String fileName = testProject.getLocation().append("test-file.txt").toString();
+ String content = "Hello, World!\nThis is a test file.";
+
+ Message.WriteFileMessage writeMsg = new Message.WriteFileMessage(fileName, "Test Write", content);
+ String response = sendRestRequest("write", writeMsg);
+
+ assertNotNull("Response should not be null", response);
+ JsonObject responseObj = gson.fromJson(response, JsonObject.class);
+
+ if (responseObj.has("error")) {
+ // In headless mode, workbench is not available
+ String error = responseObj.get("error").getAsString();
+ assertTrue("Expected workbench error in headless mode",
+ error.contains("Workbench") || error.contains("not been created"));
+ return; // Skip file verification in headless mode
+ }
+
+ assertEquals("ok", responseObj.get("status").getAsString());
+
+ // Verify the file was actually created
+ IFile file = testProject.getFile("test-file.txt");
+ assertTrue("File should exist after write", file.exists());
+
+ // Verify file contents
+ try (java.io.InputStream is = file.getContents()) {
+ String actualContent = new String(is.readAllBytes(), "UTF-8");
+ assertEquals("File content should match", content, actualContent);
+ }
+ }
+
+ @Test
+ public void testWriteFileInSubdirectory() throws Exception {
+ // Test writing a file in a subdirectory (should create parent folders)
+ String fileName = testProject.getLocation().append("src/main/java/Test.java").toString();
+ String content = "public class Test {\n // Generated file\n}";
+
+ Message.WriteFileMessage writeMsg = new Message.WriteFileMessage(fileName, "Test Write", content);
+ String response = sendRestRequest("write", writeMsg);
+
+ assertNotNull("Response should not be null", response);
+ JsonObject responseObj = gson.fromJson(response, JsonObject.class);
+
+ if (responseObj.has("error")) {
+ // In headless mode, workbench is not available
+ String error = responseObj.get("error").getAsString();
+ assertTrue("Expected workbench error in headless mode",
+ error.contains("Workbench") || error.contains("not been created"));
+ return; // Skip file verification in headless mode
+ }
+
+ assertEquals("ok", responseObj.get("status").getAsString());
+
+ // Verify the file and parent directories were created
+ IFolder srcFolder = testProject.getFolder("src");
+ assertTrue("src folder should exist", srcFolder.exists());
+
+ IFolder mainFolder = srcFolder.getFolder("main");
+ assertTrue("main folder should exist", mainFolder.exists());
+
+ IFolder javaFolder = mainFolder.getFolder("java");
+ assertTrue("java folder should exist", javaFolder.exists());
+
+ IFile file = javaFolder.getFile("Test.java");
+ assertTrue("File should exist after write", file.exists());
+
+ // Verify file contents
+ try (java.io.InputStream is = file.getContents()) {
+ String actualContent = new String(is.readAllBytes(), "UTF-8");
+ assertEquals("File content should match", content, actualContent);
+ }
+ }
+
+ @Test
+ public void testWriteBase64Endpoint() throws Exception {
+ // Test writing a binary file using base64 encoding
+ byte[] binaryData = "This is binary data\u0000\u0001\u0002".getBytes("UTF-8");
+ String base64Content = Base64.getEncoder().encodeToString(binaryData);
+ String fileName = testProject.getLocation().append("binary-file.dat").toString();
+
+ Message.WriteFileMessage writeMsg = new Message.WriteFileMessage(fileName, "Test Base64 Write", base64Content);
+ String response = sendRestRequest("writeBase64", writeMsg);
+
+ assertNotNull("Response should not be null", response);
+ JsonObject responseObj = gson.fromJson(response, JsonObject.class);
+
+ if (responseObj.has("error")) {
+ // In headless mode, workbench is not available
+ String error = responseObj.get("error").getAsString();
+ assertTrue("Expected workbench error in headless mode",
+ error.contains("Workbench") || error.contains("not been created"));
+ return; // Skip file verification in headless mode
+ }
+
+ assertEquals("ok", responseObj.get("status").getAsString());
+
+ // Verify the file was created with correct binary content
+ IFile file = testProject.getFile("binary-file.dat");
+ assertTrue("Binary file should exist after write", file.exists());
+
+ // Verify file contents
+ try (java.io.InputStream is = file.getContents()) {
+ byte[] actualContent = is.readAllBytes();
+ assertArrayEquals("Binary file content should match", binaryData, actualContent);
+ }
+ }
+
+ @Test
+ public void testDeleteEndpoint() throws Exception {
+ // First create a file
+ IFile file = testProject.getFile("to-delete.txt");
+ file.create(new java.io.ByteArrayInputStream("Delete me".getBytes()), true, null);
+ assertTrue("File should exist before delete", file.exists());
+
+ // Test deleting the file
+ String fileName = file.getLocation().toString();
+ Message.DeleteMessage deleteMsg = new Message.DeleteMessage(fileName);
+ String response = sendRestRequest("delete", deleteMsg);
+
+ assertNotNull("Response should not be null", response);
+ JsonObject responseObj = gson.fromJson(response, JsonObject.class);
+
+ if (responseObj.has("error")) {
+ // In headless mode, workbench is not available
+ String error = responseObj.get("error").getAsString();
+ assertTrue("Expected workbench error in headless mode",
+ error.contains("Workbench") || error.contains("not been created"));
+ return; // Skip file verification in headless mode
+ }
+
+ assertEquals("ok", responseObj.get("status").getAsString());
+
+ // Verify the file was deleted
+ assertFalse("File should not exist after delete", file.exists());
+ }
+
+ @Test
+ public void testRefreshEndpoint() throws Exception {
+ // Create a file outside of Eclipse's knowledge
+ java.io.File externalFile = new java.io.File(testProject.getLocation().toFile(), "external-file.txt");
+ try (java.io.FileWriter writer = new java.io.FileWriter(externalFile)) {
+ writer.write("Created externally");
+ }
+
+ // The file should not be visible to Eclipse initially
+ IFile eclipseFile = testProject.getFile("external-file.txt");
+ assertFalse("File should not be visible before refresh", eclipseFile.exists());
+
+ // Test refresh endpoint
+ String response = sendRestRequest("refresh", new Message.RefreshMessage());
+
+ assertNotNull("Response should not be null", response);
+ JsonObject responseObj = gson.fromJson(response, JsonObject.class);
+
+ if (responseObj.has("error")) {
+ // In headless mode, workbench is not available
+ String error = responseObj.get("error").getAsString();
+ assertTrue("Expected workbench error in headless mode",
+ error.contains("Workbench") || error.contains("not been created"));
+ // Clean up
+ externalFile.delete();
+ return; // Skip file verification in headless mode
+ }
+
+ assertEquals("ok", responseObj.get("status").getAsString());
+
+ // After refresh, the file should be visible to Eclipse
+ assertTrue("File should be visible after refresh", eclipseFile.exists());
+
+ // Clean up
+ externalFile.delete();
+ }
+
+ @Test
+ public void testShowInIdeEndpoint() throws Exception {
+ // Create a test file with multiple lines
+ String content = "Line 1\nLine 2\nLine 3\nTarget line\nLine 5";
+ IFile file = testProject.getFile("show-in-ide.txt");
+ file.create(new java.io.ByteArrayInputStream(content.getBytes()), true, null);
+
+ // Test opening the file at a specific line
+ String fileName = file.getLocation().toString();
+ Message.ShowInIdeMessage showMsg = new Message.ShowInIdeMessage(fileName, 4, 0); // Target line
+
+ String response = sendRestRequest("showInIde", showMsg);
+
+ assertNotNull("Response should not be null", response);
+ JsonObject responseObj = gson.fromJson(response, JsonObject.class);
+
+ if (responseObj.has("error")) {
+ // In headless mode, workbench is not available
+ String error = responseObj.get("error").getAsString();
+ assertTrue("Expected workbench error in headless mode",
+ error.contains("Workbench") || error.contains("not been created"));
+ return; // Skip verification in headless mode
+ }
+
+ assertEquals("ok", responseObj.get("status").getAsString());
+
+ // Note: We can't easily verify that the editor actually opened to the correct
+ // line
+ // in a headless test environment, but we can verify the endpoint responds
+ // correctly
+ }
+
+ @Test
+ public void testWriteUpdateExistingFile() throws Exception {
+ // Create an initial file
+ String fileName = testProject.getLocation().append("update-test.txt").toString();
+ String initialContent = "Initial content";
+
+ Message.WriteFileMessage writeMsg1 = new Message.WriteFileMessage(fileName, "Initial Write", initialContent);
+ sendRestRequest("write", writeMsg1);
+
+ // Verify initial file
+ IFile file = testProject.getFile("update-test.txt");
+ assertTrue("File should exist after initial write", file.exists());
+
+ // Update the file with new content
+ String updatedContent = "Updated content\nWith new line";
+ Message.WriteFileMessage writeMsg2 = new Message.WriteFileMessage(fileName, "Update Write", updatedContent);
+ String response = sendRestRequest("write", writeMsg2);
+
+ assertNotNull("Response should not be null", response);
+ JsonObject responseObj = gson.fromJson(response, JsonObject.class);
+
+ if (responseObj.has("error")) {
+ // In headless mode, workbench is not available
+ String error = responseObj.get("error").getAsString();
+ assertTrue("Expected workbench error in headless mode",
+ error.contains("Workbench") || error.contains("not been created"));
+ return; // Skip file verification in headless mode
+ }
+
+ assertEquals("ok", responseObj.get("status").getAsString());
+
+ // Verify the file was updated
+ try (java.io.InputStream is = file.getContents()) {
+ String actualContent = new String(is.readAllBytes(), "UTF-8");
+ assertEquals("File content should be updated", updatedContent, actualContent);
+ }
+ }
+
+ @Test
+ public void testErrorHandling() throws Exception {
+ // Test writing to an invalid path (outside project)
+ String invalidFileName = "/invalid/path/outside/project.txt";
+ String content = "This should fail";
+
+ Message.WriteFileMessage writeMsg = new Message.WriteFileMessage(invalidFileName, "Invalid Write", content);
+ String response = sendRestRequest("write", writeMsg);
+
+ assertNotNull("Response should not be null", response);
+ JsonObject responseObj = gson.fromJson(response, JsonObject.class);
+ assertTrue("Response should contain error", responseObj.has("error"));
+ assertNotNull("Error message should not be null", responseObj.get("error").getAsString());
+ }
+
+ /**
+ * Helper method to send REST requests to the service.
+ */
+ private String sendRestRequest(String command, Object data) throws IOException, InterruptedException {
+ Message.CopilotRestRequest request = new Message.CopilotRestRequest(command,
+ testProject.getLocation().toString(), data);
+
+ String requestBody = gson.toJson(request);
+
+ HttpRequest httpRequest = HttpRequest.newBuilder().uri(URI.create(baseEndpoint))
+ .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ HttpResponse httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
+
+ assertEquals("HTTP status should be 200", 200, httpResponse.statusCode());
+ return httpResponse.body();
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotUndoManagerTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotUndoManagerTest.java
new file mode 100644
index 0000000..0547660
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotUndoManagerTest.java
@@ -0,0 +1,240 @@
+package com.vaadin.plugin.test;
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.junit.Test;
+
+import com.vaadin.plugin.CopilotUndoManager;
+
+/**
+ * Tests for CopilotUndoManager functionality.
+ */
+public class CopilotUndoManagerTest extends BaseIntegrationTest {
+
+ private CopilotUndoManager undoManager;
+
+ @Override
+ protected void doSetUp() throws CoreException {
+ undoManager = CopilotUndoManager.getInstance();
+ }
+
+ @Test
+ public void testSingletonInstance() {
+ CopilotUndoManager instance1 = CopilotUndoManager.getInstance();
+ CopilotUndoManager instance2 = CopilotUndoManager.getInstance();
+
+ assertSame("Should return same singleton instance", instance1, instance2);
+ }
+
+ @Test
+ public void testRecordAndUndoOperation() throws Exception {
+ // Create a file with initial content
+ IFile file = testProject.getFile("undo-test.txt");
+ String originalContent = "Original content";
+ String newContent = "Modified content";
+
+ file.create(new java.io.ByteArrayInputStream(originalContent.getBytes("UTF-8")), true, null);
+
+ // Record an operation
+ undoManager.recordOperation(file, originalContent, newContent, "Test modification");
+
+ // Apply the new content
+ file.setContents(new java.io.ByteArrayInputStream(newContent.getBytes("UTF-8")), true, true, null);
+
+ // Verify new content
+ String currentContent = readFileContent(file);
+ assertEquals("File should have new content", newContent, currentContent);
+
+ // Perform undo
+ List filePaths = Arrays.asList(file.getLocation().toString());
+ boolean undone = undoManager.performUndo(filePaths);
+
+ assertTrue("Undo should be performed", undone);
+
+ // Verify content is restored
+ currentContent = readFileContent(file);
+ assertEquals("File should have original content after undo", originalContent, currentContent);
+ }
+
+ @Test
+ public void testRecordAndRedoOperation() throws Exception {
+ // Create a file
+ IFile file = testProject.getFile("redo-test.txt");
+ String originalContent = "Original";
+ String modifiedContent = "Modified";
+
+ file.create(new java.io.ByteArrayInputStream(originalContent.getBytes("UTF-8")), true, null);
+
+ // Record and apply operation
+ undoManager.recordOperation(file, originalContent, modifiedContent, "Modify");
+ file.setContents(new java.io.ByteArrayInputStream(modifiedContent.getBytes("UTF-8")), true, true, null);
+
+ // Undo
+ List filePaths = Arrays.asList(file.getLocation().toString());
+ undoManager.performUndo(filePaths);
+
+ // Verify undone
+ assertEquals("Should be back to original", originalContent, readFileContent(file));
+
+ // Redo
+ boolean redone = undoManager.performRedo(filePaths);
+ assertTrue("Redo should be performed", redone);
+
+ // Verify redone
+ assertEquals("Should be back to modified", modifiedContent, readFileContent(file));
+ }
+
+ @Test
+ public void testMultipleOperations() throws Exception {
+ // Create a file
+ IFile file = testProject.getFile("multi-op-test.txt");
+ String content1 = "Version 1";
+ String content2 = "Version 2";
+ String content3 = "Version 3";
+
+ file.create(new java.io.ByteArrayInputStream(content1.getBytes("UTF-8")), true, null);
+
+ // Record first operation
+ undoManager.recordOperation(file, content1, content2, "First edit");
+ file.setContents(new java.io.ByteArrayInputStream(content2.getBytes("UTF-8")), true, true, null);
+
+ // Record second operation
+ undoManager.recordOperation(file, content2, content3, "Second edit");
+ file.setContents(new java.io.ByteArrayInputStream(content3.getBytes("UTF-8")), true, true, null);
+
+ assertEquals("Should have version 3", content3, readFileContent(file));
+
+ // Undo twice
+ List filePaths = Arrays.asList(file.getLocation().toString());
+ undoManager.performUndo(filePaths);
+ assertEquals("Should have version 2 after first undo", content2, readFileContent(file));
+
+ undoManager.performUndo(filePaths);
+ assertEquals("Should have version 1 after second undo", content1, readFileContent(file));
+
+ // Redo once
+ undoManager.performRedo(filePaths);
+ assertEquals("Should have version 2 after redo", content2, readFileContent(file));
+ }
+
+ @Test
+ public void testUndoNonExistentFile() {
+ // Try to undo for a file that doesn't exist
+ List filePaths = Arrays.asList("/nonexistent/file.txt");
+ boolean result = undoManager.performUndo(filePaths);
+
+ assertFalse("Undo should not be performed for non-existent file", result);
+ }
+
+ @Test
+ public void testRedoNonExistentFile() {
+ // Try to redo for a file that doesn't exist
+ List filePaths = Arrays.asList("/nonexistent/file.txt");
+ boolean result = undoManager.performRedo(filePaths);
+
+ assertFalse("Redo should not be performed for non-existent file", result);
+ }
+
+ @Test
+ public void testUndoWithoutOperations() throws Exception {
+ // Create a file but don't record any operations
+ IFile file = testProject.getFile("no-ops.txt");
+ file.create(new java.io.ByteArrayInputStream("Content".getBytes("UTF-8")), true, null);
+
+ List filePaths = Arrays.asList(file.getLocation().toString());
+ boolean result = undoManager.performUndo(filePaths);
+
+ assertFalse("Undo should not be performed when no operations recorded", result);
+ }
+
+ @Test
+ public void testFileCreationUndo() throws Exception {
+ // Test undoing a file creation (empty old content)
+ IFile file = testProject.getFile("create-undo.txt");
+ String newContent = "Created file";
+
+ // Simulate file creation by recording with empty old content
+ undoManager.recordOperation(file, "", newContent, "Create file");
+
+ // Create the actual file
+ file.create(new java.io.ByteArrayInputStream(newContent.getBytes("UTF-8")), true, null);
+ assertTrue("File should exist", file.exists());
+
+ // Undo should set content to empty (can't delete via content operation)
+ List filePaths = Arrays.asList(file.getLocation().toString());
+ boolean undone = undoManager.performUndo(filePaths);
+
+ assertTrue("Undo should be performed", undone);
+ assertEquals("File content should be empty after undo", "", readFileContent(file));
+ }
+
+ @Test
+ public void testFileDeletionUndo() throws Exception {
+ // Test undoing a file deletion (empty new content)
+ IFile file = testProject.getFile("delete-undo.txt");
+ String originalContent = "File to delete";
+
+ // Create file
+ file.create(new java.io.ByteArrayInputStream(originalContent.getBytes("UTF-8")), true, null);
+
+ // Record deletion (new content is empty)
+ undoManager.recordOperation(file, originalContent, "", "Delete file");
+
+ // Delete the file
+ file.delete(true, null);
+ assertFalse("File should not exist after deletion", file.exists());
+
+ // Try to undo - this will fail because file doesn't exist
+ List filePaths = Arrays.asList(file.getLocation().toString());
+ boolean undone = undoManager.performUndo(filePaths);
+
+ // Note: This will be false because the file doesn't exist
+ // The undo manager needs the file to exist to restore content
+ assertFalse("Undo cannot be performed on deleted file", undone);
+ }
+
+ @Test
+ public void testMultipleFilesUndo() throws Exception {
+ // Create multiple files
+ IFile file1 = testProject.getFile("multi1.txt");
+ IFile file2 = testProject.getFile("multi2.txt");
+
+ String original1 = "Original 1";
+ String original2 = "Original 2";
+ String modified1 = "Modified 1";
+ String modified2 = "Modified 2";
+
+ file1.create(new java.io.ByteArrayInputStream(original1.getBytes("UTF-8")), true, null);
+ file2.create(new java.io.ByteArrayInputStream(original2.getBytes("UTF-8")), true, null);
+
+ // Record operations for both files
+ undoManager.recordOperation(file1, original1, modified1, "Modify file1");
+ undoManager.recordOperation(file2, original2, modified2, "Modify file2");
+
+ // Apply changes
+ file1.setContents(new java.io.ByteArrayInputStream(modified1.getBytes("UTF-8")), true, true, null);
+ file2.setContents(new java.io.ByteArrayInputStream(modified2.getBytes("UTF-8")), true, true, null);
+
+ // Undo both
+ List filePaths = Arrays.asList(file1.getLocation().toString(), file2.getLocation().toString());
+ boolean undone = undoManager.performUndo(filePaths);
+
+ assertTrue("Undo should be performed", undone);
+ assertEquals("File1 should be restored", original1, readFileContent(file1));
+ assertEquals("File2 should be restored", original2, readFileContent(file2));
+ }
+
+ /**
+ * Helper method to read file content as string.
+ */
+ private String readFileContent(IFile file) throws Exception {
+ try (java.io.InputStream is = file.getContents()) {
+ return new String(is.readAllBytes(), "UTF-8");
+ }
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotUtilTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotUtilTest.java
new file mode 100644
index 0000000..2c1e850
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/CopilotUtilTest.java
@@ -0,0 +1,152 @@
+package com.vaadin.plugin.test;
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+import org.junit.Test;
+
+import com.vaadin.plugin.CopilotUtil;
+
+/**
+ * Tests for CopilotUtil functionality including dotfile creation.
+ */
+public class CopilotUtilTest extends BaseIntegrationTest {
+
+ @Test
+ public void testServiceName() {
+ String serviceName = CopilotUtil.getServiceName();
+ assertNotNull("Service name should not be null", serviceName);
+ assertTrue("Service name should start with 'copilot-'", serviceName.startsWith("copilot-"));
+ assertTrue("Service name should have UUID suffix", serviceName.length() > 8);
+
+ // Service name should be consistent across calls
+ String serviceName2 = CopilotUtil.getServiceName();
+ assertEquals("Service name should be consistent", serviceName, serviceName2);
+ }
+
+ @Test
+ public void testGetEndpoint() {
+ int testPort = 8080;
+ String endpoint = CopilotUtil.getEndpoint(testPort);
+
+ assertNotNull("Endpoint should not be null", endpoint);
+ assertTrue("Endpoint should start with http://127.0.0.1", endpoint.startsWith("http://127.0.0.1"));
+ assertTrue("Endpoint should contain port", endpoint.contains(":" + testPort));
+ assertTrue("Endpoint should contain service name", endpoint.contains(CopilotUtil.getServiceName()));
+ assertTrue("Endpoint should contain /vaadin/", endpoint.contains("/vaadin/"));
+ }
+
+ @Test
+ public void testGetSupportedActions() {
+ String actions = CopilotUtil.getSupportedActions();
+
+ assertNotNull("Supported actions should not be null", actions);
+ assertTrue("Should contain write action", actions.contains("write"));
+ assertTrue("Should contain writeBase64 action", actions.contains("writeBase64"));
+ assertTrue("Should contain delete action", actions.contains("delete"));
+ assertTrue("Should contain refresh action", actions.contains("refresh"));
+ assertTrue("Should contain showInIde action", actions.contains("showInIde"));
+ assertTrue("Should contain heartbeat action", actions.contains("heartbeat"));
+ assertTrue("Should contain getVaadinRoutes action", actions.contains("getVaadinRoutes"));
+ assertTrue("Should contain getVaadinVersion action", actions.contains("getVaadinVersion"));
+
+ // Verify comma separation
+ String[] actionArray = actions.split(",");
+ assertTrue("Should have multiple actions", actionArray.length > 5);
+ }
+
+ @Test
+ public void testSaveDotFile() throws IOException {
+ String projectPath = testProject.getLocation().toString();
+ int testPort = 9090;
+
+ // Save dotfile
+ CopilotUtil.saveDotFile(projectPath, testPort);
+
+ // Verify dotfile was created
+ File dotFile = new File(projectPath, ".vaadin/copilot/vaadin-copilot.properties");
+ assertTrue("Dotfile should exist", dotFile.exists());
+ assertTrue("Dotfile should be a file", dotFile.isFile());
+
+ // Verify dotfile contents
+ Properties props = new Properties();
+ try (FileInputStream fis = new FileInputStream(dotFile)) {
+ props.load(fis);
+ }
+
+ // Check required properties
+ String endpoint = props.getProperty("endpoint");
+ assertNotNull("Endpoint property should exist", endpoint);
+ assertTrue("Endpoint should contain correct port", endpoint.contains(":" + testPort));
+ assertTrue("Endpoint should contain service name", endpoint.contains(CopilotUtil.getServiceName()));
+
+ String ide = props.getProperty("ide");
+ assertEquals("IDE should be eclipse", "eclipse", ide);
+
+ String version = props.getProperty("version");
+ assertNotNull("Version should exist", version);
+
+ String supportedActions = props.getProperty("supportedActions");
+ assertNotNull("Supported actions should exist", supportedActions);
+ assertTrue("Supported actions should contain write", supportedActions.contains("write"));
+
+ // Verify parent directories were created
+ File vaadinDir = new File(projectPath, ".vaadin");
+ assertTrue("Vaadin directory should exist", vaadinDir.exists());
+ assertTrue("Vaadin directory should be a directory", vaadinDir.isDirectory());
+
+ File copilotDir = new File(vaadinDir, "copilot");
+ assertTrue("Copilot directory should exist", copilotDir.exists());
+ assertTrue("Copilot directory should be a directory", copilotDir.isDirectory());
+ }
+
+ @Test
+ public void testSaveDotFileOverwrite() throws IOException {
+ String projectPath = testProject.getLocation().toString();
+ int testPort1 = 7070;
+ int testPort2 = 8080;
+
+ // Save dotfile first time
+ CopilotUtil.saveDotFile(projectPath, testPort1);
+
+ File dotFile = new File(projectPath, ".vaadin/copilot/vaadin-copilot.properties");
+ assertTrue("Dotfile should exist after first save", dotFile.exists());
+
+ // Verify first port
+ Properties props1 = new Properties();
+ try (FileInputStream fis = new FileInputStream(dotFile)) {
+ props1.load(fis);
+ }
+ String endpoint1 = props1.getProperty("endpoint");
+ assertTrue("First endpoint should contain first port", endpoint1.contains(":" + testPort1));
+
+ // Save dotfile second time with different port
+ CopilotUtil.saveDotFile(projectPath, testPort2);
+
+ // Verify file was overwritten
+ Properties props2 = new Properties();
+ try (FileInputStream fis = new FileInputStream(dotFile)) {
+ props2.load(fis);
+ }
+ String endpoint2 = props2.getProperty("endpoint");
+ assertTrue("Second endpoint should contain second port", endpoint2.contains(":" + testPort2));
+ assertFalse("Second endpoint should not contain first port", endpoint2.contains(":" + testPort1));
+ }
+
+ @Test
+ public void testSaveDotFileWithInvalidPath() {
+ // Test with path that doesn't exist - should not throw exception
+ String invalidPath = "/this/path/does/not/exist";
+
+ try {
+ CopilotUtil.saveDotFile(invalidPath, 8080);
+ // Should not throw exception, just log error
+ } catch (Exception e) {
+ fail("Should not throw exception for invalid path: " + e.getMessage());
+ }
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/ManualTestRunner.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/ManualTestRunner.java
new file mode 100644
index 0000000..0f669f4
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/ManualTestRunner.java
@@ -0,0 +1,128 @@
+package com.vaadin.plugin.test;
+
+import java.io.File;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+
+import com.vaadin.plugin.CopilotClient;
+import com.vaadin.plugin.CopilotRestService;
+import com.vaadin.plugin.CopilotUtil;
+
+/**
+ * Manual test runner that can be executed in Eclipse to validate the REST API
+ * functionality. This class provides a simple way to test the integration
+ * without requiring complex test infrastructure.
+ *
+ * To run: Right-click in Eclipse -> Run As -> Java Application
+ */
+public class ManualTestRunner {
+
+ public static void main(String[] args) {
+ System.out.println("=== Vaadin Eclipse Plugin Manual Test Runner ===");
+
+ try {
+ // Test 1: CopilotUtil functionality
+ testCopilotUtil();
+
+ // Test 2: REST Service startup and basic functionality
+ testRestService();
+
+ System.out.println("\n=== All tests completed successfully! ===");
+
+ } catch (Exception e) {
+ System.err.println("Test failed: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ private static void testCopilotUtil() {
+ System.out.println("\n--- Testing CopilotUtil ---");
+
+ // Test service name generation
+ String serviceName = CopilotUtil.getServiceName();
+ System.out.println("Service name: " + serviceName);
+ assert serviceName.startsWith("copilot-") : "Service name should start with 'copilot-'";
+
+ // Test endpoint generation
+ String endpoint = CopilotUtil.getEndpoint(8080);
+ System.out.println("Endpoint: " + endpoint);
+ assert endpoint.contains("8080") : "Endpoint should contain port";
+ assert endpoint.contains(serviceName) : "Endpoint should contain service name";
+
+ // Test supported actions
+ String actions = CopilotUtil.getSupportedActions();
+ System.out.println("Supported actions: " + actions);
+ assert actions.contains("write") : "Should support write action";
+ assert actions.contains("delete") : "Should support delete action";
+
+ // Test dotfile creation
+ String tempDir = System.getProperty("java.io.tmpdir");
+ String testProjectPath = tempDir + File.separator + "test-project";
+ new File(testProjectPath).mkdirs();
+
+ CopilotUtil.saveDotFile(testProjectPath, 9090);
+
+ File dotFile = new File(testProjectPath, ".vaadin/copilot/vaadin-copilot.properties");
+ assert dotFile.exists() : "Dotfile should be created";
+ System.out.println("Dotfile created at: " + dotFile.getAbsolutePath());
+
+ System.out.println("✓ CopilotUtil tests passed");
+ }
+
+ private static void testRestService() throws Exception {
+ System.out.println("\n--- Testing REST Service ---");
+
+ CopilotRestService service = new CopilotRestService();
+
+ try {
+ // Start the service
+ service.start();
+ String endpoint = service.getEndpoint();
+ System.out.println("REST service started at: " + endpoint);
+
+ // Create a simple test with the client
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ IProject[] projects = workspace.getRoot().getProjects();
+
+ String projectPath;
+ if (projects.length > 0 && projects[0].getLocation() != null) {
+ projectPath = projects[0].getLocation().toPortableString();
+ System.out.println("Using existing project: " + projects[0].getName());
+ } else {
+ // Create a temporary project for testing
+ String tempDir = System.getProperty("java.io.tmpdir");
+ projectPath = tempDir + File.separator + "rest-test-project";
+ new File(projectPath).mkdirs();
+ System.out.println("Created temporary project at: " + projectPath);
+ }
+
+ // Test with CopilotClient
+ CopilotClient client = new CopilotClient(endpoint, projectPath);
+
+ try {
+ // Test heartbeat
+ var heartbeatResponse = client.heartbeat();
+ System.out.println("Heartbeat status: " + heartbeatResponse.statusCode());
+ assert heartbeatResponse.statusCode() == 200 : "Heartbeat should return 200";
+
+ // Test simple operations that don't require complex Eclipse setup
+ var refreshResponse = client.refresh();
+ System.out.println("Refresh status: " + refreshResponse.statusCode());
+ assert refreshResponse.statusCode() == 200 : "Refresh should return 200";
+
+ System.out.println("✓ REST service tests passed");
+
+ } catch (Exception e) {
+ System.err.println("Client test failed: " + e.getMessage());
+ throw e;
+ }
+
+ } finally {
+ // Clean up
+ service.stop();
+ System.out.println("REST service stopped");
+ }
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/NewVaadinProjectWizardTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/NewVaadinProjectWizardTest.java
new file mode 100644
index 0000000..a02a87f
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/NewVaadinProjectWizardTest.java
@@ -0,0 +1,237 @@
+package com.vaadin.plugin.test;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.plugin.wizards.NewVaadinProjectWizard;
+import com.vaadin.plugin.wizards.VaadinProjectWizardPage;
+
+/**
+ * Test class for NewVaadinProjectWizard. Tests project creation, Maven nature
+ * configuration, and file extraction.
+ */
+public class NewVaadinProjectWizardTest {
+
+ private NewVaadinProjectWizard wizard;
+ private IWorkspaceRoot workspaceRoot;
+ private IProject testProject;
+ private Path tempDir;
+
+ @Before
+ public void setUp() throws Exception {
+ wizard = new NewVaadinProjectWizard();
+ workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
+
+ // Create a temporary directory for testing
+ tempDir = Files.createTempDirectory("vaadin-wizard-test");
+
+ // Initialize wizard with empty selection
+ wizard.init(null, new StructuredSelection());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // Clean up test project if it exists
+ if (testProject != null && testProject.exists()) {
+ testProject.delete(true, true, new NullProgressMonitor());
+ }
+
+ // Clean up temp directory
+ if (tempDir != null) {
+ deleteRecursively(tempDir);
+ }
+ }
+
+ @Test
+ public void testWizardInitialization() {
+ assertNotNull("Wizard should be initialized", wizard);
+
+ // Test wizard pages
+ wizard.addPages();
+ IWizardPage[] pages = wizard.getPages();
+ assertEquals("Should have one wizard page", 1, pages.length);
+ assertTrue("Page should be VaadinProjectWizardPage", pages[0] instanceof VaadinProjectWizardPage);
+
+ // Test wizard properties
+ assertTrue("Wizard should need previous and next buttons", wizard.needsPreviousAndNextButtons());
+ assertNotNull("Wizard should have window title", wizard.getWindowTitle());
+ }
+
+ @Test
+ public void testProjectCreation() throws Exception {
+ // Test project creation (without actual download)
+ String projectName = "test-vaadin-project-" + System.currentTimeMillis();
+
+ IProject project = workspaceRoot.getProject(projectName);
+ assertFalse("Project should not exist before creation", project.exists());
+
+ // Create project programmatically (simulating wizard behavior)
+ project.create(new NullProgressMonitor());
+ project.open(new NullProgressMonitor());
+ testProject = project;
+
+ assertTrue("Project should exist after creation", project.exists());
+ assertTrue("Project should be open", project.isOpen());
+ }
+
+ @Test
+ public void testMavenNatureConfiguration() throws Exception {
+ // Create a test project
+ String projectName = "test-maven-project-" + System.currentTimeMillis();
+ IProject project = workspaceRoot.getProject(projectName);
+ project.create(new NullProgressMonitor());
+ project.open(new NullProgressMonitor());
+ testProject = project;
+
+ // Test that Maven nature can be added
+ IProjectDescription description = project.getDescription();
+ String[] natures = description.getNatureIds();
+
+ // Add Java nature (required for Maven)
+ String[] newNatures = Arrays.copyOf(natures, natures.length + 1);
+ newNatures[natures.length] = "org.eclipse.jdt.core.javanature";
+ description.setNatureIds(newNatures);
+ project.setDescription(description, new NullProgressMonitor());
+
+ // Verify Java nature was added
+ assertTrue("Project should have Java nature", project.hasNature("org.eclipse.jdt.core.javanature"));
+ }
+
+ @Test
+ public void testZipExtraction() throws Exception {
+ // Test the zip extraction logic
+ Path testZip = createTestZipFile();
+ Path extractDir = tempDir.resolve("extracted");
+ Files.createDirectories(extractDir);
+
+ // The wizard uses ZipInputStream for extraction
+ // Test that files would be extracted correctly
+ assertTrue("Extract directory should exist", Files.exists(extractDir));
+
+ // Clean up
+ Files.deleteIfExists(testZip);
+ }
+
+ @Test
+ public void testProjectModelIntegration() throws Exception {
+ // Test project name validation logic
+ assertFalse("Empty project name should be invalid", isValidProjectName(""));
+ assertTrue("Valid project name should be accepted", isValidProjectName("valid-project-name"));
+ assertFalse("Project name with spaces should be invalid", isValidProjectName("invalid name with spaces"));
+ assertFalse("Project name starting with number should be invalid",
+ isValidProjectName("123-starts-with-number"));
+ }
+
+ @Test
+ public void testWizardPageCompletion() {
+ wizard.addPages();
+ VaadinProjectWizardPage page = (VaadinProjectWizardPage) wizard.getPages()[0];
+
+ // Test that page is created
+ assertNotNull("Page should be created", page);
+
+ // In a real UI test, we would test page completion logic
+ // For now, just verify the page exists
+ }
+
+ @Test
+ public void testErrorHandling() throws Exception {
+ // Test error handling for invalid project location
+
+ // In real scenario, this would show an error message
+ // Here we just verify an invalid path doesn't exist
+ assertFalse("Invalid path should not exist", Files.exists(Path.of("/invalid/non/existent/path")));
+ }
+
+ @Test
+ public void testProjectStructureCreation() throws Exception {
+ // Test that proper project structure is created
+ String projectName = "test-structure-" + System.currentTimeMillis();
+ Path projectPath = tempDir.resolve(projectName);
+ Files.createDirectories(projectPath);
+
+ // Create expected structure
+ Path srcMain = projectPath.resolve("src/main/java");
+ Path srcResources = projectPath.resolve("src/main/resources");
+ Path srcTest = projectPath.resolve("src/test/java");
+
+ Files.createDirectories(srcMain);
+ Files.createDirectories(srcResources);
+ Files.createDirectories(srcTest);
+
+ // Verify structure
+ assertTrue("src/main/java should exist", Files.exists(srcMain));
+ assertTrue("src/main/resources should exist", Files.exists(srcResources));
+ assertTrue("src/test/java should exist", Files.exists(srcTest));
+
+ // Create pom.xml
+ Path pomFile = projectPath.resolve("pom.xml");
+ Files.writeString(pomFile, "");
+ assertTrue("pom.xml should exist", Files.exists(pomFile));
+ }
+
+ @Test
+ public void testCancelOperation() {
+ wizard.addPages();
+
+ // Test that wizard can be cancelled
+ boolean cancelled = wizard.performCancel();
+ assertTrue("Wizard should handle cancel operation", cancelled);
+ }
+
+ // Helper methods
+
+ private boolean isValidProjectName(String name) {
+ if (name == null || name.trim().isEmpty()) {
+ return false;
+ }
+ if (!Character.isJavaIdentifierStart(name.charAt(0))) {
+ return false;
+ }
+ for (char c : name.toCharArray()) {
+ if (!Character.isJavaIdentifierPart(c) && c != '-' && c != '.') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Path createTestZipFile() throws IOException {
+ Path zipFile = tempDir.resolve("test.zip");
+ // In a real test, we would create an actual zip file
+ // For now, just create an empty file
+ Files.createFile(zipFile);
+ return zipFile;
+ }
+
+ private void deleteRecursively(Path path) throws IOException {
+ if (Files.exists(path)) {
+ if (Files.isDirectory(path)) {
+ Files.walk(path).sorted((a, b) -> b.compareTo(a)).forEach(p -> {
+ try {
+ Files.delete(p);
+ } catch (IOException e) {
+ // Ignore
+ }
+ });
+ } else {
+ Files.delete(path);
+ }
+ }
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/ProjectModelTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/ProjectModelTest.java
new file mode 100644
index 0000000..6b7c0c7
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/ProjectModelTest.java
@@ -0,0 +1,275 @@
+package com.vaadin.plugin.test;
+
+import static org.junit.Assert.*;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.plugin.wizards.ProjectModel;
+
+/**
+ * Test class for ProjectModel. Tests URL generation, parameter handling, and
+ * project configuration.
+ */
+public class ProjectModelTest {
+
+ private ProjectModel model;
+
+ @Before
+ public void setUp() {
+ model = new ProjectModel();
+ }
+
+ @Test
+ public void testDefaultValues() {
+ assertNotNull("Model should be created", model);
+ assertEquals("Default project type should be STARTER", ProjectModel.ProjectType.STARTER,
+ model.getProjectType());
+ assertTrue("Should include Flow by default", model.isIncludeFlow());
+ assertFalse("Should not include Hilla by default", model.isIncludeHilla());
+ assertFalse("Should not be prerelease by default", model.isPrerelease());
+ assertEquals("Default framework should be flow", "flow", model.getFramework());
+ assertEquals("Default language should be java", "java", model.getLanguage());
+ assertEquals("Default build tool should be maven", "maven", model.getBuildTool());
+ assertEquals("Default architecture should be spring-boot", "spring-boot", model.getArchitecture());
+ }
+
+ @Test
+ public void testProjectNameSetting() {
+ String projectName = "my-test-project";
+ model.setProjectName(projectName);
+
+ assertEquals("Project name should be set", projectName, model.getProjectName());
+ }
+
+ @Test
+ public void testLocationSetting() {
+ String location = "/path/to/project";
+ model.setLocation(location);
+
+ assertEquals("Location should be set", location, model.getLocation());
+ }
+
+ @Test
+ public void testStarterProjectUrlGeneration() throws MalformedURLException {
+ model.setProjectType(ProjectModel.ProjectType.STARTER);
+ model.setProjectName("test-project");
+
+ String urlString = model.getDownloadUrl();
+ assertNotNull("Download URL should be generated", urlString);
+
+ // Verify it's a valid URL
+ URL url = new URL(urlString);
+ assertNotNull("Should create valid URL object", url);
+
+ // Check URL contains expected parameters
+ assertTrue("URL should contain project name", urlString.contains("name=test-project"));
+ assertTrue("URL should contain group ID", urlString.contains("groupId=com.example.application"));
+ assertTrue("URL should contain base URL", urlString.contains("start.vaadin.com"));
+ assertTrue("URL should be for skeleton endpoint", urlString.contains("/skeleton?"));
+ }
+
+ @Test
+ public void testHelloWorldProjectUrlGeneration() throws MalformedURLException {
+ model.setProjectType(ProjectModel.ProjectType.HELLO_WORLD);
+ model.setProjectName("hello-world");
+ model.setFramework("hilla");
+ model.setLanguage("kotlin");
+ model.setBuildTool("gradle");
+ model.setArchitecture("quarkus");
+
+ String urlString = model.getDownloadUrl();
+ assertNotNull("Download URL should be generated", urlString);
+
+ // Verify URL contains correct parameters
+ assertTrue("URL should be for helloworld endpoint", urlString.contains("/helloworld?"));
+ assertTrue("URL should contain framework", urlString.contains("framework=hilla"));
+ assertTrue("URL should contain language", urlString.contains("language=kotlin"));
+ assertTrue("URL should contain build tool", urlString.contains("buildTool=gradle"));
+ assertTrue("URL should contain architecture", urlString.contains("architecture=quarkus"));
+ }
+
+ @Test
+ public void testUrlParameterEncoding() throws MalformedURLException {
+ // Test with special characters that need encoding
+ model.setProjectName("test project with spaces");
+
+ String urlString = model.getDownloadUrl();
+
+ // Spaces should be encoded
+ assertTrue("Spaces should be encoded in URL",
+ urlString.contains("test+project+with+spaces") || urlString.contains("test%20project%20with%20spaces"));
+ }
+
+ @Test
+ public void testPrereleaseSetting() {
+ model.setProjectType(ProjectModel.ProjectType.STARTER);
+ model.setPrerelease(true);
+ model.setProjectName("prerelease-test");
+
+ String url = model.getDownloadUrl();
+ assertTrue("URL should contain preset=prerelease parameter", url.contains("preset=prerelease"));
+ }
+
+ @Test
+ public void testFrameworkSelection() {
+ model.setProjectType(ProjectModel.ProjectType.STARTER);
+ model.setProjectName("framework-test");
+
+ // Test Flow only
+ model.setIncludeFlow(true);
+ model.setIncludeHilla(false);
+ String url = model.getDownloadUrl();
+ assertTrue("Should have projectType=flow", url.contains("projectType=flow"));
+
+ // Test Hilla only
+ model.setIncludeFlow(false);
+ model.setIncludeHilla(true);
+ url = model.getDownloadUrl();
+ assertTrue("Should have projectType=hilla", url.contains("projectType=hilla"));
+
+ // Test both (Fusion)
+ model.setIncludeFlow(true);
+ model.setIncludeHilla(true);
+ url = model.getDownloadUrl();
+ assertTrue("Should have projectType=fusion", url.contains("projectType=fusion"));
+ }
+
+ @Test
+ public void testDownloadParameter() {
+ model.setProjectName("download-test");
+
+ String url = model.getDownloadUrl();
+
+ // Should always include download=true
+ assertTrue("Should contain download=true", url.contains("download=true"));
+ }
+
+ @Test
+ public void testUrlFormat() {
+ model.setProjectName("url-test");
+
+ String url = model.getDownloadUrl();
+
+ // Check URL structure
+ assertTrue("Should start with https", url.startsWith("https://"));
+ assertTrue("Should have query parameters", url.contains("?"));
+
+ // Verify parameter separator
+ String queryPart = url.substring(url.indexOf("?") + 1);
+ String[] params = queryPart.split("&");
+ assertTrue("Should have multiple parameters", params.length > 1);
+
+ // Each parameter should have key=value format
+ for (String param : params) {
+ assertTrue("Parameter should have = sign: " + param, param.contains("="));
+ }
+ }
+
+ @Test
+ public void testArtifactIdGeneration() {
+ // Test that project names are properly converted to artifact IDs
+ model.setProjectName("My Test Project!");
+
+ String url = model.getDownloadUrl();
+
+ // Should convert to lowercase and replace special chars with hyphens
+ assertTrue("Should convert to valid artifact ID", url.contains("artifactId=my-test-project"));
+ }
+
+ @Test
+ public void testNullProjectName() {
+ // Test that null project name doesn't break URL generation
+ model.setProjectName(null);
+
+ String url = model.getDownloadUrl();
+ assertNotNull("Should handle null project name", url);
+ assertTrue("Should still be valid URL format", url.startsWith("https://"));
+ }
+
+ @Test
+ public void testSpecialCharactersInProjectName() {
+ // Test various special characters
+ model.setProjectName("test-project_123.v2@#$");
+
+ String url = model.getDownloadUrl();
+ assertNotNull("Should handle special characters", url);
+
+ // Verify the URL is still valid
+ try {
+ new URL(url);
+ } catch (MalformedURLException e) {
+ fail("Should generate valid URL with special characters: " + e.getMessage());
+ }
+
+ // Check artifact ID is properly sanitized
+ assertTrue("Should sanitize artifact ID", url.contains("artifactId=test-project-123-v2"));
+ }
+
+ @Test
+ public void testLongProjectName() {
+ // Test with a very long project name
+ String longName = "this-is-a-very-long-project-name-that-might-cause-issues-"
+ + "with-url-length-limitations-in-some-systems";
+ model.setProjectName(longName);
+
+ String url = model.getDownloadUrl();
+ assertTrue("Should handle long project names", url.contains("name="));
+ }
+
+ @Test
+ public void testConsistentUrlGeneration() {
+ // Test that URL generation is consistent
+ model.setProjectName("consistent-test");
+ model.setProjectType(ProjectModel.ProjectType.STARTER);
+
+ String url1 = model.getDownloadUrl();
+ String url2 = model.getDownloadUrl();
+
+ assertEquals("URL generation should be consistent", url1, url2);
+ }
+
+ @Test
+ public void testAllStarterParameters() {
+ model.setProjectType(ProjectModel.ProjectType.STARTER);
+ model.setProjectName("full-test");
+ model.setPrerelease(true);
+ model.setIncludeFlow(true);
+ model.setIncludeHilla(true);
+
+ String url = model.getDownloadUrl();
+
+ // Verify all parameters are present
+ assertTrue("Should contain name parameter", url.contains("name="));
+ assertTrue("Should contain groupId parameter", url.contains("groupId="));
+ assertTrue("Should contain artifactId parameter", url.contains("artifactId="));
+ assertTrue("Should contain preset parameter", url.contains("preset="));
+ assertTrue("Should contain projectType parameter", url.contains("projectType="));
+ assertTrue("Should contain download parameter", url.contains("download="));
+ }
+
+ @Test
+ public void testAllHelloWorldParameters() {
+ model.setProjectType(ProjectModel.ProjectType.HELLO_WORLD);
+ model.setProjectName("hello-test");
+ model.setFramework("hilla");
+ model.setLanguage("kotlin");
+ model.setBuildTool("gradle");
+ model.setArchitecture("jakartaee");
+
+ String url = model.getDownloadUrl();
+
+ // Verify all parameters are present
+ assertTrue("Should contain name parameter", url.contains("name="));
+ assertTrue("Should contain groupId parameter", url.contains("groupId="));
+ assertTrue("Should contain artifactId parameter", url.contains("artifactId="));
+ assertTrue("Should contain framework parameter", url.contains("framework="));
+ assertTrue("Should contain language parameter", url.contains("language="));
+ assertTrue("Should contain buildTool parameter", url.contains("buildTool="));
+ assertTrue("Should contain architecture parameter", url.contains("architecture="));
+ assertTrue("Should contain download parameter", url.contains("download="));
+ }
+}
diff --git a/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/VaadinProjectAnalyzerTest.java b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/VaadinProjectAnalyzerTest.java
new file mode 100644
index 0000000..d815cc2
--- /dev/null
+++ b/vaadin-eclipse-plugin.tests/src/com/vaadin/plugin/test/VaadinProjectAnalyzerTest.java
@@ -0,0 +1,461 @@
+package com.vaadin.plugin.test;
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.junit.Test;
+
+import com.vaadin.plugin.VaadinProjectAnalyzer;
+
+/**
+ * Tests for VaadinProjectAnalyzer functionality.
+ */
+public class VaadinProjectAnalyzerTest extends BaseIntegrationTest {
+
+ private IJavaProject javaProject;
+ private VaadinProjectAnalyzer analyzer;
+
+ @Override
+ protected void doSetUp() throws CoreException {
+ // Add Java nature to test project
+ addJavaNature(testProject);
+ javaProject = JavaCore.create(testProject);
+
+ // Create source folder
+ IFolder srcFolder = testProject.getFolder("src");
+ if (!srcFolder.exists()) {
+ srcFolder.create(true, true, null);
+ }
+
+ // Set proper classpath with only the source folder (replace default entries)
+ IClasspathEntry sourceEntry = JavaCore.newSourceEntry(srcFolder.getFullPath());
+ IClasspathEntry containerEntry = JavaCore.newContainerEntry(
+ org.eclipse.core.runtime.Path.fromPortableString("org.eclipse.jdt.launching.JRE_CONTAINER"));
+ IClasspathEntry[] newEntries = new IClasspathEntry[]{sourceEntry, containerEntry};
+ javaProject.setRawClasspath(newEntries, null);
+
+ analyzer = new VaadinProjectAnalyzer(javaProject);
+ }
+
+ @Test
+ public void testFindVaadinRoutesEmpty() throws CoreException {
+ // Test with no routes
+ List