diff --git a/cobigen-templates/templates-devon4j/.gitignore b/cobigen-templates/templates-devon4j/.gitignore
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/cobigen-templates/templates-devon4j/pom.xml b/cobigen-templates/templates-devon4j/pom.xml
index 9bcaece44e..e2ff65a882 100644
--- a/cobigen-templates/templates-devon4j/pom.xml
+++ b/cobigen-templates/templates-devon4j/pom.xml
@@ -54,6 +54,21 @@
3.10.0
test
+
+ jakarta.validation
+ jakarta.validation-api
+ 3.0.2
+
+
+ com.devonfw.cobigen
+ javaplugin
+ test
+
+
+ com.devonfw.cobigen
+ tempeng-freemarker
+ test
+
diff --git a/cobigen-templates/templates-devon4j/src/main/java/com/devonfw/cobigen/templates/devon4j/utils/JavaUtil.java b/cobigen-templates/templates-devon4j/src/main/java/com/devonfw/cobigen/templates/devon4j/utils/JavaUtil.java
index ced2f5fde2..f1bb367ccc 100644
--- a/cobigen-templates/templates-devon4j/src/main/java/com/devonfw/cobigen/templates/devon4j/utils/JavaUtil.java
+++ b/cobigen-templates/templates-devon4j/src/main/java/com/devonfw/cobigen/templates/devon4j/utils/JavaUtil.java
@@ -31,7 +31,7 @@ public JavaUtil() {
/**
* Returns the Object version of a Java primitive or the input if the input isn't a java primitive
- *
+ *
* @param simpleType String
* @return the corresponding object wrapper type simple name of the input if the input is the name of a primitive java
* type. The input itself if not. (e.g. "int" results in "Integer")
@@ -62,7 +62,7 @@ public String boxJavaPrimitives(Class> pojoClass, String fieldName) throws NoS
if (pojoClass == null) {
throw new IllegalAccessError(
- "Class object is null. Cannot generate template as it might obviously depend on reflection.");
+ "Class object is null. Cannot generate template as it might obviously depend on reflection.");
}
if (equalsJavaPrimitive(pojoClass, fieldName)) {
@@ -122,7 +122,7 @@ public boolean equalsJavaPrimitiveOrWrapper(String simpleType) {
* @throws SecurityException if the field cannot be accessed.
*/
public boolean equalsJavaPrimitive(Class> pojoClass, String fieldName)
- throws NoSuchFieldException, SecurityException {
+ throws NoSuchFieldException, SecurityException {
if (pojoClass == null) {
return false;
@@ -169,10 +169,10 @@ public boolean equalsJavaPrimitiveIncludingArrays(String simpleType) {
* @throws SecurityException if the field cannot be accessed.
*/
public boolean equalsJavaPrimitiveIncludingArrays(Class> pojoClass, String fieldName)
- throws NoSuchFieldException, SecurityException {
+ throws NoSuchFieldException, SecurityException {
return equalsJavaPrimitive(pojoClass, fieldName) || (pojoClass.getDeclaredField(fieldName).getType().isArray()
- && pojoClass.getDeclaredField(fieldName).getType().getComponentType().isPrimitive());
+ && pojoClass.getDeclaredField(fieldName).getType().getComponentType().isPrimitive());
}
/**
@@ -206,7 +206,7 @@ public String castJavaPrimitives(String simpleType, String varName) throws Class
* @throws SecurityException if the field cannot be accessed.
*/
public String castJavaPrimitives(Class> pojoClass, String fieldName)
- throws NoSuchFieldException, SecurityException {
+ throws NoSuchFieldException, SecurityException {
if (equalsJavaPrimitive(pojoClass, fieldName)) {
return String.format("((%1$s)%2$s)", boxJavaPrimitives(pojoClass, fieldName), fieldName);
@@ -334,7 +334,7 @@ public String getReturnType(Class> pojoClass, String methodName) throws NoSuch
if (pojoClass == null) {
throw new IllegalAccessError(
- "Class object is null. Cannot generate template as it might obviously depend on reflection.");
+ "Class object is null. Cannot generate template as it might obviously depend on reflection.");
}
String s = "-";
Method method = findMethod(pojoClass, methodName);
@@ -357,11 +357,11 @@ public String getReturnType(Class> pojoClass, String methodName) throws NoSuch
*/
@SuppressWarnings("unchecked")
public String getReturnTypeOfMethodAnnotatedWith(Class> pojoClass, String annotatedClassName)
- throws ClassNotFoundException {
+ throws ClassNotFoundException {
if (pojoClass == null) {
throw new IllegalAccessError(
- "Class object is null. Cannot generate template as it might obviously depend on reflection.");
+ "Class object is null. Cannot generate template as it might obviously depend on reflection.");
}
Method[] methods = pojoClass.getDeclaredMethods();
@@ -414,7 +414,7 @@ private Method findMethod(Class> pojoClass, String methodName) {
if (pojoClass == null) {
throw new IllegalAccessError(
- "Class object is null. Cannot generate template as it might obviously depend on reflection.");
+ "Class object is null. Cannot generate template as it might obviously depend on reflection.");
}
for (Method m : pojoClass.getMethods()) {
if (m.getName().equals(methodName)) {
diff --git a/cobigen-templates/templates-devon4j/src/main/java/com/devonfw/cobigen/templates/devon4j/utils/SQLUtil.java b/cobigen-templates/templates-devon4j/src/main/java/com/devonfw/cobigen/templates/devon4j/utils/SQLUtil.java
new file mode 100644
index 0000000000..2795d835b1
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/main/java/com/devonfw/cobigen/templates/devon4j/utils/SQLUtil.java
@@ -0,0 +1,300 @@
+package com.devonfw.cobigen.templates.devon4j.utils;
+
+import java.util.*;
+import java.util.function.Function;
+
+/**
+ * Provides operations to identify and process SQL specific information
+ *
+ */
+public class SQLUtil {
+
+ private static int DEFAULT_FIELD_LENGTH = 255;
+
+ /**
+ * The constructor.
+ */
+ public SQLUtil() {
+
+ // Empty for CobiGen to automatically instantiate it
+ }
+
+ /**
+ * Unwraps type to autogenerate a name for the table following devonfw naming convention.
+ *
+ * @param entityType String that represents the entity class type
+ * @return parsed table name
+ */
+ public String tableName(String entityType) {
+
+ return entityType.replaceFirst(".*<", "").replaceFirst(">.*", "").replace("Entity", "").toUpperCase();
+ }
+
+ /**
+ * Parses a @JoinColumn annotation directly into a Foreign Key statement for a @JoinTable
+ *
+ * @param joinColumnAnnotation
+ * @param defaultTableName Possible to pass TableName in case it's not specified in the annotation and has to be
+ * implied from context
+ * @return column + foreign key constraint for this @JoinColumn annotation
+ */
+ public String parseJoinColumn(Map joinColumnAnnotation, String defaultTableName) {
+
+ Function extract = (fieldKey) -> Objects.requireNonNull(getValue(joinColumnAnnotation, fieldKey));
+ String name = extract.apply("name");
+ boolean nullable = Boolean.parseBoolean(extract.apply("nullable"));
+ boolean unique = Boolean.parseBoolean(extract.apply("unique"));
+ String table = extract.apply("table");
+ String referencedColumnName = extract.apply("referencedColumnName");
+ // Check if some fields haven't been defined and replace with defaults
+ if (referencedColumnName.equals("")) {
+ referencedColumnName = "ID";
+ }
+ if (table.equals("")) {
+ table = defaultTableName;
+ }
+ if (table.equals("")) {
+ throw new IllegalStateException(
+ "Cannot infer name for reference table! Error encountered while parsing JoinColumnAnnotation: "
+ + joinColumnAnnotation.toString());
+ }
+ // If the name is empty build the fieldName by appending ID to the tableName
+ if (name.equals("")) {
+ name = table + "_ID";
+ }
+ String statement = name + " BIGINT";
+ if (unique) {
+ statement += " UNIQUE";
+ }
+ if (!nullable) {
+ statement += " NOT NULL";
+ }
+ String foreignKeyDef = String.format("FOREIGN KEY (%s) REFERENCES %s(%s)", name, table, referencedColumnName);
+
+ return statement + ", " + foreignKeyDef;
+ }
+
+ /**
+ * Generates a primary key statement from the given field
+ * @param field Dynamic hashmap containing field data
+ * @return SQL Primary key statement for table as String
+ */
+ public String primaryKeyStatement(Map field) {
+
+ String fieldName = getFieldName(field);
+ Map annotations = getValue(field, "annotations");
+ Map columnAnnotations = getValue(annotations, "javax_persistence_Column");
+ // Check for @Column to override default fieldname
+ if (columnAnnotations != null) {
+ String columnFieldName = getValue(columnAnnotations, "name");
+ if (columnFieldName != null)
+ fieldName = columnFieldName;
+ }
+ String incrementType = "AUTO_INCREMENT";
+ return String.format("%s BIGINT %s PRIMARY KEY", fieldName, incrementType);
+ }
+
+ /**
+ * Generates a foreign key statement from the given field
+ * @param field Dynamic hashmap containing field data
+ * @return SQL Foreign key statement as String
+ */
+ public String foreignKeyStatement(Map field) {
+
+ Map annotations = getValue(field, "annotations");
+ Map joinColumnAnnotation = getValue(annotations, "javax_persistence_JoinColumn");
+ String fieldName = Objects.requireNonNull(getValue(field, "name")), fieldType = "BIGINT",
+ refTable = Objects.requireNonNull(getValue(field, "type")), referencedColumnName = "id";
+ Boolean unique = false, nullable = true;
+
+ // Try extracting tablename from type following devonfw naming conventions: AwdeEntity -> AWDE
+ refTable = refTable.replace("Entity", "").toUpperCase();
+
+ // Try autogenerating foreign key name through naming convention
+ fieldName = fieldName.replace("Entity", "").toUpperCase() + "_ID";
+
+ // Parse @JoinColumn values and override defaults if values are present
+ if (joinColumnAnnotation != null) {
+ // Each field is extracted and controlled, if present override the defaults.
+ String nameOverride = getValue(joinColumnAnnotation, "name");
+ if (!Objects.equals(nameOverride, ""))
+ fieldName = nameOverride;
+
+ String tableOverride = getValue(joinColumnAnnotation, "table");
+ if (!Objects.equals(tableOverride, ""))
+ refTable = tableOverride;
+
+ String refColOverride = getValue(joinColumnAnnotation, "referencedColumnName");
+ if (!Objects.equals(refColOverride, ""))
+ referencedColumnName = refColOverride;
+
+ unique = isUnique(joinColumnAnnotation);
+ nullable = isNullable(joinColumnAnnotation);
+ }
+ // Build column definition
+ String columnDef = fieldName + " " + fieldType;
+ if (unique)
+ columnDef += " UNIQUE";
+ if (!nullable)
+ columnDef += " NOT NULL";
+ // Build Foreign Key constraint and append it to column definition
+ String foreignKeyDef = String.format("FOREIGN KEY (%s) REFERENCES %s(%s)", fieldName, refTable,
+ referencedColumnName);
+ return columnDef + ", " + foreignKeyDef;
+ }
+
+ /**
+ * Basic SQL column statements derived from field hashmaps
+ * @param field Dynamic hashmap with field data
+ * @return Basic SQL statement as String
+ */
+ public String basicStatement(Map field) {
+
+ Map columnAnnotation = chainAccess(field, new String[] { "annotations", "javax_persistence_Column" });
+ String fieldName = getFieldName(field), typeString = Objects.requireNonNull(getValue(field, "type")),
+ fieldType = mapType(typeString);
+ Integer fieldLength = DEFAULT_FIELD_LENGTH;
+ boolean nullable = true, unique = false;
+ // Try to infer fieldType from possible annotations
+ Map enumerateAnnotation = chainAccess(field,
+ new String[] { "annotations", "javax_persistence_Enumerated" });
+ if (enumerateAnnotation != null) {
+ String enumType = Objects.requireNonNull(getValue(enumerateAnnotation, "value"));
+ if (enumType.equals("STRING")) {
+ fieldType = "VARCHAR";
+ } else {
+ fieldType = "INTEGER";
+ }
+ }
+ // Parse @Column if present
+ if (columnAnnotation != null) {
+ fieldLength = Integer.parseInt(Objects.requireNonNull(getValue(columnAnnotation, "length")));
+ nullable = isNullable(columnAnnotation);
+ unique = isUnique(columnAnnotation);
+ }
+
+ // If fieldType is still empty throw exception
+ if (fieldType == null)
+ throw new IllegalArgumentException("Couldn't map Java type to SQL type for typeString: " + typeString);
+
+ // Add size to VARCHAR fields
+ if (fieldType.equals("VARCHAR")) {
+ fieldType = String.format("VARCHAR(%d)", fieldLength);
+ }
+ String statement = String.format("%s %s", fieldName, fieldType);
+ if (!nullable)
+ statement += " NOT NULL";
+ if (unique)
+ statement += " UNIQUE";
+ return statement;
+ }
+
+ /**
+ * Extracts value of nullable from @Column and @JoinColumn annotations
+ *
+ * @param columnAnnotation Map for the Column and JoinColumn annotations
+ */
+ private static boolean isNullable(Map columnAnnotation) {
+
+ return Boolean.parseBoolean(getValue(columnAnnotation, "nullable"));
+ }
+
+ /**
+ * Extracts value of unique from @Column and @JoinColumn annotations
+ *
+ * @param columnAnnotation Map for the Column and JoinColumn annotations
+ */
+ private static boolean isUnique(Map columnAnnotation) {
+
+ return Boolean.parseBoolean(getValue(columnAnnotation, "unique"));
+ }
+
+ /**
+ * Helper function to map simple Java types to SQL types, returns null on unmappable type
+ * @param typeString JavaType as String (int, Long, String...)
+ * @return SQLType as String
+ */
+ public static String mapType(String typeString) {
+
+ // Shortcut for case insensitive regex matching with start and ending ignore
+ Function match = (regex) -> typeString.matches("(?i).*" + "(" + regex + ")" + ".*");
+ if (match.apply("(integer)|(int)")) {
+ return "INTEGER";
+ } else if (match.apply("long")) {
+ return "BIGINT";
+ } else if (match.apply("short")) {
+ return "SMALLINT";
+ } else if (match.apply("BigDecimal")) {
+ return "NUMERIC";
+ } else if (match.apply("String")) {
+ return "VARCHAR";
+ } else if (match.apply("(char)|(Character)")) {
+ return "CHAR(1)";
+ } else if (match.apply("byte\\[\\]")) {
+ return "BLOB";
+ } else if (match.apply("byte")) {
+ return "TINYINT";
+ } else if (match.apply("boolean")) {
+ return "BIT";
+ } else if (match.apply("Date")) {
+ return "DATE";
+ } else if (match.apply("(Class)|(Locale)|(TimeZone)|(Currency)")) {
+ return "VARCHAR";
+ } else if (match.apply("(Timestamp)|(Calendar)")) {
+ return "TIMESTAMP";
+ } else if (match.apply("Time")) {
+ return "TIME";
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Extracts the name of the field from the Map whilst checking for name-override in @Column annotation
+ * @param field Dynamic map for field
+ * @return simple name for field as String
+ */
+ static private String getFieldName(Map field) {
+
+ String fieldName = chainAccess(field, new String[] { "annotations", "javax_persistence_Column", "name" });
+ if (fieldName != null && !fieldName.equals("")) {
+ return fieldName;
+ } else {
+ return Objects.requireNonNull(getValue(field, "name"));
+ }
+ }
+
+ /**
+ * Helper function to navigate nested maps dynamically. Returns null on any type of error
+ * @param map Dynamic map from which to extract data
+ * @param nestedFields ordered array of fields that need to be navigated in the map
+ * @return value if found, null otherwise (both on casting errors and value not found)
+ */
+ static private T chainAccess(Map map, String[] nestedFields) {
+
+ try {
+ for (int i = 0; i < nestedFields.length - 1; i++) {
+ String key = nestedFields[i];
+ map = getValue(map, key);
+ }
+ return (T) getValue(map, nestedFields[nestedFields.length - 1]);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ /**
+ * Parametrized helper function to dynamically extract data from a map. Returns null on casting errors
+ * @param map Dynamic map from which to extract data
+ * @param key key for the value
+ * @return value if found and cast succeeds, null otherwise
+ */
+ static private T getValue(Map map, String key) {
+
+ try {
+ return (T) map.get(key);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+}
diff --git a/cobigen-templates/templates-devon4j/src/main/templates/sql_java_app/context.xml b/cobigen-templates/templates-devon4j/src/main/templates/sql_java_app/context.xml
new file mode 100644
index 0000000000..7dc46c7425
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/main/templates/sql_java_app/context.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cobigen-templates/templates-devon4j/src/main/templates/sql_java_app/templates.xml b/cobigen-templates/templates-devon4j/src/main/templates/sql_java_app/templates.xml
new file mode 100644
index 0000000000..d3952f36f2
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/main/templates/sql_java_app/templates.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cobigen-templates/templates-devon4j/src/main/templates/sql_java_app/templates/V0000__Create_${variables.entityName}Entity.sql.ftl b/cobigen-templates/templates-devon4j/src/main/templates/sql_java_app/templates/V0000__Create_${variables.entityName}Entity.sql.ftl
new file mode 100644
index 0000000000..91c519eec9
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/main/templates/sql_java_app/templates/V0000__Create_${variables.entityName}Entity.sql.ftl
@@ -0,0 +1,73 @@
+<#if pojo.annotations.javax_persistence_Table??>
+ <#assign tableName = pojo.annotations.javax_persistence_Table.name>
+<#else>
+ <#assign tableName = SQLUtil.tableName(pojo.name)>
+#if>
+<#assign statements = []>
+<#assign joinTables = []>
+<#list pojo.methodAccessibleFields as field>
+<#-- Skip Transient fields -->
+ <#if field.annotations.javax_persistence_Transient??>
+ <#continue>
+ <#-- Primary Key statement -->
+ <#elseif field.annotations.javax_persistence_Id??>
+ <#assign statements += [SQLUtil.primaryKeyStatement(field)]>
+ <#-- OneToOne statement -->
+ <#elseif field.annotations.javax_persistence_OneToOne??>
+ <#-- Key mapped on other table, skip -->
+ <#if field.annotations.javax_persistence_OneToOne.mappedBy != "">
+ <#continue>
+ #if>
+ <#assign statements += [SQLUtil.foreignKeyStatement(field)]>
+ <#-- Skip OneToMany as it's just a Foreign Key on a different table -->
+ <#elseif field.annotations.javax_persistence_OneToMany??>
+ <#continue>
+ <#-- ManyToOne: create Foreign Keystatement from the field -->
+ <#elseif field.annotations.javax_persistence_ManyToOne??>
+ <#assign statements += [SQLUtil.foreignKeyStatement(field)]>
+ <#-- TODO: Check if there is a JoinTable specified that should be created. -->
+ <#elseif field.annotations.javax_persistence_ManyToMany?? && field.annotations.javax_persistence_JoinTable??>
+ <#assign joinTableAnnotation = field.annotations.javax_persistence_JoinTable>
+
+ <#-- Parse joinColumns to generate Foreign Keys -->
+ <#assign joinColumns = joinTableAnnotation.joinColumns>
+ <#assign inverseJoinColumns = joinTableAnnotation.inverseJoinColumns>
+ <#-- Statement collector list -->
+ <#assign statements = [] >
+ <#assign defaultFieldTable = SQLUtil.tableName(field.type)>
+ <#list joinColumns as jcol>
+ <#assign jcolAnnotation = jcol.javax_persistence_JoinColumn>
+ <#assign statements += [SQLUtil.parseJoinColumn(jcolAnnotation, defaultFieldTable)]>
+ #list>
+
+ <#-- When parsing inverse join columns pass the tableName as a default for the reference to this parsed Entity -->
+ <#list inverseJoinColumns as jcol>
+ <#assign jcolAnnotation = jcol.javax_persistence_JoinColumn>
+ <#assign statements += [SQLUtil.parseJoinColumn(jcolAnnotation, tableName)]>
+ #list>
+ <#-- Build joinTable with parsed data -->
+ <#assign joinTable = {}>
+ <#assign joinTable += { "statements": statements }>
+ <#assign joinTable += { "name": joinTableAnnotation.name }>
+ <#-- Append result to collector list -->
+ <#assign joinTables += [joinTable]>
+
+ <#-- Try generating simple SQL statement from field -->
+ <#else>
+ <#assign statements += [SQLUtil.basicStatement(field)]>
+ #if>
+#list>
+CREATE TABLE ${tableName} (
+<#list statements as statement>
+ ${statement},
+#list>
+);
+<#-- TODO: parse generated JoinTables -->
+<#list joinTables as tb>
+CREATE TABLE ${tb.name} (
+ ID BIGINT AUTO_INCREMENT PRIMARY KEY,
+<#list tb.statements as statement>
+ ${statement},
+#list>
+);
+#list>
\ No newline at end of file
diff --git a/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/AbstractJavaTemplateTest.java b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/AbstractJavaTemplateTest.java
new file mode 100644
index 0000000000..7f94480974
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/AbstractJavaTemplateTest.java
@@ -0,0 +1,113 @@
+package com.devonfw.cobigen.templates.devon4j.test.templates;
+
+import com.devonfw.cobigen.api.extension.TextTemplate;
+import com.devonfw.cobigen.api.extension.TextTemplateEngine;
+import com.devonfw.cobigen.javaplugin.inputreader.JavaInputReader;
+import com.devonfw.cobigen.tempeng.freemarker.FreeMarkerTemplateEngine;
+import org.junit.Before;
+
+import java.io.StringWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class AbstractJavaTemplateTest {
+ /**
+ * Model used by freemarker
+ */
+ public Map model;
+ /**
+ * Engine that will generate output from model and template
+ */
+ public FreeMarkerTemplateEngine engine;
+ /**
+ * Implementation of TextTemplate for template processing
+ */
+ public TextTemplate template;
+
+ /**
+ * Creates an anonymous TextTemplate object that works on the given template files
+ * @param relativePath Relative Path to the template from the /templates folder, most likely coincides with template
+ * filename
+ * @param relativeAbsolutePath Relative path from source root to the template
+ * @return anonymous instance of TextTemplate that holds the data in Overridden interface methods
+ */
+ public TextTemplate createTemplate(String relativePath, Path relativeAbsolutePath) {
+
+ return new TextTemplate() {
+ @Override
+ public String getRelativeTemplatePath() {
+
+ return relativePath;
+ }
+
+ @Override
+ public Path getAbsoluteTemplatePath() {
+
+ return relativeAbsolutePath;
+ }
+ };
+ }
+
+ /**
+ * Creates a template engine for the given template path
+ *
+ * @param templateFolderPath Relative path to template folder from project source root
+ * @return {@link TextTemplateEngine} implementation for Apache FreeMarker.
+ */
+ public FreeMarkerTemplateEngine createEngine(String templateFolderPath) {
+
+ FreeMarkerTemplateEngine engine = new FreeMarkerTemplateEngine();
+ engine.setTemplateFolder(Paths.get(templateFolderPath));
+ return engine;
+ }
+
+ /**
+ * Instanciates a class of the given Type and adds it to the model with it's simplename.
+ * (Just like all Utils following the naming convention)
+ * @param clazz Class with a public NoArgsConstructor to instanciate it
+ */
+ public void addUtil(Class> clazz) {
+ try {
+ String name = clazz.getSimpleName();
+ Object instance = clazz.getConstructor().newInstance();
+ model.put(name, instance);
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ throw new RuntimeException("Failed adding the Util to the Model, please check the error stacktrace and fix it or add the util manually!");
+ }
+
+ }
+
+ /**
+ * Consumes the given class to produce the template
+ * @param modelClass Class to auto-generate reflective pojo model
+ * @return Produced file
+ */
+ public String process(Class> modelClass) {
+ Path tp = Paths.get(getTemplatePath());
+ String filename = tp.getFileName().toString();
+ Path templateFolder = tp.getParent();
+ model = new JavaInputReader().createModel(modelClass);
+ template = createTemplate(filename, templateFolder);
+ engine = new FreeMarkerTemplateEngine();
+ engine.setTemplateFolder(templateFolder);
+ for (Class> utilClass : getUtils()) {
+ addUtil(utilClass);
+ }
+ StringWriter out = new StringWriter();
+ engine.process(template, model, out, "UTF-8");
+ return out.toString();
+ }
+
+ /**
+ * @return utils Classes to auto-instanciate and inject into freemarker for the template
+ */
+ abstract public Class>[] getUtils();
+
+ /**
+ * @return templatePath Path to the template relative to subproject source root
+ */
+ abstract public String getTemplatePath();
+}
diff --git a/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/SQLTemplateGenerationTest.java b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/SQLTemplateGenerationTest.java
new file mode 100644
index 0000000000..0c2ae96324
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/SQLTemplateGenerationTest.java
@@ -0,0 +1,78 @@
+package com.devonfw.cobigen.templates.devon4j.test.templates;
+
+import static org.assertj.core.api.Assertions.*;
+
+import com.devonfw.cobigen.templates.devon4j.test.templates.testclasses.SQLTestJoinTableEntity;
+import org.junit.Test;
+
+import com.devonfw.cobigen.templates.devon4j.test.templates.testclasses.SQLTestEntity;
+import com.devonfw.cobigen.templates.devon4j.test.templates.testclasses.SQLTestDataTypesEntity;
+import com.devonfw.cobigen.templates.devon4j.test.templates.testclasses.SQLTestForeignKeysEntity;
+import com.devonfw.cobigen.templates.devon4j.utils.SQLUtil;
+
+/**
+ * Test class for SQL template generation
+ *
+ */
+public class SQLTemplateGenerationTest extends AbstractJavaTemplateTest {
+
+ @Override
+ public Class>[] getUtils() {
+
+ return new Class>[] { SQLUtil.class };
+ }
+
+ @Override
+ public String getTemplatePath() {
+
+ return "src/main/templates/sql_java_app/templates/V0000__Create_${variables.entityName}Entity.sql.ftl";
+ }
+
+ /**
+ * Tests the correct generation of the enumerated type, the primary key, and name overriding
+ */
+ @Test
+ public void testEnumType() {
+
+ String output = process(SQLTestEntity.class);
+ assertThat(output).contains("CREATE TABLE SQLTEST").contains("ENUM_TEST_FIELD_NAME_OVERRIDE VARCHAR(420)")
+ .contains("MY_ID_FIELD BIGINT AUTO_INCREMENT PRIMARY KEY");
+ }
+
+ /**
+ * Tests the correct generation of data types
+ */
+ @Test
+ public void testDatatypeMapping() {
+
+ String ouptut = process(SQLTestDataTypesEntity.class);
+ assertThat(ouptut).contains("timestamp2 TIMESTAMP").contains("bit BIT,").contains("date DATE")
+ .contains("tinyint TINYINT").contains("integer2 INTEGER").contains("bigint BIGINT")
+ .contains("varchar3 VARCHAR(255)").contains("integer1 INTEGER").contains("varchar4 VARCHAR(255)")
+ .contains("blob BLOB").contains("varchar VARCHAR(255)").contains("char2 CHAR(1)").contains("smallint SMALLINT")
+ .contains("char1 CHAR(1)").contains("timestamp TIMESTAMP").contains("time TIME").contains("numeric NUMERIC")
+ .contains("varchar2 VARCHAR(255)").contains("CREATE TABLE SQLDataTypeTest").contains("varchar5 VARCHAR(255)");
+
+ }
+
+ /**
+ * Tests the correct generation of foreign key statements
+ */
+ @Test
+ public void testForeignKeyStatements() {
+
+ String output = process(SQLTestForeignKeysEntity.class);
+ assertThat(output).contains("test_id BIGINT, FOREIGN KEY (test_id) REFERENCES SQLTEST(MY_ID_FIELD)");
+ }
+
+ /**
+ * Tests successful generation of a second CREATE TABLE statement from the @JoinTable annotation
+ */
+ @Test
+ public void testJoinTableGeneration() {
+ String output = process(SQLTestJoinTableEntity.class);
+ assertThat(output).contains("CREATE TABLE MY_AWESOME_JOINTABLE")
+ .contains("REF_ENTITY_ID BIGINT UNIQUE, FOREIGN KEY (REF_ENTITY_ID) REFERENCES REFERENCE(OVERRIDE_ID)")
+ .contains("SQLTESTJOINTABLE_ID BIGINT, FOREIGN KEY (SQLTESTJOINTABLE_ID) REFERENCES SQLTESTJOINTABLE(ID)");
+ }
+}
diff --git a/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/EnumForTest.java b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/EnumForTest.java
new file mode 100644
index 0000000000..b504f0b8d4
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/EnumForTest.java
@@ -0,0 +1,7 @@
+package com.devonfw.cobigen.templates.devon4j.test.templates.testclasses;
+
+public enum EnumForTest {
+ VALUE,
+ OK,
+ MAKING_THINGS_UP
+}
diff --git a/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/ReferenceEntity.java b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/ReferenceEntity.java
new file mode 100644
index 0000000000..329b33b71c
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/ReferenceEntity.java
@@ -0,0 +1,19 @@
+package com.devonfw.cobigen.templates.devon4j.test.templates.testclasses;
+
+import javax.persistence.*;
+
+@Entity
+public class ReferenceEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+}
diff --git a/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestDataTypesEntity.java b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestDataTypesEntity.java
new file mode 100644
index 0000000000..fcf9d51637
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestDataTypesEntity.java
@@ -0,0 +1,385 @@
+package com.devonfw.cobigen.templates.devon4j.test.templates.testclasses;
+
+import java.math.BigDecimal;
+import java.security.Timestamp;
+import java.sql.Time;
+import java.util.Calendar;
+import java.util.Currency;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+/**
+ * Test class to test sql data type mapping
+ *
+ */
+@Entity
+@Table(name = "SQLDataTypeTest")
+public class SQLTestDataTypesEntity {
+
+ @Column()
+ private int integer1;
+
+ @Column()
+ private Integer integer2;
+
+ @Column()
+ private long bigint;
+
+ @Column()
+ private short smallint;
+
+ @Column()
+ private BigDecimal numeric;
+
+ @Column()
+ private String varchar;
+
+ @Column()
+ private char char1;
+
+ @Column()
+ private Character char2;
+
+ @Column()
+ private byte tinyint;
+
+ @Column()
+ private boolean bit;
+
+ @Column()
+ private Date date;
+
+ @Column()
+ private Time time;
+
+ @Column()
+ private Timestamp timestamp;
+
+ @Column()
+ private Calendar timestamp2;
+
+ @Column()
+ private byte[] blob;
+
+ @Column()
+ private Class> varchar2;
+
+ @Column()
+ private Locale varchar3;
+
+ @Column()
+ private TimeZone varchar4;
+
+ @Column()
+ private Currency varchar5;
+
+ /**
+ * @return integer1
+ */
+ public int getInteger1() {
+
+ return this.integer1;
+ }
+
+ /**
+ * @param integer1 new value of {@link #getinteger1}.
+ */
+ public void setInteger1(int integer1) {
+
+ this.integer1 = integer1;
+ }
+
+ /**
+ * @return integer2
+ */
+ public Integer getInteger2() {
+
+ return this.integer2;
+ }
+
+ /**
+ * @param integer2 new value of {@link #getinteger2}.
+ */
+ public void setInteger2(Integer integer2) {
+
+ this.integer2 = integer2;
+ }
+
+ /**
+ * @return bigint
+ */
+ public long getBigint() {
+
+ return this.bigint;
+ }
+
+ /**
+ * @param bigint new value of {@link #getbigint}.
+ */
+ public void setBigint(long bigint) {
+
+ this.bigint = bigint;
+ }
+
+ /**
+ * @return smallint
+ */
+ public short getSmallint() {
+
+ return this.smallint;
+ }
+
+ /**
+ * @param smallint new value of {@link #getsmallint}.
+ */
+ public void setSmallint(short smallint) {
+
+ this.smallint = smallint;
+ }
+
+ /**
+ * @return numeric
+ */
+ public BigDecimal getNumeric() {
+
+ return this.numeric;
+ }
+
+ /**
+ * @param numeric new value of {@link #getnumeric}.
+ */
+ public void setNumeric(BigDecimal numeric) {
+
+ this.numeric = numeric;
+ }
+
+ /**
+ * @return varchar
+ */
+ public String getVarchar() {
+
+ return this.varchar;
+ }
+
+ /**
+ * @param varchar new value of {@link #getvarchar}.
+ */
+ public void setVarchar(String varchar) {
+
+ this.varchar = varchar;
+ }
+
+ /**
+ * @return char1
+ */
+ public char getChar1() {
+
+ return this.char1;
+ }
+
+ /**
+ * @param char1 new value of {@link #getchar1}.
+ */
+ public void setChar1(char char1) {
+
+ this.char1 = char1;
+ }
+
+ /**
+ * @return char2
+ */
+ public Character getChar2() {
+
+ return this.char2;
+ }
+
+ /**
+ * @param char2 new value of {@link #getchar2}.
+ */
+ public void setChar2(Character char2) {
+
+ this.char2 = char2;
+ }
+
+ /**
+ * @return tinyint
+ */
+ public byte getTinyint() {
+
+ return this.tinyint;
+ }
+
+ /**
+ * @param tinyint new value of {@link #gettinyint}.
+ */
+ public void setTinyint(byte tinyint) {
+
+ this.tinyint = tinyint;
+ }
+
+ /**
+ * @return bit
+ */
+ public boolean isBit() {
+
+ return this.bit;
+ }
+
+ /**
+ * @param bit new value of {@link #getbit}.
+ */
+ public void setBit(boolean bit) {
+
+ this.bit = bit;
+ }
+
+ /**
+ * @return date
+ */
+ public Date getDate() {
+
+ return this.date;
+ }
+
+ /**
+ * @param date new value of {@link #getdate}.
+ */
+ public void setDate(Date date) {
+
+ this.date = date;
+ }
+
+ /**
+ * @return time
+ */
+ public Time getTime() {
+
+ return this.time;
+ }
+
+ /**
+ * @param time new value of {@link #gettime}.
+ */
+ public void setTime(Time time) {
+
+ this.time = time;
+ }
+
+ /**
+ * @return timestamp
+ */
+ public Timestamp getTimestamp() {
+
+ return this.timestamp;
+ }
+
+ /**
+ * @param timestamp new value of {@link #gettimestamp}.
+ */
+ public void setTimestamp(Timestamp timestamp) {
+
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * @return timestamp2
+ */
+ public Calendar getTimestamp2() {
+
+ return this.timestamp2;
+ }
+
+ /**
+ * @param timestamp2 new value of {@link #gettimestamp2}.
+ */
+ public void setTimestamp2(Calendar timestamp2) {
+
+ this.timestamp2 = timestamp2;
+ }
+
+ /**
+ * @return blob
+ */
+ public byte[] getBlob() {
+
+ return this.blob;
+ }
+
+ /**
+ * @param blob new value of {@link #getblob}.
+ */
+ public void setBlob(byte[] blob) {
+
+ this.blob = blob;
+ }
+
+ /**
+ * @return varchar2
+ */
+ public Class> getVarchar2() {
+
+ return this.varchar2;
+ }
+
+ /**
+ * @param varchar2 new value of {@link #getvarchar2}.
+ */
+ public void setVarchar2(Class> varchar2) {
+
+ this.varchar2 = varchar2;
+ }
+
+ /**
+ * @return varchar3
+ */
+ public Locale getVarchar3() {
+
+ return this.varchar3;
+ }
+
+ /**
+ * @param varchar3 new value of {@link #getvarchar3}.
+ */
+ public void setVarchar3(Locale varchar3) {
+
+ this.varchar3 = varchar3;
+ }
+
+ /**
+ * @return varchar4
+ */
+ public TimeZone getVarchar4() {
+
+ return this.varchar4;
+ }
+
+ /**
+ * @param varchar4 new value of {@link #getvarchar4}.
+ */
+ public void setVarchar4(TimeZone varchar4) {
+
+ this.varchar4 = varchar4;
+ }
+
+ /**
+ * @return varchar5
+ */
+ public Currency getVarchar5() {
+
+ return this.varchar5;
+ }
+
+ /**
+ * @param varchar5 new value of {@link #getvarchar5}.
+ */
+ public void setVarchar5(Currency varchar5) {
+
+ this.varchar5 = varchar5;
+ }
+
+}
diff --git a/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestEntity.java b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestEntity.java
new file mode 100644
index 0000000000..b4c97bfeda
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestEntity.java
@@ -0,0 +1,58 @@
+package com.devonfw.cobigen.templates.devon4j.test.templates.testclasses;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * Test entity to test the correct generation of the enumerated type, the primary key, and name overriding
+ *
+ */
+@Entity
+@Table(name = "SQLTEST")
+public class SQLTestEntity {
+ @Id
+ @Column(name = "MY_ID_FIELD")
+ private Long id;
+
+ @Column(name = "VALUENAME")
+ private Integer integerValue;
+
+ @Enumerated(EnumType.STRING)
+ @Column(length = 420, name = "ENUM_TEST_FIELD_NAME_OVERRIDE")
+ private EnumForTest enumForTest;
+
+ public Long getId() {
+
+ return this.id;
+ }
+
+ public void setId(Long id) {
+
+ this.id = id;
+ }
+
+ public Integer getIntegerValue() {
+
+ return this.integerValue;
+ }
+
+ public void setIntegerValue(Integer value) {
+
+ this.integerValue = value;
+ }
+
+ public EnumForTest getEnumForTest() {
+
+ return this.enumForTest;
+ }
+
+ public void setEnumForTest(EnumForTest enumForTest) {
+
+ this.enumForTest = enumForTest;
+ }
+
+}
diff --git a/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestForeignKeysEntity.java b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestForeignKeysEntity.java
new file mode 100644
index 0000000000..b634e861b9
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestForeignKeysEntity.java
@@ -0,0 +1,61 @@
+package com.devonfw.cobigen.templates.devon4j.test.templates.testclasses;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+
+/**
+ * Test entity to test the correct generation of the enumerated type, the primary key, and name overriding
+ *
+ */
+@Entity
+@Table(name = "SQLTEST")
+public class SQLTestForeignKeysEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ @Column(name = "id")
+ private Long id;
+
+ @OneToOne()
+ @JoinColumn(referencedColumnName = "MY_ID_FIELD", name = "test_id")
+ private SQLTestEntity sqlTestEntity;
+
+ /**
+ * @return id
+ */
+ public Long getId() {
+
+ return this.id;
+ }
+
+ /**
+ * @param id new value of {@link #getid}.
+ */
+ public void setId(Long id) {
+
+ this.id = id;
+ }
+
+ /**
+ * @return sqlTestEntity
+ */
+ public SQLTestEntity getSqlTestEntity() {
+
+ return this.sqlTestEntity;
+ }
+
+ /**
+ * @param sqlTestEntity new value of {@link #getsqlTestEntity}.
+ */
+ public void setSqlTestEntity(SQLTestEntity sqlTestEntity) {
+
+ this.sqlTestEntity = sqlTestEntity;
+ }
+
+}
diff --git a/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestJoinTableEntity.java b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestJoinTableEntity.java
new file mode 100644
index 0000000000..b28402fdac
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/templates/testclasses/SQLTestJoinTableEntity.java
@@ -0,0 +1,35 @@
+package com.devonfw.cobigen.templates.devon4j.test.templates.testclasses;
+
+import javax.persistence.*;
+import java.util.Set;
+
+@Entity
+public class SQLTestJoinTableEntity {
+ @Id
+ @Column(name = "id", nullable = false)
+ private Long id;
+
+ @ManyToMany
+ @JoinTable(
+ name = "MY_AWESOME_JOINTABLE",
+ joinColumns = @JoinColumn(name = "REF_ENTITY_ID", table = "REFERENCE", unique = true, referencedColumnName = "OVERRIDE_ID"),
+ inverseJoinColumns = @JoinColumn
+ )
+ private Set referenceEntities;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Set getReferenceEntities() {
+ return referenceEntities;
+ }
+
+ public void setReferenceEntities(Set referenceEntities) {
+ this.referenceEntities = referenceEntities;
+ }
+}
diff --git a/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/utils/SQLUtilTest.java b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/utils/SQLUtilTest.java
new file mode 100644
index 0000000000..408fd62239
--- /dev/null
+++ b/cobigen-templates/templates-devon4j/src/test/java/com/devonfw/cobigen/templates/devon4j/test/utils/SQLUtilTest.java
@@ -0,0 +1,27 @@
+package com.devonfw.cobigen.templates.devon4j.test.utils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+import com.devonfw.cobigen.templates.devon4j.utils.SQLUtil;
+
+/**
+ *
+ * Test class for {@link SQLUtil}
+ *
+ */
+public class SQLUtilTest {
+
+ /**
+ * Tests type mappings between simple Java types and corresponding SQL types
+ */
+ @Test
+ public void testTypeMappings() {
+ assertThat(SQLUtil.mapType("Class>")).isEqualTo("VARCHAR");
+ assertThat(SQLUtil.mapType("byte[]")).isEqualTo("BLOB");
+ assertThat(SQLUtil.mapType("Timestamp")).isEqualTo("TIMESTAMP");
+ assertThat(SQLUtil.mapType("TimeZone")).isEqualTo("VARCHAR");
+ assertThat(SQLUtil.mapType("Calendar")).isEqualTo("TIMESTAMP");
+ }
+}
diff --git a/cobigen/cobigen-core/src/test/resources/testdata/unittest/ExternalProcess/DummyExe.exe b/cobigen/cobigen-core/src/test/resources/testdata/unittest/ExternalProcess/DummyExe.exe
deleted file mode 100644
index 4612730a84..0000000000
Binary files a/cobigen/cobigen-core/src/test/resources/testdata/unittest/ExternalProcess/DummyExe.exe and /dev/null differ