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.
+ *
+ *
-- GETTER -- Returns the workflow of the configuration.
+ */
+ @Getter @Setter private File workflow;
+
+ /**
+ * 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.
+ *
+ * @param metamodelLocation The metamodel location to add.
+ */
+ 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.
+ *
+ * @param paths The metamodel argument string.
+ */
+ public void setMetaModelLocations(String paths) {
+ Resource.Factory.Registry reg = Resource.Factory.Registry.INSTANCE;
+ reg.getExtensionToFactoryMap().put("ecore", new XMIResourceFactoryImpl());
+ reg.getExtensionToFactoryMap().put("genmodel", new XMIResourceFactoryImpl());
+
+ 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 = "";
+
+ 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());
+ 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 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..6f0ef01
--- /dev/null
+++ b/src/main/java/tools/vitruv/methodologist/setup/emf/EMFModelInitializer.java
@@ -0,0 +1,176 @@
+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;
+import java.util.HashMap;
+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.
+ *
+ * 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();
+
+ 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());
+ }
+ });
+
+ 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(ErrorMessages.EMF_CLASSES_DIR_NOT_FOUND, classesPath);
+ return packages;
+ }
+
+ 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(ErrorMessages.EMF_CLASSES_INIT_ERROR, classesPath, e);
+ }
+
+ logger.info(InfoMessages.EMF_CLASSES_INIT_SUCCESS, 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) {}
+
+ /**
+ * 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 {
+ Class> factoryClass = Class.forName(className);
+
+ Field instanceField = factoryClass.getField("eINSTANCE");
+ Object factoryInstance = instanceField.get(null);
+
+ String packageClassName =
+ className.substring(0, className.lastIndexOf("FactoryImpl")) + "Package";
+ Class> packageClass = Class.forName(packageClassName);
+ Field instance = packageClass.getField("eINSTANCE");
+ EPackage ePackage = (EPackage) instance.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..543196a
--- /dev/null
+++ b/src/main/java/tools/vitruv/methodologist/setup/exception/MissingModelException.java
@@ -0,0 +1,20 @@
+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. */
+ public MissingModelException(String message) {
+ super(message);
+ }
+
+ /** Exception thrown when a required model is missing. */
+ public MissingModelException(String message, Throwable cause) {
+ super(message, 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/messages/ErrorMessages.java b/src/main/java/tools/vitruv/methodologist/setup/messages/ErrorMessages.java
index 6a064b8..b43e2b7 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,14 @@ 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";
+ 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");
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";
}
}
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..39d8737
--- /dev/null
+++ b/src/main/java/tools/vitruv/methodologist/setup/util/FileUtils.java
@@ -0,0 +1,176 @@
+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 {
+
+ /**
+ * 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());
+
+ private FileUtils() {}
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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())) {
+ 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..b2e187c
--- /dev/null
+++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/controller/VsumController.java
@@ -0,0 +1,146 @@
+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.nio.file.NoSuchFileException;
+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.PostMapping;
+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 {
+
+ /** 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;
+
+ /**
+ * 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 reaction files
+ * @return zip file response containing generated project
+ * @throws NoSuchFileException when the build does not produce the expected artifact
+ */
+ @PostMapping(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 = "Reaction files", required = true) @RequestPart("reactionFiles")
+ List reactionFiles)
+ throws NoSuchFileException {
+
+ 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);
+ }
+
+ /**
+ * 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
+ */
+ @PostMapping(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) {
+
+ 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.
+ *
+ * @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..ea5b4cb
--- /dev/null
+++ b/src/main/java/tools/vitruv/methodologist/setup/vsum/service/GenerateFromTemplate.java
@@ -0,0 +1,328 @@
+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 {
+ private static final String PACKAGE_NAME = "packageName";
+
+ /** Constructor. */
+ public GenerateFromTemplate() {}
+
+ 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