liquibase.change.AbstractChange Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of liquibase-core Show documentation
Show all versions of liquibase-core Show documentation
Liquibase is a tool for managing and executing database changes.
package liquibase.change;
import liquibase.ChecksumVersion;
import liquibase.Scope;
import liquibase.change.visitor.ChangeVisitor;
import liquibase.changelog.ChangeSet;
import liquibase.database.Database;
import liquibase.exception.*;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.executor.LoggingExecutor;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.plugin.AbstractPlugin;
import liquibase.resource.ResourceAccessor;
import liquibase.serializer.LiquibaseSerializable;
import liquibase.serializer.core.string.StringChangeLogSerializer;
import liquibase.sqlgenerator.SqlGeneratorFactory;
import liquibase.statement.SqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.util.BooleanUtil;
import liquibase.util.ObjectUtil;
import liquibase.util.StringUtil;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.*;
import static liquibase.statement.SqlStatement.EMPTY_SQL_STATEMENT;
/**
* Standard superclass to simplify {@link Change } implementations. You can implement Change directly, this class is
* purely for convenience but recommended.
*
* By default, this base class relies on annotations such as {@link DatabaseChange} and {@link DatabaseChangeProperty}
* and delegating logic to the {@link liquibase.sqlgenerator.SqlGenerator} objects created to do the actual change work.
* Place the @DatabaseChangeProperty annotations on the read "get" methods to control property metadata.
*/
public abstract class AbstractChange extends AbstractPlugin implements Change {
protected static final String NODENAME_COLUMN = "column";
private ChangeSet changeSet;
public AbstractChange() {
}
/**
* Default implementation is a no-op
*/
@Override
public void finishInitialization() throws SetupException {
}
/**
* Generate the ChangeMetaData for this class. Default implementation reads from the @{@link DatabaseChange } annotation
* and calls out to {@link #createChangeParameterMetadata(String)} for each property.
*
* @throws UnexpectedLiquibaseException if no @DatabaseChange annotation on this Change class
*/
@Override
public ChangeMetaData createChangeMetaData() {
try {
DatabaseChange databaseChange = this.getClass().getAnnotation(DatabaseChange.class);
if (databaseChange == null) {
throw new UnexpectedLiquibaseException("No @DatabaseChange annotation for " + getClass().getName());
}
Set params = new HashSet<>();
for (PropertyDescriptor property : ObjectUtil.getDescriptors(getClass())) {
if (isInvalidProperty(property)) {
continue;
}
Method readMethod = property.getReadMethod();
Method writeMethod = property.getWriteMethod();
if (readMethod == null) {
try {
readMethod = this.getClass().getMethod(
"is" + StringUtil.upperCaseFirst(property.getName())
);
} catch (NoSuchMethodException|SecurityException ignore) {
//it was worth a try
}
}
if ((readMethod != null) && (writeMethod != null)) {
DatabaseChangeProperty annotation = findDatabaseChangePropertyAnnotationMatchingCurrentChecksumVersion(readMethod);
if ((annotation == null) || annotation.isChangeProperty()) {
params.add(createChangeParameterMetadata(property, readMethod));
}
}
}
Map notes = new HashMap<>();
for (DatabaseChangeNote note : databaseChange.databaseNotes()) {
notes.put(note.database(), note.notes());
}
return new ChangeMetaData(databaseChange.name(), databaseChange.description(), databaseChange.priority(),
databaseChange.appliesTo(), notes, params);
} catch (UnexpectedLiquibaseException|IntrospectionException e) {
throw new UnexpectedLiquibaseException(e);
}
}
/**
* Given a particular method, find the {@link DatabaseChangeProperty} annotation on it that most precisely matches
* the requested checksum version.
*
* The logic can be summarized as follows:
* 1. If only one {@link DatabaseChangeProperty} annotation is on the particular method, then return it.
* 2. If no {@link DatabaseChangeProperty} annotations are present on the particular method, return null;
* 3. In the event that multiple {@link DatabaseChangeProperty} annotations are present on the particular method,
* read all of them to determine which one most closely matches the requested checksum version. If an annotation
* exists that specifies its version as the same version that is being requested, it is returned. If no annotation
* exists with a matching version, the first annotation on the method without a version is returned. If no
* annotations with no version exist, then null is returned.
*/
private DatabaseChangeProperty findDatabaseChangePropertyAnnotationMatchingCurrentChecksumVersion(Method readMethod) {
DatabaseChangeProperty[] annotations = readMethod.getAnnotationsByType(DatabaseChangeProperty.class);
if (annotations.length == 1) {
return annotations[0];
} else if (annotations.length > 1) {
// first try to find the annotation that matches the current checksum version
Optional versionMatchingAnnotation = Arrays.stream(annotations)
.filter(ann ->
Arrays.stream(ann.version()).anyMatch(annotationVersion -> annotationVersion == Scope.getCurrentScope().getChecksumVersion()))
.findFirst();
// If found, use that annotation, if not found, use the first annotation with no version specified
// (it is assumed that this is the default and should apply to all versions not explicitly specified)
// If no annotation with no version is found, return null.
return versionMatchingAnnotation.orElseGet(
() -> Arrays.stream(annotations)
.filter(ann -> ann.version().length == 0).findFirst().orElse(null)
);
} else {
return null;
}
}
protected boolean isInvalidProperty(PropertyDescriptor property) {
return "metaClass".equals(property.getDisplayName());
}
/**
* Called by {@link #createChangeMetaData()} to create metadata for a given parameter. It finds the method that corresponds to the parameter
* and calls the corresponding create*MetaData methods such as {@link #createRequiredDatabasesMetaData(String, DatabaseChangeProperty)} to determine the
* correct values for the ChangeParameterMetaData fields.
*
* @throws UnexpectedLiquibaseException if the passed parameter does not exist
*/
protected ChangeParameterMetaData createChangeParameterMetadata(String parameterName) {
try {
PropertyDescriptor property = null;
for (PropertyDescriptor prop : ObjectUtil.getDescriptors(getClass())) {
if (prop.getDisplayName().equals(parameterName)) {
property = prop;
break;
}
}
if (property == null) {
throw new UnexpectedLiquibaseException("Could not find property " + parameterName);
}
Method readMethod = property.getReadMethod();
if (readMethod == null) {
readMethod = getClass().getMethod("is" + StringUtil.upperCaseFirst(property.getName()));
}
return createChangeParameterMetadata(property, readMethod);
} catch (IntrospectionException|NoSuchMethodException|SecurityException e) {
throw new UnexpectedLiquibaseException(e);
}
}
private ChangeParameterMetaData createChangeParameterMetadata(PropertyDescriptor property, Method readMethod) {
try {
String parameterName = property.getDisplayName();
String displayName = parameterName.replaceAll("([A-Z])", " $1");
displayName = displayName.substring(0, 1).toUpperCase() + displayName.substring(1);
Type type = readMethod.getGenericReturnType();
DatabaseChangeProperty changePropertyAnnotation = findDatabaseChangePropertyAnnotationMatchingCurrentChecksumVersion(readMethod);
String mustEqualExisting = createMustEqualExistingMetaData(parameterName, changePropertyAnnotation);
String description = createDescriptionMetaData(parameterName, changePropertyAnnotation);
Map examples = createExampleValueMetaData(parameterName, changePropertyAnnotation);
String since = createSinceMetaData(parameterName, changePropertyAnnotation);
SerializationType serializationType = createSerializationTypeMetaData(
parameterName, changePropertyAnnotation
);
String[] requiredForDatabase = createRequiredDatabasesMetaData(parameterName, changePropertyAnnotation);
String[] supportsDatabase = createSupportedDatabasesMetaData(parameterName, changePropertyAnnotation);
return new ChangeParameterMetaData(this, parameterName, displayName, description, examples, since,
type, requiredForDatabase, supportsDatabase, mustEqualExisting, serializationType).withAccessors(readMethod, property.getWriteMethod());
} catch (UnexpectedLiquibaseException e) {
throw e;
}
}
/**
* Create the {@link ChangeParameterMetaData} "since" value. Uses the value on the DatabaseChangeProperty
* annotation or returns null as a default.
*/
@SuppressWarnings("UnusedParameters")
protected String createSinceMetaData(String parameterName, DatabaseChangeProperty changePropertyAnnotation) {
if (changePropertyAnnotation == null) {
return null;
}
return StringUtil.trimToNull(changePropertyAnnotation.since());
}
/**
* Create the {@link ChangeParameterMetaData} "description" value. Uses the value on the DatabaseChangeProperty
* annotation or returns null as a default.
*/
@SuppressWarnings("UnusedParameters")
protected String createDescriptionMetaData(String parameterName, DatabaseChangeProperty changePropertyAnnotation) {
if (changePropertyAnnotation == null) {
return null;
}
return StringUtil.trimToNull(changePropertyAnnotation.description());
}
/**
* Create the {@link ChangeParameterMetaData} "serializationType" value. Uses the value on the
* DatabaseChangeProperty annotation or returns
* {@link liquibase.serializer.LiquibaseSerializable.SerializationType#NAMED_FIELD} as a default.
*/
@SuppressWarnings("UnusedParameters")
protected liquibase.serializer.LiquibaseSerializable.SerializationType createSerializationTypeMetaData(
String parameterName, DatabaseChangeProperty changePropertyAnnotation
) {
if (changePropertyAnnotation == null) {
return SerializationType.NAMED_FIELD;
}
return changePropertyAnnotation.serializationType();
}
/**
* Create the {@link ChangeParameterMetaData} "mustEqual" value. Uses the value on the DatabaseChangeProperty
* annotation or returns null as a default.
*/
@SuppressWarnings("UnusedParameters")
protected String createMustEqualExistingMetaData(
String parameterName, DatabaseChangeProperty changePropertyAnnotation
) {
if (changePropertyAnnotation == null) {
return null;
}
return changePropertyAnnotation.mustEqualExisting();
}
/**
* Create the {@link ChangeParameterMetaData} "example" value. Uses the value on the DatabaseChangeProperty annotation or returns null as a default.
* Returns map with key=database short name, value=example. Use short-name "all" as the fallback.
*/
@SuppressWarnings("UnusedParameters")
protected Map createExampleValueMetaData(
String parameterName, DatabaseChangeProperty changePropertyAnnotation
) {
if (changePropertyAnnotation == null) {
return null;
}
Map examples = new HashMap<>();
examples.put("all", StringUtil.trimToNull(changePropertyAnnotation.exampleValue()));
return examples;
}
/**
* Create the {@link ChangeParameterMetaData} "requiredDatabases" value.
* Uses the value on the DatabaseChangeProperty annotation or returns an array containing the string "COMPUTE"
* as a default. "COMPUTE" will cause ChangeParameterMetaData to attempt to determine the required databases based
* on the generated Statements
*/
@SuppressWarnings("UnusedParameters")
protected String[] createRequiredDatabasesMetaData(
String parameterName, DatabaseChangeProperty changePropertyAnnotation
) {
if (changePropertyAnnotation == null) {
return new String[]{ChangeParameterMetaData.COMPUTE};
} else {
return changePropertyAnnotation.requiredForDatabase();
}
}
/**
* Create the {@link ChangeParameterMetaData} "supportedDatabase" value.
* Uses the value on the DatabaseChangeProperty annotation or returns an array containing the string "COMPUTE"
* as a default. "COMPUTE" will cause ChangeParameterMetaData to attempt to determine the required databases
* based on the generated Statements
*/
@SuppressWarnings("UnusedParameters")
protected String[] createSupportedDatabasesMetaData(
String parameterName, DatabaseChangeProperty changePropertyAnnotation
) {
if (changePropertyAnnotation == null) {
return new String[]{ChangeParameterMetaData.COMPUTE};
} else {
return changePropertyAnnotation.supportsDatabase();
}
}
/**
* {@inheritDoc}
*/
@Override
@DatabaseChangeProperty(isChangeProperty = false)
public ChangeSet getChangeSet() {
return changeSet;
}
/**
* {@inheritDoc}
*/
@Override
public void setChangeSet(ChangeSet changeSet) {
this.changeSet = changeSet;
}
/**
* Implementation delegates logic to the
* {@link liquibase.sqlgenerator.SqlGenerator#generateStatementsIsVolatile(Database) } method on the
* {@link SqlStatement} objects returned by {@link #generateStatements }.
* If zero or null SqlStatements are returned by generateStatements then this method returns false.
*/
@Override
public boolean generateStatementsVolatile(Database database) {
SqlStatement[] statements = generateStatements(database);
if (statements == null) {
return false;
}
for (SqlStatement statement : statements) {
if (SqlGeneratorFactory.getInstance().generateStatementsVolatile(statement, database)) {
return true;
}
}
return false;
}
/**
* Implementation delegates logic to the
* {@link liquibase.sqlgenerator.SqlGenerator#generateRollbackStatementsIsVolatile(Database) }
* method on the {@link SqlStatement} objects returned by {@link #generateStatements }
* If no or null SqlStatements are returned by generateRollbackStatements then this method returns false.
*/
@Override
public boolean generateRollbackStatementsVolatile(Database database) {
if (generateStatementsVolatile(database)) {
return true;
}
SqlStatement[] statements = generateStatements(database);
if (statements == null) {
return false;
}
for (SqlStatement statement : statements) {
if (SqlGeneratorFactory.getInstance().generateRollbackStatementsVolatile(statement, database)) {
return true;
}
}
return false;
}
/**
* Implementation delegates logic to the
* {@link liquibase.sqlgenerator.SqlGenerator#supports(liquibase.statement.SqlStatement, liquibase.database.Database)}
* method on the {@link SqlStatement} objects returned by {@link #generateStatements }.
* If no or null SqlStatements are returned by generateStatements then this method returns true.
* If {@link #generateStatementsVolatile(liquibase.database.Database)} returns true, we cannot call generateStatements and so assume true.
*/
@Override
public boolean supports(Database database) {
if (generateStatementsVolatile(database)) {
return true;
}
SqlStatement[] statements = generateStatements(database);
if (statements == null) {
return true;
}
for (SqlStatement statement : statements) {
if (!SqlGeneratorFactory.getInstance().supports(statement, database)) {
return false;
}
}
return true;
}
/**
* Implementation delegates logic to the
* {@link liquibase.sqlgenerator.SqlGenerator#warn(liquibase.statement.SqlStatement, liquibase.database.Database,
* liquibase.sqlgenerator.SqlGeneratorChain)} method on the {@link SqlStatement} objects returned by
* {@link #generateStatements }.
* If a generated statement is not supported for the given database, no warning will be added since that is a
* validation error. If no or null SqlStatements are returned by generateStatements then this method returns no
* warnings.
*/
@Override
public Warnings warn(Database database) {
Warnings warnings = new Warnings();
if (generateStatementsVolatile(database)) {
return warnings;
}
SqlStatement[] statements = generateStatements(database);
if (statements == null) {
return warnings;
}
for (SqlStatement statement : statements) {
if (SqlGeneratorFactory.getInstance().supports(statement, database)) {
warnings.addAll(SqlGeneratorFactory.getInstance().warn(statement, database));
} else if (statement.skipOnUnsupported()) {
warnings.addWarning(
statement.getClass().getName() + " is not supported on " + database.getDisplayName() +
", but " + Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getName() +
" will still execute");
}
}
return warnings;
}
/**
* Implementation checks the ChangeParameterMetaData for declared required fields
* and also delegates logic to the
* {@link liquibase.sqlgenerator.SqlGenerator#validate(liquibase.statement.SqlStatement,
* liquibase.database.Database, liquibase.sqlgenerator.SqlGeneratorChain)} method on the {@link SqlStatement}
* objects returned by {@link #generateStatements }.
* If no or null SqlStatements are returned by generateStatements then this method returns no errors.
* If there are no parameters than this method returns no errors
*/
@Override
public ValidationErrors validate(Database database) {
ValidationErrors changeValidationErrors = new ValidationErrors(this);
// Record an error if a parameter is not set, but that parameter is required by database.
for (ChangeParameterMetaData param :
Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getParameters().values()) {
if (param.isRequiredFor(database)) {
changeValidationErrors.checkRequiredField (
param.getParameterName(), param.getCurrentValue(this)
, " on " + database.getShortName());
}
}
// We cannot proceed to the next validation if we have missing parameters
if (changeValidationErrors.hasErrors()) {
return changeValidationErrors;
}
// Record warnings if statements are unsupported on database
if (!generateStatementsVolatile(database)) {
String unsupportedWarning = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getName()
+ " is not supported on " + database.getDisplayName();
boolean sawUnsupportedError = false;
SqlStatement[] statements = generateStatements(database);
if (statements != null) {
for (SqlStatement statement : statements) {
boolean supported = SqlGeneratorFactory.getInstance().supports(statement, database);
if (!supported && !sawUnsupportedError) {
if (!statement.skipOnUnsupported()) {
changeValidationErrors.addError(unsupportedWarning);
sawUnsupportedError = true;
}
} else {
changeValidationErrors.addAll(SqlGeneratorFactory.getInstance().validate(statement, database));
}
}
}
}
return changeValidationErrors;
}
@Override
public ChangeStatus checkStatus(Database database) {
return new ChangeStatus().unknown("Not implemented");
}
//
//
/**
*
* Return if this change should execute
*
* @param database Database we are working on
* @return boolean
*
*/
public boolean shouldExecuteChange(Database database) {
Boolean shouldExecute = Scope.getCurrentScope().get(Change.SHOULD_EXECUTE, Boolean.class);
Executor executor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", database);
return ! (executor instanceof LoggingExecutor) && (shouldExecute == null || BooleanUtil.isTrue(shouldExecute));
}
/**
* Implementation relies on value returned from {@link #createInverses()}.
*/
@Override
public SqlStatement[] generateRollbackStatements(Database database) throws RollbackImpossibleException {
return generateRollbackStatementsFromInverse(database);
}
/**
* Implementation returns true if {@link #createInverses()} returns a non-null value.
*/
@Override
public boolean supportsRollback(Database database) {
return createInverses() != null;
}
/**
* Implementation generates checksum by serializing the change with {@link StringChangeLogSerializer}
*/
@Override
public CheckSum generateCheckSum() {
return CheckSum.compute(new StringChangeLogSerializer(new StringChangeLogSerializer.FieldFilter() {
@Override
public boolean include(Object obj, String field, Object value) {
if(Arrays.stream(getExcludedFieldFilters(Scope.getCurrentScope().getChecksumVersion())).anyMatch(filter -> filter.equals(field))) {
return false;
}
return super.include(obj, field, value);
}
}).serialize(this, false));
}
public String[] getExcludedFieldFilters(ChecksumVersion version) {
return new String[0];
}
/*
* Generates rollback statements from the inverse changes returned by createInverses().
* Throws RollbackImpossibleException if the changes created by createInverses() is not supported for the
* passed database.
*/
private SqlStatement[] generateRollbackStatementsFromInverse(Database database) throws RollbackImpossibleException {
Change[] inverses = createInverses();
if (inverses == null) {
throw new RollbackImpossibleException("No inverse to " + getClass().getName() + " created");
}
List statements = new ArrayList<>();
try {
for (Change inverse : inverses) {
if (!inverse.supports(database)) {
throw new RollbackImpossibleException(
Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(inverse).getName() + " is not supported on " +
database.getDisplayName()
);
}
statements.addAll(Arrays.asList(inverse.generateStatements(database)));
}
} catch (LiquibaseException e) {
throw new RollbackImpossibleException(e);
}
return statements.toArray(EMPTY_SQL_STATEMENT);
}
/**
* Create inverse changes that can roll back this change. This method is intended
* to be overridden by Change implementations that have a logical inverse operation. Default implementation
* returns null.
*
* If {@link #generateRollbackStatements(liquibase.database.Database)} is overridden, this method may not be called.
*
* @return Return null if there is no corresponding inverse and therefore automatic rollback is not possible.
* Return an empty array to have a no-op rollback.
* @see #generateRollbackStatements #supportsRollback
*/
protected Change[] createInverses() {
return null;
}
/**
* @inheritDoc
* @deprecated Should get from {@link Scope}
*/
@DatabaseChangeProperty(isChangeProperty = false)
public ResourceAccessor getResourceAccessor() {
return Scope.getCurrentScope().getResourceAccessor();
}
@Override
public void setResourceAccessor(ResourceAccessor resourceAccessor) {
Scope.getCurrentScope().getLog(getClass()).info("As of Liquibase 4.0, cannot set resource accessor on "+getClass().getName()+". Must add it to the Scope");
}
/**
* Implementation delegates logic to the
* {@link liquibase.sqlgenerator.SqlGeneratorFactory#getAffectedDatabaseObjects(liquibase.statement.SqlStatement,
* liquibase.database.Database)} method on the {@link SqlStatement} objects returned by {@link #generateStatements }
* Returns empty set if change is not supported for the passed database
*/
@Override
public Set getAffectedDatabaseObjects(Database database) {
if (this.generateStatementsVolatile(database)) {
return new HashSet<>();
}
Set affectedObjects = new HashSet<>();
SqlStatement[] statements = generateStatements(database);
if (statements != null) {
for (SqlStatement statement : statements) {
affectedObjects.addAll(SqlGeneratorFactory.getInstance()
.getAffectedDatabaseObjects(statement, database));
}
}
return affectedObjects;
}
/**
* Returns the fields on this change that are serializable.
*/
@Override
public Set getSerializableFields() {
return Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getParameters().keySet();
}
@Override
public Object getSerializableFieldValue(String field) {
ChangeParameterMetaData fieldMetaData = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this)
.getParameters().get(field);
if (fieldMetaData == null) {
return null;
}
return fieldMetaData.getCurrentValue(this);
}
@Override
public String getSerializedObjectName() {
return Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getName();
}
@Override
public SerializationType getSerializableFieldType(String field) {
return Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getParameters().get(field).getSerializationType();
}
@Override
public String getSerializedObjectNamespace() {
return GENERIC_CHANGELOG_EXTENSION_NAMESPACE;
}
@Override
public String getSerializableFieldNamespace(String field) {
return getSerializedObjectNamespace();
}
@Override
public String toString() {
return Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getName();
}
@Override
public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException {
ChangeMetaData metaData = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this);
try {
Collection changeParameters = metaData.getParameters().values();
for (ChangeParameterMetaData param : changeParameters) {
if (Collection.class.isAssignableFrom(param.getDataTypeClass())) {
if (param.getDataTypeClassParameters().length == 1) {
Class collectionType = (Class) param.getDataTypeClassParameters()[0];
if (ColumnConfig.class.isAssignableFrom(collectionType)) {
List columnNodes = new ArrayList<>(
parsedNode.getChildren(null, param.getParameterName())
);
columnNodes.addAll(parsedNode.getChildren(null, NODENAME_COLUMN));
Object nodeValue = parsedNode.getValue();
if (nodeValue instanceof ParsedNode) {
columnNodes.add((ParsedNode) nodeValue);
} else if (nodeValue instanceof Collection) {
for (Object nodeValueChild : ((Collection) nodeValue)) {
if (nodeValueChild instanceof ParsedNode) {
columnNodes.add((ParsedNode) nodeValueChild);
}
}
}
for (ParsedNode child : columnNodes) {
if (NODENAME_COLUMN.equals(child.getName()) || "columns".equals(child.getName())) {
List columnChildren = child.getChildren(null, NODENAME_COLUMN);
if ((columnChildren != null) && !columnChildren.isEmpty()) {
for (ParsedNode columnChild : columnChildren) {
ColumnConfig columnConfig = createEmptyColumnConfig(collectionType);
columnConfig.load(columnChild, resourceAccessor);
((ChangeWithColumns) this).addColumn(columnConfig);
}
} else {
ColumnConfig columnConfig = createEmptyColumnConfig(collectionType);
columnConfig.load(child, resourceAccessor);
((ChangeWithColumns) this).addColumn(columnConfig);
}
}
}
} else if (
(LiquibaseSerializable.class.isAssignableFrom(collectionType))
&& (!collectionType.isInterface())
&& (!Modifier.isAbstract(collectionType.getModifiers()))
) {
String elementName = ((LiquibaseSerializable) collectionType.getConstructor().newInstance())
.getSerializedObjectName();
List nodes = new ArrayList<>(
parsedNode.getChildren(null, param.getParameterName())
);
if (!elementName.equals(param.getParameterName())) {
nodes.addAll(parsedNode.getChildren(null, elementName));
}
Object nodeValue = parsedNode.getValue();
if (nodeValue instanceof ParsedNode) {
nodes.add((ParsedNode) nodeValue);
} else if (nodeValue instanceof Collection) {
for (Object nodeValueChild : ((Collection) nodeValue)) {
if (nodeValueChild instanceof ParsedNode) {
nodes.add((ParsedNode) nodeValueChild);
}
}
}
for (ParsedNode node : nodes) {
if (node.getName().equals(elementName)
|| node.getName().equals(param.getParameterName())) {
List childNodes = node.getChildren(null, elementName);
if ((childNodes != null) && !childNodes.isEmpty()) {
for (ParsedNode childNode : childNodes) {
LiquibaseSerializable childObject =
(LiquibaseSerializable)collectionType.getConstructor().newInstance();
childObject.load(childNode, resourceAccessor);
((Collection) param.getCurrentValue(this)).add(childObject);
}
} else {
LiquibaseSerializable childObject =
(LiquibaseSerializable) collectionType.getConstructor().newInstance();
childObject.load(node, resourceAccessor);
((Collection) param.getCurrentValue(this)).add(childObject);
}
}
}
}
}
} else if (LiquibaseSerializable.class.isAssignableFrom(param.getDataTypeClass())) {
if (!param.getDataTypeClass().isInterface()
&& !Modifier.isAbstract(param.getDataTypeClass().getModifiers())) {
try {
ParsedNode child = parsedNode.getChild(null, param.getParameterName());
if (child != null) {
LiquibaseSerializable serializableChild =
(LiquibaseSerializable) param.getDataTypeClass().getConstructor().newInstance();
serializableChild.load(child, resourceAccessor);
param.setValue(this, serializableChild);
}
} catch (ReflectiveOperationException e) {
throw new UnexpectedLiquibaseException(e);
}
}
} else {
Object childValue = parsedNode.getChildValue(
null, param.getParameterName(), param.getDataTypeClass()
);
if ((childValue == null) && (param.getSerializationType() == SerializationType.DIRECT_VALUE)) {
childValue = parsedNode.getValue();
}
if(null != childValue) {
param.setValue(this, childValue);
}
}
}
} catch (ReflectiveOperationException e) {
throw new UnexpectedLiquibaseException(e);
}
customLoadLogic(parsedNode, resourceAccessor);
try {
this.finishInitialization();
} catch (SetupException e) {
throw new ParsedNodeException(e);
}
}
protected ColumnConfig createEmptyColumnConfig(Class collectionType)
throws ReflectiveOperationException {
return (ColumnConfig) collectionType.getConstructor().newInstance();
}
protected void customLoadLogic(ParsedNode parsedNode, ResourceAccessor resourceAccessor)
throws ParsedNodeException {
}
@Override
public ParsedNode serialize() throws ParsedNodeException {
ParsedNode node = new ParsedNode(null, getSerializedObjectName());
ChangeMetaData metaData = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this);
for (ChangeParameterMetaData param : metaData.getSetParameters(this).values()) {
Object currentValue = param.getCurrentValue(this);
currentValue = serializeValue(currentValue);
if (currentValue != null) {
node.addChild(null, param.getParameterName(), currentValue);
}
}
return node;
}
protected Object serializeValue(Object value) throws ParsedNodeException {
if (value instanceof Collection) {
List returnList = new ArrayList<>();
for (Object obj : (Collection) value) {
Object objValue = serializeValue(obj);
if (objValue != null) {
returnList.add(objValue);
}
}
if (((Collection>) value).isEmpty()) {
return null;
} else {
return returnList;
}
} else if (value instanceof LiquibaseSerializable) {
return ((LiquibaseSerializable) value).serialize();
} else {
return value;
}
}
@Override
public String getDescription() {
ChangeMetaData metaData = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this);
String description = metaData.getName();
SortedSet names = new TreeSet<>();
for (Map.Entry entry : metaData.getParameters().entrySet()) {
String lowerCaseKey = entry.getKey().toLowerCase();
if ((lowerCaseKey.endsWith("name")
&& !lowerCaseKey.contains("schema")
&& !lowerCaseKey.contains("catalog"))
|| lowerCaseKey.equals("path")) {
Object currentValue = entry.getValue().getCurrentValue(this);
if (currentValue != null) {
names.add(entry.getKey()+"="+ currentValue);
}
}
}
if (!names.isEmpty()) {
description += " "+ StringUtil.join(names, ", ");
}
return description;
}
@Override
public void modify(ChangeVisitor changeVisitor) throws ParsedNodeException{
}
@Override
public boolean equals(Object reference) {
return (this == reference);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), changeSet);
}
}