All Downloads are FREE. Search and download functionalities are using the official Maven repository.

liquibase.change.custom.CustomChangeWrapper Maven / Gradle / Ivy

There is a newer version: 4.29.2
Show newest version
package liquibase.change.custom;

import liquibase.Scope;
import liquibase.change.AbstractChange;
import liquibase.change.ChangeMetaData;
import liquibase.change.DatabaseChange;
import liquibase.change.DatabaseChangeProperty;
import liquibase.database.Database;
import liquibase.exception.*;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.resource.ResourceAccessor;
import liquibase.statement.SqlStatement;
import liquibase.util.ObjectUtil;

import java.util.*;
import liquibase.util.OsgiUtil;

/**
 * Adapts CustomChange implementations to the standard change system used by Liquibase.
 * Custom change implementations should implement CustomSqlChange or CustomTaskChange
 *
 * @see liquibase.change.custom.CustomSqlChange
 * @see liquibase.change.custom.CustomTaskChange
 */
@DatabaseChange(name="customChange",
    description = "Although Liquibase tries to provide a wide range of database refactorings, there are times you may" +
        " want to create your own custom refactoring class.\n" +
                "\n" +
                "To create your own custom refactoring, simply create a class that implements the liquibase.change.custom.CustomSqlChange " +
                "or liquibase.change.custom.CustomTaskChange interface and use the  tag in your change set.\n" +
                "\n" +
                "If your change can be rolled back, implement the liquibase.change.custom.CustomSqlRollback interface as well.\n" +
                "\n" +
                "For a sample custom change class, see liquibase.change.custom.ExampleCustomSqlChange",
        priority = ChangeMetaData.PRIORITY_DEFAULT)
public class CustomChangeWrapper extends AbstractChange {

    /**
     * Non-private access only for testing.
     */
    CustomChange customChange;
    
    private String className;

    private SortedSet params = new TreeSet<>();

    private Map paramValues = new LinkedHashMap<>();

    private boolean configured;

    @Override
    public boolean generateStatementsVolatile(Database database) {
        return true;
    }

    /**
     * Return the CustomChange instance created by the call to {@link #setClass(String)}.
     */
    @DatabaseChangeProperty(isChangeProperty = false)
    public CustomChange getCustomChange() {
        return customChange;
    }

    /**
     * Specify the name of the class to use as the CustomChange and assigns it to {@link #getCustomChange()}.
     */
    public CustomChangeWrapper setClass(String className) throws CustomChangeException {
        if (className == null) {
            return this;
        }
        this.className = className;
        try {
           Boolean osgiPlatform = Scope.getCurrentScope().get(Scope.Attr.osgiPlatform, Boolean.class);
           if (Boolean.TRUE.equals(osgiPlatform)) {
              customChange = (CustomChange)OsgiUtil.loadClass(className).getConstructor().newInstance();
           } else {
              try {
                  customChange = (CustomChange) Class.forName(className, true, Scope.getCurrentScope().getClassLoader()).getConstructor().newInstance();
              } catch (ClassCastException e) { //fails in Ant in particular
                try {
                  customChange = (CustomChange) Thread.currentThread().getContextClassLoader().loadClass(className).getConstructor().newInstance();
                } catch (ClassNotFoundException e1) {
                  customChange = (CustomChange) Class.forName(className).getConstructor().newInstance();
                }
              }
           }
        } catch (Exception e) {
            throw new CustomChangeException(e);
        }

        return this;
    }

    /**
     * Returns the name of the custom class set in {@link #setClass(String)}
     */
    @DatabaseChangeProperty(description = "Name class that implements the custom change.")
    public String getClassName() {
        return className;
    }

    /**
     * Specify a parameter on the CustomChange object to set before executing {@link liquibase.change.Change#generateStatements(liquibase.database.Database)}  or {@link #generateRollbackStatements(liquibase.database.Database)} on it.
     * The CustomChange class must have a set method for the given parameter. For example, to call setParam("lastName", "X") you must have a method setLastName(String val) on your class.
     */
    public void setParam(String name, String value) {
        this.params.add(name);
        this.paramValues.put(name, value);
    }

    /**
     * Returns the parameters set by {@link #setParam(String, String)}. If no parameters are set, an empty set will be returned
     */
    @DatabaseChangeProperty(isChangeProperty = false)
    public SortedSet getParams() {
        return Collections.unmodifiableSortedSet(params);
    }

    /**
     * Get the value of a parameter set by {@link #setParam(String, String)}. If the parameter was not set, null will be returned.
     */
    public String getParamValue(String key) {
        return paramValues.get(key);
    }

    /**
     * Call the {@link CustomChange#validate(liquibase.database.Database)} method and return the result.
     */
    @Override
    public ValidationErrors validate(Database database) {
        if (!configured) {
            try {
                configureCustomChange();
            } catch (CustomChangeException e) {
                throw new UnexpectedLiquibaseException(e);
            }
        }

        try {
            return customChange.validate(database);
        } catch (Exception e) {
            return new ValidationErrors().addError("Exception thrown calling "+getClassName()+".validate():"+ e.getMessage());
        }
    }

    /**
     * Required for the Change interface, but not supported by CustomChanges. Returns an empty Warnings object.
     */
    @Override
    public Warnings warn(Database database) {
        //does not support warns
        return new Warnings();
    }

