package liquibase.datatype; import liquibase.change.ColumnConfig; import liquibase.database.Database; import liquibase.database.core.OracleDatabase; import liquibase.datatype.core.BigIntType; import liquibase.datatype.core.CharType; import liquibase.datatype.core.IntType; import liquibase.datatype.core.UnknownType; import liquibase.exception.UnexpectedLiquibaseException; import liquibase.servicelocator.ServiceLocator; import liquibase.structure.core.DataType; import liquibase.util.ObjectUtil; import liquibase.util.StringUtils; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class DataTypeFactory { private static DataTypeFactory instance; private Map<String, List<Class<? extends LiquibaseDataType>>> registry = new ConcurrentHashMap<String, List<Class<? extends LiquibaseDataType>>>(); protected DataTypeFactory() { Class<? extends LiquibaseDataType>[] classes; try { classes = ServiceLocator.getInstance().findClasses(LiquibaseDataType.class); for (Class<? extends LiquibaseDataType> clazz : classes) { //noinspection unchecked register(clazz); } } catch (Exception e) { throw new UnexpectedLiquibaseException(e); } } public static synchronized DataTypeFactory getInstance() { if (instance == null) { instance = new DataTypeFactory(); } return instance; } public static synchronized void reset() { instance = new DataTypeFactory(); } public void register(Class<? extends LiquibaseDataType> dataTypeClass) { try { LiquibaseDataType example = dataTypeClass.newInstance(); List<String> names = new ArrayList<String>(); names.add(example.getName()); names.addAll(Arrays.asList(example.getAliases())); Comparator<Class<? extends LiquibaseDataType>> comparator = new Comparator<Class<? extends LiquibaseDataType>>() { @Override public int compare(Class<? extends LiquibaseDataType> o1, Class<? extends LiquibaseDataType> o2) { try { return -1 * Integer.valueOf(o1.newInstance().getPriority()).compareTo(o2.newInstance().getPriority()); } catch (Exception e) { throw new UnexpectedLiquibaseException(e); } } }; for (String name : names) { name = name.toLowerCase(); if (registry.get(name) == null) { registry.put(name, new ArrayList<Class<? extends LiquibaseDataType>>()); } List<Class<? extends LiquibaseDataType>> classes = registry.get(name); classes.add(dataTypeClass); Collections.sort(classes, comparator); } } catch (Exception e) { throw new UnexpectedLiquibaseException(e); } } public void unregister(String name) { registry.remove(name.toLowerCase()); } // public Map<String, SortedSet<Class<? extends LiquibaseDataType>>> getRegistry() { // return registry; // } // public LiquibaseDataType fromDescription(String dataTypeDefinition) { // return fromDescription(dataTypeDefinition, null); // } public LiquibaseDataType fromDescription(String dataTypeDefinition, Database database) { String dataTypeName = dataTypeDefinition; if (dataTypeName.matches(".+\\(.*\\).*")) { dataTypeName = dataTypeName.replaceFirst("\\s*\\(.*\\)", ""); } if (dataTypeName.matches(".+\\{.*")) { dataTypeName = dataTypeName.replaceFirst("\\s*\\{.*", ""); } boolean autoIncrement = false; if (dataTypeName.endsWith(" identity")) { dataTypeName = dataTypeName.replaceFirst(" identity$", ""); autoIncrement = true; } // unquote delimited identifiers final String[][] quotePairs = new String[][] { { "\"", "\"" }, // double quotes { "[", "]" }, // square brackets (a la mssql) { "`", "`" }, // backticks (a la mysql) { "'", "'" } // single quotes }; for (String[] quotePair : quotePairs) { String openQuote = quotePair[0]; String closeQuote = quotePair[1]; if (dataTypeName.startsWith(openQuote)) { int indexOfCloseQuote = dataTypeName.indexOf(closeQuote, openQuote.length()); if (indexOfCloseQuote != -1 && dataTypeName.indexOf(closeQuote, indexOfCloseQuote + closeQuote.length()) == -1) { dataTypeName = dataTypeName.substring(openQuote.length(), indexOfCloseQuote) + dataTypeName.substring(indexOfCloseQuote + closeQuote.length(), dataTypeName.length()); break; } } } String additionalInfo = null; if (dataTypeName.toLowerCase().startsWith("bit varying") || dataTypeName.toLowerCase().startsWith("character varying")) { //not going to do anything. Special case for postgres in our tests, need to better support handling these types of differences } else { String[] splitTypeName = dataTypeName.split("\\s+", 2); dataTypeName = splitTypeName[0]; if (splitTypeName.length > 1) { additionalInfo = splitTypeName[1]; } } Collection<Class<? extends LiquibaseDataType>> classes = registry.get(dataTypeName.toLowerCase()); LiquibaseDataType liquibaseDataType = null; if (classes == null) { if (dataTypeName.toUpperCase().startsWith("INTERVAL")) { liquibaseDataType = new UnknownType(dataTypeDefinition); } else { liquibaseDataType = new UnknownType(dataTypeName); } } else { Iterator<Class<? extends LiquibaseDataType>> iterator = classes.iterator(); do { try { liquibaseDataType = iterator.next().newInstance(); } catch (Exception e) { throw new UnexpectedLiquibaseException(e); } } while ((database != null) && !liquibaseDataType.supports(database) && iterator.hasNext()); } if ((database != null) && !liquibaseDataType.supports(database)) { throw new UnexpectedLiquibaseException("Could not find type for "+liquibaseDataType.toString()+" for databaes "+database.getShortName()); } if (liquibaseDataType == null) { liquibaseDataType = new UnknownType(dataTypeName); } liquibaseDataType.setAdditionalInformation(additionalInfo); if (dataTypeDefinition.matches(".+\\s*\\(.*")) { String paramStrings = dataTypeDefinition.replaceFirst(".*?\\(", "").replaceFirst("\\).*", ""); String[] params = paramStrings.split(","); for (String param : params) { param = StringUtils.trimToNull(param); if (param != null) { if (liquibaseDataType instanceof CharType && !(database instanceof OracleDatabase)) { param = param.replaceFirst(" BYTE", ""); //only use byte types on oracle, not sure what else supports it } liquibaseDataType.addParameter(param); } } } /* The block below seems logically incomplete. It will always end up putting the second word after the entire type name e.g. character varying will become CHARACTER VARYING varying //try to something like "int(11) unsigned" or int unsigned but not "varchar(11 bytes)" String lookingForAdditionalInfo = dataTypeDefinition; lookingForAdditionalInfo = lookingForAdditionalInfo.replaceFirst("\\(.*\\)", ""); if (lookingForAdditionalInfo.contains(" ")) { liquibaseDataType.setAdditionalInformation(lookingForAdditionalInfo.split(" ", 2)[1]); }*/ if (dataTypeDefinition.matches(".*\\{.*")) { String paramStrings = dataTypeDefinition.replaceFirst(".*?\\{", "").replaceFirst("\\}.*", ""); String[] params = paramStrings.split(","); for (String param : params) { param = StringUtils.trimToNull(param); if (param != null) { String[] paramAndValue = param.split(":", 2); try { ObjectUtil.setProperty(liquibaseDataType, paramAndValue[0], paramAndValue[1]); } catch (Exception e) { throw new RuntimeException("Unknown property "+paramAndValue[0]+" for "+liquibaseDataType.getClass().getName()+" "+liquibaseDataType.toString()); } } } } if (autoIncrement && liquibaseDataType instanceof IntType) { ((IntType) liquibaseDataType).setAutoIncrement(true); } if (autoIncrement && liquibaseDataType instanceof BigIntType) { ((BigIntType) liquibaseDataType).setAutoIncrement(true); } liquibaseDataType.finishInitialization(dataTypeDefinition); return liquibaseDataType; } public LiquibaseDataType fromObject(Object object, Database database) { if (object instanceof ColumnConfig.ValueNumeric) { object = ((ColumnConfig.ValueNumeric) object).getDelegate(); } return fromDescription(object.getClass().getName(), database); } public LiquibaseDataType from(DataType type, Database database) { if (type == null) { return null; } return fromDescription(type.toString(), database); } public LiquibaseDataType from(DatabaseDataType type, Database database) { if (type == null) { return null; } return fromDescription(type.toString(), database); } public String getTrueBooleanValue(Database database) { return fromDescription("boolean", database).objectToSql(true, database); } public String getFalseBooleanValue(Database database) { return fromDescription("boolean", database).objectToSql(false, database); } }