package joist.codegen; import java.sql.Date; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import joist.codegen.passes.FindCodeValuesPass; import joist.codegen.passes.FindForeignKeysPass; import joist.codegen.passes.FindManyToManyPropertiesPass; import joist.codegen.passes.FindPrimitivePropertiesPass; import joist.codegen.passes.FindTablesPass; import joist.codegen.passes.GenerateAliasesPass; import joist.codegen.passes.GenerateBuilderClassIfNotExistsPass; import joist.codegen.passes.GenerateBuilderCodegenPass; import joist.codegen.passes.GenerateBuildersClassPass; import joist.codegen.passes.GenerateCodesPass; import joist.codegen.passes.GenerateDomainClassIfNotExistsPass; import joist.codegen.passes.GenerateDomainCodegenPass; import joist.codegen.passes.GenerateFlushFunction; import joist.codegen.passes.GenerateQueriesCodegenPass; import joist.codegen.passes.GenerateQueriesIfNotExistsPass; import joist.codegen.passes.GenerateSchemaHash; import joist.codegen.passes.OutputPass; import joist.codegen.passes.Pass; import joist.domain.AbstractDomainObject; import joist.domain.AbstractQueries; import joist.domain.orm.Db; import joist.domain.orm.queries.columns.BooleanAliasColumn; import joist.domain.orm.queries.columns.ByteArrayAliasColumn; import joist.domain.orm.queries.columns.DateAliasColumn; import joist.domain.orm.queries.columns.IdAliasColumn; import joist.domain.orm.queries.columns.IntAliasColumn; import joist.domain.orm.queries.columns.LongAliasColumn; import joist.domain.orm.queries.columns.ShortAliasColumn; import joist.domain.orm.queries.columns.StringAliasColumn; import joist.domain.util.ConnectionSettings; import joist.migrations.columns.PrimaryKeyColumn; import joist.sourcegen.GSettings; import joist.util.Copy; import joist.util.Inflector; public class Config { /** Where the generated-once subclasses (e.g. Employee) that you add business logic go. @return E.g. <code>src/main</code> */ public String outputSourceDirectory = "./src/main/java"; /** Where the re-generated base classes (e.g. EmployeeCodegen) that you do not edit go. @return E.g. <code>src/codegen</code> */ public String outputCodegenDirectory = "./src/codegen/java"; /** The package name of your domain objects. @return E.g. <code>app.domain</code> */ public String domainObjectPackage = "project.domain"; /** The package name of your query objects. @return E.g. <code>app.domain.queries</code> */ public String queriesPackage = "project.domain.queries"; /** The package name of your builder objects. @return E.g. <code>app.domain.builders</code> */ public String buildersPackage = "project.domain.builders"; /** The base class for all the generated base classes (e.g. EmployeeCodegen). @return E.g. <code>YourAbstractDomainObject</code> */ public String domainObjectBaseClass = AbstractDomainObject.class.getName(); /** The base class for the once-touched queries objects. */ public String queriesBaseClass = AbstractQueries.class.getName() + "<{}>"; /** The path for the database backup. */ public String databaseBackupPath; /** The classpath for database backups (for running migrations from a jar). */ public String databaseBackupResourceLocation; /** Whether the codegen directory will be pruned of un-needed (to us) files. Affects only directories that contained generated classes. */ public boolean pruneCodegenDirectory = true; /** * Whether the source directory will be pruned of un-needed (to us) files. * * E.g. removes the old {@code Child.java} file (and {@code ChildQueries.java} and * {@code ChildBuilder.java}) if the {@code child} table has been deleted. * * Note that {@code Child.java} may have had business logic in, which, if enabled, you'll * have to recover from {@code git diff}. */ public boolean pruneSourceDirectory = true; /** Where to look for migrations to apply. */ public List<String> packageNamesContainingMigrations = new ArrayList<String>(); /** The target database, MySQL or PostgreSQL. */ public Db db; /** Used for system-level actions like creating/deleting the local database. */ public ConnectionSettings dbSystemSettings; /** Used for system-level access to the local database for creating tables/permissions. */ public ConnectionSettings dbAppSaSettings; /** Used for user-level access to the local database. */ public ConnectionSettings dbAppUserSettings; /** For MySQL, the host to use when creating/granting user-level permissions. Defaults to '%'. */ public String userhost = System.getProperty("db.userhost", "%"); public boolean useHistoryTriggers; /** * Whether we'll scan for "b00X" migrations after applying the main/master migrations. * * This is helpful for giving branches their own temporary namespace to avoid renumbering * migrations after every merge from master. */ public boolean allowBranchMigrations = false; private final Map<String, String> javaTypeByDataType = new HashMap<String, String>(); private final Map<String, String> javaTypeByColumnName = new HashMap<String, String>(); private final Map<TypeAndPattern, String> javaTypeByPattern = new HashMap<TypeAndPattern, String>(); private final Map<String, String> aliasTypeByDataType = new HashMap<String, String>(); private final Map<String, String> aliasTypeByColumnName = new HashMap<String, String>(); private final Map<TypeAndPattern, String> aliasTypeByPattern = new HashMap<TypeAndPattern, String>(); private final Map<String, String> getterAccessByTableAndColumn = new HashMap<String, String>(); private final Map<String, String> setterAccessByTableAndColumn = new HashMap<String, String>(); private final Map<String, String> builderDefaultsByJavaType = new HashMap<String, String>(); private final Map<String, String> oneToManyName = new HashMap<String, String>(); private final MarkedList<String> skipCollections = new MarkedList<String>(); private final List<String> skipTables = new ArrayList<String>(); private final List<String> skipProperties = new ArrayList<String>(); private final List<String> notAbstractEvenThoughSubclassed = new ArrayList<String>(); private final List<String> stableTables = new ArrayList<String>(); private final Map<String, List<String>> customRulesByJavaType = new HashMap<String, List<String>>(); private final String amountSuffix = ".*amount$"; private final List<Pass<Schema>> dataPasses; private final List<Pass<Codegen>> codegenPasses; public Config(String projectName, Db db) { this(projectName, Inflector.underscore(projectName), db); } @SuppressWarnings("unchecked") public Config(String projectName, String defaultDatabaseName, Db db) { this.db = db; this.databaseBackupPath = "./" + defaultDatabaseName + ".sql"; this.dbAppUserSettings = ConnectionSettings.forApp(db, defaultDatabaseName); this.dbAppSaSettings = ConnectionSettings.forAppSa(db, defaultDatabaseName); this.dbSystemSettings = ConnectionSettings.forSystemSa(db, defaultDatabaseName); this.setProjectNameForDefaults(projectName); // default history triggers on, but only supported by mysql this.useHistoryTriggers = db.isMySQL(); this.setJavaType("integer", Integer.class.getName(), IntAliasColumn.class.getName()); this.setJavaType("character", String.class.getName(), StringAliasColumn.class.getName()); this.setJavaType("character varying", String.class.getName(), StringAliasColumn.class.getName()); this.setJavaType("text", String.class.getName(), StringAliasColumn.class.getName()); this.setJavaType("smallint", Short.class.getName(), ShortAliasColumn.class.getName()); this.setJavaType("bigint", Long.class.getName(), LongAliasColumn.class.getName()); this.setJavaType("boolean", Boolean.class.getName(), BooleanAliasColumn.class.getName()); this.setJavaType("bytea", "byte[]", ByteArrayAliasColumn.class.getName()); this.setJavaType("date", "com.domainlanguage.time.CalendarDate", "joist.domain.orm.queries.columns.CalendarDateAliasColumn"); this.setJavaType("timestamp without time zone", "com.domainlanguage.time.TimePoint", "joist.domain.orm.queries.columns.TimePointAliasColumn"); this.setJavaType("timestamp", "com.domainlanguage.time.TimePoint", "joist.domain.orm.queries.columns.TimePointAliasColumn"); // mysql this.setJavaType("datetime", "com.domainlanguage.time.TimePoint", "joist.domain.orm.queries.columns.TimePointAliasColumn"); // mysql this.setJavaTypePattern("integer", this.amountSuffix, "com.domainlanguage.money.Money", "joist.domain.orm.queries.columns.MoneyAliasColumn"); this.setJavaTypePattern("bigint", this.amountSuffix, "com.domainlanguage.money.Money", "joist.domain.orm.queries.columns.MoneyAliasColumn"); this.setJavaTypePattern("int", this.amountSuffix, "com.domainlanguage.money.Money", "joist.domain.orm.queries.columns.MoneyAliasColumn"); this.builderDefaultsByJavaType.put(Integer.class.getName(), "0"); this.builderDefaultsByJavaType.put(Short.class.getName(), "(short) 0"); this.builderDefaultsByJavaType.put(Long.class.getName(), "0l"); this.builderDefaultsByJavaType.put(Boolean.class.getName(), "false"); this.builderDefaultsByJavaType.put("com.domainlanguage.time.TimePoint", "TimePoint.from(0)"); this.builderDefaultsByJavaType.put("com.domainlanguage.time.CalendarDate", "CalendarDate.from(1970, 1, 1)"); this.builderDefaultsByJavaType.put("com.domainlanguage.money.Money", "Money.dollars(0)"); this.setJavaType("int", Integer.class.getName(), IntAliasColumn.class.getName()); this.setJavaType("bit", Boolean.class.getName(), BooleanAliasColumn.class.getName()); this.setJavaType("varchar", String.class.getName(), StringAliasColumn.class.getName()); this.setJavaType("tinyint", Short.class.getName(), ShortAliasColumn.class.getName()); this.dataPasses = Copy.list( new FindTablesPass(), new FindPrimitivePropertiesPass(), new FindForeignKeysPass(), new FindCodeValuesPass(), new FindManyToManyPropertiesPass()); this.codegenPasses = Copy.list( new GenerateCodesPass(), new GenerateDomainClassIfNotExistsPass(), new GenerateDomainCodegenPass(), new GenerateQueriesIfNotExistsPass(), new GenerateQueriesCodegenPass(), new GenerateAliasesPass(), new GenerateFlushFunction(), new GenerateSchemaHash(), new GenerateBuilderClassIfNotExistsPass(), new GenerateBuilderCodegenPass(), new GenerateBuildersClassPass(), new OutputPass()); } public List<Pass<Schema>> getDataPasses() { return this.dataPasses; } public List<Pass<Codegen>> getCodegenPasses() { return this.codegenPasses; } public void addPassBeforeOutput(Pass<Codegen> pass) { this.getCodegenPasses().add(this.getCodegenPasses().size() - 2, pass); } public Config doNotUseTimeAndMoney() { this.setJavaType("date", Date.class.getName(), DateAliasColumn.class.getName()); this.setJavaType("timestamp without time zone", Date.class.getName(), DateAliasColumn.class.getName()); this.javaTypeByPattern.remove(new TypeAndPattern("integer", this.amountSuffix)); this.javaTypeByPattern.remove(new TypeAndPattern("bigint", this.amountSuffix)); this.javaTypeByPattern.remove(new TypeAndPattern("int", this.amountSuffix)); this.aliasTypeByPattern.remove(new TypeAndPattern("integer", this.amountSuffix)); this.aliasTypeByPattern.remove(new TypeAndPattern("bigint", this.amountSuffix)); this.aliasTypeByPattern.remove(new TypeAndPattern("int", this.amountSuffix)); return this; } public Config doNotUseMoney() { this.javaTypeByPattern.remove(new TypeAndPattern("integer", this.amountSuffix)); this.javaTypeByPattern.remove(new TypeAndPattern("bigint", this.amountSuffix)); this.aliasTypeByPattern.remove(new TypeAndPattern("integer", this.amountSuffix)); this.aliasTypeByPattern.remove(new TypeAndPattern("bigint", this.amountSuffix)); return this; } public void setProjectNameForDefaults(String projectName) { this.domainObjectPackage = projectName + ".domain"; this.queriesPackage = projectName + ".domain.queries"; this.buildersPackage = projectName + ".domain.builders"; this.packageNamesContainingMigrations.add(projectName + ".migrations"); } public void setJavaType(String jdbcDataType, String javaType, String aliasColumnType) { this.javaTypeByDataType.put(jdbcDataType, javaType); this.aliasTypeByDataType.put(jdbcDataType, aliasColumnType); } public void setJavaType(String tableName, String columnName, String javaType, String aliasColumnType) { this.javaTypeByColumnName.put(tableName + "." + columnName, javaType); this.aliasTypeByColumnName.put(tableName + "." + columnName, aliasColumnType); } public void setIndentation(String indentation) { GSettings.setDefaultIndentation(indentation); } public void setJavaTypePattern(String jdbcType, String columnNameRegex, String javaType, String aliasColumnType) { this.javaTypeByPattern.put(new TypeAndPattern(jdbcType, columnNameRegex), javaType); this.aliasTypeByPattern.put(new TypeAndPattern(jdbcType, columnNameRegex), aliasColumnType); } public String getJavaType(String tableName, String columnName, String dataType) { if ("id".equals(columnName) || "version".equals(columnName)) { return "Long"; } if (this.javaTypeByColumnName.containsKey(tableName + "." + columnName)) { return this.javaTypeByColumnName.get(tableName + "." + columnName); } for (Map.Entry<TypeAndPattern, String> e : this.javaTypeByPattern.entrySet()) { if (e.getKey().matches(dataType, columnName)) { return e.getValue(); } } if (this.javaTypeByDataType.containsKey(dataType)) { return this.javaTypeByDataType.get(dataType); } throw new RuntimeException("Unmatched data type: " + dataType); } public String getAliasType(String tableName, String columnName, String dataType) { if ("id".equals(columnName)) { return IdAliasColumn.class.getName(); } if ("version".equals(columnName)) { return LongAliasColumn.class.getName(); } if (this.aliasTypeByColumnName.containsKey(tableName + "." + columnName)) { return this.aliasTypeByColumnName.get(tableName + "." + columnName); } for (Map.Entry<TypeAndPattern, String> e : this.aliasTypeByPattern.entrySet()) { if (e.getKey().matches(dataType, columnName)) { return e.getValue(); } } if (this.aliasTypeByDataType.containsKey(dataType)) { return this.aliasTypeByDataType.get(dataType); } throw new RuntimeException("Unmatched data type: " + dataType); } public String getOneToManyPropertyName(String tableName, String columnName) { return this.oneToManyName.get(tableName + "." + columnName); } public void setOneToManyPropertyName(String tableName, String columnName, String manySidePropertyName) { this.oneToManyName.put(tableName + "." + columnName, manySidePropertyName); } public String getBuildersDefault(String javaType) { return this.builderDefaultsByJavaType.get(javaType); } public void setGetterAccess(String tableName, String columnName, String access) { this.getterAccessByTableAndColumn.put(tableName + "." + columnName, access); } public String getGetterAccess(String tableName, String columnName) { String key = tableName + "." + columnName; if (this.getterAccessByTableAndColumn.containsKey(key)) { return this.getterAccessByTableAndColumn.get(key); } else { return "public"; } } public void setSetterAccess(String tableName, String columnName, String access) { this.setterAccessByTableAndColumn.put(tableName + "." + columnName, access); } public String getSetterAccess(String tableName, String columnName) { String key = tableName + "." + columnName; if (this.setterAccessByTableAndColumn.containsKey(key)) { return this.setterAccessByTableAndColumn.get(key); } else { return "public"; } } public void setNotAbstractEvenThoughSubclassed(String tableName) { this.notAbstractEvenThoughSubclassed.add(tableName); } public boolean isNotAbstractEvenThoughSubclassed(String tableName) { return this.notAbstractEvenThoughSubclassed.contains(tableName); } public void setCollectionSkipped(String objectName, String variableName) { this.skipCollections.add(objectName + "." + variableName); } public boolean isCollectionSkipped(String objectName, String variableName) { return this.skipCollections.contains(objectName + "." + variableName); } public List<String> getStaleSkippedCollections() { return this.skipCollections.getStaleValues(); } public void setPropertySkipped(String objectName, String variableName) { this.skipProperties.add(objectName + "." + variableName); } public boolean isPropertySkipped(String objectName, String variableName) { return this.skipProperties.contains(objectName + "." + variableName); } public void setTableSkipped(String tableName) { this.skipTables.add(tableName); } public boolean isTableSkipped(String tableName) { return this.skipTables.contains(tableName); } public void setStableTable(String tableName) { this.stableTables.add(tableName); } public boolean isStableTable(String tableName) { return this.stableTables.contains(tableName); } public void addPackageForMigrations(String packageName) { this.packageNamesContainingMigrations.add(packageName); } public void addCustomRule(String javaType, String rule) { List<String> rules = this.customRulesByJavaType.get(javaType); if (rules == null) { rules = new ArrayList<String>(); this.customRulesByJavaType.put(javaType, rules); } rules.add(rule); } public List<String> getCustomRules(String entityType, String javaType, String variableName) { if (!this.customRulesByJavaType.containsKey(javaType)) { return new ArrayList<String>(); } List<String> rendered = new ArrayList<String>(); for (String rule : this.customRulesByJavaType.get(javaType)) { rendered.add(rule.replaceAll("\\$variableName", variableName).replaceAll("\\$entityType", entityType)); } return rendered; } public String getDomainObjectPackage() { return this.domainObjectPackage; } public String getBuildersPackage() { return this.buildersPackage; } public String getQueriesPackage() { return this.queriesPackage; } public String getDomainObjectBaseClass() { return this.domainObjectBaseClass; } public String getQueriesBaseClass() { return this.queriesBaseClass; } public String getOutputSourceDirectory() { return this.outputSourceDirectory; } public String getOutputCodegenDirectory() { return this.outputCodegenDirectory; } public void setKeyColumnType(String columnType) { PrimaryKeyColumn.keyColumnType = columnType; } private class TypeAndPattern { private final String type; private final String regex; private final Pattern pattern; private TypeAndPattern(String type, String pattern) { this.type = type; this.regex = pattern; this.pattern = Pattern.compile(pattern); } private boolean matches(String dataType, String columnName) { return this.type.equals(dataType) && this.pattern.matcher(columnName).matches(); } @Override public boolean equals(Object o) { if (o instanceof TypeAndPattern) { TypeAndPattern other = (TypeAndPattern) o; return other.type.equals(this.type) && other.regex.equals(this.regex); } return false; } @Override public int hashCode() { return this.type.hashCode() + this.regex.hashCode(); } } }