    /**
     * Finishes configuring the CustomChange based on the values passed to {@link #setParam(String, String)} then calls {@link CustomSqlChange#generateStatements(liquibase.database.Database)}
     * or {@link CustomTaskChange#execute(liquibase.database.Database)} depending on the CustomChange implementation.
     * 

* If the CustomChange returns a null SqlStatement array, this method returns an empty array. If a CustomTaskChange is being used, this method will return an empty array. */ @Override public SqlStatement[] generateStatements(Database database) { SqlStatement[] statements = null; try { configureCustomChange(); if (customChange instanceof CustomSqlChange) { statements = ((CustomSqlChange) customChange).generateStatements(database); } else if (customChange instanceof CustomTaskChange) { ((CustomTaskChange) customChange).execute(database); } else { throw new UnexpectedLiquibaseException(customChange.getClass().getName() + " does not implement " + CustomSqlChange.class.getName() + " or " + CustomTaskChange.class.getName()); } } catch (CustomChangeException e) { throw new UnexpectedLiquibaseException(e); } if (statements == null) { statements = new SqlStatement[0]; } return statements; } /** * Finishes configuring the CustomChange based on the values passed to {@link #setParam(String, String)} then calls {@link CustomSqlRollback#generateRollbackStatements(liquibase.database.Database)} * or {@link CustomTaskRollback#rollback(liquibase.database.Database)} depending on the CustomChange implementation. *

* If the CustomChange returns a null SqlStatement array, this method returns an empty array. If a CustomTaskChange is being used, this method will return an empty array. * Any {@link RollbackImpossibleException} exceptions thrown by the CustomChange will thrown by this method. */ @Override public SqlStatement[] generateRollbackStatements(Database database) throws RollbackImpossibleException { SqlStatement[] statements = null; try { configureCustomChange(); if (customChange instanceof CustomSqlRollback) { statements = ((CustomSqlRollback) customChange).generateRollbackStatements(database); } else if (customChange instanceof CustomTaskRollback) { ((CustomTaskRollback) customChange).rollback(database); } else { throw new RollbackImpossibleException("Unknown rollback type: "+customChange.getClass().getName()); } } catch (CustomChangeException e) { throw new UnexpectedLiquibaseException(e); } if (statements == null) { statements = new SqlStatement[0]; } return statements; } /** * Returns true if the customChange supports rolling back. * {@link #generateRollbackStatements} may still trow a {@link RollbackImpossibleException} when it is actually exectued, even if this method returns true. * Currently only checks if the customChange implements {@link CustomSqlRollback} */ @Override public boolean supportsRollback(Database database) { return (customChange instanceof CustomSqlRollback) || (customChange instanceof CustomTaskRollback); } /** * Return the customChange's {@link CustomChange#getConfirmationMessage} message as the Change's message. */ @Override public String getConfirmationMessage() { try { configureCustomChange(); } catch (CustomChangeException e) { throw new UnexpectedLiquibaseException(e); } return customChange.getConfirmationMessage(); } private void configureCustomChange() throws CustomChangeException { if (configured) { return; } try { for (String param : params) { ObjectUtil.setProperty(customChange, param, paramValues.get(param)); } customChange.setFileOpener(Scope.getCurrentScope().getResourceAccessor()); customChange.setUp(); configured = true; } catch (Exception e) { throw new CustomChangeException(e); } } @Override public SerializationType getSerializableFieldType(String field) { switch (field) { case "class": return SerializationType.NAMED_FIELD; case "param": return SerializationType.NAMED_FIELD; default: throw new UnexpectedLiquibaseException("Unexpected CustomChangeWrapper field " + field); } } @Override public Object getSerializableFieldValue(String field) { switch (field) { case "class": return getClassName(); case "param": return this.paramValues; default: throw new UnexpectedLiquibaseException("Unexpected CustomChangeWrapper field " + field); } } @Override public Set getSerializableFields() { return new HashSet<>(Arrays.asList("class", "param")); } @Override public String getSerializedObjectNamespace() { return STANDARD_CHANGELOG_NAMESPACE; } @Override public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException { try { String classNameValue = parsedNode.getChildValue(null, "class", String.class); if (classNameValue == null) { throw new ParsedNodeException("Custom change node has no 'class' attribute!"); } setClass(classNameValue); } catch (CustomChangeException e) { throw new ParsedNodeException(e); } super.load(parsedNode, resourceAccessor); } @Override public void customLoadLogic(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException { ParsedNode paramsNode = parsedNode.getChild(null, "params"); if (paramsNode == null) { paramsNode = parsedNode; } for (ParsedNode child : paramsNode.getChildren(null, "param")) { Object value = child.getValue(); if (value == null) { value = child.getChildValue(null, "value"); } if (value != null) { value = value.toString(); } String paramName = child.getChildValue(null, "name", String.class); if (paramName == null) { throw new ParsedNodeException("Custom change param " + child + " does not have a 'name' attribute"); } this.setParam(paramName, (String) value); } CustomChange customChange = null; try { Boolean osgiPlatform = Scope.getCurrentScope().get(Scope.Attr.osgiPlatform, Boolean.class); if (Boolean.TRUE.equals(osgiPlatform)) { customChange = (CustomChange)OsgiUtil.loadClass(className).getConstructor().newInstance(); } else { customChange = (CustomChange) Class.forName(className, false, Scope.getCurrentScope().getClassLoader()).getConstructor().newInstance(); } } catch (Exception e) { throw new UnexpectedLiquibaseException(e); } for (ParsedNode node : parsedNode.getChildren()) { Object value = node.getValue(); if ((value != null) && ObjectUtil.hasProperty(customChange, node.getName())) { this.setParam(node.getName(), value.toString()); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy