liquibase.change.AbstractChange Maven / Gradle / Ivy
package liquibase.change;
import java.lang.reflect.Type;
import java.util.*;
import liquibase.changelog.ChangeSet;
import liquibase.database.Database;
import liquibase.structure.DatabaseObject;
import liquibase.exception.*;
import liquibase.resource.ResourceAccessor;
import liquibase.serializer.core.string.StringChangeLogSerializer;
import liquibase.sqlgenerator.SqlGeneratorFactory;
import liquibase.statement.SqlStatement;
import liquibase.util.StringUtils;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
/**
* 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 implements Change {
private ResourceAccessor resourceAccessor;
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 : Introspector.getBeanInfo(this.getClass()).getPropertyDescriptors()) {
Method readMethod = property.getReadMethod();
Method writeMethod = property.getWriteMethod();
if (readMethod == null) {
try {
readMethod = this.getClass().getMethod("is"+ StringUtils.upperCaseFirst(property.getName()));
} catch (Exception ignore) {
//it was worth a try
}
}
if (readMethod != null && writeMethod != null) {
DatabaseChangeProperty annotation = readMethod.getAnnotation(DatabaseChangeProperty.class);
if (annotation == null || annotation.isChangeProperty()) {
params.add(createChangeParameterMetadata(property.getDisplayName()));
}
}
}
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 (Throwable e) {
throw new UnexpectedLiquibaseException(e);
}
}
/**
* 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 {
String displayName = parameterName.replaceAll("([A-Z])", " $1");
displayName = displayName.substring(0, 1).toUpperCase() + displayName.substring(1);
PropertyDescriptor property = null;
for (PropertyDescriptor prop : Introspector.getBeanInfo(this.getClass()).getPropertyDescriptors()) {
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"+StringUtils.upperCaseFirst(property.getName()));
}
Type type = readMethod.getGenericReturnType();
DatabaseChangeProperty changePropertyAnnotation = readMethod.getAnnotation(DatabaseChangeProperty.class);
String mustEqualExisting = createMustEqualExistingMetaData(parameterName, changePropertyAnnotation);
String description = createDescriptionMetaData(parameterName, changePropertyAnnotation);
String example = 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, example, since, type, requiredForDatabase, supportsDatabase, mustEqualExisting, serializationType);
} catch (Exception e) {
throw new UnexpectedLiquibaseException(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 StringUtils.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 StringUtils.trimToNull(changePropertyAnnotation.description());
}
/**
* Create the {@link ChangeParameterMetaData} "serializationType" value. Uses the value on the DatabaseChangeProperty annotation or returns {@link SerializationType}.NAMED_FIELD as a default.
*/
@SuppressWarnings("UnusedParameters")
protected 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.
*/
@SuppressWarnings("UnusedParameters")
protected String createExampleValueMetaData(String parameterName, DatabaseChangeProperty changePropertyAnnotation) {
if (changePropertyAnnotation == null) {
return null;
}
return StringUtils.trimToNull(changePropertyAnnotation.exampleValue());
}
/**
* 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) {
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.getShortName() + ", but "+ChangeFactory.getInstance().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();
for (ChangeParameterMetaData param : ChangeFactory.getInstance().getChangeMetaData(this).getParameters().values()) {
if (param.isRequiredFor(database) && param.getCurrentValue(this) == null) {
changeValidationErrors.addError(param.getParameterName() + " is required for " + ChangeFactory.getInstance().getChangeMetaData(this).getName() + " on " + database.getShortName());
}
}
if (changeValidationErrors.hasErrors()) {
return changeValidationErrors;
}
String unsupportedWarning = ChangeFactory.getInstance().getChangeMetaData(this).getName() + " is not supported on " + database.getShortName();
if (!this.supports(database)) {
changeValidationErrors.addError(unsupportedWarning);
} else {
boolean sawUnsupportedError = false;
SqlStatement[] statements;
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;
}
/**
* 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().serialize(this, false));
}
/*
* 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(ChangeFactory.getInstance().getChangeMetaData(inverse).getName()+" is not supported on "+database.getShortName());
}
statements.addAll(Arrays.asList(inverse.generateStatements(database)));
}
} catch (LiquibaseException e) {
throw new RollbackImpossibleException(e);
}
return statements.toArray(new SqlStatement[statements.size()]);
}
/**
* Create inverse changes that can roll back this change. This method is intended
* to be overriden 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.
* @also #generateRollbackStatements #supportsRollback
*/
protected Change[] createInverses() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void setResourceAccessor(ResourceAccessor resourceAccessor) {
this.resourceAccessor = resourceAccessor;
}
/**
* @{inheritDoc}
*/
@DatabaseChangeProperty(isChangeProperty = false)
public ResourceAccessor getResourceAccessor() {
return resourceAccessor;
}
/**
* 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 ChangeFactory.getInstance().getChangeMetaData(this).getParameters().keySet();
}
@Override
public Object getSerializableFieldValue(String field) {
return ChangeFactory.getInstance().getChangeMetaData(this).getParameters().get(field).getCurrentValue(this);
}
@Override
public String getSerializedObjectName() {
return ChangeFactory.getInstance().getChangeMetaData(this).getName();
}
@Override
public SerializationType getSerializableFieldType(String field) {
return ChangeFactory.getInstance().getChangeMetaData(this).getParameters().get(field).getSerializationType();
}
@Override
public String getSerializedObjectNamespace() {
return GENERIC_CHANGELOG_EXTENSION_NAMESPACE;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy