From 28ac6b3b1ed9f8ff9ee8c9d0f5ce6c1afe1b4944 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Thu, 21 May 2026 19:47:41 +0200 Subject: [PATCH 01/19] Fix relativePath formatting in pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ac85398..4922f8d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent 4.0.6 - + tools.vitruv methodologist-setup-service From 7929bc9f25578be6e0300eb8a8db4d760f5fb59b Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:31:33 +0200 Subject: [PATCH 02/19] Fix relativePath formatting in pom.xml --- .mvn/wrapper/maven-wrapper.properties | 3 + pom.xml | 4 + .../setup/config/CustomClassLoader.java | 26 ++ .../setup/config/MetamodelLocation.java | 11 + .../setup/config/VitruvConfiguration.java | 150 ++++++++ .../setup/emf/EMFModelInitializer.java | 178 ++++++++++ .../exception/MissingModelException.java | 33 ++ .../methodologist/setup/util/FileUtils.java | 167 +++++++++ .../setup/vsum/controller/VsumController.java | 89 +++++ .../vsum/service/GenerateFromTemplate.java | 327 ++++++++++++++++++ .../vsum/service/VsumProjectBuildService.java | 320 +++++++++++++++++ .../setup/vsum/service/VsumService.java | 304 ++++++++++++++++ .../resources/templates/consistencyPom.ftl | 143 ++++++++ .../templates/depencyReducedEmfUtilPom.ftl | 43 +++ src/main/resources/templates/emfutilsPom.ftl | 67 ++++ src/main/resources/templates/generator.ftl | 18 + src/main/resources/templates/javautilsPom.ftl | 66 ++++ .../resources/templates/modelComponent.ftl | 6 + src/main/resources/templates/modelPom.ftl | 51 +++ .../resources/templates/p2wrappersPom.ftl | 68 ++++ src/main/resources/templates/plugin.ftl | 13 + .../resources/templates/pluginExtension.ftl | 7 + src/main/resources/templates/project.ftl | 29 ++ src/main/resources/templates/rootPom.ftl | 100 ++++++ src/main/resources/templates/viewTypes.ftl | 1 + src/main/resources/templates/vsumExample.ftl | 71 ++++ src/main/resources/templates/vsumPom.ftl | 109 ++++++ src/main/resources/templates/vsumTest.ftl | 68 ++++ .../resources/templates/xannotationsPom.ftl | 59 ++++ .../vsum/controller/VsumControllerTest.java | 104 ++++++ .../service/GenerateFromTemplateTest.java | 26 ++ 31 files changed, 2661 insertions(+) create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 src/main/java/tools/vitruv/methodologist/setup/config/CustomClassLoader.java create mode 100644 src/main/java/tools/vitruv/methodologist/setup/config/MetamodelLocation.java create mode 100644 src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java create mode 100644 src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java create mode 100644 src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java create mode 100644 src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java create mode 100644 src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java create mode 100644 src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java create mode 100644 src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java create mode 100644 src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java create mode 100644 src/main/resources/templates/consistencyPom.ftl create mode 100644 src/main/resources/templates/depencyReducedEmfUtilPom.ftl create mode 100644 src/main/resources/templates/emfutilsPom.ftl create mode 100644 src/main/resources/templates/generator.ftl create mode 100644 src/main/resources/templates/javautilsPom.ftl create mode 100644 src/main/resources/templates/modelComponent.ftl create mode 100644 src/main/resources/templates/modelPom.ftl create mode 100644 src/main/resources/templates/p2wrappersPom.ftl create mode 100644 src/main/resources/templates/plugin.ftl create mode 100644 src/main/resources/templates/pluginExtension.ftl create mode 100644 src/main/resources/templates/project.ftl create mode 100644 src/main/resources/templates/rootPom.ftl create mode 100644 src/main/resources/templates/viewTypes.ftl create mode 100644 src/main/resources/templates/vsumExample.ftl create mode 100644 src/main/resources/templates/vsumPom.ftl create mode 100644 src/main/resources/templates/vsumTest.ftl create mode 100644 src/main/resources/templates/xannotationsPom.ftl create mode 100644 src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java create mode 100644 src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/pom.xml b/pom.xml index 4922f8d..2beb5b0 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,10 @@ 21 + + org.freemarker + freemarker + org.springframework.boot spring-boot-starter-actuator diff --git a/src/main/java/tools/vitruv/methodologist/setup/config/CustomClassLoader.java b/src/main/java/tools/vitruv/methodologist/setup/config/CustomClassLoader.java new file mode 100644 index 0000000..5bd3253 --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/config/CustomClassLoader.java @@ -0,0 +1,26 @@ +package tools.vitruv.methodologist.setup.config; + +import java.net.URL; +import java.net.URLClassLoader; + +/** The CustomClassLoader class is used to load classes from a custom classpath. */ +public class CustomClassLoader extends URLClassLoader { + /** + * The constructor of the CustomClassLoader class. + * + * @param urls The URLs of the classpath. + * @param parent The parent class loader. + */ + public CustomClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + + /** + * Adds a JAR file to the classpath. + * + * @param url The URL of the JAR file. + */ + public void addJar(URL url) { + this.addURL(url); + } +} diff --git a/src/main/java/tools/vitruv/methodologist/setup/config/MetamodelLocation.java b/src/main/java/tools/vitruv/methodologist/setup/config/MetamodelLocation.java new file mode 100644 index 0000000..67793d3 --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/config/MetamodelLocation.java @@ -0,0 +1,11 @@ +package tools.vitruv.methodologist.setup.config; + +import java.io.File; + +/** + * The MetamodelLocation class is used to store the location of a metamodel and + * its corresponding + * genmodel. + */ +public record MetamodelLocation(File metamodel, File genmodel, String genmodelUri, String modelDirectory) { +} diff --git a/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java new file mode 100644 index 0000000..c75a410 --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java @@ -0,0 +1,150 @@ +package tools.vitruv.methodologist.setup.config; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.emf.codegen.ecore.genmodel.GenModel; +import org.eclipse.emf.codegen.ecore.genmodel.GenModelPackage; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl; + +/** The VitruvConfiguration class is used to store the configuration of the Vitruv CLI. */ +@Slf4j +public class VitruvConfiguration { + + /** + * -- SETTER -- Sets the local path of the configuration. + * + *

-- GETTER -- Returns the local path of the configuration. + */ + @Getter @Setter private Path localPath; + + /** -- GETTER -- Returns the package name. */ + @Getter private String packageName; + + /** + * -- SETTER -- Sets the workflow of the configuration. + * + *

-- GETTER -- Returns the workflow of the configuration. + */ + @Getter @Setter private File workflow; + + /** -- GETTER -- Returns the model names. */ + @Getter private final List modelNames = new ArrayList<>(); + + private final List metamodelLocations = new ArrayList<>(); + + /** -- GETTER -- Returns the reaction file locations used by the CLI. */ + @Getter private final List reactionLocations = new ArrayList<>(); + + /** + * Adds a metamodel location to the configuration. + * + * @param metamodelLocation The metamodel location to add. + */ + public void addMetamodelLocations(MetamodelLocation metamodelLocation) { + this.metamodelLocations.add(metamodelLocation); + } + + /** + * Sets the metamodel locations using a semicolon-separated list of {@code ecore,genmodel} pairs. + * + * @param paths The metamodel argument string. + */ + public void setMetaModelLocations(String paths) { + // Register the GenModel resource factory + String nsUri = ""; + Resource.Factory.Registry reg = Resource.Factory.Registry.INSTANCE; + reg.getExtensionToFactoryMap().put("ecore", new XMIResourceFactoryImpl()); + reg.getExtensionToFactoryMap().put("genmodel", new XMIResourceFactoryImpl()); + + // Register the GenModel package + GenModelPackage.eINSTANCE.eClass(); + + for (String modelPaths : paths.split(";")) { + String metamodelPath = modelPaths.split(",")[0]; + String genmodelPath = modelPaths.split(",")[1]; + + File metamodel = new File(metamodelPath); + File genmodel = new File(genmodelPath); + + String localModelDirectory = ""; + + // getting the URI from the genmodels + ResourceSet resourceSet = new ResourceSetImpl(); + URI uri = URI.createFileURI(metamodel.getAbsolutePath().trim()); + Resource resource = resourceSet.getResource(uri, true); + if (!resource.getContents().isEmpty() + && resource.getContents().get(0) instanceof EPackage ePackage) { + // Load the GenModel to get the modelPluginID + URI genmodelURI = URI.createFileURI(genmodel.getAbsolutePath()); + nsUri = genmodelURI.toString(); + Resource genmodelResource = resourceSet.getResource(genmodelURI, true); + modelNames.add(ePackage.getName()); + if (!genmodelResource.getContents().isEmpty() + && genmodelResource.getContents().get(0) instanceof GenModel genModel) { + String packageString = removeLastSegment(genModel.getModelPluginID()); + log.info("--------------------->>>> " + packageString); + this.setPackageName(packageString); + localModelDirectory = genModel.getModelDirectory(); + } + } + + this.addMetamodelLocations( + new MetamodelLocation(metamodel, genmodel, nsUri, localModelDirectory)); + } + } + + /** + * Sets the reaction file locations used by the CLI. + * + * @param reactionLocations list of paths to reaction files. + */ + public void setReactionLocations(List reactionLocations) { + this.reactionLocations.clear(); + if (reactionLocations != null) { + this.reactionLocations.addAll(reactionLocations); + } + } + + /** + * Removes the last segment of a string. + * + * @param input The input string. + * @return The input string without the last segment. + */ + public static String removeLastSegment(String input) { + int lastDotIndex = input.lastIndexOf('.'); + if (lastDotIndex == -1) { + // No dot found, return the original string + return input; + } + return input.substring(0, lastDotIndex); + } + + /** + * Returns the metamodel locations. + * + * @return The metamodel locations. + */ + public List getMetaModelLocations() { + return this.metamodelLocations; + } + + /** + * Sets the package name. + * + * @param packageName The package name. + */ + public void setPackageName(String packageName) { + this.packageName = packageName.replace("\\s", ""); + } +} diff --git a/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java b/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java new file mode 100644 index 0000000..dbb7da8 --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java @@ -0,0 +1,178 @@ +package tools.vitruv.methodologist.setup.emf; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.emf.ecore.EPackage; + +/** + * Utility class for initializing EMF packages before Xtext validation. + * + *

This class loads metamodel classes from a generated model JAR and registers them with EMF's + * package registry to ensure Xtext can resolve nsURIs during validation. + */ +public class EMFModelInitializer { + private static final Logger logger = LogManager.getLogger(EMFModelInitializer.class); + + /** + * Initializes EMF packages from a model JAR file. + * + * @param modelJarPath path to the generated model JAR + * @return map of nsURI to EPackage for all registered packages + */ + public static Map initializeFromJar(String modelJarPath) { + Map packages = new HashMap<>(); + + File jarFile = new File(modelJarPath); + if (!jarFile.exists()) { + logger.warn("Model JAR not found: {}", modelJarPath); + return packages; + } + + try (JarFile jar = new JarFile(jarFile)) { + Collection entries = jar.stream().toList(); + + // Look for all ecore files + entries.stream() + .filter(entry -> entry.getName().endsWith(".ecore")) + .forEach( + entry -> { + try { + loadEcorePackage(jar.getInputStream(entry), packages); + } catch (Exception e) { + logger.warn("Failed to load ecore from {}: {}", entry.getName(), e.getMessage()); + } + }); + + // Look for FactoryImpl classes that indicate metamodel packages + entries.stream() + .filter(entry -> entry.getName().endsWith("FactoryImpl.class")) + .map(entry -> entry.getName().replace('/', '.').replace(".class", "")) + .forEach( + className -> { + try { + loadMetamodelPackage(className, packages); + } catch (Exception e) { + logger.debug("Could not load package from {}: {}", className, e.getMessage()); + } + }); + + } catch (Exception e) { + logger.error("Error initializing EMF packages from JAR: {}", modelJarPath, e); + } + + logger.info("Initialized {} EMF packages from model JAR", packages.size()); + return packages; + } + + /** + * Initializes EMF packages from a classes directory. + * + * @param classesPath path to the generated classes directory + * @param classLoader classloader to use for loading classes + * @return map of nsURI to EPackage for all registered packages + */ + public static Map initializeFromClasses( + String classesPath, ClassLoader classLoader) { + Map packages = new HashMap<>(); + + try { + Path classesDir = Path.of(classesPath); + if (!Files.exists(classesDir)) { + logger.warn("Classes directory not found: {}", classesPath); + return packages; + } + + // Find all FactoryImpl classes + Files.walk(classesDir) + .filter(p -> Files.isRegularFile(p)) + .filter(p -> p.toString().endsWith("FactoryImpl.class")) + .forEach( + classPath -> { + try { + String className = + classesDir + .relativize(classPath) + .toString() + .replace(File.separator, ".") + .replace(".class", ""); + loadMetamodelPackage(className, packages); + } catch (Exception e) { + logger.debug("Could not load package from {}: {}", classPath, e.getMessage()); + } + }); + + } catch (Exception e) { + logger.error("Error initializing EMF packages from classes directory: {}", classesPath, e); + } + + logger.info("Initialized {} EMF packages from classes directory", packages.size()); + return packages; + } + + /** + * Registers an EPackage with EMF's package registry. + * + * @param nsUri the nsURI for the package + * @param ePackage the EPackage instance + */ + public static void registerPackage(String nsUri, EPackage ePackage) { + if (nsUri != null && ePackage != null) { + EPackage.Registry.INSTANCE.put(nsUri, ePackage); + logger.debug("Registered EPackage: {} -> {}", nsUri, ePackage.getName()); + } + } + + /** + * Loads an ecore model and registers its packages. + * + * @param ecoreStream input stream containing an EMF ecore file + * @param packages map to collect registered packages + */ + private static void loadEcorePackage( + java.io.InputStream ecoreStream, Map packages) { + // This would load and parse the ecore file + // For now, this is a placeholder that could be extended with actual implementation + } + + /** + * Loads a metamodel package class and registers it. + * + * @param className fully qualified name of a FactoryImpl class + * @param packages map to collect registered packages + */ + private static void loadMetamodelPackage(String className, Map packages) { + try { + // Load the FactoryImpl class + Class factoryClass = Class.forName(className); + + // Get the eINSTANCE field + java.lang.reflect.Field instanceField = factoryClass.getField("eINSTANCE"); + Object factoryInstance = instanceField.get(null); + + // Get the eClass field from the package + String packageClassName = + className.substring(0, className.lastIndexOf("FactoryImpl")) + "Package"; + Class packageClass = Class.forName(packageClassName); + java.lang.reflect.Field eINSTANCE = packageClass.getField("eINSTANCE"); + EPackage ePackage = (EPackage) eINSTANCE.get(null); + + if (ePackage != null) { + String nsUri = ePackage.getNsURI(); + packages.put(nsUri, ePackage); + registerPackage(nsUri, ePackage); + } + } catch (ClassNotFoundException e) { + logger.debug("Metamodel class not found on this classpath: {}", className); + } catch (Exception e) { + logger.trace("Error loading metamodel package {}: {}", className, e.getMessage()); + } + } +} diff --git a/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java b/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java new file mode 100644 index 0000000..e2be40d --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java @@ -0,0 +1,33 @@ +package tools.vitruv.methodologist.setup.exception; + +/** Exception thrown when a required model is missing. */ +public class MissingModelException extends Exception { + + /** + * Exception thrown when a required model is missing. + * + * @param message + */ + public MissingModelException(String message) { + super(message); + } + + /** + * Exception thrown when a required model is missing. + * + * @param message + * @param cause + */ + public MissingModelException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Exception thrown when a required model is missing. + * + * @param cause + */ + public MissingModelException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java b/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java new file mode 100644 index 0000000..18e40e0 --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java @@ -0,0 +1,167 @@ +package tools.vitruv.methodologist.setup.util; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import lombok.extern.slf4j.Slf4j; +import tools.vitruv.methodologist.setup.config.CustomClassLoader; + +/** The FileUtils class provides utility methods for file operations. */ +@Slf4j +public final class FileUtils { + + private FileUtils() {} + + /** + * The CLASS_LOADER is used to load classes from JAR files at runtime. It is used to load the + * classes of the virtual model builder. + */ + public static final CustomClassLoader CLASS_LOADER = + new CustomClassLoader(new URL[] {}, ClassLoader.getSystemClassLoader()); + + /** + * Copy a file to a new location. + * + * @param filePath The path of the file that should be copied. + * @param folderPath The path of the folder to which the file should be copied. + * @param relativeSubfolder The relative subfolder in which the file should be copied. + * @return The target file. + */ + public static File copyFile(String filePath, Path folderPath, String relativeSubfolder) { + File source; + File target; + if (new File(filePath).isAbsolute()) { + source = Path.of(filePath).toFile(); + } else { + source = Path.of(new File("").getAbsolutePath().trim() + "/" + filePath.trim()).toFile(); + } + if (folderPath.isAbsolute()) { + target = + Path.of(folderPath.toString().trim() + "/" + relativeSubfolder + source.getName().trim()) + .toFile(); + } else { + + target = + Path.of( + new File("").getAbsolutePath().trim() + + "/" + + folderPath.toString().trim() + + "/" + + relativeSubfolder + + source.getName().trim()) + .toFile(); + } + // Files.copy throws a misleading Exception if the target File and/or the + // folders of the target file are not existing. + log.info("Copying file " + source.getAbsolutePath() + " to " + target.getAbsolutePath()); + target.getParentFile().mkdirs(); + try { + target.createNewFile(); + Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + e.printStackTrace(); + } + return target; + } + + /** + * Create a new file in the given path. + * + * @param filePath The path of the file that should be created. + */ + public static void createFile(String filePath) { + File file = new File(filePath); + try { + // Ensure the directory exists + File parentDir = file.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + } + // Create the file + if (file.createNewFile()) { + log.info("File created: " + file.getAbsolutePath()); + } else { + log.info("File already exists: " + file.getAbsolutePath()); + } + } catch (IOException e) { + log.error("An error occurred while creating the file: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Create a new folder in the given path. + * + * @param path The path of the folder that should be created. + * @param folder The name of the folder that should be created. + * @return The created folder. + */ + public static Path createNewFolder(Path path, String folder) { + Path folderPath = path.resolve(folder); + File file = folderPath.toFile(); + if (file.mkdirs()) { + log.info("Directory created: " + file.getAbsolutePath()); + } else { + log.info("Directory already exists: " + file.getAbsolutePath()); + } + return folderPath; + } + + public static String findOption(File file, String option) { + try { + for (String line : Files.readAllLines(file.toPath())) { + if (line.startsWith(option)) { + return line.substring(option.length()).trim(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + throw new IllegalArgumentException("Option: " + option + "not found in given file!"); + } + + /** + * Adding Jar to a class path. + * + * @param jarPath The path of the JAR file that should be added to the class path. + */ + public static void addJarToClassPath(String jarPath) { + try { + URL jarUrl = new URL("file:///" + jarPath); + CLASS_LOADER.addJar(jarUrl); + } catch (Exception e) { + e.printStackTrace(); + } + + try { + // Open the JAR file + JarFile jarFile = new JarFile(new File(jarPath)); + + // Get the entries in the JAR file + Enumeration entries = jarFile.entries(); + + // Iterate through the entries + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + + // Check if the entry is a class file + if (entry.getName().endsWith(".class")) { + // Print the class name + String className = entry.getName().replace("/", ".").replace(".class", ""); + log.info(className); + } + } + + // Close the JAR file + jarFile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java new file mode 100644 index 0000000..a52125e --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java @@ -0,0 +1,89 @@ +package tools.vitruv.methodologist.setup.vsum.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import tools.vitruv.methodologist.setup.vsum.service.VsumProjectBuildService; + +/** REST API for VSUM project generation and packaging. */ +@RestController +@RequestMapping("/api/vsum") +@RequiredArgsConstructor +@Tag(name = "VSUM", description = "VSUM project generation endpoints") +public class VsumController { + + private final VsumProjectBuildService vsumProjectBuildService; + + /** + * Accepts model and reaction files, builds a VSUM project, and returns the built project zip. + * + * @param metamodelFiles metamodel files + * @param genmodelFiles genmodel files paired by index with metamodel files + * @param reactionFiles optional reaction files + * @return zip file response containing generated project + */ + @GetMapping(value = "/build", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "Build VSUM project", + description = + "Uploads metamodel/genmodel files, builds the project from templates, and returns a zip archive", + responses = { + @ApiResponse( + responseCode = "200", + description = "Project built successfully", + content = @Content(mediaType = "application/zip")), + @ApiResponse(responseCode = "400", description = "Invalid input files"), + @ApiResponse(responseCode = "500", description = "Build failed") + }) + public ResponseEntity buildProject( + @Parameter(description = "Metamodel files", required = true) + @RequestPart("metamodelFiles") + List metamodelFiles, + @Parameter(description = "Genmodel files paired by index with metamodel files", required = true) + @RequestPart("genmodelFiles") + List genmodelFiles, + @Parameter(description = "Optional reaction files") + @RequestPart(value = "reactionFiles", required = false) + List reactionFiles) { + + byte[] archive = + vsumProjectBuildService.buildProjectArchive(metamodelFiles, genmodelFiles, reactionFiles); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/zip")); + headers.setContentDisposition( + ContentDisposition.attachment() + .filename(generateArchiveFilename(), StandardCharsets.UTF_8) + .build()); + headers.setContentLength(archive.length); + + return ResponseEntity.ok().headers(headers).body(archive); + } + + /** + * Creates a deterministic archive filename prefix for build downloads. + * + * @return zip filename + */ + private String generateArchiveFilename() { + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + return "vsum-project-" + timestamp + ".zip"; + } +} + diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java new file mode 100644 index 0000000..c65cb56 --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java @@ -0,0 +1,327 @@ +package tools.vitruv.methodologist.setup.vsum.service; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tools.vitruv.methodologist.setup.config.MetamodelLocation; +import tools.vitruv.methodologist.setup.config.VitruvConfiguration; +import tools.vitruv.methodologist.setup.exception.MissingModelException; +import tools.vitruv.methodologist.setup.util.FileUtils; + +/** This class is responsible for generating files from templates. */ +@Slf4j +@Service +public class GenerateFromTemplate { + /** Constructor. */ + public GenerateFromTemplate() {} + + private static final String PACKAGE_NAME = "packageName"; + + private Configuration getConfiguration() { + Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); + cfg.setDefaultEncoding("UTF-8"); + cfg.setClassForTemplateLoading(this.getClass(), "/templates"); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + cfg.setLogTemplateExceptions(false); + cfg.setWrapUncheckedExceptions(true); + return cfg; + } + + private void writeTemplate(Template template, File filePath, Map data) + throws IOException { + FileUtils.createFile(filePath.getAbsolutePath()); + try (Writer fileWriter = new FileWriter(filePath.getAbsolutePath(), false)) { + template.process(data, fileWriter); + fileWriter.flush(); + log.info("writing to " + filePath.getAbsolutePath()); + } catch (TemplateException e) { + throw new IOException("Failed to process template for file: " + filePath.getAbsolutePath(), e); + } + } + + private Template loadTemplate(Configuration cfg, String templateName) throws IOException { + try { + return cfg.getTemplate(templateName); + } catch (IOException e) { + throw new IOException("Could not load Freemarker template: " + templateName, e); + } + } + + /** + * Generates the root pom file. + * + * @param filePath The file path to write the root pom file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + * @throws MissingModelException If the package name is missing. + */ + public void generateRootPom(File filePath, String packageName) + throws IOException, MissingModelException { + + if (packageName == null || packageName.isEmpty()) { + throw new MissingModelException("-m ModelOption is missing the PackageName"); + } + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put("packageName", packageName.trim()); + + Template template = loadTemplate(cfg, "rootPom.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the vsum pom file. + * + * @param filePath The file path to write the vsum pom file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateVsumPom(File filePath, String packageName) throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put("packageName", packageName.trim()); + + Template template = loadTemplate(cfg, "vsumPom.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the vsum example file. + * + * @param filePath The file path to write the vsum example file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateVsumExample(File filePath, String packageName, List models) + throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put("packageName", packageName.trim()); + data.put("models", models); + + Template template = loadTemplate(cfg, "vsumExample.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the p2wrappers pom file. + * + * @param filePath The file path to write the p2wrappers pom file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateP2WrappersPom(File filePath, String packageName) throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put("packageName", packageName.trim()); + + Template template = loadTemplate(cfg, "p2wrappersPom.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the javautils pom file. + * + * @param filePath The file path to write the javautils pom file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateJavaUtilsPom(File filePath, String packageName) throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put("packageName", packageName.trim()); + + Template template = loadTemplate(cfg, "javautilsPom.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the xannotations pom file. + * + * @param filePath The file path to write the xannotations pom file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateXAnnotationsPom(File filePath, String packageName) throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put("packageName", packageName.trim()); + + Template template = loadTemplate(cfg, "xannotationsPom.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the emfutils pom file. + * + * @param filePath The file path to write the emfutils pom file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateEMFUtilsPom(File filePath, String packageName) throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put("packageName", packageName.trim()); + + Template template = loadTemplate(cfg, "emfutilsPom.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the vsum test file. + * + * @param filePath The file path to write the vsum test file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateVsumTest(File filePath, String packageName) throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put("packageName", packageName.trim()); + + Template template = loadTemplate(cfg, "vsumTest.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the project file. + * + * @param filePath The file path to write the project file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateProjectFile(File filePath, String packageName) throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put("packageName", packageName.trim()); + + Template template = loadTemplate(cfg, "project.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the model pom file. + * + * @param filePath The file path to write the model pom file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateModelPom(File filePath, String packageName) throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put(PACKAGE_NAME, packageName); + + Template template = loadTemplate(cfg, "modelPom.ftl"); + writeTemplate(template, filePath, data); + } + + /** + * Generates the consistency pom file. + * + * @param filePath The file path to write the consistency pom file to. + * @param packageName The package name from the genmodel. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateConsistencyPom(File filePath, String packageName) throws IOException { + Configuration cfg = getConfiguration(); + + Map data = new HashMap<>(); + data.put(PACKAGE_NAME, packageName); + + Template template = loadTemplate(cfg, "consistencyPom.ftl"); + writeTemplate(template, filePath, data); + } + + private String getNormalizedDirectoryString(String targetDir) { + return targetDir.replace("\\", "/").replaceAll("//+", "/"); + } + + /** + * Generates the mwe2 file. + * + * @param filePath the file path to write the mwe2 file to. + * @param models the list of metamodel locations. + * @param config the vitruv cli configuration. + * @throws java.io.IOException If the file cannot be written. + */ + public void generateMwe2( + File filePath, List models, VitruvConfiguration config) + throws IOException { + + Configuration cfg = getConfiguration(); + List> items = new ArrayList<>(); + for (MetamodelLocation model : models) { + items.add( + Map.of( + "targetDir", + getNormalizedDirectoryString(config.getLocalPath().toString().trim()), + "modelName", + model.genmodel().getName(), + "modelDirectory", + getNormalizedDirectoryString(model.modelDirectory().trim()), + "packageName", + config.getPackageName().trim().concat(".model"))); + } + Template template = loadTemplate(cfg, "generator.ftl"); + Map data = new HashMap<>(); + data.put("items", items); + + writeTemplate(template, filePath, data); + } + + /** + * Generates the plugin file. + * + * @param filePath the file path to write the plugin file to. + * @param config the vitruv cli configuration. + * @param models the list of metamodel locations. + * @throws java.io.IOException If the file cannot be written. + */ + public void generatePlugin( + File filePath, VitruvConfiguration config, List models) + throws IOException { + Configuration cfg = getConfiguration(); + List> items = new ArrayList<>(); + for (MetamodelLocation model : models) { + items.add( + Map.of( + PACKAGE_NAME, + config.getPackageName(), + "modelUri", + model.genmodelUri(), + "modelNameCap", + model.genmodel().getName().substring(0, 1).toUpperCase() + + model + .genmodel() + .getName() + .substring(1, model.genmodel().getName().indexOf('.')), + "genmodelName", + model.genmodel().getName())); + } + Template template = loadTemplate(cfg, "plugin.ftl"); + Map data = new HashMap<>(); + data.put("items", items); + writeTemplate(template, filePath, data); + } +} diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java new file mode 100644 index 0000000..5ab4ffb --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -0,0 +1,320 @@ +package tools.vitruv.methodologist.setup.vsum.service; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilderFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import tools.vitruv.methodologist.setup.exception.MethodologistSetupException; +import tools.vitruv.methodologist.setup.exception.MissingModelException; + +/** Coordinates VSUM project build operations from uploaded files. */ +@Service +@RequiredArgsConstructor +public class VsumProjectBuildService { + + private static final String VSUM_BUILD_ERROR_CODE = "VSUM_BUILD_ERROR"; + private static final String VSUM_INPUT_ERROR_CODE = "VSUM_INPUT_ERROR"; + private static final Pattern REACTION_IMPORT_PATTERN = Pattern.compile("import\\s+\"([^\"]+)\""); + + private final VsumService vsumService; + + /** + * Builds a VSUM project from uploaded files and returns a zip archive. + * + * @param metamodelFiles metamodel files + * @param genmodelFiles genmodel files in the same order as metamodel files + * @param reactionFiles optional reaction files + * @return built project archive bytes + */ + public byte[] buildProjectArchive( + List metamodelFiles, + List genmodelFiles, + List reactionFiles) { + validateInputs(metamodelFiles, genmodelFiles); + + Path uploadWorkspace = null; + try { + uploadWorkspace = Files.createTempDirectory("vsum-upload-"); + List modelPairs = + toModelPairs(uploadWorkspace, metamodelFiles, genmodelFiles); + List copiedReactionFiles = toReactionFiles(uploadWorkspace, reactionFiles); + normalizeReactionImports(modelPairs, copiedReactionFiles); + Map metamodelNamespaceMap = extractMetamodelNamespaceMap(modelPairs); + return vsumService.generateProjectArchive(modelPairs, copiedReactionFiles, metamodelNamespaceMap); + } catch (IOException | MissingModelException e) { + throw new MethodologistSetupException( + VSUM_BUILD_ERROR_CODE, "Failed to build VSUM project archive", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new MethodologistSetupException( + VSUM_BUILD_ERROR_CODE, "VSUM project build was interrupted", e); + } finally { + deleteRecursively(uploadWorkspace); + } + } + + /** + * Validates uploaded metamodel/genmodel lists. + * + * @param metamodelFiles metamodel files + * @param genmodelFiles genmodel files + */ + private void validateInputs(List metamodelFiles, List genmodelFiles) { + if (metamodelFiles == null + || genmodelFiles == null + || metamodelFiles.isEmpty() + || genmodelFiles.isEmpty()) { + throw new MethodologistSetupException( + VSUM_INPUT_ERROR_CODE, "At least one metamodel and one genmodel file are required"); + } + + if (metamodelFiles.size() != genmodelFiles.size()) { + throw new MethodologistSetupException( + VSUM_INPUT_ERROR_CODE, + "Metamodel and genmodel file counts must be identical to build file pairs"); + } + + for (MultipartFile file : metamodelFiles) { + validateUploadedFile(file, "metamodel file"); + } + for (MultipartFile file : genmodelFiles) { + validateUploadedFile(file, "genmodel file"); + } + } + + /** + * Converts uploaded metamodel/genmodel files into local file pairs. + * + * @param root upload root directory + * @param metamodelFiles metamodel files + * @param genmodelFiles genmodel files + * @return model file pairs + * @throws IOException when writing files fails + */ + private List toModelPairs( + Path root, List metamodelFiles, List genmodelFiles) + throws IOException { + Path modelUploadPath = Files.createDirectories(root.resolve("models")); + List modelPairs = new ArrayList<>(); + + for (int index = 0; index < metamodelFiles.size(); index++) { + File metamodel = writeMultipartFile(metamodelFiles.get(index), modelUploadPath, "metamodel", index); + File genmodel = writeMultipartFile(genmodelFiles.get(index), modelUploadPath, "genmodel", index); + modelPairs.add(new VsumService.ModelFiles(metamodel, genmodel)); + } + + return modelPairs; + } + + /** + * Converts uploaded reaction files into local files. + * + * @param root upload root directory + * @param reactionFiles reaction files + * @return copied reaction files + * @throws IOException when writing files fails + */ + private List toReactionFiles(Path root, List reactionFiles) throws IOException { + if (reactionFiles == null || reactionFiles.isEmpty()) { + return List.of(); + } + + Path reactionUploadPath = Files.createDirectories(root.resolve("reactions")); + List files = new ArrayList<>(); + for (int index = 0; index < reactionFiles.size(); index++) { + MultipartFile reactionFile = reactionFiles.get(index); + validateUploadedFile(reactionFile, "reaction file"); + files.add(writeMultipartFile(reactionFile, reactionUploadPath, "reaction", index)); + } + return files; + } + + /** + * Normalizes imported model URIs in reaction files to match uploaded metamodel nsURIs. + * + * @param modelPairs metamodel/genmodel file pairs + * @param reactionFiles copied reaction files + */ + private void normalizeReactionImports(List modelPairs, List reactionFiles) + throws IOException { + if (reactionFiles == null || reactionFiles.isEmpty()) { + return; + } + + Map packageNameToNsUri = new HashMap<>(); + Set nsUris = new HashSet<>(); + for (VsumService.ModelFiles pair : modelPairs) { + MetamodelInfo info = readMetamodelInfo(pair.metamodelFile()); + if (info != null) { + if (info.packageName != null && !info.packageName.isBlank()) { + packageNameToNsUri.put(info.packageName, info.nsUri); + } + nsUris.add(info.nsUri); + } + } + + for (File reactionFile : reactionFiles) { + String content = Files.readString(reactionFile.toPath(), StandardCharsets.UTF_8); + String updated = normalizeReactionImports(content, packageNameToNsUri, nsUris); + if (!updated.equals(content)) { + Files.writeString(reactionFile.toPath(), updated, StandardCharsets.UTF_8); + } + } + } + + /** + * Stores metamodel nsURI information for build-time EMF initialization. + * + * @param modelPairs metamodel/genmodel file pairs + * @return map of model names to nsURIs + */ + Map extractMetamodelNamespaceMap(List modelPairs) { + Map modelNameToNsUri = new HashMap<>(); + for (VsumService.ModelFiles pair : modelPairs) { + MetamodelInfo info = readMetamodelInfo(pair.metamodelFile()); + if (info != null && info.nsUri != null && !info.nsUri.isBlank()) { + modelNameToNsUri.put( + info.packageName != null ? info.packageName : "model", + info.nsUri); + } + } + return modelNameToNsUri; + } + + private String normalizeReactionImports( + String content, Map packageNameToNsUri, Set knownNsUris) { + Matcher matcher = REACTION_IMPORT_PATTERN.matcher(content); + StringBuffer rewritten = new StringBuffer(); + while (matcher.find()) { + String importUri = matcher.group(1); + String replacementUri = importUri; + if (!knownNsUris.contains(importUri)) { + String candidatePackageName = extractLastSegment(importUri); + String mappedUri = packageNameToNsUri.get(candidatePackageName); + if (mappedUri != null && !mappedUri.isBlank()) { + replacementUri = mappedUri; + } + } + + matcher.appendReplacement(rewritten, "import \"" + Matcher.quoteReplacement(replacementUri) + "\""); + } + matcher.appendTail(rewritten); + return rewritten.toString(); + } + + private String extractLastSegment(String uri) { + int slash = uri.lastIndexOf('/'); + int hash = uri.lastIndexOf('#'); + int separator = Math.max(slash, hash); + return separator >= 0 ? uri.substring(separator + 1) : uri; + } + + private MetamodelInfo readMetamodelInfo(File metamodelFile) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + Document document = factory.newDocumentBuilder().parse(metamodelFile); + Element root = document.getDocumentElement(); + String nsUri = root.getAttribute("nsURI"); + String packageName = root.getAttribute("name"); + if (nsUri == null || nsUri.isBlank()) { + return null; + } + return new MetamodelInfo(packageName, nsUri); + } catch (Exception exception) { + return null; + } + } + + private static class MetamodelInfo { + private final String packageName; + private final String nsUri; + + private MetamodelInfo(String packageName, String nsUri) { + this.packageName = packageName; + this.nsUri = nsUri; + } + } + + /** + * Writes a multipart file to disk. + * + * @param file multipart file + * @param directory target directory + * @param type logical file type label + * @param index index for deterministic fallback naming + * @return copied file + * @throws IOException when writing file fails + */ + private File writeMultipartFile(MultipartFile file, Path directory, String type, int index) + throws IOException { + String originalName = file.getOriginalFilename(); + String safeFileName = + originalName == null || originalName.isBlank() + ? type + "-" + index + : Path.of(originalName).getFileName().toString(); + + Path targetFile = directory.resolve(safeFileName); + file.transferTo(targetFile); + return targetFile.toFile(); + } + + /** + * Ensures an uploaded file is present and non-empty. + * + * @param file uploaded file + * @param label logical file label + */ + private void validateUploadedFile(MultipartFile file, String label) { + if (file == null || file.isEmpty()) { + throw new MethodologistSetupException( + VSUM_INPUT_ERROR_CODE, "Uploaded " + label + " must not be empty"); + } + } + + /** + * Deletes a directory tree if it exists. + * + * @param root root directory + */ + private void deleteRecursively(Path root) { + if (root == null || !Files.exists(root)) { + return; + } + + try (Stream paths = Files.walk(root)) { + paths.sorted(Comparator.reverseOrder()).forEach(this::deleteQuietly); + } catch (IOException ignored) { + } + } + + /** + * Deletes a single path while ignoring IO errors. + * + * @param path file system path + */ + private void deleteQuietly(Path path) { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + } +} + diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java new file mode 100644 index 0000000..3fe913e --- /dev/null +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java @@ -0,0 +1,304 @@ +package tools.vitruv.methodologist.setup.vsum.service; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import tools.vitruv.methodologist.setup.config.VitruvConfiguration; +import tools.vitruv.methodologist.setup.exception.MissingModelException; + +/** Business service for building and packaging VSUM projects from uploaded model files. */ +@Service +@RequiredArgsConstructor +public class VsumService { + + private final GenerateFromTemplate generateFromTemplate; + + public record ModelFiles(File metamodelFile, File genmodelFile) { + public ModelFiles { + Objects.requireNonNull(metamodelFile, "metamodelFile must not be null"); + Objects.requireNonNull(genmodelFile, "genmodelFile must not be null"); + } + } + + /** + * Generates a full VSUM project, builds it with Maven, and returns the project archive as bytes. + * + * @param modelFiles paired metamodel/genmodel files + * @param reactionFiles optional reaction files + * @param metamodelNamespaceMap map of model names to nsURIs + * @return zip archive bytes containing generated project and build artifacts + * @throws IOException when file IO or build execution fails + * @throws InterruptedException when the build process is interrupted + * @throws MissingModelException when the model configuration is invalid + */ + public byte[] generateProjectArchive( + List modelFiles, List reactionFiles, Map metamodelNamespaceMap) + throws IOException, InterruptedException, MissingModelException { + validateInputs(modelFiles, reactionFiles); + + Path workspace = Files.createTempDirectory("vitruv-cli-project-"); + try { + VitruvConfiguration configuration = new VitruvConfiguration(); + configuration.setLocalPath(workspace); + + List copiedModelFiles = copyModelFiles(workspace, modelFiles); + configuration.setMetaModelLocations(buildModelLocations(copiedModelFiles)); + + List copiedReactionFiles = copyReactionFiles(workspace, reactionFiles); + configuration.setReactionLocations(copiedReactionFiles); + + generateProjectFiles(configuration); + runMavenBuild(workspace, metamodelNamespaceMap); + return zipDirectory(workspace); + } finally { + deleteRecursively(workspace); + } + } + + /** + * Generates a full VSUM project, builds it with Maven, and returns the project archive as bytes. + * + * @param modelFiles paired metamodel/genmodel files + * @param reactionFiles optional reaction files + * @return zip archive bytes containing generated project and build artifacts + * @throws IOException when file IO or build execution fails + * @throws InterruptedException when the build process is interrupted + * @throws MissingModelException when the model configuration is invalid + */ + public byte[] generateProjectArchive(List modelFiles, List reactionFiles) + throws IOException, InterruptedException, MissingModelException { + return generateProjectArchive(modelFiles, reactionFiles, Map.of()); + } + + private void validateInputs(List modelFiles, List reactionFiles) { + if (modelFiles == null || modelFiles.isEmpty()) { + throw new IllegalArgumentException("At least one metamodel/genmodel pair is required."); + } + + for (ModelFiles pair : modelFiles) { + validateFile(pair.metamodelFile(), "metamodelFile"); + validateFile(pair.genmodelFile(), "genmodelFile"); + } + + if (reactionFiles != null) { + for (File reactionFile : reactionFiles) { + validateFile(reactionFile, "reactionFile"); + } + } + } + + private void validateFile(File file, String label) { + if (file == null) { + throw new IllegalArgumentException(label + " must not be null."); + } + if (!file.exists()) { + throw new IllegalArgumentException(label + " does not exist: " + file.getAbsolutePath()); + } + if (!file.isFile()) { + throw new IllegalArgumentException(label + " is not a file: " + file.getAbsolutePath()); + } + } + + private List copyModelFiles(Path workspace, List modelFiles) + throws IOException { + Path targetDirectory = workspace.resolve("model/src/main/ecore"); + Files.createDirectories(targetDirectory); + + List copied = new ArrayList<>(); + for (ModelFiles pair : modelFiles) { + File copiedMetamodel = copyFile(pair.metamodelFile(), targetDirectory); + File copiedGenmodel = copyFile(pair.genmodelFile(), targetDirectory); + copied.add(new ModelFiles(copiedMetamodel, copiedGenmodel)); + } + return copied; + } + + private List copyReactionFiles(Path workspace, List reactionFiles) + throws IOException { + if (reactionFiles == null || reactionFiles.isEmpty()) { + return List.of(); + } + + Path targetDirectory = workspace.resolve("consistency/src/main/reactions"); + Files.createDirectories(targetDirectory); + + List copied = new ArrayList<>(); + for (File reactionFile : reactionFiles) { + copied.add(copyFile(reactionFile, targetDirectory).toPath()); + } + return copied; + } + + private File copyFile(File source, Path targetDirectory) throws IOException { + Path target = targetDirectory.resolve(source.getName()); + Files.copy(source.toPath(), target, StandardCopyOption.REPLACE_EXISTING); + return target.toFile(); + } + + private String buildModelLocations(List copiedModelFiles) { + return copiedModelFiles.stream() + .map( + pair -> + pair.metamodelFile().getAbsolutePath() + + "," + + pair.genmodelFile().getAbsolutePath()) + .collect(Collectors.joining(";")); + } + + private void generateProjectFiles(VitruvConfiguration configuration) + throws IOException, MissingModelException { + generateFromTemplate.generateRootPom( + new File((configuration.getLocalPath() + "/pom.xml").trim()), + configuration.getPackageName()); + generateFromTemplate.generateConsistencyPom( + new File((configuration.getLocalPath() + "/consistency/pom.xml").trim()), + configuration.getPackageName()); + generateFromTemplate.generateModelPom( + new File((configuration.getLocalPath() + "/model/pom.xml").trim()), + configuration.getPackageName()); + generateFromTemplate.generateVsumPom( + new File((configuration.getLocalPath() + "/vsum/pom.xml").trim()), + configuration.getPackageName()); + generateFromTemplate.generateP2WrappersPom( + new File((configuration.getLocalPath() + "/p2wrappers/pom.xml").trim()), + configuration.getPackageName()); + generateFromTemplate.generateJavaUtilsPom( + new File((configuration.getLocalPath() + "/p2wrappers/javautils/pom.xml").trim()), + configuration.getPackageName()); + generateFromTemplate.generateXAnnotationsPom( + new File( + (configuration.getLocalPath() + "/p2wrappers/activextendannotations/pom.xml").trim()), + configuration.getPackageName()); + generateFromTemplate.generateEMFUtilsPom( + new File((configuration.getLocalPath() + "/p2wrappers/emfutils/pom.xml").trim()), + configuration.getPackageName()); + generateFromTemplate.generateVsumExample( + new File((configuration.getLocalPath() + "/vsum/src/main/java/VSUMExample.java").trim()), + configuration.getPackageName(), + configuration.getModelNames()); + generateFromTemplate.generateVsumTest( + new File( + (configuration.getLocalPath() + "/vsum/src/test/java/VSUMExampleTest.java").trim()), + configuration.getPackageName()); + generateFromTemplate.generateProjectFile( + new File((configuration.getLocalPath() + "/model/.project").trim()), + configuration.getPackageName()); + + File workflow = + new File((configuration.getLocalPath() + "/model/workflow/generate.mwe2").trim()); + configuration.setWorkflow(workflow); + generateFromTemplate.generateMwe2( + workflow, configuration.getMetaModelLocations(), configuration); + generateFromTemplate.generatePlugin( + new File((configuration.getLocalPath() + "/model/plugin.xml").trim()), + configuration, + configuration.getMetaModelLocations()); + } + + protected void runMavenBuild(Path projectRoot) throws IOException, InterruptedException { + runMavenBuild(projectRoot, Map.of()); + } + + protected void runMavenBuild(Path projectRoot, Map metamodelNamespaceMap) + throws IOException, InterruptedException { + ProcessBuilder processBuilder = createMavenProcessBuilder(projectRoot, metamodelNamespaceMap); + Process process = processBuilder.start(); + + StringWriter output = new StringWriter(); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + reader.transferTo(output); + } + + process.waitFor(); + if (process.exitValue() != 0) { + throw new IOException( + "Maven build failed with exit code " + + process.exitValue() + + ". Output: " + + output); + } + } + + private ProcessBuilder createMavenProcessBuilder( + Path projectRoot, Map metamodelNamespaceMap) { + ProcessBuilder processBuilder = new ProcessBuilder("mvn", "clean", "verify", "-DskipTests"); + + // Pass metamodel namespace information as system properties + if (metamodelNamespaceMap != null && !metamodelNamespaceMap.isEmpty()) { + for (var entry : metamodelNamespaceMap.entrySet()) { + String propName = "metamodel." + entry.getKey() + ".nsuri"; + String propValue = "-D" + propName + "=" + entry.getValue(); + processBuilder.command().add(propValue); + } + } + + processBuilder.directory(projectRoot.toFile()); + processBuilder.redirectErrorStream(true); + return processBuilder; + } + + private byte[] zipDirectory(Path root) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream); + Stream paths = Files.walk(root)) { + for (Path path : paths.sorted(Comparator.naturalOrder()).toList()) { + if (root.equals(path)) { + continue; + } + + String entryName = root.relativize(path).toString().replace('\\', '/'); + if (Files.isDirectory(path)) { + zipOutputStream.putNextEntry(new ZipEntry(entryName + "/")); + zipOutputStream.closeEntry(); + continue; + } + + zipOutputStream.putNextEntry(new ZipEntry(entryName)); + Files.copy(path, zipOutputStream); + zipOutputStream.closeEntry(); + } + } + return outputStream.toByteArray(); + } + + private void deleteRecursively(Path root) { + if (root == null || !Files.exists(root)) { + return; + } + + try (Stream paths = Files.walk(root)) { + paths + .sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + // Best-effort cleanup; ignore delete failures. + } + }); + } catch (IOException e) { + // Best-effort cleanup; ignore cleanup failures. + } + } +} diff --git a/src/main/resources/templates/consistencyPom.ftl b/src/main/resources/templates/consistencyPom.ftl new file mode 100644 index 0000000..4c2b6af --- /dev/null +++ b/src/main/resources/templates/consistencyPom.ftl @@ -0,0 +1,143 @@ + + + + 4.0.0 + + tools.vitruv + ${packageName} + 0.1.0-SNAPSHOT + + ${packageName}.consistency + <#noparse> + Consistency + + + + + + ${project.groupId} + + ${packageName}.model + <#noparse> + ${project.version} + compile + + + + + tools.vitruv + tools.vitruv.change.composite + + + tools.vitruv + tools.vitruv.change.propagation + + + tools.vitruv + tools.vitruv.dsls.reactions.runtime + + + + + org.eclipse.emf + org.eclipse.emf.ecore + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-reactions + generate-sources + + add-source + + + + ${project.basedir}/src/main/reactions + ${project.build.directory}/generated-sources/reactions + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.5.0 + + + copy-model-dependencies + generate-sources + + copy-dependencies + + + ${project.groupId} + ${packageName}.model + ${project.build.directory}/model-lib + + + + + + org.eclipse.xtext + xtext-maven-plugin + 2.39.0 + + + + tools.vitruv.dsls.reactions.ReactionsLanguageStandaloneSetup + + + + ${project.build.directory}/generated-sources/reactions + + + + + org.eclipse.xtend.core.XtendStandaloneSetup + + + + ${project.build.directory}/generated-sources/xtend + + + + + + + + tools.vitruv + tools.vitruv.dsls.reactions.language + 3.1.0 + + + tools.vitruv + tools.vitruv.dsls.reactions.runtime + 3.1.0 + + + tools.vitruv + tools.vitruv.dsls.common + 3.1.0 + + + ${project.groupId} + + ${packageName}.model + <#noparse> + ${project.version} + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/depencyReducedEmfUtilPom.ftl b/src/main/resources/templates/depencyReducedEmfUtilPom.ftl new file mode 100644 index 0000000..3307aa8 --- /dev/null +++ b/src/main/resources/templates/depencyReducedEmfUtilPom.ftl @@ -0,0 +1,43 @@ + + + 4.0.0 + tools.vitruv + tools.vitruv.change.p2wrappers.emfutils + p2 Dependency Wrapper EMF Utils + 3.2.0-SNAPSHOT + wrapper for the p2 dependency sdq-commons:edu.kit.ipd.sdq.commons.util.emf + https://github.com/vitruv-tools/Vitruv-Change/tools.vitruv.change.p2wrappers/tools.vitruv.change.p2wrappers.emfutils + + + Vitruvius Developers + vitruv-dev@lists.kit.edu + Karlsruhe Institute of Technology (KIT), Germany + https://kit.edu + + + + + Eclipse Public License - v 1.0 + https://www.eclipse.org/org/documents/epl-v10.php + + + + scm:git:git://github.com/vitruv-tools/Vitruv-Change.git/tools.vitruv.change.p2wrappers/tools.vitruv.change.p2wrappers.emfutils + scm:git:https://github.com/vitruv-tools/Vitruv-Change.git/tools.vitruv.change.p2wrappers/tools.vitruv.change.p2wrappers.emfutils + https://github.com/vitruv-tools/Vitruv-Change/tree/main/tools.vitruv.change.p2wrappers/tools.vitruv.change.p2wrappers.emfutils + + + Vitruvius Tools + https://vitruv.tools/ + + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + \ No newline at end of file diff --git a/src/main/resources/templates/emfutilsPom.ftl b/src/main/resources/templates/emfutilsPom.ftl new file mode 100644 index 0000000..9c7fddc --- /dev/null +++ b/src/main/resources/templates/emfutilsPom.ftl @@ -0,0 +1,67 @@ + + + + 4.0.0 + + + tools.vitruv + ${packageName}.p2wrappers + 0.1.0-SNAPSHOT + + + ${packageName}.p2wrappers.emfutils + + p2 Dependency Wrapper EMF Utils + wrapper for the p2 dependency sdq-commons:edu.kit.ipd.sdq.commons.util.emf + + <#noparse> + + + + org.openntf.maven + p2-layout-resolver + + + org.codehaus.mojo + flatten-maven-plugin + + + maven-shade-plugin + + + maven-jar-plugin + + + + + + 2.2.0 + + + + + sdq-commons + SDQ Commons + https://kit-sdq.github.io/updatesite/release/commons/${repo.sdq-commons.version} + p2 + + + + + + sdq-commons + edu.kit.ipd.sdq.commons.util.emf + 2.3.0.202304271319 + + + + * + * + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/generator.ftl b/src/main/resources/templates/generator.ftl new file mode 100644 index 0000000..f4d68bb --- /dev/null +++ b/src/main/resources/templates/generator.ftl @@ -0,0 +1,18 @@ +module tools.vitruv.framework.cli + +import org.eclipse.emf.mwe2.ecore.EcoreGenerator +import org.eclipse.emf.mwe.utils.StandaloneSetup + +var workspaceRoot = ".." + +Workflow { + + bean = StandaloneSetup { + scanClassPath = true + platformUri = workspaceRoot + } + <#list items as item> + <#include "modelComponent.ftl"> + + +} \ No newline at end of file diff --git a/src/main/resources/templates/javautilsPom.ftl b/src/main/resources/templates/javautilsPom.ftl new file mode 100644 index 0000000..1fe2e5c --- /dev/null +++ b/src/main/resources/templates/javautilsPom.ftl @@ -0,0 +1,66 @@ + + + + 4.0.0 + + + tools.vitruv + ${packageName}.p2wrappers + 0.1.0-SNAPSHOT + + + ${packageName}.p2wrappers.javautils + + p2 Dependency Wrapper Java Utils + wrapper for the p2 dependency sdq-commons:edu.kit.ipd.sdq.commons.util.java + <#noparse> + + + + org.openntf.maven + p2-layout-resolver + + + org.codehaus.mojo + flatten-maven-plugin + + + maven-shade-plugin + + + maven-jar-plugin + + + + + + 2.2.0 + + + + + sdq-commons + SDQ Commons + https://kit-sdq.github.io/updatesite/release/commons/${repo.sdq-commons.version} + p2 + + + + + + sdq-commons + edu.kit.ipd.sdq.commons.util.java + 2.3.0.202304271319 + + + + * + * + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/modelComponent.ftl b/src/main/resources/templates/modelComponent.ftl new file mode 100644 index 0000000..365ba90 --- /dev/null +++ b/src/main/resources/templates/modelComponent.ftl @@ -0,0 +1,6 @@ + component = EcoreGenerator { + genModel = "platform:/resource/${item.packageName}/src/main/ecore/${item.modelName}" + srcPath = "platform:/resource/${item.targetDir}/target/${item.modelDirectory}" + generateCustomClasses = false + } + \ No newline at end of file diff --git a/src/main/resources/templates/modelPom.ftl b/src/main/resources/templates/modelPom.ftl new file mode 100644 index 0000000..8f49f88 --- /dev/null +++ b/src/main/resources/templates/modelPom.ftl @@ -0,0 +1,51 @@ + + + + 4.0.0 + + + tools.vitruv + ${packageName} + 0.1.0-SNAPSHOT + + + ${packageName}.model + <#noparse> + Model + Model package of the Vitruv cli thing + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + org.codehaus.mojo + exec-maven-plugin + + + maven-jar-plugin + + + ${project.basedir}/META-INF/MANIFEST.MF + + + + + + + + + org.eclipse.emf + org.eclipse.emf.common + + + org.eclipse.emf + org.eclipse.emf.ecore + + + + \ No newline at end of file diff --git a/src/main/resources/templates/p2wrappersPom.ftl b/src/main/resources/templates/p2wrappersPom.ftl new file mode 100644 index 0000000..e9f7402 --- /dev/null +++ b/src/main/resources/templates/p2wrappersPom.ftl @@ -0,0 +1,68 @@ + + + + 4.0.0 + + + tools.vitruv + ${packageName} + 0.1.0-SNAPSHOT + + + ${packageName}.p2wrappers + pom + + p2 Dependency Wrappers + + + + activextendannotations + emfutils + javautils + + + + + + + + org.openntf.maven + p2-layout-resolver + 1.9.0 + true + + + + maven-jar-plugin + 3.4.2 + + + javadoc-jar + package + + jar + + + javadoc + + + + + + + + + + + + artifactory.openntf.org + artifactory.openntf.org + https://artifactory.openntf.org/openntf + + false + + + + \ No newline at end of file diff --git a/src/main/resources/templates/plugin.ftl b/src/main/resources/templates/plugin.ftl new file mode 100644 index 0000000..47add64 --- /dev/null +++ b/src/main/resources/templates/plugin.ftl @@ -0,0 +1,13 @@ + + + + + + + + <#list items as item> + <#include "pluginExtension.ftl"> + + + \ No newline at end of file diff --git a/src/main/resources/templates/pluginExtension.ftl b/src/main/resources/templates/pluginExtension.ftl new file mode 100644 index 0000000..ee73f4f --- /dev/null +++ b/src/main/resources/templates/pluginExtension.ftl @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/project.ftl b/src/main/resources/templates/project.ftl new file mode 100644 index 0000000..2e5faac --- /dev/null +++ b/src/main/resources/templates/project.ftl @@ -0,0 +1,29 @@ + + + ${packageName}.model + + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + + + + 1700568570080 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/rootPom.ftl b/src/main/resources/templates/rootPom.ftl new file mode 100644 index 0000000..1c0876a --- /dev/null +++ b/src/main/resources/templates/rootPom.ftl @@ -0,0 +1,100 @@ + + + + 4.0.0 + + + + tools.vitruv + parent + 3.2.0 + + + + ${packageName} + 0.1.0-SNAPSHOT + pom + <#noparse> + Methodologist Template + + + + + + + model + consistency + vsum + p2wrappers + + + + 3.2.3 + + + + + + + tools.vitruv + tools.vitruv.change.atomic + ${vitruv.version} + + + tools.vitruv + tools.vitruv.change.composite + ${vitruv.version} + + + tools.vitruv + tools.vitruv.change.interaction + ${vitruv.version} + + + tools.vitruv + tools.vitruv.change.propagation + ${vitruv.version} + + + tools.vitruv + tools.vitruv.change.testutils.integration + ${vitruv.version} + + + tools.vitruv + tools.vitruv.dsls.reactions.runtime + ${vitruv.version} + + + tools.vitruv + tools.vitruv.framework.views + ${vitruv.version} + + + tools.vitruv + tools.vitruv.framework.vsum + ${vitruv.version} + + + + + org.eclipse.emf + org.eclipse.emf.common + 2.42.0 + + + org.eclipse.emf + org.eclipse.emf.ecore + 2.39.0 + + + org.junit.jupiter + junit-jupiter-api + 5.12.0 + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/viewTypes.ftl b/src/main/resources/templates/viewTypes.ftl new file mode 100644 index 0000000..b40bf84 --- /dev/null +++ b/src/main/resources/templates/viewTypes.ftl @@ -0,0 +1 @@ +viewTypes.add(ViewTypeFactory.createIdentityMappingViewType("${model}")); diff --git a/src/main/resources/templates/vsumExample.ftl b/src/main/resources/templates/vsumExample.ftl new file mode 100644 index 0000000..908f400 --- /dev/null +++ b/src/main/resources/templates/vsumExample.ftl @@ -0,0 +1,71 @@ +package ${packageName}.vsum; + +import tools.vitruv.framework.vsum.VirtualModelBuilder; +import ${packageName}.model.model.ModelFactory; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import java.nio.file.Path; +import java.util.function.Consumer; +import mir.reactions.model2Model2.Model2Model2ChangePropagationSpecification; +import tools.vitruv.change.testutils.TestUserInteraction; +import tools.vitruv.framework.views.CommittableView; +import tools.vitruv.framework.views.View; +import tools.vitruv.framework.views.ViewTypeFactory; +import tools.vitruv.framework.views.ViewType; +import tools.vitruv.framework.vsum.VirtualModel; +import tools.vitruv.framework.remote.server.*; +import java.util.Scanner; +import java.io.IOException; + +/** + * This class provides an example how to define and use a VSUM. + */ +public class VSUMExample { + public static void main(String[] args) { + try { + VitruvServer server = new VitruvServer(VSUMExample::createDefaultVirtualModel); + server.start(); + Scanner scanner = new Scanner(System.in); + while (!scanner.nextLine().equals("quit")) { + + } + scanner.close(); + server.stop(); + } catch (IOException e) { + System.out.println("Something went wrong " + e); + } + } + + private static VirtualModel createDefaultVirtualModel() { + Collection> viewTypes = createDefaultViewTypes(); + VirtualModel model = new VirtualModelBuilder() + .withStorageFolder(Path.of("vsumexample")) + .withUserInteractorForResultProvider(new TestUserInteraction.ResultProvider(new TestUserInteraction())) + .withChangePropagationSpecifications(new Model2Model2ChangePropagationSpecification()).withViewTypes(viewTypes) + .buildAndInitialize(); + getDefaultView(model); + return model; + } + + private static View getDefaultView(VirtualModel vsum) { + var selector = vsum.createSelector(ViewTypeFactory.createIdentityMappingViewType("default")); + selector.getSelectableElements().forEach(it -> selector.setSelected(it, true)); + return selector.createView(); + } + + private static void modifyView(CommittableView view, Consumer modificationFunction) { + modificationFunction.accept(view); + view.commitChanges(); + } + + private static Collection> createDefaultViewTypes() { + List> viewTypes = new ArrayList<>(); + <#list models as model> + <#include "viewTypes.ftl"> + + return viewTypes; + } + +} \ No newline at end of file diff --git a/src/main/resources/templates/vsumPom.ftl b/src/main/resources/templates/vsumPom.ftl new file mode 100644 index 0000000..f10fa3d --- /dev/null +++ b/src/main/resources/templates/vsumPom.ftl @@ -0,0 +1,109 @@ + + + +4.0.0 + + + tools.vitruv + ${packageName} + 0.1.0-SNAPSHOT + + +${packageName}.vsum + +Vsum + + + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + tools.vitruv.methodologisttemplate.vsum.VSUMExample + + + + + + server-jar + package + + single + + + + + + + + + 3.2.3 + + + + + + + <#noparse> + ${project.groupId} + + ${packageName}.model + <#noparse> + ${project.version} + + +${project.groupId} + +${packageName}.consistency +<#noparse> + ${project.version} + + + tools.vitruv + tools.vitruv.server.remote + ${vitruv.version} + + + + + tools.vitruv + tools.vitruv.change.interaction + + + tools.vitruv + tools.vitruv.change.propagation + + + tools.vitruv + tools.vitruv.change.testutils.integration + + + tools.vitruv + tools.vitruv.framework.views + + + tools.vitruv + tools.vitruv.framework.vsum + + + + + org.eclipse.emf + org.eclipse.emf.ecore + + + + org.junit.jupiter + junit-jupiter-api + test + + + + \ No newline at end of file diff --git a/src/main/resources/templates/vsumTest.ftl b/src/main/resources/templates/vsumTest.ftl new file mode 100644 index 0000000..484eb07 --- /dev/null +++ b/src/main/resources/templates/vsumTest.ftl @@ -0,0 +1,68 @@ +package ${packageName}.vsum; + +import tools.vitruv.framework.vsum.VirtualModelBuilder; +import ${packageName}.model.model.ModelFactory; + +import java.nio.file.Path; +import java.util.function.Consumer; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import mir.reactions.model2Model2.Model2Model2ChangePropagationSpecification; +import tools.vitruv.change.testutils.TestUserInteraction; +import tools.vitruv.framework.views.CommittableView; +import tools.vitruv.framework.views.View; +import tools.vitruv.framework.views.ViewTypeFactory; +import tools.vitruv.framework.vsum.VirtualModel; +import org.junit.jupiter.api.Disabled; + +/** + * This class provides an example how to define and use a VSUM. + */ +public class VSUMExampleTest { + + static final Path projectPath = Path.of("target/vsumexample"); + + @BeforeAll + static void setup() { + Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("model", new XMIResourceFactoryImpl()); + } + + @Disabled + @Test + void test() { + VirtualModel vsum = createDefaultVirtualModel(); + CommittableView view = getDefaultView(vsum).withChangeDerivingTrait(); + modifyView(view, (CommittableView v) -> { + v.registerRoot( + ModelFactory.eINSTANCE.createSystem(), + URI.createURI(projectPath.resolve("example.model").toString())); + }); + Assertions.assertFalse(getDefaultView(vsum).getRootObjects().isEmpty(),"Modification of view failed"); + } + + private VirtualModel createDefaultVirtualModel() { + return new VirtualModelBuilder() + .withStorageFolder(projectPath) + .withUserInteractorForResultProvider(new TestUserInteraction.ResultProvider(new TestUserInteraction())) + .withChangePropagationSpecifications(new Model2Model2ChangePropagationSpecification()) + .buildAndInitialize(); + } + + private View getDefaultView(VirtualModel vsum) { + var selector = vsum.createSelector(ViewTypeFactory.createIdentityMappingViewType("default")); + selector.getSelectableElements().forEach(it -> selector.setSelected(it, true)); + return selector.createView(); + } + + private void modifyView(CommittableView view, Consumer modificationFunction) { + modificationFunction.accept(view); + view.commitChanges(); + } + +} \ No newline at end of file diff --git a/src/main/resources/templates/xannotationsPom.ftl b/src/main/resources/templates/xannotationsPom.ftl new file mode 100644 index 0000000..b29b573 --- /dev/null +++ b/src/main/resources/templates/xannotationsPom.ftl @@ -0,0 +1,59 @@ + + + + 4.0.0 + + + tools.vitruv + ${packageName}.p2wrappers + 0.1.0-SNAPSHOT + + + ${packageName}.p2wrappers.activextendannotations + + p2 Dependency Wrapper Active Xtend Annotations + wrapper for the p2 dependency xannotations:edu.kit.ipd.sdq.activextendannotations + <#noparse> + + + + org.openntf.maven + p2-layout-resolver + + + org.codehaus.mojo + flatten-maven-plugin + + + maven-shade-plugin + + + maven-jar-plugin + + + + + + 1.6.0 + + + + + xannotations + XAnnotations + p2 + https://kit-sdq.github.io/updatesite/release/xannotations/${repo.xannotations.version} + + + + + + xannotations + edu.kit.ipd.sdq.activextendannotations + 1.6.0 + + + + \ No newline at end of file diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java new file mode 100644 index 0000000..9eb3cc4 --- /dev/null +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java @@ -0,0 +1,104 @@ +package tools.vitruv.methodologist.setup.vsum.controller; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.file.Files; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import tools.vitruv.methodologist.setup.exception.MethodologistSetupException; +import tools.vitruv.methodologist.setup.vsum.service.VsumProjectBuildService; +import tools.vitruv.methodologist.setup.vsum.service.VsumService; + +/** Unit tests for VSUM project build orchestration logic. */ +@ExtendWith(MockitoExtension.class) +class VsumControllerTest { + + @Mock private VsumService vsumService; + + @InjectMocks private VsumProjectBuildService vsumProjectBuildService; + + /** Verifies uploaded files are transformed and delegated to VSUM generation service. */ + @Test + void buildProjectArchiveDelegatesToVsumService() throws Exception { + byte[] archive = "zip-content".getBytes(); + when(vsumService.generateProjectArchive(anyList(), anyList(), any())).thenReturn(archive); + + MockMultipartFile metamodel = + new MockMultipartFile( + "metamodelFiles", "model.ecore", "application/octet-stream", "meta".getBytes()); + MockMultipartFile genmodel = + new MockMultipartFile( + "genmodelFiles", "model.genmodel", "application/octet-stream", "gen".getBytes()); + + byte[] result = + vsumProjectBuildService.buildProjectArchive(List.of(metamodel), List.of(genmodel), List.of()); + + assertArrayEquals(archive, result); + verify(vsumService).generateProjectArchive(anyList(), anyList(), any()); + } + + /** Ensures mismatched metamodel/genmodel upload counts are rejected. */ + @Test + void buildProjectArchiveRejectsMismatchedPairs() { + MockMultipartFile metamodel = + new MockMultipartFile( + "metamodelFiles", "model.ecore", "application/octet-stream", "meta".getBytes()); + + MethodologistSetupException exception = + assertThrows( + MethodologistSetupException.class, + () -> + vsumProjectBuildService.buildProjectArchive( + List.of(metamodel), List.of(), List.of())); + + org.junit.jupiter.api.Assertions.assertEquals("VSUM_INPUT_ERROR", exception.getErrorCode()); + } + + /** Verifies reaction imports are rewritten to the uploaded metamodel nsURI when needed. */ + @Test + void buildProjectArchiveNormalizesReactionImportUris() throws Exception { + byte[] archive = "zip-content".getBytes(); + doAnswer( + invocation -> { + @SuppressWarnings("unchecked") + List reactionFiles = invocation.getArgument(1); + String rewrittenContent = Files.readString(reactionFiles.getFirst().toPath()); + assertTrue(rewrittenContent.contains("http://example.org/model")); + return archive; + }) + .when(vsumService) + .generateProjectArchive(anyList(), anyList(), any()); + + String ecore = + "" + + ""; + String reaction = "import \"http://vitruv.tools/methodologisttemplate/model\" as m"; + + MockMultipartFile metamodel = + new MockMultipartFile("metamodelFiles", "model.ecore", "application/xml", ecore.getBytes()); + MockMultipartFile genmodel = + new MockMultipartFile( + "genmodelFiles", "model.genmodel", "application/xml", "genmodel".getBytes()); + MockMultipartFile reactionFile = + new MockMultipartFile("reactionFiles", "rules.reactions", "text/plain", reaction.getBytes()); + + vsumProjectBuildService.buildProjectArchive( + List.of(metamodel), List.of(genmodel), List.of(reactionFile)); + + verify(vsumService).generateProjectArchive(anyList(), anyList(), any()); + } +} + diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java new file mode 100644 index 0000000..3e72efa --- /dev/null +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java @@ -0,0 +1,26 @@ +package tools.vitruv.methodologist.setup.vsum.service; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class GenerateFromTemplateTest { + + @TempDir Path tempDir; + + @Test + void generateRootPomCreatesFileFromTemplate() throws Exception { + GenerateFromTemplate generator = new GenerateFromTemplate(); + Path target = tempDir.resolve("pom.xml"); + + generator.generateRootPom(target.toFile(), "tools.vitruv.generated"); + + assertTrue(Files.exists(target)); + String content = Files.readString(target); + assertTrue(content.contains("tools.vitruv.generated")); + } +} + From ef44e2044a360c7a2f2c783239b9b8d09a003c26 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:07:47 +0200 Subject: [PATCH 03/19] Write test cases for vsumService and generateProjectArchive --- .../setup/config/VitruvConfiguration.java | 5 - .../service/GenerateFromTemplateTest.java | 354 +++++++++++++++++- .../service/VsumProjectBuildServiceTest.java | 247 ++++++++++++ .../setup/vsum/service/VsumServiceTest.java | 204 ++++++++++ 4 files changed, 796 insertions(+), 14 deletions(-) create mode 100644 src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java create mode 100644 src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java diff --git a/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java index c75a410..630a9a0 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java +++ b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java @@ -60,13 +60,11 @@ public void addMetamodelLocations(MetamodelLocation metamodelLocation) { * @param paths The metamodel argument string. */ public void setMetaModelLocations(String paths) { - // Register the GenModel resource factory String nsUri = ""; Resource.Factory.Registry reg = Resource.Factory.Registry.INSTANCE; reg.getExtensionToFactoryMap().put("ecore", new XMIResourceFactoryImpl()); reg.getExtensionToFactoryMap().put("genmodel", new XMIResourceFactoryImpl()); - // Register the GenModel package GenModelPackage.eINSTANCE.eClass(); for (String modelPaths : paths.split(";")) { @@ -78,13 +76,11 @@ public void setMetaModelLocations(String paths) { String localModelDirectory = ""; - // getting the URI from the genmodels ResourceSet resourceSet = new ResourceSetImpl(); URI uri = URI.createFileURI(metamodel.getAbsolutePath().trim()); Resource resource = resourceSet.getResource(uri, true); if (!resource.getContents().isEmpty() && resource.getContents().get(0) instanceof EPackage ePackage) { - // Load the GenModel to get the modelPluginID URI genmodelURI = URI.createFileURI(genmodel.getAbsolutePath()); nsUri = genmodelURI.toString(); Resource genmodelResource = resourceSet.getResource(genmodelURI, true); @@ -124,7 +120,6 @@ public void setReactionLocations(List reactionLocations) { public static String removeLastSegment(String input) { int lastDotIndex = input.lastIndexOf('.'); if (lastDotIndex == -1) { - // No dot found, return the original string return input; } return input.substring(0, lastDotIndex); diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java index 3e72efa..86d6eae 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java @@ -1,26 +1,362 @@ package tools.vitruv.methodologist.setup.vsum.service; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import tools.vitruv.methodologist.setup.config.MetamodelLocation; +import tools.vitruv.methodologist.setup.config.VitruvConfiguration; +import tools.vitruv.methodologist.setup.exception.MissingModelException; +/** + * Unit tests for {@link GenerateFromTemplate}. + * + *

The templates are loaded from the production classpath ({@code /templates}), so every test + * exercises the real FreeMarker rendering pipeline against the bundled template files. + */ +@DisplayName("GenerateFromTemplate Tests") class GenerateFromTemplateTest { + private static final String PACKAGE_NAME = "tools.vitruv.generated"; + @TempDir Path tempDir; - @Test - void generateRootPomCreatesFileFromTemplate() throws Exception { - GenerateFromTemplate generator = new GenerateFromTemplate(); - Path target = tempDir.resolve("pom.xml"); + private GenerateFromTemplate generator; - generator.generateRootPom(target.toFile(), "tools.vitruv.generated"); + /** Creates a fresh generator instance before each test. */ + @BeforeEach + void setUp() { + generator = new GenerateFromTemplate(); + } - assertTrue(Files.exists(target)); - String content = Files.readString(target); - assertTrue(content.contains("tools.vitruv.generated")); + /** + * Reads the rendered file content for a target produced by the generator. + * + * @param target the rendered file + * @return the file content as a string + * @throws IOException when the file cannot be read + */ + private String read(Path target) throws IOException { + return Files.readString(target); } -} + @Nested + @DisplayName("generateRootPom") + class GenerateRootPom { + + /** Verifies the root pom is rendered and contains the supplied package name. */ + @Test + @DisplayName("Should render root pom containing the package name") + void rendersRootPom() throws Exception { + Path target = tempDir.resolve("pom.xml"); + + generator.generateRootPom(target.toFile(), PACKAGE_NAME); + + assertTrue(Files.exists(target)); + assertTrue(read(target).contains(PACKAGE_NAME)); + } + + /** Verifies a surrounding-whitespace package name is trimmed before rendering. */ + @Test + @DisplayName("Should trim the package name before rendering") + void trimsPackageName() throws Exception { + Path target = tempDir.resolve("pom.xml"); + + generator.generateRootPom(target.toFile(), " " + PACKAGE_NAME + " "); + + String content = read(target); + assertTrue(content.contains("" + PACKAGE_NAME + "")); + } + + /** Verifies a {@code null} package name is rejected with a {@link MissingModelException}. */ + @Test + @DisplayName("Should reject a null package name") + void rejectsNullPackageName() { + File target = tempDir.resolve("pom.xml").toFile(); + + assertThrows( + MissingModelException.class, () -> generator.generateRootPom(target, null)); + } + + /** Verifies an empty package name is rejected with a {@link MissingModelException}. */ + @Test + @DisplayName("Should reject an empty package name") + void rejectsEmptyPackageName() { + File target = tempDir.resolve("pom.xml").toFile(); + + assertThrows(MissingModelException.class, () -> generator.generateRootPom(target, "")); + } + + /** Verifies parent directories are created automatically for a nested target path. */ + @Test + @DisplayName("Should create missing parent directories") + void createsParentDirectories() throws Exception { + Path target = tempDir.resolve("nested/deep/pom.xml"); + + generator.generateRootPom(target.toFile(), PACKAGE_NAME); + + assertTrue(Files.exists(target)); + } + } + + @Nested + @DisplayName("single package-name pom generators") + class SinglePackageNamePoms { + + /** Verifies the vsum pom is rendered with the package name. */ + @Test + @DisplayName("Should render vsum pom") + void rendersVsumPom() throws Exception { + Path target = tempDir.resolve("vsum/pom.xml"); + + generator.generateVsumPom(target.toFile(), PACKAGE_NAME); + + assertTrue(read(target).contains(PACKAGE_NAME)); + } + + /** Verifies the p2wrappers pom is rendered with the package name. */ + @Test + @DisplayName("Should render p2wrappers pom") + void rendersP2WrappersPom() throws Exception { + Path target = tempDir.resolve("p2wrappers/pom.xml"); + + generator.generateP2WrappersPom(target.toFile(), PACKAGE_NAME); + + assertTrue(read(target).contains(PACKAGE_NAME)); + } + + /** Verifies the javautils pom is rendered with the package name. */ + @Test + @DisplayName("Should render javautils pom") + void rendersJavaUtilsPom() throws Exception { + Path target = tempDir.resolve("javautils/pom.xml"); + + generator.generateJavaUtilsPom(target.toFile(), PACKAGE_NAME); + + assertTrue(read(target).contains(PACKAGE_NAME)); + } + + /** Verifies the xannotations pom is rendered with the package name. */ + @Test + @DisplayName("Should render xannotations pom") + void rendersXAnnotationsPom() throws Exception { + Path target = tempDir.resolve("xannotations/pom.xml"); + + generator.generateXAnnotationsPom(target.toFile(), PACKAGE_NAME); + + assertTrue(read(target).contains(PACKAGE_NAME)); + } + + /** Verifies the emfutils pom is rendered with the package name. */ + @Test + @DisplayName("Should render emfutils pom") + void rendersEmfUtilsPom() throws Exception { + Path target = tempDir.resolve("emfutils/pom.xml"); + + generator.generateEMFUtilsPom(target.toFile(), PACKAGE_NAME); + + assertTrue(read(target).contains(PACKAGE_NAME)); + } + + /** Verifies the model pom is rendered with the package name. */ + @Test + @DisplayName("Should render model pom") + void rendersModelPom() throws Exception { + Path target = tempDir.resolve("model/pom.xml"); + + generator.generateModelPom(target.toFile(), PACKAGE_NAME); + + assertTrue(read(target).contains(PACKAGE_NAME)); + } + + /** Verifies the consistency pom is rendered with the package name. */ + @Test + @DisplayName("Should render consistency pom") + void rendersConsistencyPom() throws Exception { + Path target = tempDir.resolve("consistency/pom.xml"); + + generator.generateConsistencyPom(target.toFile(), PACKAGE_NAME); + + assertTrue(read(target).contains(PACKAGE_NAME)); + } + + /** Verifies the vsum test class is rendered with the package declaration. */ + @Test + @DisplayName("Should render vsum test class") + void rendersVsumTest() throws Exception { + Path target = tempDir.resolve("vsum/src/test/java/VSUMExampleTest.java"); + + generator.generateVsumTest(target.toFile(), PACKAGE_NAME); + + assertTrue(read(target).contains("package " + PACKAGE_NAME + ".vsum;")); + } + + /** Verifies the Eclipse {@code .project} file is rendered with the package name. */ + @Test + @DisplayName("Should render project file") + void rendersProjectFile() throws Exception { + Path target = tempDir.resolve("model/.project"); + + generator.generateProjectFile(target.toFile(), PACKAGE_NAME); + + assertTrue(read(target).contains(PACKAGE_NAME)); + } + } + + @Nested + @DisplayName("generateVsumExample") + class GenerateVsumExample { + + /** Verifies the example renders one view type entry per supplied model name. */ + @Test + @DisplayName("Should render a view type for every model name") + void rendersViewTypesPerModel() throws Exception { + Path target = tempDir.resolve("vsum/src/main/java/VSUMExample.java"); + + generator.generateVsumExample(target.toFile(), PACKAGE_NAME, List.of("Alpha", "Beta")); + + String content = read(target); + assertTrue(content.contains("package " + PACKAGE_NAME + ".vsum;")); + assertTrue(content.contains("createIdentityMappingViewType(\"Alpha\")")); + assertTrue(content.contains("createIdentityMappingViewType(\"Beta\")")); + } + + /** Verifies the example still renders when no model names are supplied. */ + @Test + @DisplayName("Should render with an empty model list") + void rendersWithEmptyModelList() throws Exception { + Path target = tempDir.resolve("vsum/src/main/java/VSUMExample.java"); + + generator.generateVsumExample(target.toFile(), PACKAGE_NAME, List.of()); + + assertTrue(read(target).contains("class VSUMExample")); + } + } + + @Nested + @DisplayName("generateMwe2") + class GenerateMwe2 { + + /** Verifies the mwe2 workflow embeds model, package and target directory information. */ + @Test + @DisplayName("Should render a component per metamodel location") + void rendersComponentsPerModel() throws Exception { + Path target = tempDir.resolve("model/workflow/generate.mwe2"); + VitruvConfiguration config = configuration(); + List models = List.of(metamodelLocation("src/main/ecore")); + + generator.generateMwe2(target.toFile(), models, config); + + String content = read(target); + assertTrue(content.contains("model.genmodel")); + assertTrue(content.contains(PACKAGE_NAME + ".model")); + assertTrue(content.contains("src/main/ecore")); + } + + /** Verifies backslashes and duplicated slashes in the model directory are normalized. */ + @Test + @DisplayName("Should normalize the model directory separators") + void normalizesModelDirectory() throws Exception { + Path target = tempDir.resolve("model/workflow/generate.mwe2"); + VitruvConfiguration config = configuration(); + List models = List.of(metamodelLocation("a\\b//c")); + + generator.generateMwe2(target.toFile(), models, config); + + String content = read(target); + assertTrue(content.contains("a/b/c")); + assertFalse(content.contains("a\\b")); + } + } + + @Nested + @DisplayName("generatePlugin") + class GeneratePlugin { + + /** Verifies the plugin descriptor embeds the model URI, package and capitalized model name. */ + @Test + @DisplayName("Should render an extension per metamodel location") + void rendersExtensionPerModel() throws Exception { + Path target = tempDir.resolve("model/plugin.xml"); + VitruvConfiguration config = configuration(); + List models = List.of(metamodelLocation("src/main/ecore")); + + generator.generatePlugin(target.toFile(), config, models); + + String content = read(target); + assertTrue(content.contains("http://example.org/model")); + assertTrue(content.contains(PACKAGE_NAME + ".model.model.ModelPackage")); + assertTrue(content.contains("src/main/ecore/model.genmodel")); + } + + /** Verifies the plugin descriptor renders without extensions when no models are supplied. */ + @Test + @DisplayName("Should render an empty plugin descriptor") + void rendersEmptyPlugin() throws Exception { + Path target = tempDir.resolve("model/plugin.xml"); + + generator.generatePlugin(target.toFile(), configuration(), List.of()); + + assertTrue(read(target).contains("")); + } + } + + @Nested + @DisplayName("error handling") + class ErrorHandling { + + /** Verifies an {@link IOException} is raised when the target path is a directory. */ + @Test + @DisplayName("Should fail when the target path is a directory") + void failsWhenTargetIsDirectory() throws Exception { + Path directoryTarget = Files.createDirectory(tempDir.resolve("collision")); + + assertThrows( + IOException.class, + () -> generator.generateVsumPom(directoryTarget.toFile(), PACKAGE_NAME)); + } + } + + /** + * Builds a configuration with a local path and package name suitable for the mwe2 and plugin + * generators. + * + * @return a populated configuration + */ + private VitruvConfiguration configuration() { + VitruvConfiguration config = new VitruvConfiguration(); + config.setLocalPath(tempDir); + config.setPackageName(PACKAGE_NAME); + return config; + } + + /** + * Builds a metamodel location whose genmodel file name is {@code model.genmodel}. + * + * @param modelDirectory the model directory used by the mwe2 generator + * @return a metamodel location + */ + private MetamodelLocation metamodelLocation(String modelDirectory) { + File metamodel = tempDir.resolve("model.ecore").toFile(); + File genmodel = tempDir.resolve("model.genmodel").toFile(); + return new MetamodelLocation(metamodel, genmodel, "http://example.org/model", modelDirectory); + } + + /** Verifies the placeholder for the unused private field assertion utility. */ + @Test + @DisplayName("Should expose a no-argument constructor") + void exposesNoArgConstructor() { + assertEquals(GenerateFromTemplate.class, new GenerateFromTemplate().getClass()); + } +} diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java new file mode 100644 index 0000000..4e18c25 --- /dev/null +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java @@ -0,0 +1,247 @@ +package tools.vitruv.methodologist.setup.vsum.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; +import tools.vitruv.methodologist.setup.exception.MethodologistSetupException; +import tools.vitruv.methodologist.setup.exception.MissingModelException; + +class VsumProjectBuildServiceTest { + + private final VsumService vsumService = mock(VsumService.class); + private final VsumProjectBuildService service = new VsumProjectBuildService(vsumService); + @TempDir java.nio.file.Path tempDir; + + @Test + void buildProjectArchive_shouldThrowWhenMetamodelFilesNull() { + + assertThatThrownBy( + () -> + service.buildProjectArchive( + null, List.of(mockMultipart("model.genmodel")), List.of())) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("At least one metamodel"); + } + + @Test + void buildProjectArchive_shouldThrowWhenGenmodelFilesNull() { + + assertThatThrownBy( + () -> + service.buildProjectArchive(List.of(mockMultipart("model.ecore")), null, List.of())) + .isInstanceOf(MethodologistSetupException.class); + } + + @Test + void buildProjectArchive_shouldThrowWhenCountsDoNotMatch() { + + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(mockMultipart("a.ecore"), mockMultipart("b.ecore")), + List.of(mockMultipart("a.genmodel")), + List.of())) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("counts must be identical"); + } + + @Test + void buildProjectArchive_shouldThrowWhenMetamodelEmpty() { + + MultipartFile empty = + new MockMultipartFile("file", "model.ecore", "application/xml", new byte[0]); + + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(empty), List.of(mockMultipart("model.genmodel")), List.of())) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("metamodel file"); + } + + @Test + void buildProjectArchive_shouldThrowWhenGenmodelEmpty() { + + MultipartFile empty = + new MockMultipartFile("file", "model.genmodel", "application/xml", new byte[0]); + + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(mockMultipart("model.ecore")), List.of(empty), List.of())) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("genmodel file"); + } + + @Test + void buildProjectArchive_shouldBuildArchive() throws Exception { + + byte[] expected = {1, 2, 3}; + + when(vsumService.generateProjectArchive(anyList(), anyList(), anyMap())).thenReturn(expected); + + byte[] archive = + service.buildProjectArchive( + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(reactionFile())); + + assertThat(archive).isEqualTo(expected); + + verify(vsumService).generateProjectArchive(anyList(), anyList(), anyMap()); + } + + @Test + void buildProjectArchive_shouldBuildArchiveWithoutReactionFiles() throws Exception { + + when(vsumService.generateProjectArchive(anyList(), anyList(), anyMap())) + .thenReturn(new byte[] {1}); + + byte[] archive = + service.buildProjectArchive( + List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), List.of()); + + assertThat(archive).isNotEmpty(); + } + + @Test + void buildProjectArchive_shouldWrapIOException() throws Exception { + + when(vsumService.generateProjectArchive(anyList(), anyList(), anyMap())) + .thenThrow(new java.io.IOException("boom")); + + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), List.of())) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("Failed to build VSUM project archive"); + } + + @Test + void buildProjectArchive_shouldWrapMissingModelException() throws Exception { + + when(vsumService.generateProjectArchive(anyList(), anyList(), anyMap())) + .thenThrow(new MissingModelException("missing")); + + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), List.of())) + .isInstanceOf(MethodologistSetupException.class); + } + + @Test + void buildProjectArchive_shouldWrapInterruptedException() throws Exception { + + when(vsumService.generateProjectArchive(anyList(), anyList(), anyMap())) + .thenThrow(new InterruptedException()); + + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), List.of())) + .isInstanceOf(MethodologistSetupException.class); + + assertThat(Thread.currentThread().isInterrupted()).isTrue(); + + Thread.interrupted(); + } + + @Test + void extractMetamodelNamespaceMap_shouldExtractNamespaceInformation() { + + File ecore = + writeTempEcore( + """ + + + """); + + VsumService.ModelFiles pair = + new VsumService.ModelFiles(ecore, tempDir.resolve("test.genmodel").toFile()); + + Map result = service.extractMetamodelNamespaceMap(List.of(pair)); + + assertThat(result).containsEntry("pcm", "http://pcm"); + } + + @Test + void extractMetamodelNamespaceMap_shouldIgnoreInvalidMetamodel() { + + File invalid = writeTempFile("broken.ecore", "not xml"); + + VsumService.ModelFiles pair = + new VsumService.ModelFiles(invalid, tempDir.resolve("test.genmodel").toFile()); + + Map result = service.extractMetamodelNamespaceMap(List.of(pair)); + + assertThat(result).isEmpty(); + } + + private MockMultipartFile mockMultipart(String filename) { + + return new MockMultipartFile( + "file", filename, "application/octet-stream", "content".getBytes(StandardCharsets.UTF_8)); + } + + private MockMultipartFile reactionFile() { + + return new MockMultipartFile( + "file", + "sample.reactions", + "text/plain", + """ + import "pcm" + """ + .getBytes(StandardCharsets.UTF_8)); + } + + private MockMultipartFile validMetamodel() { + + return new MockMultipartFile( + "file", + "model.ecore", + "application/xml", + """ + + + """ + .getBytes(StandardCharsets.UTF_8)); + } + + private File writeTempEcore(String content) { + + return writeTempFile("model.ecore", content); + } + + private File writeTempFile(String name, String content) { + + try { + File file = tempDir.resolve(name).toFile(); + java.nio.file.Files.writeString(file.toPath(), content); + return file; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java new file mode 100644 index 0000000..1b68d14 --- /dev/null +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java @@ -0,0 +1,204 @@ +package tools.vitruv.methodologist.setup.vsum.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class VsumServiceTest { + + private final GenerateFromTemplate generateFromTemplate = mock(GenerateFromTemplate.class); + @TempDir Path tempDir; + + @Test + void generateProjectArchive_shouldThrowWhenModelFilesNull() { + VsumService service = spy(new VsumService(generateFromTemplate)); + + assertThatThrownBy(() -> service.generateProjectArchive(null, List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("At least one metamodel/genmodel pair"); + } + + @Test + void generateProjectArchive_shouldThrowWhenModelFilesEmpty() { + VsumService service = spy(new VsumService(generateFromTemplate)); + + assertThatThrownBy(() -> service.generateProjectArchive(List.of(), List.of())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void generateProjectArchive_shouldThrowWhenMetamodelMissing() throws IOException { + File missing = tempDir.resolve("missing.ecore").toFile(); + + File genmodel = Files.createFile(tempDir.resolve("model.genmodel")).toFile(); + + VsumService.ModelFiles modelFiles = new VsumService.ModelFiles(missing, genmodel); + + VsumService service = spy(new VsumService(generateFromTemplate)); + + assertThatThrownBy(() -> service.generateProjectArchive(List.of(modelFiles), List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("metamodelFile does not exist"); + } + + @Test + void generateProjectArchive_shouldThrowWhenReactionFileMissing() throws IOException { + File ecore = Files.createFile(tempDir.resolve("model.ecore")).toFile(); + File genmodel = Files.createFile(tempDir.resolve("model.genmodel")).toFile(); + + File missingReaction = tempDir.resolve("missing.reactions").toFile(); + + VsumService.ModelFiles modelFiles = new VsumService.ModelFiles(ecore, genmodel); + + VsumService service = spy(new VsumService(generateFromTemplate)); + + assertThatThrownBy( + () -> service.generateProjectArchive(List.of(modelFiles), List.of(missingReaction))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("reactionFile does not exist"); + } + + @Test + void generateProjectArchive_shouldGenerateArchive() throws Exception { + + File ecore = Files.createFile(tempDir.resolve("model.ecore")).toFile(); + Files.writeString( + ecore.toPath(), + """ + + + + """); + File genmodel = Files.createFile(tempDir.resolve("model.genmodel")).toFile(); + Files.writeString( + genmodel.toPath(), + """ + + + + """); + File reaction = Files.createFile(tempDir.resolve("sample.reactions")).toFile(); + + VsumService.ModelFiles modelFiles = new VsumService.ModelFiles(ecore, genmodel); + + VsumService service = spy(new VsumService(generateFromTemplate)); + + doAnswer( + invocation -> { + Path workspace = ((File) invocation.getArgument(0)).toPath().getParent().getParent(); + + Files.createDirectories(workspace); + return null; + }) + .when(generateFromTemplate) + .generateRootPom(any(File.class), anyString()); + + doNothing().when(service).runMavenBuild(any(Path.class), anyMap()); + + byte[] archive = + service.generateProjectArchive( + List.of(modelFiles), List.of(reaction), Map.of("pcm", "http://pcm/ns")); + + assertThat(archive).isNotNull(); + assertThat(archive.length).isGreaterThan(0); + + verify(service).runMavenBuild(any(Path.class), eq(Map.of("pcm", "http://pcm/ns"))); + } + + @Test + void generateProjectArchive_shouldDelegateToThreeArgumentMethod() throws Exception { + + File ecore = mock(File.class); + File genmodel = mock(File.class); + + VsumService.ModelFiles modelFiles = new VsumService.ModelFiles(ecore, genmodel); + + VsumService service = spy(new VsumService(generateFromTemplate)); + + byte[] expected = {1, 2, 3}; + + doReturn(expected) + .when(service) + .generateProjectArchive(eq(List.of(modelFiles)), eq(List.of()), eq(Map.of())); + + byte[] result = service.generateProjectArchive(List.of(modelFiles), List.of()); + + assertThat(result).isEqualTo(expected); + } + + @Test + void modelFiles_shouldRejectNullMetamodel() { + File genmodel = new File("test.genmodel"); + + assertThatThrownBy(() -> new VsumService.ModelFiles(null, genmodel)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("metamodelFile"); + } + + @Test + void modelFiles_shouldRejectNullGenmodel() { + File ecore = new File("test.ecore"); + + assertThatThrownBy(() -> new VsumService.ModelFiles(ecore, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("genmodelFile"); + } + + @Test + void runMavenBuild_shouldFailWhenExitCodeNonZero() throws Exception { + + VsumService service = new VsumService(generateFromTemplate); + + Path projectRoot = tempDir; + + assertThatThrownBy(() -> service.runMavenBuild(projectRoot, Map.of("invalid", "value"))) + .isInstanceOf(IOException.class); + } + + @Test + void createMavenProcessBuilderNamespaceProperties_shouldBePassed() throws Exception { + + VsumService service = new VsumService(generateFromTemplate); + + var method = + VsumService.class.getDeclaredMethod("createMavenProcessBuilder", Path.class, Map.class); + + method.setAccessible(true); + + ProcessBuilder builder = + (ProcessBuilder) + method.invoke( + service, + tempDir, + Map.of( + "pcm", "http://pcm", + "uml", "http://uml")); + + assertThat(builder.command()) + .contains("-Dmetamodel.pcm.nsuri=http://pcm") + .contains("-Dmetamodel.uml.nsuri=http://uml"); + + assertThat(builder.directory()).isEqualTo(tempDir.toFile()); + } +} From 335f117d8bb110a0fdb66e1c4802ab71448945f7 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:37:20 +0200 Subject: [PATCH 04/19] reformat --- .../setup/config/MetamodelLocation.java | 7 +- .../setup/config/VitruvConfiguration.java | 83 +++++++++---------- .../methodologist/setup/util/FileUtils.java | 4 +- .../setup/vsum/controller/VsumController.java | 8 +- .../vsum/service/GenerateFromTemplate.java | 4 +- .../vsum/service/VsumProjectBuildService.java | 20 ++--- .../setup/vsum/service/VsumService.java | 17 ++-- .../vsum/controller/VsumControllerTest.java | 4 +- .../service/GenerateFromTemplateTest.java | 64 +++++++------- 9 files changed, 102 insertions(+), 109 deletions(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/config/MetamodelLocation.java b/src/main/java/tools/vitruv/methodologist/setup/config/MetamodelLocation.java index 67793d3..ad11253 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/config/MetamodelLocation.java +++ b/src/main/java/tools/vitruv/methodologist/setup/config/MetamodelLocation.java @@ -3,9 +3,8 @@ import java.io.File; /** - * The MetamodelLocation class is used to store the location of a metamodel and - * its corresponding + * The MetamodelLocation class is used to store the location of a metamodel and its corresponding * genmodel. */ -public record MetamodelLocation(File metamodel, File genmodel, String genmodelUri, String modelDirectory) { -} +public record MetamodelLocation( + File metamodel, File genmodel, String genmodelUri, String modelDirectory) {} diff --git a/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java index 630a9a0..e686b8d 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java +++ b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java @@ -20,16 +20,19 @@ @Slf4j public class VitruvConfiguration { + /** -- GETTER -- Returns the model names. */ + @Getter private final List modelNames = new ArrayList<>(); + private final List metamodelLocations = new ArrayList<>(); + /** -- GETTER -- Returns the reaction file locations used by the CLI. */ + @Getter private final List reactionLocations = new ArrayList<>(); /** * -- SETTER -- Sets the local path of the configuration. * *

-- GETTER -- Returns the local path of the configuration. */ @Getter @Setter private Path localPath; - /** -- GETTER -- Returns the package name. */ @Getter private String packageName; - /** * -- SETTER -- Sets the workflow of the configuration. * @@ -37,13 +40,19 @@ public class VitruvConfiguration { */ @Getter @Setter private File workflow; - /** -- GETTER -- Returns the model names. */ - @Getter private final List modelNames = new ArrayList<>(); - - private final List metamodelLocations = new ArrayList<>(); - - /** -- GETTER -- Returns the reaction file locations used by the CLI. */ - @Getter private final List reactionLocations = new ArrayList<>(); + /** + * Removes the last segment of a string. + * + * @param input The input string. + * @return The input string without the last segment. + */ + public static String removeLastSegment(String input) { + int lastDotIndex = input.lastIndexOf('.'); + if (lastDotIndex == -1) { + return input; + } + return input.substring(0, lastDotIndex); + } /** * Adds a metamodel location to the configuration. @@ -54,6 +63,27 @@ public void addMetamodelLocations(MetamodelLocation metamodelLocation) { this.metamodelLocations.add(metamodelLocation); } + /** + * Sets the reaction file locations used by the CLI. + * + * @param reactionLocations list of paths to reaction files. + */ + public void setReactionLocations(List reactionLocations) { + this.reactionLocations.clear(); + if (reactionLocations != null) { + this.reactionLocations.addAll(reactionLocations); + } + } + + /** + * Returns the metamodel locations. + * + * @return The metamodel locations. + */ + public List getMetaModelLocations() { + return this.metamodelLocations; + } + /** * Sets the metamodel locations using a semicolon-separated list of {@code ecore,genmodel} pairs. * @@ -99,41 +129,6 @@ public void setMetaModelLocations(String paths) { } } - /** - * Sets the reaction file locations used by the CLI. - * - * @param reactionLocations list of paths to reaction files. - */ - public void setReactionLocations(List reactionLocations) { - this.reactionLocations.clear(); - if (reactionLocations != null) { - this.reactionLocations.addAll(reactionLocations); - } - } - - /** - * Removes the last segment of a string. - * - * @param input The input string. - * @return The input string without the last segment. - */ - public static String removeLastSegment(String input) { - int lastDotIndex = input.lastIndexOf('.'); - if (lastDotIndex == -1) { - return input; - } - return input.substring(0, lastDotIndex); - } - - /** - * Returns the metamodel locations. - * - * @return The metamodel locations. - */ - public List getMetaModelLocations() { - return this.metamodelLocations; - } - /** * Sets the package name. * diff --git a/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java b/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java index 18e40e0..83580d7 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java +++ b/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java @@ -16,8 +16,6 @@ @Slf4j public final class FileUtils { - private FileUtils() {} - /** * The CLASS_LOADER is used to load classes from JAR files at runtime. It is used to load the * classes of the virtual model builder. @@ -25,6 +23,8 @@ private FileUtils() {} public static final CustomClassLoader CLASS_LOADER = new CustomClassLoader(new URL[] {}, ClassLoader.getSystemClassLoader()); + private FileUtils() {} + /** * Copy a file to a new location. * diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java index a52125e..06bed3f 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java @@ -52,10 +52,11 @@ public class VsumController { @ApiResponse(responseCode = "500", description = "Build failed") }) public ResponseEntity buildProject( - @Parameter(description = "Metamodel files", required = true) - @RequestPart("metamodelFiles") + @Parameter(description = "Metamodel files", required = true) @RequestPart("metamodelFiles") List metamodelFiles, - @Parameter(description = "Genmodel files paired by index with metamodel files", required = true) + @Parameter( + description = "Genmodel files paired by index with metamodel files", + required = true) @RequestPart("genmodelFiles") List genmodelFiles, @Parameter(description = "Optional reaction files") @@ -86,4 +87,3 @@ private String generateArchiveFilename() { return "vsum-project-" + timestamp + ".zip"; } } - diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java index c65cb56..2ec4296 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java @@ -23,11 +23,11 @@ @Slf4j @Service public class GenerateFromTemplate { + private static final String PACKAGE_NAME = "packageName"; + /** Constructor. */ public GenerateFromTemplate() {} - private static final String PACKAGE_NAME = "packageName"; - private Configuration getConfiguration() { Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); cfg.setDefaultEncoding("UTF-8"); diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java index 5ab4ffb..d7a8546 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -243,16 +243,6 @@ private MetamodelInfo readMetamodelInfo(File metamodelFile) { } } - private static class MetamodelInfo { - private final String packageName; - private final String nsUri; - - private MetamodelInfo(String packageName, String nsUri) { - this.packageName = packageName; - this.nsUri = nsUri; - } - } - /** * Writes a multipart file to disk. * @@ -316,5 +306,15 @@ private void deleteQuietly(Path path) { } catch (IOException ignored) { } } + + private static class MetamodelInfo { + private final String packageName; + private final String nsUri; + + private MetamodelInfo(String packageName, String nsUri) { + this.packageName = packageName; + this.nsUri = nsUri; + } + } } diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java index 3fe913e..319b7c1 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java @@ -5,8 +5,8 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -21,7 +21,6 @@ import java.util.zip.ZipOutputStream; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; - import tools.vitruv.methodologist.setup.config.VitruvConfiguration; import tools.vitruv.methodologist.setup.exception.MissingModelException; @@ -32,13 +31,6 @@ public class VsumService { private final GenerateFromTemplate generateFromTemplate; - public record ModelFiles(File metamodelFile, File genmodelFile) { - public ModelFiles { - Objects.requireNonNull(metamodelFile, "metamodelFile must not be null"); - Objects.requireNonNull(genmodelFile, "genmodelFile must not be null"); - } - } - /** * Generates a full VSUM project, builds it with Maven, and returns the project archive as bytes. * @@ -301,4 +293,11 @@ private void deleteRecursively(Path root) { // Best-effort cleanup; ignore cleanup failures. } } + + public record ModelFiles(File metamodelFile, File genmodelFile) { + public ModelFiles { + Objects.requireNonNull(metamodelFile, "metamodelFile must not be null"); + Objects.requireNonNull(genmodelFile, "genmodelFile must not be null"); + } + } } diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java index 9eb3cc4..e036f1f 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java @@ -1,10 +1,10 @@ package tools.vitruv.methodologist.setup.vsum.controller; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.anyList; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java index 86d6eae..6255b74 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java @@ -51,6 +51,38 @@ private String read(Path target) throws IOException { return Files.readString(target); } + /** + * Builds a configuration with a local path and package name suitable for the mwe2 and plugin + * generators. + * + * @return a populated configuration + */ + private VitruvConfiguration configuration() { + VitruvConfiguration config = new VitruvConfiguration(); + config.setLocalPath(tempDir); + config.setPackageName(PACKAGE_NAME); + return config; + } + + /** + * Builds a metamodel location whose genmodel file name is {@code model.genmodel}. + * + * @param modelDirectory the model directory used by the mwe2 generator + * @return a metamodel location + */ + private MetamodelLocation metamodelLocation(String modelDirectory) { + File metamodel = tempDir.resolve("model.ecore").toFile(); + File genmodel = tempDir.resolve("model.genmodel").toFile(); + return new MetamodelLocation(metamodel, genmodel, "http://example.org/model", modelDirectory); + } + + /** Verifies the placeholder for the unused private field assertion utility. */ + @Test + @DisplayName("Should expose a no-argument constructor") + void exposesNoArgConstructor() { + assertEquals(GenerateFromTemplate.class, new GenerateFromTemplate().getClass()); + } + @Nested @DisplayName("generateRootPom") class GenerateRootPom { @@ -327,36 +359,4 @@ void failsWhenTargetIsDirectory() throws Exception { () -> generator.generateVsumPom(directoryTarget.toFile(), PACKAGE_NAME)); } } - - /** - * Builds a configuration with a local path and package name suitable for the mwe2 and plugin - * generators. - * - * @return a populated configuration - */ - private VitruvConfiguration configuration() { - VitruvConfiguration config = new VitruvConfiguration(); - config.setLocalPath(tempDir); - config.setPackageName(PACKAGE_NAME); - return config; - } - - /** - * Builds a metamodel location whose genmodel file name is {@code model.genmodel}. - * - * @param modelDirectory the model directory used by the mwe2 generator - * @return a metamodel location - */ - private MetamodelLocation metamodelLocation(String modelDirectory) { - File metamodel = tempDir.resolve("model.ecore").toFile(); - File genmodel = tempDir.resolve("model.genmodel").toFile(); - return new MetamodelLocation(metamodel, genmodel, "http://example.org/model", modelDirectory); - } - - /** Verifies the placeholder for the unused private field assertion utility. */ - @Test - @DisplayName("Should expose a no-argument constructor") - void exposesNoArgConstructor() { - assertEquals(GenerateFromTemplate.class, new GenerateFromTemplate().getClass()); - } } From d6da0fe2b904b039588af78daf09394c87dc9fbc Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:42:36 +0200 Subject: [PATCH 05/19] reformat --- pom.xml | 2 +- .../setup/config/VitruvConfiguration.java | 5 ++++ .../vsum/service/GenerateFromTemplate.java | 3 ++- .../vsum/service/VsumProjectBuildService.java | 27 ++++++++++--------- .../setup/vsum/service/VsumService.java | 12 ++++----- .../vsum/controller/VsumControllerTest.java | 7 ++--- .../service/GenerateFromTemplateTest.java | 3 +-- 7 files changed, 34 insertions(+), 25 deletions(-) diff --git a/pom.xml b/pom.xml index 2beb5b0..1943c08 100644 --- a/pom.xml +++ b/pom.xml @@ -113,7 +113,7 @@ - 1.22.0 + 1.28.0 diff --git a/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java index e686b8d..6a908c8 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java +++ b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java @@ -22,17 +22,22 @@ public class VitruvConfiguration { /** -- GETTER -- Returns the model names. */ @Getter private final List modelNames = new ArrayList<>(); + private final List metamodelLocations = new ArrayList<>(); + /** -- GETTER -- Returns the reaction file locations used by the CLI. */ @Getter private final List reactionLocations = new ArrayList<>(); + /** * -- SETTER -- Sets the local path of the configuration. * *

-- GETTER -- Returns the local path of the configuration. */ @Getter @Setter private Path localPath; + /** -- GETTER -- Returns the package name. */ @Getter private String packageName; + /** * -- SETTER -- Sets the workflow of the configuration. * diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java index 2ec4296..ea5b4cb 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java @@ -46,7 +46,8 @@ private void writeTemplate(Template template, File filePath, Map fileWriter.flush(); log.info("writing to " + filePath.getAbsolutePath()); } catch (TemplateException e) { - throw new IOException("Failed to process template for file: " + filePath.getAbsolutePath(), e); + throw new IOException( + "Failed to process template for file: " + filePath.getAbsolutePath(), e); } } diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java index d7a8546..21ab6da 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -57,7 +57,8 @@ public byte[] buildProjectArchive( List copiedReactionFiles = toReactionFiles(uploadWorkspace, reactionFiles); normalizeReactionImports(modelPairs, copiedReactionFiles); Map metamodelNamespaceMap = extractMetamodelNamespaceMap(modelPairs); - return vsumService.generateProjectArchive(modelPairs, copiedReactionFiles, metamodelNamespaceMap); + return vsumService.generateProjectArchive( + modelPairs, copiedReactionFiles, metamodelNamespaceMap); } catch (IOException | MissingModelException e) { throw new MethodologistSetupException( VSUM_BUILD_ERROR_CODE, "Failed to build VSUM project archive", e); @@ -76,7 +77,8 @@ public byte[] buildProjectArchive( * @param metamodelFiles metamodel files * @param genmodelFiles genmodel files */ - private void validateInputs(List metamodelFiles, List genmodelFiles) { + private void validateInputs( + List metamodelFiles, List genmodelFiles) { if (metamodelFiles == null || genmodelFiles == null || metamodelFiles.isEmpty() @@ -115,8 +117,10 @@ private List toModelPairs( List modelPairs = new ArrayList<>(); for (int index = 0; index < metamodelFiles.size(); index++) { - File metamodel = writeMultipartFile(metamodelFiles.get(index), modelUploadPath, "metamodel", index); - File genmodel = writeMultipartFile(genmodelFiles.get(index), modelUploadPath, "genmodel", index); + File metamodel = + writeMultipartFile(metamodelFiles.get(index), modelUploadPath, "metamodel", index); + File genmodel = + writeMultipartFile(genmodelFiles.get(index), modelUploadPath, "genmodel", index); modelPairs.add(new VsumService.ModelFiles(metamodel, genmodel)); } @@ -131,7 +135,8 @@ private List toModelPairs( * @return copied reaction files * @throws IOException when writing files fails */ - private List toReactionFiles(Path root, List reactionFiles) throws IOException { + private List toReactionFiles(Path root, List reactionFiles) + throws IOException { if (reactionFiles == null || reactionFiles.isEmpty()) { return List.of(); } @@ -152,8 +157,8 @@ private List toReactionFiles(Path root, List reactionFiles) * @param modelPairs metamodel/genmodel file pairs * @param reactionFiles copied reaction files */ - private void normalizeReactionImports(List modelPairs, List reactionFiles) - throws IOException { + private void normalizeReactionImports( + List modelPairs, List reactionFiles) throws IOException { if (reactionFiles == null || reactionFiles.isEmpty()) { return; } @@ -190,9 +195,7 @@ Map extractMetamodelNamespaceMap(List mo for (VsumService.ModelFiles pair : modelPairs) { MetamodelInfo info = readMetamodelInfo(pair.metamodelFile()); if (info != null && info.nsUri != null && !info.nsUri.isBlank()) { - modelNameToNsUri.put( - info.packageName != null ? info.packageName : "model", - info.nsUri); + modelNameToNsUri.put(info.packageName != null ? info.packageName : "model", info.nsUri); } } return modelNameToNsUri; @@ -213,7 +216,8 @@ private String normalizeReactionImports( } } - matcher.appendReplacement(rewritten, "import \"" + Matcher.quoteReplacement(replacementUri) + "\""); + matcher.appendReplacement( + rewritten, "import \"" + Matcher.quoteReplacement(replacementUri) + "\""); } matcher.appendTail(rewritten); return rewritten.toString(); @@ -317,4 +321,3 @@ private MetamodelInfo(String packageName, String nsUri) { } } } - diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java index 319b7c1..7cd62c3 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java @@ -43,7 +43,9 @@ public class VsumService { * @throws MissingModelException when the model configuration is invalid */ public byte[] generateProjectArchive( - List modelFiles, List reactionFiles, Map metamodelNamespaceMap) + List modelFiles, + List reactionFiles, + Map metamodelNamespaceMap) throws IOException, InterruptedException, MissingModelException { validateInputs(modelFiles, reactionFiles); @@ -217,17 +219,15 @@ protected void runMavenBuild(Path projectRoot, Map metamodelName StringWriter output = new StringWriter(); try (BufferedReader reader = - new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { reader.transferTo(output); } process.waitFor(); if (process.exitValue() != 0) { throw new IOException( - "Maven build failed with exit code " - + process.exitValue() - + ". Output: " - + output); + "Maven build failed with exit code " + process.exitValue() + ". Output: " + output); } } diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java index e036f1f..755293f 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java @@ -43,7 +43,8 @@ void buildProjectArchiveDelegatesToVsumService() throws Exception { "genmodelFiles", "model.genmodel", "application/octet-stream", "gen".getBytes()); byte[] result = - vsumProjectBuildService.buildProjectArchive(List.of(metamodel), List.of(genmodel), List.of()); + vsumProjectBuildService.buildProjectArchive( + List.of(metamodel), List.of(genmodel), List.of()); assertArrayEquals(archive, result); verify(vsumService).generateProjectArchive(anyList(), anyList(), any()); @@ -93,7 +94,8 @@ void buildProjectArchiveNormalizesReactionImportUris() throws Exception { new MockMultipartFile( "genmodelFiles", "model.genmodel", "application/xml", "genmodel".getBytes()); MockMultipartFile reactionFile = - new MockMultipartFile("reactionFiles", "rules.reactions", "text/plain", reaction.getBytes()); + new MockMultipartFile( + "reactionFiles", "rules.reactions", "text/plain", reaction.getBytes()); vsumProjectBuildService.buildProjectArchive( List.of(metamodel), List.of(genmodel), List.of(reactionFile)); @@ -101,4 +103,3 @@ void buildProjectArchiveNormalizesReactionImportUris() throws Exception { verify(vsumService).generateProjectArchive(anyList(), anyList(), any()); } } - diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java index 6255b74..c2ad7a4 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplateTest.java @@ -117,8 +117,7 @@ void trimsPackageName() throws Exception { void rejectsNullPackageName() { File target = tempDir.resolve("pom.xml").toFile(); - assertThrows( - MissingModelException.class, () -> generator.generateRootPom(target, null)); + assertThrows(MissingModelException.class, () -> generator.generateRootPom(target, null)); } /** Verifies an empty package name is rejected with a {@link MissingModelException}. */ From 8785967cd80bf781784021e33d6ae3d9f8add2f1 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:56:34 +0200 Subject: [PATCH 06/19] reformat --- .../exception/MissingModelException.java | 13 +-- .../vsum/service/VsumProjectBuildService.java | 33 ++++++ .../setup/vsum/service/VsumService.java | 107 ++++++++++++++++++ .../setup/vsum/service/VsumServiceTest.java | 12 +- 4 files changed, 152 insertions(+), 13 deletions(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java b/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java index e2be40d..ce01a1f 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java +++ b/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java @@ -3,21 +3,12 @@ /** Exception thrown when a required model is missing. */ public class MissingModelException extends Exception { - /** - * Exception thrown when a required model is missing. - * - * @param message - */ + /** Exception thrown when a required model is missing. */ public MissingModelException(String message) { super(message); } - /** - * Exception thrown when a required model is missing. - * - * @param message - * @param cause - */ + /** Exception thrown when a required model is missing. */ public MissingModelException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java index 21ab6da..dacfda6 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -156,6 +156,7 @@ private List toReactionFiles(Path root, List reactionFiles) * * @param modelPairs metamodel/genmodel file pairs * @param reactionFiles copied reaction files + * @throws IOException when reading or rewriting a reaction file fails */ private void normalizeReactionImports( List modelPairs, List reactionFiles) throws IOException { @@ -201,6 +202,16 @@ Map extractMetamodelNamespaceMap(List mo return modelNameToNsUri; } + /** + * Rewrites each {@code import "..."} statement in a single reaction file's content, replacing + * imports that do not match a known nsURI with the nsURI mapped from the import's last path + * segment. + * + * @param content the reaction file content to rewrite + * @param packageNameToNsUri map of metamodel package names to their nsURIs + * @param knownNsUris the set of nsURIs that are already valid and should be left unchanged + * @return the rewritten content, identical to the input when no import required replacement + */ private String normalizeReactionImports( String content, Map packageNameToNsUri, Set knownNsUris) { Matcher matcher = REACTION_IMPORT_PATTERN.matcher(content); @@ -223,6 +234,13 @@ private String normalizeReactionImports( return rewritten.toString(); } + /** + * Extracts the last segment of a URI, i.e. the substring following the final {@code /} or {@code + * #}. + * + * @param uri the URI to inspect + * @return the substring after the last separator, or the whole URI when no separator is present + */ private String extractLastSegment(String uri) { int slash = uri.lastIndexOf('/'); int hash = uri.lastIndexOf('#'); @@ -230,6 +248,14 @@ private String extractLastSegment(String uri) { return separator >= 0 ? uri.substring(separator + 1) : uri; } + /** + * Parses an ecore metamodel file and extracts the root element's {@code name} and {@code nsURI} + * attributes. + * + * @param metamodelFile the metamodel (ecore) file to parse + * @return the extracted metamodel info, or {@code null} when the file cannot be parsed or its + * nsURI is missing or blank + */ private MetamodelInfo readMetamodelInfo(File metamodelFile) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); @@ -311,10 +337,17 @@ private void deleteQuietly(Path path) { } } + /** Immutable holder for a metamodel's package name and nsURI. */ private static class MetamodelInfo { private final String packageName; private final String nsUri; + /** + * Creates a holder for the given metamodel attributes. + * + * @param packageName the metamodel's package name (root element {@code name} attribute) + * @param nsUri the metamodel's namespace URI (root element {@code nsURI} attribute) + */ private MetamodelInfo(String packageName, String nsUri) { this.packageName = packageName; this.nsUri = nsUri; diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java index 7cd62c3..8355ebd 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java @@ -83,6 +83,13 @@ public byte[] generateProjectArchive(List modelFiles, List rea return generateProjectArchive(modelFiles, reactionFiles, Map.of()); } + /** + * Validates that the supplied model and reaction files are present and usable. + * + * @param modelFiles paired metamodel/genmodel files; must contain at least one pair + * @param reactionFiles optional reaction files; may be {@code null} + * @throws IllegalArgumentException when no model pairs are provided or any file is invalid + */ private void validateInputs(List modelFiles, List reactionFiles) { if (modelFiles == null || modelFiles.isEmpty()) { throw new IllegalArgumentException("At least one metamodel/genmodel pair is required."); @@ -100,6 +107,13 @@ private void validateInputs(List modelFiles, List reactionFile } } + /** + * Validates that a single file is non-null and refers to an existing regular file. + * + * @param file the file to validate + * @param label human-readable name used in error messages + * @throws IllegalArgumentException when the file is null, missing, or not a regular file + */ private void validateFile(File file, String label) { if (file == null) { throw new IllegalArgumentException(label + " must not be null."); @@ -112,6 +126,14 @@ private void validateFile(File file, String label) { } } + /** + * Copies each metamodel/genmodel pair into the workspace's ecore source directory. + * + * @param workspace root directory of the generated project + * @param modelFiles paired metamodel/genmodel files to copy + * @return the copied pairs pointing at their new locations within the workspace + * @throws IOException when creating the target directory or copying a file fails + */ private List copyModelFiles(Path workspace, List modelFiles) throws IOException { Path targetDirectory = workspace.resolve("model/src/main/ecore"); @@ -126,6 +148,14 @@ private List copyModelFiles(Path workspace, List modelFi return copied; } + /** + * Copies any reaction files into the workspace's reactions source directory. + * + * @param workspace root directory of the generated project + * @param reactionFiles reaction files to copy; may be {@code null} or empty + * @return the paths of the copied reaction files, or an empty list when none were provided + * @throws IOException when creating the target directory or copying a file fails + */ private List copyReactionFiles(Path workspace, List reactionFiles) throws IOException { if (reactionFiles == null || reactionFiles.isEmpty()) { @@ -142,12 +172,29 @@ private List copyReactionFiles(Path workspace, List reactionFiles) return copied; } + /** + * Copies a single file into the target directory, overwriting any existing file with the same + * name. + * + * @param source the file to copy + * @param targetDirectory the directory to copy the file into + * @return the copied file at its new location + * @throws IOException when the copy operation fails + */ private File copyFile(File source, Path targetDirectory) throws IOException { Path target = targetDirectory.resolve(source.getName()); Files.copy(source.toPath(), target, StandardCopyOption.REPLACE_EXISTING); return target.toFile(); } + /** + * Builds the metamodel locations string consumed by the Vitruv configuration, pairing each + * metamodel with its genmodel. + * + * @param copiedModelFiles the copied metamodel/genmodel pairs + * @return a string of comma-separated metamodel/genmodel paths, with pairs separated by + * semicolons + */ private String buildModelLocations(List copiedModelFiles) { return copiedModelFiles.stream() .map( @@ -158,6 +205,15 @@ private String buildModelLocations(List copiedModelFiles) { .collect(Collectors.joining(";")); } + /** + * Generates all project files from templates, including the POMs for each module, the VSUM + * example and test sources, the Eclipse project descriptor, the MWE2 workflow, and the plugin + * descriptor. + * + * @param configuration the Vitruv configuration describing the project to generate + * @throws IOException when writing a generated file fails + * @throws MissingModelException when the model configuration is invalid + */ private void generateProjectFiles(VitruvConfiguration configuration) throws IOException, MissingModelException { generateFromTemplate.generateRootPom( @@ -208,10 +264,27 @@ private void generateProjectFiles(VitruvConfiguration configuration) configuration.getMetaModelLocations()); } + /** + * Runs the Maven build for the generated project without any metamodel namespace properties. + * + * @param projectRoot root directory of the generated project + * @throws IOException when the build fails or its output cannot be read + * @throws InterruptedException when the build process is interrupted while waiting + */ protected void runMavenBuild(Path projectRoot) throws IOException, InterruptedException { runMavenBuild(projectRoot, Map.of()); } + /** + * Runs the Maven {@code clean verify} build for the generated project, passing metamodel + * namespace information as system properties, and fails when the build exits with a non-zero + * status. + * + * @param projectRoot root directory of the generated project + * @param metamodelNamespaceMap map of model names to nsURIs passed as build properties + * @throws IOException when the build fails or its output cannot be read + * @throws InterruptedException when the build process is interrupted while waiting + */ protected void runMavenBuild(Path projectRoot, Map metamodelNamespaceMap) throws IOException, InterruptedException { ProcessBuilder processBuilder = createMavenProcessBuilder(projectRoot, metamodelNamespaceMap); @@ -231,6 +304,15 @@ protected void runMavenBuild(Path projectRoot, Map metamodelName } } + /** + * Creates the {@link ProcessBuilder} for the Maven build, appending each metamodel namespace + * entry as a {@code -Dmetamodel..nsuri=} system property and redirecting the error + * stream into standard output. + * + * @param projectRoot root directory of the generated project, used as the working directory + * @param metamodelNamespaceMap map of model names to nsURIs to append as system properties + * @return a configured process builder ready to be started + */ private ProcessBuilder createMavenProcessBuilder( Path projectRoot, Map metamodelNamespaceMap) { ProcessBuilder processBuilder = new ProcessBuilder("mvn", "clean", "verify", "-DskipTests"); @@ -249,6 +331,14 @@ private ProcessBuilder createMavenProcessBuilder( return processBuilder; } + /** + * Zips the entire contents of the given directory into an in-memory archive, preserving the + * directory structure with forward-slash separators and deterministic entry ordering. + * + * @param root the directory whose contents should be archived + * @return the bytes of the resulting zip archive + * @throws IOException when walking the directory tree or writing an entry fails + */ private byte[] zipDirectory(Path root) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream); @@ -273,6 +363,12 @@ private byte[] zipDirectory(Path root) throws IOException { return outputStream.toByteArray(); } + /** + * Recursively deletes the given directory and all of its contents on a best-effort basis, + * silently ignoring any individual deletion failures. + * + * @param root the directory to delete; ignored when {@code null} or non-existent + */ private void deleteRecursively(Path root) { if (root == null || !Files.exists(root)) { return; @@ -294,7 +390,18 @@ private void deleteRecursively(Path root) { } } + /** + * A paired metamodel (ecore) file and its corresponding genmodel file. + * + * @param metamodelFile the metamodel (ecore) file; must not be {@code null} + * @param genmodelFile the genmodel file; must not be {@code null} + */ public record ModelFiles(File metamodelFile, File genmodelFile) { + /** + * Validates that both files are present. + * + * @throws NullPointerException when either file is {@code null} + */ public ModelFiles { Objects.requireNonNull(metamodelFile, "metamodelFile must not be null"); Objects.requireNonNull(genmodelFile, "genmodelFile must not be null"); diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java index 1b68d14..a8303c4 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java @@ -2,8 +2,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import java.io.File; import java.io.IOException; From ece8b8974c19d54658ff7c1af920c080628c23f4 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:05:03 +0200 Subject: [PATCH 07/19] reformat --- .../setup/config/VitruvConfiguration.java | 2 +- .../setup/emf/EMFModelInitializer.java | 18 +++------- .../exception/MissingModelException.java | 6 +--- .../methodologist/setup/util/FileUtils.java | 9 +++++ .../setup/vsum/controller/VsumController.java | 4 ++- .../vsum/service/VsumProjectBuildService.java | 34 +++++++++---------- 6 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java index 6a908c8..c740c04 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java +++ b/src/main/java/tools/vitruv/methodologist/setup/config/VitruvConfiguration.java @@ -95,7 +95,6 @@ public List getMetaModelLocations() { * @param paths The metamodel argument string. */ public void setMetaModelLocations(String paths) { - String nsUri = ""; Resource.Factory.Registry reg = Resource.Factory.Registry.INSTANCE; reg.getExtensionToFactoryMap().put("ecore", new XMIResourceFactoryImpl()); reg.getExtensionToFactoryMap().put("genmodel", new XMIResourceFactoryImpl()); @@ -114,6 +113,7 @@ public void setMetaModelLocations(String paths) { ResourceSet resourceSet = new ResourceSetImpl(); URI uri = URI.createFileURI(metamodel.getAbsolutePath().trim()); Resource resource = resourceSet.getResource(uri, true); + String nsUri = ""; if (!resource.getContents().isEmpty() && resource.getContents().get(0) instanceof EPackage ePackage) { URI genmodelURI = URI.createFileURI(genmodel.getAbsolutePath()); diff --git a/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java b/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java index dbb7da8..12ace91 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java +++ b/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java @@ -1,6 +1,7 @@ package tools.vitruv.methodologist.setup.emf; import java.io.File; +import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; @@ -39,7 +40,6 @@ public static Map initializeFromJar(String modelJarPath) { try (JarFile jar = new JarFile(jarFile)) { Collection entries = jar.stream().toList(); - // Look for all ecore files entries.stream() .filter(entry -> entry.getName().endsWith(".ecore")) .forEach( @@ -51,7 +51,6 @@ public static Map initializeFromJar(String modelJarPath) { } }); - // Look for FactoryImpl classes that indicate metamodel packages entries.stream() .filter(entry -> entry.getName().endsWith("FactoryImpl.class")) .map(entry -> entry.getName().replace('/', '.').replace(".class", "")) @@ -90,7 +89,6 @@ public static Map initializeFromClasses( return packages; } - // Find all FactoryImpl classes Files.walk(classesDir) .filter(p -> Files.isRegularFile(p)) .filter(p -> p.toString().endsWith("FactoryImpl.class")) @@ -137,10 +135,7 @@ public static void registerPackage(String nsUri, EPackage ePackage) { * @param packages map to collect registered packages */ private static void loadEcorePackage( - java.io.InputStream ecoreStream, Map packages) { - // This would load and parse the ecore file - // For now, this is a placeholder that could be extended with actual implementation - } + java.io.InputStream ecoreStream, Map packages) {} /** * Loads a metamodel package class and registers it. @@ -150,19 +145,16 @@ private static void loadEcorePackage( */ private static void loadMetamodelPackage(String className, Map packages) { try { - // Load the FactoryImpl class Class factoryClass = Class.forName(className); - // Get the eINSTANCE field - java.lang.reflect.Field instanceField = factoryClass.getField("eINSTANCE"); + Field instanceField = factoryClass.getField("eINSTANCE"); Object factoryInstance = instanceField.get(null); - // Get the eClass field from the package String packageClassName = className.substring(0, className.lastIndexOf("FactoryImpl")) + "Package"; Class packageClass = Class.forName(packageClassName); - java.lang.reflect.Field eINSTANCE = packageClass.getField("eINSTANCE"); - EPackage ePackage = (EPackage) eINSTANCE.get(null); + Field instance = packageClass.getField("eINSTANCE"); + EPackage ePackage = (EPackage) instance.get(null); if (ePackage != null) { String nsUri = ePackage.getNsURI(); diff --git a/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java b/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java index ce01a1f..543196a 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java +++ b/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java @@ -13,11 +13,7 @@ public MissingModelException(String message, Throwable cause) { super(message, cause); } - /** - * Exception thrown when a required model is missing. - * - * @param cause - */ + /** Exception thrown when a required model is missing. */ public MissingModelException(Throwable cause) { super(cause); } diff --git a/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java b/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java index 83580d7..39d8737 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java +++ b/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java @@ -113,6 +113,15 @@ public static Path createNewFolder(Path path, String folder) { return folderPath; } + /** + * Finds the value of an option in the given file by scanning for the first line that starts with + * the option prefix and returning the trimmed remainder of that line. + * + * @param file The file to search. + * @param option The option prefix to look for at the start of a line. + * @return The trimmed value following the option prefix. + * @throws IllegalArgumentException when no line starting with the option is found. + */ public static String findOption(File file, String option) { try { for (String line : Files.readAllLines(file.toPath())) { diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java index 06bed3f..24abe18 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java @@ -42,7 +42,9 @@ public class VsumController { @Operation( summary = "Build VSUM project", description = - "Uploads metamodel/genmodel files, builds the project from templates, and returns a zip archive", + "Uploads metamodel/genmodel files," + + " builds the project from templates," + + " and returns a zip archive", responses = { @ApiResponse( responseCode = "200", diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java index dacfda6..2d414f8 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -185,23 +185,6 @@ private void normalizeReactionImports( } } - /** - * Stores metamodel nsURI information for build-time EMF initialization. - * - * @param modelPairs metamodel/genmodel file pairs - * @return map of model names to nsURIs - */ - Map extractMetamodelNamespaceMap(List modelPairs) { - Map modelNameToNsUri = new HashMap<>(); - for (VsumService.ModelFiles pair : modelPairs) { - MetamodelInfo info = readMetamodelInfo(pair.metamodelFile()); - if (info != null && info.nsUri != null && !info.nsUri.isBlank()) { - modelNameToNsUri.put(info.packageName != null ? info.packageName : "model", info.nsUri); - } - } - return modelNameToNsUri; - } - /** * Rewrites each {@code import "..."} statement in a single reaction file's content, replacing * imports that do not match a known nsURI with the nsURI mapped from the import's last path @@ -234,6 +217,23 @@ private String normalizeReactionImports( return rewritten.toString(); } + /** + * Stores metamodel nsURI information for build-time EMF initialization. + * + * @param modelPairs metamodel/genmodel file pairs + * @return map of model names to nsURIs + */ + Map extractMetamodelNamespaceMap(List modelPairs) { + Map modelNameToNsUri = new HashMap<>(); + for (VsumService.ModelFiles pair : modelPairs) { + MetamodelInfo info = readMetamodelInfo(pair.metamodelFile()); + if (info != null && info.nsUri != null && !info.nsUri.isBlank()) { + modelNameToNsUri.put(info.packageName != null ? info.packageName : "model", info.nsUri); + } + } + return modelNameToNsUri; + } + /** * Extracts the last segment of a URI, i.e. the substring following the final {@code /} or {@code * #}. From dfb429a9376d698975fb5b74f8844380c703f890 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:08:30 +0200 Subject: [PATCH 08/19] reformat --- .../setup/vsum/service/VsumProjectBuildService.java | 8 ++++++-- .../methodologist/setup/vsum/service/VsumService.java | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java index 2d414f8..ada7cc6 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -21,10 +21,12 @@ import org.springframework.web.multipart.MultipartFile; import org.w3c.dom.Document; import org.w3c.dom.Element; +import lombok.extern.slf4j.Slf4j; import tools.vitruv.methodologist.setup.exception.MethodologistSetupException; import tools.vitruv.methodologist.setup.exception.MissingModelException; /** Coordinates VSUM project build operations from uploaded files. */ +@Slf4j @Service @RequiredArgsConstructor public class VsumProjectBuildService { @@ -321,7 +323,8 @@ private void deleteRecursively(Path root) { try (Stream paths = Files.walk(root)) { paths.sorted(Comparator.reverseOrder()).forEach(this::deleteQuietly); - } catch (IOException ignored) { + } catch (IOException e) { + log.error(e.getMessage()); } } @@ -333,7 +336,8 @@ private void deleteRecursively(Path root) { private void deleteQuietly(Path path) { try { Files.deleteIfExists(path); - } catch (IOException ignored) { + } catch (IOException e) { + log.error(e.getMessage()); } } diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java index 8355ebd..addca01 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java @@ -21,10 +21,12 @@ import java.util.zip.ZipOutputStream; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; import tools.vitruv.methodologist.setup.config.VitruvConfiguration; import tools.vitruv.methodologist.setup.exception.MissingModelException; /** Business service for building and packaging VSUM projects from uploaded model files. */ +@Slf4j @Service @RequiredArgsConstructor public class VsumService { @@ -382,11 +384,11 @@ private void deleteRecursively(Path root) { try { Files.deleteIfExists(path); } catch (IOException e) { - // Best-effort cleanup; ignore delete failures. + log.error(e.getMessage()); } }); } catch (IOException e) { - // Best-effort cleanup; ignore cleanup failures. + log.error(e.getMessage()); } } From f8ff3bfce12f0586bdaed82878e837fbc39620fa Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:10:01 +0200 Subject: [PATCH 09/19] reformat --- .../setup/vsum/service/VsumProjectBuildService.java | 2 +- .../vitruv/methodologist/setup/vsum/service/VsumService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java index ada7cc6..fa9d08e 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -17,11 +17,11 @@ import java.util.stream.Stream; import javax.xml.parsers.DocumentBuilderFactory; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import org.w3c.dom.Document; import org.w3c.dom.Element; -import lombok.extern.slf4j.Slf4j; import tools.vitruv.methodologist.setup.exception.MethodologistSetupException; import tools.vitruv.methodologist.setup.exception.MissingModelException; diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java index addca01..3fcce0f 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java @@ -20,8 +20,8 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; import tools.vitruv.methodologist.setup.config.VitruvConfiguration; import tools.vitruv.methodologist.setup.exception.MissingModelException; From d6f3a02f8a670476f2dc17d44864ff3204b2563f Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:54:22 +0200 Subject: [PATCH 10/19] added new api to get exact java jar file --- .../setup/config/GlobalExceptionHandler.java | 25 +++ .../setup/messages/ErrorMessages.java | 4 + .../setup/vsum/controller/VsumController.java | 65 ++++++- .../vsum/service/VsumProjectBuildService.java | 100 ++++++++++- .../setup/vsum/service/VsumService.java | 132 +++++++++++++-- .../vsum/controller/VsumControllerTest.java | 67 +++++++- .../service/VsumProjectBuildServiceTest.java | 158 +++++++++++++++++- .../setup/vsum/service/VsumServiceTest.java | 130 ++++++++++++++ 8 files changed, 643 insertions(+), 38 deletions(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/config/GlobalExceptionHandler.java b/src/main/java/tools/vitruv/methodologist/setup/config/GlobalExceptionHandler.java index 302dc01..ca097db 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/config/GlobalExceptionHandler.java +++ b/src/main/java/tools/vitruv/methodologist/setup/config/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package tools.vitruv.methodologist.setup.config; +import java.nio.file.NoSuchFileException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -69,6 +70,30 @@ public ResponseEntity handleGenmodelException( return new ResponseEntity<>(errorResponse, HttpStatus.UNPROCESSABLE_ENTITY); } + /** + * Handles NoSuchFileException raised when an expected build artifact is missing. + * + * @param ex the exception + * @param request the web request + * @return error response with bad request status + */ + @ExceptionHandler(NoSuchFileException.class) + public ResponseEntity handleNoSuchFileException( + NoSuchFileException ex, WebRequest request) { + log.error("NoSuchFileException occurred: {}", ex.getMessage(), ex); + + ErrorResponseDTO errorResponse = + ErrorResponseDTO.builder() + .errorCode("VSUM_ARTIFACT_NOT_FOUND") + .message(ex.getMessage()) + .status(HttpStatus.BAD_REQUEST.value()) + .timestamp(System.currentTimeMillis()) + .path(request.getDescription(false).replace("uri=", "")) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + /** * Handles all other exceptions. * diff --git a/src/main/java/tools/vitruv/methodologist/setup/messages/ErrorMessages.java b/src/main/java/tools/vitruv/methodologist/setup/messages/ErrorMessages.java index 6a064b8..b6ce962 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/messages/ErrorMessages.java +++ b/src/main/java/tools/vitruv/methodologist/setup/messages/ErrorMessages.java @@ -18,6 +18,10 @@ public final class ErrorMessages { public static final String MULTIPART_FILE_UPLOAD_ERROR = "Failed to save uploaded file"; public static final String XML_STREAM_ERROR = "Failed to process XML stream"; public static final String UNEXPECTED_ERROR = "An unexpected error occurred"; + public static final String VSUM_JAR_NOT_FOUND = + "Expected VSUM jar was not produced by the build: %s"; + public static final String VSUM_REACTION_FILES_REQUIRED = + "At least one reaction file is required"; private ErrorMessages() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java index 24abe18..f6911ac 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.nio.charset.StandardCharsets; +import java.nio.file.NoSuchFileException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; @@ -28,6 +29,10 @@ @Tag(name = "VSUM", description = "VSUM project generation endpoints") public class VsumController { + /** Download filename for the executable VSUM jar-with-dependencies. */ + private static final String VSUM_JAR_FILENAME = + "tools.vitruv.methodologisttemplate.vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar"; + private final VsumProjectBuildService vsumProjectBuildService; /** @@ -35,8 +40,9 @@ public class VsumController { * * @param metamodelFiles metamodel files * @param genmodelFiles genmodel files paired by index with metamodel files - * @param reactionFiles optional reaction files + * @param reactionFiles reaction files * @return zip file response containing generated project + * @throws NoSuchFileException when the build does not produce the expected artifact */ @GetMapping(value = "/build", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( @@ -61,9 +67,10 @@ public ResponseEntity buildProject( required = true) @RequestPart("genmodelFiles") List genmodelFiles, - @Parameter(description = "Optional reaction files") - @RequestPart(value = "reactionFiles", required = false) - List reactionFiles) { + @Parameter(description = "Reaction files", required = true) + @RequestPart("reactionFiles") + List reactionFiles) + throws NoSuchFileException { byte[] archive = vsumProjectBuildService.buildProjectArchive(metamodelFiles, genmodelFiles, reactionFiles); @@ -79,6 +86,56 @@ public ResponseEntity buildProject( return ResponseEntity.ok().headers(headers).body(archive); } + /** + * Accepts model and reaction files, builds a VSUM project, and returns only the executable VSUM + * jar-with-dependencies produced under {@code vsum/target}, instead of the whole project archive. + * + * @param metamodelFiles metamodel files + * @param genmodelFiles genmodel files paired by index with metamodel files + * @param reactionFiles reaction files + * @return response containing the built VSUM jar + * @throws NoSuchFileException when the build does not produce the expected jar + */ + @GetMapping(value = "/jar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "Build VSUM jar", + description = + "Uploads metamodel/genmodel files," + + " builds the project from templates," + + " and returns only the executable VSUM jar-with-dependencies", + responses = { + @ApiResponse( + responseCode = "200", + description = "Jar built successfully", + content = @Content(mediaType = "application/java-archive")), + @ApiResponse(responseCode = "400", description = "Invalid input files"), + @ApiResponse(responseCode = "500", description = "Build failed") + }) + public ResponseEntity buildJar( + @Parameter(description = "Metamodel files", required = true) @RequestPart("metamodelFiles") + List metamodelFiles, + @Parameter( + description = "Genmodel files paired by index with metamodel files", + required = true) + @RequestPart("genmodelFiles") + List genmodelFiles, + @Parameter(description = "Reaction files", required = true) + @RequestPart("reactionFiles") + List reactionFiles) + throws NoSuchFileException { + + byte[] jar = + vsumProjectBuildService.buildProjectJar(metamodelFiles, genmodelFiles, reactionFiles); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/java-archive")); + headers.setContentDisposition( + ContentDisposition.attachment().filename(VSUM_JAR_FILENAME, StandardCharsets.UTF_8).build()); + headers.setContentLength(jar.length); + + return ResponseEntity.ok().headers(headers).body(jar); + } + /** * Creates a deterministic archive filename prefix for build downloads. * diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java index fa9d08e..e166c2f 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; @@ -24,6 +25,7 @@ import org.w3c.dom.Element; import tools.vitruv.methodologist.setup.exception.MethodologistSetupException; import tools.vitruv.methodologist.setup.exception.MissingModelException; +import tools.vitruv.methodologist.setup.messages.ErrorMessages; /** Coordinates VSUM project build operations from uploaded files. */ @Slf4j @@ -38,18 +40,62 @@ public class VsumProjectBuildService { private final VsumService vsumService; /** - * Builds a VSUM project from uploaded files and returns a zip archive. + * Builds a VSUM project from uploaded files and returns a zip archive of the whole project. * * @param metamodelFiles metamodel files * @param genmodelFiles genmodel files in the same order as metamodel files - * @param reactionFiles optional reaction files + * @param reactionFiles reaction files * @return built project archive bytes + * @throws NoSuchFileException when the build does not produce the expected artifact */ public byte[] buildProjectArchive( List metamodelFiles, List genmodelFiles, - List reactionFiles) { - validateInputs(metamodelFiles, genmodelFiles); + List reactionFiles) + throws NoSuchFileException { + return buildArtifact( + metamodelFiles, genmodelFiles, reactionFiles, vsumService::generateProjectArchive); + } + + /** + * Builds a VSUM project from uploaded files and returns only the executable VSUM + * jar-with-dependencies produced under the project's {@code vsum/target} directory, rather than + * the whole project archive. + * + * @param metamodelFiles metamodel files + * @param genmodelFiles genmodel files in the same order as metamodel files + * @param reactionFiles reaction files + * @return the bytes of the built VSUM jar + * @throws NoSuchFileException when the build does not produce the expected jar + */ + public byte[] buildProjectJar( + List metamodelFiles, + List genmodelFiles, + List reactionFiles) + throws NoSuchFileException { + return buildArtifact( + metamodelFiles, genmodelFiles, reactionFiles, vsumService::generateProjectJar); + } + + /** + * Validates the uploaded files, materializes them into a temporary workspace, and delegates to + * the supplied generator to produce the requested build artifact, cleaning up the workspace + * afterwards regardless of outcome. + * + * @param metamodelFiles metamodel files + * @param genmodelFiles genmodel files in the same order as metamodel files + * @param reactionFiles reaction files + * @param generator the VSUM generation strategy producing the artifact bytes + * @return the produced artifact bytes + * @throws NoSuchFileException when the expected build artifact is missing + */ + private byte[] buildArtifact( + List metamodelFiles, + List genmodelFiles, + List reactionFiles, + ProjectArtifactGenerator generator) + throws NoSuchFileException { + validateInputs(metamodelFiles, genmodelFiles, reactionFiles); Path uploadWorkspace = null; try { @@ -59,8 +105,9 @@ public byte[] buildProjectArchive( List copiedReactionFiles = toReactionFiles(uploadWorkspace, reactionFiles); normalizeReactionImports(modelPairs, copiedReactionFiles); Map metamodelNamespaceMap = extractMetamodelNamespaceMap(modelPairs); - return vsumService.generateProjectArchive( - modelPairs, copiedReactionFiles, metamodelNamespaceMap); + return generator.generate(modelPairs, copiedReactionFiles, metamodelNamespaceMap); + } catch (NoSuchFileException e) { + throw e; } catch (IOException | MissingModelException e) { throw new MethodologistSetupException( VSUM_BUILD_ERROR_CODE, "Failed to build VSUM project archive", e); @@ -74,13 +121,17 @@ public byte[] buildProjectArchive( } /** - * Validates uploaded metamodel/genmodel lists. + * Validates uploaded metamodel, genmodel, and reaction lists. Metamodel, genmodel, and reaction + * files are all mandatory. * * @param metamodelFiles metamodel files * @param genmodelFiles genmodel files + * @param reactionFiles reaction files */ private void validateInputs( - List metamodelFiles, List genmodelFiles) { + List metamodelFiles, + List genmodelFiles, + List reactionFiles) { if (metamodelFiles == null || genmodelFiles == null || metamodelFiles.isEmpty() @@ -101,6 +152,14 @@ private void validateInputs( for (MultipartFile file : genmodelFiles) { validateUploadedFile(file, "genmodel file"); } + + if (reactionFiles == null || reactionFiles.isEmpty()) { + throw new MethodologistSetupException( + VSUM_INPUT_ERROR_CODE, ErrorMessages.VSUM_REACTION_FILES_REQUIRED); + } + for (MultipartFile file : reactionFiles) { + validateUploadedFile(file, "reaction file"); + } } /** @@ -341,6 +400,31 @@ private void deleteQuietly(Path path) { } } + /** + * Strategy selecting which VSUM build artifact to generate from the prepared model inputs, so the + * shared upload-and-build pipeline can produce either the whole project archive or a single + * artifact. + */ + @FunctionalInterface + private interface ProjectArtifactGenerator { + /** + * Generates the requested artifact from the prepared model inputs. + * + * @param modelPairs metamodel/genmodel file pairs + * @param reactionFiles copied reaction files + * @param metamodelNamespaceMap map of model names to nsURIs + * @return the produced artifact bytes + * @throws IOException when file IO or artifact extraction fails + * @throws InterruptedException when the build process is interrupted + * @throws MissingModelException when the model configuration is invalid + */ + byte[] generate( + List modelPairs, + List reactionFiles, + Map metamodelNamespaceMap) + throws IOException, InterruptedException, MissingModelException; + } + /** Immutable holder for a metamodel's package name and nsURI. */ private static class MetamodelInfo { private final String packageName; diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java index 3fcce0f..d3d028b 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java @@ -8,6 +8,7 @@ import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; @@ -24,6 +25,7 @@ import org.springframework.stereotype.Service; import tools.vitruv.methodologist.setup.config.VitruvConfiguration; import tools.vitruv.methodologist.setup.exception.MissingModelException; +import tools.vitruv.methodologist.setup.messages.ErrorMessages; /** Business service for building and packaging VSUM projects from uploaded model files. */ @Slf4j @@ -31,6 +33,13 @@ @RequiredArgsConstructor public class VsumService { + /** + * Path, relative to the generated project root, of the executable VSUM jar produced by the Maven + * build. + */ + static final String VSUM_JAR_RELATIVE_PATH = + "vsum/target/tools.vitruv.methodologisttemplate.vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar"; + private final GenerateFromTemplate generateFromTemplate; /** @@ -49,6 +58,80 @@ public byte[] generateProjectArchive( List reactionFiles, Map metamodelNamespaceMap) throws IOException, InterruptedException, MissingModelException { + return buildProject(modelFiles, reactionFiles, metamodelNamespaceMap, this::zipDirectory); + } + + /** + * Generates a full VSUM project, builds it with Maven, and returns the project archive as bytes. + * + * @param modelFiles paired metamodel/genmodel files + * @param reactionFiles optional reaction files + * @return zip archive bytes containing generated project and build artifacts + * @throws IOException when file IO or build execution fails + * @throws InterruptedException when the build process is interrupted + * @throws MissingModelException when the model configuration is invalid + */ + public byte[] generateProjectArchive(List modelFiles, List reactionFiles) + throws IOException, InterruptedException, MissingModelException { + return generateProjectArchive(modelFiles, reactionFiles, Map.of()); + } + + /** + * Generates a full VSUM project, builds it with Maven, and returns only the executable VSUM jar + * (the {@value #VSUM_JAR_RELATIVE_PATH} produced under the project's {@code vsum/target} + * directory) as bytes, instead of the whole project archive. + * + * @param modelFiles paired metamodel/genmodel files + * @param reactionFiles optional reaction files + * @param metamodelNamespaceMap map of model names to nsURIs + * @return the bytes of the built VSUM jar-with-dependencies + * @throws IOException when file IO, build execution, or jar extraction fails + * @throws InterruptedException when the build process is interrupted + * @throws MissingModelException when the model configuration is invalid + */ + public byte[] generateProjectJar( + List modelFiles, + List reactionFiles, + Map metamodelNamespaceMap) + throws IOException, InterruptedException, MissingModelException { + return buildProject(modelFiles, reactionFiles, metamodelNamespaceMap, this::extractVsumJar); + } + + /** + * Generates a full VSUM project, builds it with Maven, and returns only the executable VSUM jar as + * bytes. + * + * @param modelFiles paired metamodel/genmodel files + * @param reactionFiles optional reaction files + * @return the bytes of the built VSUM jar-with-dependencies + * @throws IOException when file IO, build execution, or jar extraction fails + * @throws InterruptedException when the build process is interrupted + * @throws MissingModelException when the model configuration is invalid + */ + public byte[] generateProjectJar(List modelFiles, List reactionFiles) + throws IOException, InterruptedException, MissingModelException { + return generateProjectJar(modelFiles, reactionFiles, Map.of()); + } + + /** + * Generates the project from templates, runs the Maven build, and extracts a single build + * artifact from the workspace, deleting the workspace afterwards regardless of outcome. + * + * @param modelFiles paired metamodel/genmodel files + * @param reactionFiles optional reaction files + * @param metamodelNamespaceMap map of model names to nsURIs passed to the build + * @param artifactExtractor extracts the resulting bytes from the built workspace + * @return the extracted artifact bytes + * @throws IOException when file IO, build execution, or artifact extraction fails + * @throws InterruptedException when the build process is interrupted + * @throws MissingModelException when the model configuration is invalid + */ + private byte[] buildProject( + List modelFiles, + List reactionFiles, + Map metamodelNamespaceMap, + ArtifactExtractor artifactExtractor) + throws IOException, InterruptedException, MissingModelException { validateInputs(modelFiles, reactionFiles); Path workspace = Files.createTempDirectory("vitruv-cli-project-"); @@ -64,27 +147,12 @@ public byte[] generateProjectArchive( generateProjectFiles(configuration); runMavenBuild(workspace, metamodelNamespaceMap); - return zipDirectory(workspace); + return artifactExtractor.extract(workspace); } finally { deleteRecursively(workspace); } } - /** - * Generates a full VSUM project, builds it with Maven, and returns the project archive as bytes. - * - * @param modelFiles paired metamodel/genmodel files - * @param reactionFiles optional reaction files - * @return zip archive bytes containing generated project and build artifacts - * @throws IOException when file IO or build execution fails - * @throws InterruptedException when the build process is interrupted - * @throws MissingModelException when the model configuration is invalid - */ - public byte[] generateProjectArchive(List modelFiles, List reactionFiles) - throws IOException, InterruptedException, MissingModelException { - return generateProjectArchive(modelFiles, reactionFiles, Map.of()); - } - /** * Validates that the supplied model and reaction files are present and usable. * @@ -333,6 +401,21 @@ private ProcessBuilder createMavenProcessBuilder( return processBuilder; } + /** + * Extracts the executable VSUM jar produced by the Maven build from the built workspace. + * + * @param workspace root directory of the generated and built project + * @return the bytes of the {@value #VSUM_JAR_RELATIVE_PATH} artifact + * @throws IOException when the expected jar is missing or cannot be read + */ + private byte[] extractVsumJar(Path workspace) throws IOException { + Path jarPath = workspace.resolve(VSUM_JAR_RELATIVE_PATH); + if (!Files.isRegularFile(jarPath)) { + throw new NoSuchFileException(String.format(ErrorMessages.VSUM_JAR_NOT_FOUND, jarPath)); + } + return Files.readAllBytes(jarPath); + } + /** * Zips the entire contents of the given directory into an in-memory archive, preserving the * directory structure with forward-slash separators and deterministic entry ordering. @@ -392,6 +475,23 @@ private void deleteRecursively(Path root) { } } + /** + * Strategy for extracting the bytes to return from a fully built project workspace, allowing the + * same generate-and-build pipeline to produce either the whole project archive or a single + * artifact. + */ + @FunctionalInterface + private interface ArtifactExtractor { + /** + * Extracts the result bytes from the built workspace. + * + * @param workspace root directory of the generated and built project + * @return the bytes to return to the caller + * @throws IOException when reading or assembling the artifact fails + */ + byte[] extract(Path workspace) throws IOException; + } + /** * A paired metamodel (ecore) file and its corresponding genmodel file. * diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java index 755293f..e747af1 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java @@ -1,11 +1,13 @@ package tools.vitruv.methodologist.setup.vsum.controller; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -16,6 +18,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import tools.vitruv.methodologist.setup.exception.MethodologistSetupException; import tools.vitruv.methodologist.setup.vsum.service.VsumProjectBuildService; @@ -41,10 +44,13 @@ void buildProjectArchiveDelegatesToVsumService() throws Exception { MockMultipartFile genmodel = new MockMultipartFile( "genmodelFiles", "model.genmodel", "application/octet-stream", "gen".getBytes()); + MockMultipartFile reaction = + new MockMultipartFile( + "reactionFiles", "rules.reactions", "text/plain", "import \"x\"".getBytes()); byte[] result = vsumProjectBuildService.buildProjectArchive( - List.of(metamodel), List.of(genmodel), List.of()); + List.of(metamodel), List.of(genmodel), List.of(reaction)); assertArrayEquals(archive, result); verify(vsumService).generateProjectArchive(anyList(), anyList(), any()); @@ -102,4 +108,63 @@ void buildProjectArchiveNormalizesReactionImportUris() throws Exception { verify(vsumService).generateProjectArchive(anyList(), anyList(), any()); } + + /** Verifies the build endpoint returns the zip archive with the expected download headers. */ + @Test + void buildProjectReturnsZipResponse() throws Exception { + VsumProjectBuildService buildService = mock(VsumProjectBuildService.class); + byte[] archive = {1, 2, 3, 4}; + when(buildService.buildProjectArchive(anyList(), anyList(), anyList())).thenReturn(archive); + + VsumController controller = new VsumController(buildService); + + MockMultipartFile metamodel = + new MockMultipartFile( + "metamodelFiles", "model.ecore", "application/octet-stream", "meta".getBytes()); + MockMultipartFile genmodel = + new MockMultipartFile( + "genmodelFiles", "model.genmodel", "application/octet-stream", "gen".getBytes()); + + ResponseEntity response = + controller.buildProject(List.of(metamodel), List.of(genmodel), List.of()); + + assertEquals(200, response.getStatusCode().value()); + assertArrayEquals(archive, response.getBody()); + assertEquals("application/zip", response.getHeaders().getContentType().toString()); + assertEquals(archive.length, response.getHeaders().getContentLength()); + String filename = response.getHeaders().getContentDisposition().getFilename(); + assertTrue(filename.startsWith("vsum-project-")); + assertTrue(filename.endsWith(".zip")); + } + + /** Verifies the jar endpoint returns the jar bytes with the expected download headers. */ + @Test + void buildJarReturnsJarResponse() { + VsumProjectBuildService buildService = mock(VsumProjectBuildService.class); + byte[] jar = {9, 8, 7}; + when(buildService.buildProjectJar(anyList(), anyList(), any())).thenReturn(jar); + + VsumController controller = new VsumController(buildService); + + MockMultipartFile metamodel = + new MockMultipartFile( + "metamodelFiles", "model.ecore", "application/octet-stream", "meta".getBytes()); + MockMultipartFile genmodel = + new MockMultipartFile( + "genmodelFiles", "model.genmodel", "application/octet-stream", "gen".getBytes()); + + ResponseEntity response = + controller.buildJar(List.of(metamodel), List.of(genmodel), null); + + assertEquals(200, response.getStatusCode().value()); + assertArrayEquals(jar, response.getBody()); + assertEquals( + "application/java-archive", response.getHeaders().getContentType().toString()); + assertEquals(jar.length, response.getHeaders().getContentLength()); + assertEquals( + "tools.vitruv.methodologisttemplate.vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar", + response.getHeaders().getContentDisposition().getFilename()); + + verify(buildService).buildProjectJar(anyList(), anyList(), any()); + } } diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java index 4e18c25..06d96d8 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java @@ -105,16 +105,41 @@ void buildProjectArchive_shouldBuildArchive() throws Exception { } @Test - void buildProjectArchive_shouldBuildArchiveWithoutReactionFiles() throws Exception { + void buildProjectArchive_shouldThrowWhenReactionFilesNull() { - when(vsumService.generateProjectArchive(anyList(), anyList(), anyMap())) - .thenReturn(new byte[] {1}); + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), null)) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("reaction file is required"); + } - byte[] archive = - service.buildProjectArchive( - List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), List.of()); + @Test + void buildProjectArchive_shouldThrowWhenReactionFilesEmpty() { + + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), List.of())) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("reaction file is required"); + } + + @Test + void buildProjectArchive_shouldThrowWhenReactionFileEmpty() { - assertThat(archive).isNotEmpty(); + MultipartFile empty = + new MockMultipartFile("file", "sample.reactions", "text/plain", new byte[0]); + + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(empty))) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("reaction file"); } @Test @@ -126,11 +151,29 @@ void buildProjectArchive_shouldWrapIOException() throws Exception { assertThatThrownBy( () -> service.buildProjectArchive( - List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), List.of())) + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(reactionFile()))) .isInstanceOf(MethodologistSetupException.class) .hasMessageContaining("Failed to build VSUM project archive"); } + @Test + void buildProjectArchive_shouldPropagateNoSuchFileException() throws Exception { + + when(vsumService.generateProjectArchive(anyList(), anyList(), anyMap())) + .thenThrow(new java.nio.file.NoSuchFileException("missing.jar")); + + assertThatThrownBy( + () -> + service.buildProjectArchive( + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(reactionFile()))) + .isInstanceOf(java.nio.file.NoSuchFileException.class) + .hasMessageContaining("missing.jar"); + } + @Test void buildProjectArchive_shouldWrapMissingModelException() throws Exception { @@ -140,7 +183,9 @@ void buildProjectArchive_shouldWrapMissingModelException() throws Exception { assertThatThrownBy( () -> service.buildProjectArchive( - List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), List.of())) + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(reactionFile()))) .isInstanceOf(MethodologistSetupException.class); } @@ -153,7 +198,102 @@ void buildProjectArchive_shouldWrapInterruptedException() throws Exception { assertThatThrownBy( () -> service.buildProjectArchive( + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(reactionFile()))) + .isInstanceOf(MethodologistSetupException.class); + + assertThat(Thread.currentThread().isInterrupted()).isTrue(); + + Thread.interrupted(); + } + + @Test + void buildProjectJar_shouldThrowWhenCountsDoNotMatch() { + + assertThatThrownBy( + () -> + service.buildProjectJar( + List.of(mockMultipart("a.ecore"), mockMultipart("b.ecore")), + List.of(mockMultipart("a.genmodel")), + List.of())) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("counts must be identical"); + } + + @Test + void buildProjectJar_shouldReturnJarBytes() throws Exception { + + byte[] expected = {7, 8, 9}; + + when(vsumService.generateProjectJar(anyList(), anyList(), anyMap())).thenReturn(expected); + + byte[] jar = + service.buildProjectJar( + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(reactionFile())); + + assertThat(jar).isEqualTo(expected); + + verify(vsumService).generateProjectJar(anyList(), anyList(), anyMap()); + } + + @Test + void buildProjectJar_shouldThrowWhenReactionFilesEmpty() { + + assertThatThrownBy( + () -> + service.buildProjectJar( List.of(validMetamodel()), List.of(mockMultipart("model.genmodel")), List.of())) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("reaction file is required"); + } + + @Test + void buildProjectJar_shouldWrapIOException() throws Exception { + + when(vsumService.generateProjectJar(anyList(), anyList(), anyMap())) + .thenThrow(new java.io.IOException("boom")); + + assertThatThrownBy( + () -> + service.buildProjectJar( + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(reactionFile()))) + .isInstanceOf(MethodologistSetupException.class) + .hasMessageContaining("Failed to build VSUM project"); + } + + @Test + void buildProjectJar_shouldPropagateNoSuchFileException() throws Exception { + + when(vsumService.generateProjectJar(anyList(), anyList(), anyMap())) + .thenThrow(new java.nio.file.NoSuchFileException("missing.jar")); + + assertThatThrownBy( + () -> + service.buildProjectJar( + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(reactionFile()))) + .isInstanceOf(java.nio.file.NoSuchFileException.class) + .hasMessageContaining("missing.jar"); + } + + @Test + void buildProjectJar_shouldWrapInterruptedException() throws Exception { + + when(vsumService.generateProjectJar(anyList(), anyList(), anyMap())) + .thenThrow(new InterruptedException()); + + assertThatThrownBy( + () -> + service.buildProjectJar( + List.of(validMetamodel()), + List.of(mockMultipart("model.genmodel")), + List.of(reactionFile()))) .isInstanceOf(MethodologistSetupException.class); assertThat(Thread.currentThread().isInterrupted()).isTrue(); diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java index a8303c4..732c8de 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java @@ -155,6 +155,82 @@ void generateProjectArchive_shouldDelegateToThreeArgumentMethod() throws Excepti assertThat(result).isEqualTo(expected); } + @Test + void generateProjectJar_shouldReturnBuiltJarBytes() throws Exception { + + File ecore = writeValidEcore(); + File genmodel = writeValidGenmodel(); + + VsumService.ModelFiles modelFiles = new VsumService.ModelFiles(ecore, genmodel); + + VsumService service = spy(new VsumService(generateFromTemplate)); + + byte[] jarBytes = {10, 20, 30, 40}; + stubProjectGeneration(service, jarBytes); + + byte[] result = + service.generateProjectJar( + List.of(modelFiles), List.of(), Map.of("pcm", "http://pcm/ns")); + + assertThat(result).isEqualTo(jarBytes); + verify(service).runMavenBuild(any(Path.class), eq(Map.of("pcm", "http://pcm/ns"))); + } + + @Test + void generateProjectJar_shouldThrowWhenJarMissing() throws Exception { + + File ecore = writeValidEcore(); + File genmodel = writeValidGenmodel(); + + VsumService.ModelFiles modelFiles = new VsumService.ModelFiles(ecore, genmodel); + + VsumService service = spy(new VsumService(generateFromTemplate)); + + doAnswer( + invocation -> { + Path workspace = ((File) invocation.getArgument(0)).toPath().getParent().getParent(); + Files.createDirectories(workspace); + return null; + }) + .when(generateFromTemplate) + .generateRootPom(any(File.class), anyString()); + doNothing().when(service).runMavenBuild(any(Path.class), anyMap()); + + assertThatThrownBy(() -> service.generateProjectJar(List.of(modelFiles), List.of())) + .isInstanceOf(java.nio.file.NoSuchFileException.class) + .hasMessageContaining("jar-with-dependencies.jar"); + } + + @Test + void generateProjectJar_shouldDelegateToThreeArgumentMethod() throws Exception { + + File ecore = mock(File.class); + File genmodel = mock(File.class); + + VsumService.ModelFiles modelFiles = new VsumService.ModelFiles(ecore, genmodel); + + VsumService service = spy(new VsumService(generateFromTemplate)); + + byte[] expected = {4, 5, 6}; + + doReturn(expected) + .when(service) + .generateProjectJar(eq(List.of(modelFiles)), eq(List.of()), eq(Map.of())); + + byte[] result = service.generateProjectJar(List.of(modelFiles), List.of()); + + assertThat(result).isEqualTo(expected); + } + + @Test + void generateProjectJar_shouldThrowWhenModelFilesNull() { + VsumService service = spy(new VsumService(generateFromTemplate)); + + assertThatThrownBy(() -> service.generateProjectJar(null, List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("At least one metamodel/genmodel pair"); + } + @Test void modelFiles_shouldRejectNullMetamodel() { File genmodel = new File("test.genmodel"); @@ -209,4 +285,58 @@ void createMavenProcessBuilderNamespaceProperties_shouldBePassed() throws Except assertThat(builder.directory()).isEqualTo(tempDir.toFile()); } + + private File writeValidGenmodel() throws IOException { + File genmodel = Files.createFile(tempDir.resolve("model.genmodel")).toFile(); + Files.writeString( + genmodel.toPath(), + """ + + + + """); + return genmodel; + } + + private File writeValidEcore() throws IOException { + File ecore = Files.createFile(tempDir.resolve("model.ecore")).toFile(); + Files.writeString( + ecore.toPath(), + """ + + + """); + return ecore; + } + + /** + * Stubs project generation so that invoking the root POM generator materializes the expected VSUM + * jar inside the workspace, and skips the real Maven build. + * + * @param service the spied service whose Maven build should be stubbed + * @param jarBytes the bytes to write as the built VSUM jar + * @throws Exception when stubbing fails + */ + private void stubProjectGeneration(VsumService service, byte[] jarBytes) throws Exception { + doAnswer( + invocation -> { + Path workspace = ((File) invocation.getArgument(0)).toPath().getParent(); + Path jar = workspace.resolve(VsumService.VSUM_JAR_RELATIVE_PATH); + Files.createDirectories(jar.getParent()); + Files.write(jar, jarBytes); + return null; + }) + .when(generateFromTemplate) + .generateRootPom(any(File.class), anyString()); + doNothing().when(service).runMavenBuild(any(Path.class), anyMap()); + } } From cebc79ea5299634fc7879997291c172b2dbf28ab Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:43:34 +0200 Subject: [PATCH 11/19] add sonar.projectKey --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index 1943c08..c9505fd 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,11 @@ 21 + + https://sonarcloud.io + vitruv-tools + vitruv-tools_Methodologist-SetupService + From 722aac7ba5f407787614590e6bfbb52637935c46 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:52:57 +0200 Subject: [PATCH 12/19] reformat: update method --- .../setup/vsum/controller/VsumController.java | 14 ++++++-------- .../vsum/service/VsumProjectBuildService.java | 15 +++------------ .../setup/vsum/service/VsumService.java | 4 ++-- .../setup/vsum/controller/VsumControllerTest.java | 6 +++--- .../setup/vsum/service/VsumServiceTest.java | 3 +-- 5 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java index f6911ac..488dec2 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java @@ -67,8 +67,7 @@ public ResponseEntity buildProject( required = true) @RequestPart("genmodelFiles") List genmodelFiles, - @Parameter(description = "Reaction files", required = true) - @RequestPart("reactionFiles") + @Parameter(description = "Reaction files", required = true) @RequestPart("reactionFiles") List reactionFiles) throws NoSuchFileException { @@ -94,7 +93,6 @@ public ResponseEntity buildProject( * @param genmodelFiles genmodel files paired by index with metamodel files * @param reactionFiles reaction files * @return response containing the built VSUM jar - * @throws NoSuchFileException when the build does not produce the expected jar */ @GetMapping(value = "/jar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( @@ -119,10 +117,8 @@ public ResponseEntity buildJar( required = true) @RequestPart("genmodelFiles") List genmodelFiles, - @Parameter(description = "Reaction files", required = true) - @RequestPart("reactionFiles") - List reactionFiles) - throws NoSuchFileException { + @Parameter(description = "Reaction files", required = true) @RequestPart("reactionFiles") + List reactionFiles) { byte[] jar = vsumProjectBuildService.buildProjectJar(metamodelFiles, genmodelFiles, reactionFiles); @@ -130,7 +126,9 @@ public ResponseEntity buildJar( HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.parseMediaType("application/java-archive")); headers.setContentDisposition( - ContentDisposition.attachment().filename(VSUM_JAR_FILENAME, StandardCharsets.UTF_8).build()); + ContentDisposition.attachment() + .filename(VSUM_JAR_FILENAME, StandardCharsets.UTF_8) + .build()); headers.setContentLength(jar.length); return ResponseEntity.ok().headers(headers).body(jar); diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java index e166c2f..9e2c2fd 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; @@ -46,13 +45,11 @@ public class VsumProjectBuildService { * @param genmodelFiles genmodel files in the same order as metamodel files * @param reactionFiles reaction files * @return built project archive bytes - * @throws NoSuchFileException when the build does not produce the expected artifact */ public byte[] buildProjectArchive( List metamodelFiles, List genmodelFiles, - List reactionFiles) - throws NoSuchFileException { + List reactionFiles) { return buildArtifact( metamodelFiles, genmodelFiles, reactionFiles, vsumService::generateProjectArchive); } @@ -66,13 +63,11 @@ public byte[] buildProjectArchive( * @param genmodelFiles genmodel files in the same order as metamodel files * @param reactionFiles reaction files * @return the bytes of the built VSUM jar - * @throws NoSuchFileException when the build does not produce the expected jar */ public byte[] buildProjectJar( List metamodelFiles, List genmodelFiles, - List reactionFiles) - throws NoSuchFileException { + List reactionFiles) { return buildArtifact( metamodelFiles, genmodelFiles, reactionFiles, vsumService::generateProjectJar); } @@ -87,14 +82,12 @@ public byte[] buildProjectJar( * @param reactionFiles reaction files * @param generator the VSUM generation strategy producing the artifact bytes * @return the produced artifact bytes - * @throws NoSuchFileException when the expected build artifact is missing */ private byte[] buildArtifact( List metamodelFiles, List genmodelFiles, List reactionFiles, - ProjectArtifactGenerator generator) - throws NoSuchFileException { + ProjectArtifactGenerator generator) { validateInputs(metamodelFiles, genmodelFiles, reactionFiles); Path uploadWorkspace = null; @@ -106,8 +99,6 @@ private byte[] buildArtifact( normalizeReactionImports(modelPairs, copiedReactionFiles); Map metamodelNamespaceMap = extractMetamodelNamespaceMap(modelPairs); return generator.generate(modelPairs, copiedReactionFiles, metamodelNamespaceMap); - } catch (NoSuchFileException e) { - throw e; } catch (IOException | MissingModelException e) { throw new MethodologistSetupException( VSUM_BUILD_ERROR_CODE, "Failed to build VSUM project archive", e); diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java index d3d028b..a7827b7 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java @@ -98,8 +98,8 @@ public byte[] generateProjectJar( } /** - * Generates a full VSUM project, builds it with Maven, and returns only the executable VSUM jar as - * bytes. + * Generates a full VSUM project, builds it with Maven, and returns only the executable VSUM jar + * as bytes. * * @param modelFiles paired metamodel/genmodel files * @param reactionFiles optional reaction files diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java index e747af1..eb6c2f2 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.when; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -139,7 +140,7 @@ void buildProjectReturnsZipResponse() throws Exception { /** Verifies the jar endpoint returns the jar bytes with the expected download headers. */ @Test - void buildJarReturnsJarResponse() { + void buildJarReturnsJarResponse() throws NoSuchFileException { VsumProjectBuildService buildService = mock(VsumProjectBuildService.class); byte[] jar = {9, 8, 7}; when(buildService.buildProjectJar(anyList(), anyList(), any())).thenReturn(jar); @@ -158,8 +159,7 @@ void buildJarReturnsJarResponse() { assertEquals(200, response.getStatusCode().value()); assertArrayEquals(jar, response.getBody()); - assertEquals( - "application/java-archive", response.getHeaders().getContentType().toString()); + assertEquals("application/java-archive", response.getHeaders().getContentType().toString()); assertEquals(jar.length, response.getHeaders().getContentLength()); assertEquals( "tools.vitruv.methodologisttemplate.vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar", diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java index 732c8de..f516e1c 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumServiceTest.java @@ -169,8 +169,7 @@ void generateProjectJar_shouldReturnBuiltJarBytes() throws Exception { stubProjectGeneration(service, jarBytes); byte[] result = - service.generateProjectJar( - List.of(modelFiles), List.of(), Map.of("pcm", "http://pcm/ns")); + service.generateProjectJar(List.of(modelFiles), List.of(), Map.of("pcm", "http://pcm/ns")); assertThat(result).isEqualTo(jarBytes); verify(service).runMavenBuild(any(Path.class), eq(Map.of("pcm", "http://pcm/ns"))); From 1e32f05f1e84e3344a42f41bd57828f79a0e03aa Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:56:19 +0200 Subject: [PATCH 13/19] remove unnecessary test cases --- .../service/VsumProjectBuildServiceTest.java | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java index 06d96d8..dd2bb08 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildServiceTest.java @@ -158,22 +158,6 @@ void buildProjectArchive_shouldWrapIOException() throws Exception { .hasMessageContaining("Failed to build VSUM project archive"); } - @Test - void buildProjectArchive_shouldPropagateNoSuchFileException() throws Exception { - - when(vsumService.generateProjectArchive(anyList(), anyList(), anyMap())) - .thenThrow(new java.nio.file.NoSuchFileException("missing.jar")); - - assertThatThrownBy( - () -> - service.buildProjectArchive( - List.of(validMetamodel()), - List.of(mockMultipart("model.genmodel")), - List.of(reactionFile()))) - .isInstanceOf(java.nio.file.NoSuchFileException.class) - .hasMessageContaining("missing.jar"); - } - @Test void buildProjectArchive_shouldWrapMissingModelException() throws Exception { @@ -266,22 +250,6 @@ void buildProjectJar_shouldWrapIOException() throws Exception { .hasMessageContaining("Failed to build VSUM project"); } - @Test - void buildProjectJar_shouldPropagateNoSuchFileException() throws Exception { - - when(vsumService.generateProjectJar(anyList(), anyList(), anyMap())) - .thenThrow(new java.nio.file.NoSuchFileException("missing.jar")); - - assertThatThrownBy( - () -> - service.buildProjectJar( - List.of(validMetamodel()), - List.of(mockMultipart("model.genmodel")), - List.of(reactionFile()))) - .isInstanceOf(java.nio.file.NoSuchFileException.class) - .hasMessageContaining("missing.jar"); - } - @Test void buildProjectJar_shouldWrapInterruptedException() throws Exception { From d8261f6588088bcfd070512dd3b253a72cbf2f02 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:59:00 +0200 Subject: [PATCH 14/19] reformat --- .../vitruv/methodologist/setup/vsum/service/VsumService.java | 3 ++- .../setup/vsum/controller/VsumControllerTest.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java index a7827b7..2ef6a61 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumService.java @@ -38,7 +38,8 @@ public class VsumService { * build. */ static final String VSUM_JAR_RELATIVE_PATH = - "vsum/target/tools.vitruv.methodologisttemplate.vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar"; + "vsum/target/tools.vitruv.methodologisttemplate." + + "vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar"; private final GenerateFromTemplate generateFromTemplate; diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java index eb6c2f2..d4be869 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java @@ -162,7 +162,8 @@ void buildJarReturnsJarResponse() throws NoSuchFileException { assertEquals("application/java-archive", response.getHeaders().getContentType().toString()); assertEquals(jar.length, response.getHeaders().getContentLength()); assertEquals( - "tools.vitruv.methodologisttemplate.vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar", + "tools.vitruv.methodologisttemplate." + + "vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar", response.getHeaders().getContentDisposition().getFilename()); verify(buildService).buildProjectJar(anyList(), anyList(), any()); From 86b73bdb72c8da7911e6c3c870e064275213f71f Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:00:31 +0200 Subject: [PATCH 15/19] reformat --- .../setup/vsum/controller/VsumControllerTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java index d4be869..eb6c2f2 100644 --- a/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java +++ b/src/test/java/tools/vitruv/methodologist/setup/vsum/controller/VsumControllerTest.java @@ -162,8 +162,7 @@ void buildJarReturnsJarResponse() throws NoSuchFileException { assertEquals("application/java-archive", response.getHeaders().getContentType().toString()); assertEquals(jar.length, response.getHeaders().getContentLength()); assertEquals( - "tools.vitruv.methodologisttemplate." + - "vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar", + "tools.vitruv.methodologisttemplate.vsum-0.1.0-SNAPSHOT-jar-with-dependencies.jar", response.getHeaders().getContentDisposition().getFilename()); verify(buildService).buildProjectJar(anyList(), anyList(), any()); From 2335f4d64cb0258c62a75ba24830e09170c7ef6f Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:17:56 +0200 Subject: [PATCH 16/19] Use try-with-resources or close this "Stream" in a "finally" clause --- .../setup/emf/EMFModelInitializer.java | 46 +++++++++++-------- .../setup/messages/ErrorMessages.java | 4 ++ .../setup/messages/InfoMessages.java | 2 + 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java b/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java index 12ace91..6f0ef01 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java +++ b/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java @@ -9,9 +9,12 @@ import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.emf.ecore.EPackage; +import tools.vitruv.methodologist.setup.messages.ErrorMessages; +import tools.vitruv.methodologist.setup.messages.InfoMessages; /** * Utility class for initializing EMF packages before Xtext validation. @@ -85,33 +88,36 @@ public static Map initializeFromClasses( try { Path classesDir = Path.of(classesPath); if (!Files.exists(classesDir)) { - logger.warn("Classes directory not found: {}", classesPath); + logger.warn(ErrorMessages.EMF_CLASSES_DIR_NOT_FOUND, classesPath); return packages; } - Files.walk(classesDir) - .filter(p -> Files.isRegularFile(p)) - .filter(p -> p.toString().endsWith("FactoryImpl.class")) - .forEach( - classPath -> { - try { - String className = - classesDir - .relativize(classPath) - .toString() - .replace(File.separator, ".") - .replace(".class", ""); - loadMetamodelPackage(className, packages); - } catch (Exception e) { - logger.debug("Could not load package from {}: {}", classPath, e.getMessage()); - } - }); + try (Stream paths = Files.walk(classesDir)) { + paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith("FactoryImpl.class")) + .forEach( + classPath -> { + try { + String className = + classesDir + .relativize(classPath) + .toString() + .replace(File.separator, ".") + .replace(".class", ""); + loadMetamodelPackage(className, packages); + } catch (Exception e) { + logger.debug( + ErrorMessages.EMF_CLASSES_LOAD_PACKAGE_ERROR, classPath, e.getMessage()); + } + }); + } } catch (Exception e) { - logger.error("Error initializing EMF packages from classes directory: {}", classesPath, e); + logger.error(ErrorMessages.EMF_CLASSES_INIT_ERROR, classesPath, e); } - logger.info("Initialized {} EMF packages from classes directory", packages.size()); + logger.info(InfoMessages.EMF_CLASSES_INIT_SUCCESS, packages.size()); return packages; } diff --git a/src/main/java/tools/vitruv/methodologist/setup/messages/ErrorMessages.java b/src/main/java/tools/vitruv/methodologist/setup/messages/ErrorMessages.java index b6ce962..b43e2b7 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/messages/ErrorMessages.java +++ b/src/main/java/tools/vitruv/methodologist/setup/messages/ErrorMessages.java @@ -22,6 +22,10 @@ public final class ErrorMessages { "Expected VSUM jar was not produced by the build: %s"; public static final String VSUM_REACTION_FILES_REQUIRED = "At least one reaction file is required"; + public static final String EMF_CLASSES_DIR_NOT_FOUND = "Classes directory not found: {}"; + public static final String EMF_CLASSES_LOAD_PACKAGE_ERROR = "Could not load package from {}: {}"; + public static final String EMF_CLASSES_INIT_ERROR = + "Error initializing EMF packages from classes directory: {}"; private ErrorMessages() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); diff --git a/src/main/java/tools/vitruv/methodologist/setup/messages/InfoMessages.java b/src/main/java/tools/vitruv/methodologist/setup/messages/InfoMessages.java index 5ba7120..830870f 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/messages/InfoMessages.java +++ b/src/main/java/tools/vitruv/methodologist/setup/messages/InfoMessages.java @@ -26,6 +26,8 @@ public final class InfoMessages { public static final String FOREIGN_MODEL_ADDED = "Added missing foreignModel entry: %s"; public static final String FOREIGN_MODEL_WOULD_ADD = "Would add missing foreignModel entry: %s"; public static final String UNNAMED_PACKAGE = ""; + public static final String EMF_CLASSES_INIT_SUCCESS = + "Initialized {} EMF packages from classes directory"; private InfoMessages() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); From b4df818b86928cbe6e2f12b6f8b48775ae6852ea Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:21:57 +0200 Subject: [PATCH 17/19] Disable access to external entities in XML parsing. --- .../setup/vsum/service/VsumProjectBuildService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java index 9e2c2fd..d5342d4 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/VsumProjectBuildService.java @@ -312,6 +312,12 @@ private MetamodelInfo readMetamodelInfo(File metamodelFile) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); Document document = factory.newDocumentBuilder().parse(metamodelFile); Element root = document.getDocumentElement(); String nsUri = root.getAttribute("nsURI"); From 1bd8f4146a7068894a779d2d08f3b7aa3ae00e67 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:40:20 +0200 Subject: [PATCH 18/19] modify Dockerfile --- Dockerfile | 20 +++++-------------- .../setup/vsum/controller/VsumController.java | 6 +++--- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index cc41292..dbeaca0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,11 @@ -FROM eclipse-temurin:25-jdk AS build -WORKDIR /workspace +FROM eclipse-temurin:21-jdk RUN apt-get update \ - && apt-get install -y --no-install-recommends maven \ - && rm -rf /var/lib/apt/lists/* + && apt-get install -y maven \ + && rm -rf /var/lib/apt/lists/* -COPY pom.xml . -COPY src ./src - -RUN mvn -q -DskipTests package - -FROM eclipse-temurin:25-jre -WORKDIR /app - -COPY --from=build /workspace/target/*.jar app.jar +COPY methodologist-setup-service-0.0.1-SNAPSHOT.jar /app/app.jar EXPOSE 8090 -ENTRYPOINT ["java", "-jar", "/app/app.jar"] - +ENTRYPOINT ["java", "-jar", "/app/app.jar"] \ No newline at end of file diff --git a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java index 488dec2..b2e187c 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java +++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java @@ -15,7 +15,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; @@ -44,7 +44,7 @@ public class VsumController { * @return zip file response containing generated project * @throws NoSuchFileException when the build does not produce the expected artifact */ - @GetMapping(value = "/build", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "/build", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( summary = "Build VSUM project", description = @@ -94,7 +94,7 @@ public ResponseEntity buildProject( * @param reactionFiles reaction files * @return response containing the built VSUM jar */ - @GetMapping(value = "/jar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "/jar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( summary = "Build VSUM jar", description = From 9fcbb57809e6c25450c7c8be44a46dc704fb2575 Mon Sep 17 00:00:00 2001 From: Mohammadali Mirzaei <93537795+ma-mirzaei@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:47:47 +0200 Subject: [PATCH 19/19] modify Dockerfile and --- .../setup/model/controller/GenmodelController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/tools/vitruv/methodologist/setup/model/controller/GenmodelController.java b/src/main/java/tools/vitruv/methodologist/setup/model/controller/GenmodelController.java index 5b9e2d6..135a71e 100644 --- a/src/main/java/tools/vitruv/methodologist/setup/model/controller/GenmodelController.java +++ b/src/main/java/tools/vitruv/methodologist/setup/model/controller/GenmodelController.java @@ -156,6 +156,6 @@ private String generateProcessedFilename(String originalFilename) { originalFilename.contains(".") ? originalFilename.substring(0, originalFilename.lastIndexOf(".")) : originalFilename; - return nameWithoutExt + "_processed.genmodel"; + return nameWithoutExt + ".genmodel"; } }