liquibase.snapshot.DatabaseSnapshot 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.snapshot;
import liquibase.CatalogAndSchema;
import liquibase.Scope;
import liquibase.GlobalConfiguration;
import liquibase.database.Database;
import liquibase.database.DatabaseConnection;
import liquibase.database.OfflineConnection;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.compare.DatabaseObjectComparatorFactory;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.Logger;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.resource.ResourceAccessor;
import liquibase.serializer.LiquibaseSerializable;
import liquibase.structure.DatabaseObject;
import liquibase.structure.DatabaseObjectCollection;
import liquibase.structure.core.*;
import liquibase.util.BooleanUtil;
import liquibase.util.ISODateFormat;
import liquibase.util.ObjectUtil;
import liquibase.util.StringUtil;
import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public abstract class DatabaseSnapshot implements LiquibaseSerializable {
private static final Logger LOGGER = Scope.getCurrentScope().getLog(DatabaseSnapshot.class);
public static final String ALL_CATALOGS_STRING_SCRATCH_KEY = "DatabaseSnapshot.allCatalogsString";
private final DatabaseObject[] originalExamples;
private final HashSet serializableFields;
private final SnapshotControl snapshotControl;
private final Database database;
private final DatabaseObjectCollection allFound;
private final DatabaseObjectCollection referencedObjects;
private final Map, Set> knownNull = new ConcurrentHashMap<>();
private final Map snapshotScratchPad = new ConcurrentHashMap<>();
private final Map resultSetCaches = new ConcurrentHashMap<>();
private CompareControl.SchemaComparison[] schemaComparisons;
private Map metadata = new ConcurrentHashMap<>();
DatabaseSnapshot(DatabaseObject[] examples, Database database, SnapshotControl snapshotControl) throws DatabaseException, InvalidExampleException {
this.database = database;
allFound = new DatabaseObjectCollection(database);
referencedObjects = new DatabaseObjectCollection(database);
this.snapshotControl = snapshotControl;
this.originalExamples = ((examples == null) ? new DatabaseObject[0] : examples);
init(examples);
this.serializableFields = new HashSet<>();
this.serializableFields.add("snapshotControl");
this.serializableFields.add("objects");
this.serializableFields.add("referencedObjects");
this.serializableFields.add("database");
this.serializableFields.add("created");
this.serializableFields.add("metadata");
}
public DatabaseSnapshot(DatabaseObject[] examples, Database database) throws DatabaseException, InvalidExampleException {
this(examples, database, new SnapshotControl(database));
}
protected void init(DatabaseObject[] examples) throws DatabaseException, InvalidExampleException {
if (examples != null) {
Set catalogs = new HashSet<>();
for (DatabaseObject object : examples) {
if (object instanceof Schema) {
catalogs.add(((Schema) object).getCatalog());
}
}
this.setScratchData("DatabaseSnapshot.allCatalogs", catalogs);
if (catalogs.size() > 1) {
List quotedCatalogs = new ArrayList<>();
for (Catalog catalog : catalogs) {
quotedCatalogs.add("'" + catalog.getName() + "'");
}
if (CatalogAndSchema.CatalogAndSchemaCase.ORIGINAL_CASE.equals(database.getSchemaAndCatalogCase())) {
this.setScratchData(ALL_CATALOGS_STRING_SCRATCH_KEY, StringUtil.join(quotedCatalogs, ", "));
} else {
this.setScratchData(ALL_CATALOGS_STRING_SCRATCH_KEY, StringUtil.join(quotedCatalogs, ", ").toUpperCase());
}
}
if (getDatabase().supportsCatalogs()) {
for (Catalog catalog : catalogs) {
this.snapshotControl.addType(catalog.getClass(), database);
include(catalog);
}
}
for (DatabaseObject obj : examples) {
this.snapshotControl.addType(obj.getClass(), database);
include(obj);
}
}
}
/**
* Searches the current snapshot content for the given examples. Returns a new DatabaseSnapshot
* containing a clone of every object from the examples array that was found.
*
* @param examples The array of snapshot objects to search and clone
* @return a new DatabaseSnapshot object with the clones of the desired objects. If no object is
* found, an empty DatabaseSnapshot will be returned.
*/
public DatabaseSnapshot clone(DatabaseObject[] examples) {
try {
DatabaseSnapshot returnSnapshot = new RestoredDatabaseSnapshot(this.database);
for (DatabaseObject example : examples) {
DatabaseObject existingObject = this.get(example);
if (existingObject == null) {
continue;
}
if (example instanceof Schema) {
for (Class extends DatabaseObject> type : this.snapshotControl.getTypesToInclude()) {
for (DatabaseObject object : this.get(type)) {
if (object.getSchema() == null) {
if (object instanceof Catalog) {
if (DatabaseObjectComparatorFactory.getInstance().isSameObject(object, ((Schema) example).getCatalog(), null, database)) {
returnSnapshot.allFound.add(object);
}
} else {
returnSnapshot.allFound.add(object);
}
} else {
if (DatabaseObjectComparatorFactory.getInstance().isSameObject(object.getSchema(), example, null, database)) {
returnSnapshot.allFound.add(object);
} else {
if (object.getClass().getName().contains("Synonym")
&& !object.getAttribute("private", false)) {
Schema objectSchema = object.getAttribute("objectSchema", Schema.class);
if (DatabaseObjectComparatorFactory.getInstance().isSameObject(objectSchema, example, null, database)) {
returnSnapshot.allFound.add(object);
}
}
}
}
}
}
} else {
returnSnapshot.allFound.add(existingObject);
}
}
returnSnapshot.getMetadata().putAll(this.getMetadata());
return returnSnapshot;
} catch (Exception e) {
throw new UnexpectedLiquibaseException(e);
}
}
/**
*
* Method which merges two object snapshot models into one
*
* @param snapshotToMerge Another object snapshot model
* @return DatabaseSnapshot Merged object model
*
*/
public DatabaseSnapshot merge(DatabaseSnapshot snapshotToMerge) {
DatabaseSnapshot returnSnapshot = this;
Map, Set extends DatabaseObject>> allFoundMap = snapshotToMerge.allFound.toMap();
Map, Set extends DatabaseObject>> referencedObjectsMap = snapshotToMerge.referencedObjects.toMap();
for (Set extends DatabaseObject> setOfDatabaseObject : allFoundMap.values()) {
for (DatabaseObject dbObject : setOfDatabaseObject) {
returnSnapshot.allFound.add(dbObject);
}
}
for (Set extends DatabaseObject> setOfDatabaseObject : referencedObjectsMap.values()) {
for (DatabaseObject dbObject : setOfDatabaseObject) {
returnSnapshot.referencedObjects.add(dbObject);
}
}
return returnSnapshot;
}
public SnapshotControl getSnapshotControl() {
return snapshotControl;
}
@Override
public String getSerializedObjectName() {
return "snapshot";
}
@Override
public String getSerializedObjectNamespace() {
return STANDARD_SNAPSHOT_NAMESPACE;
}
@Override
public String getSerializableFieldNamespace(String field) {
return getSerializedObjectNamespace();
}
@Override
public Set getSerializableFields() {
return serializableFields;
}
@Override
public Object getSerializableFieldValue(String field) {
switch (field) {
case "snapshotControl":
return snapshotControl;
case "objects":
return allFound;
case "referencedObjects":
return referencedObjects;
case "metadata":
return metadata;
case "created":
return new ISODateFormat().format(new Timestamp(new Date().getTime()));
case "database":
Map map = new HashMap<>();
map.put("shortName", database.getShortName());
map.put("productName", database.getDatabaseProductName());
map.put("url", database.getConnection().getURL());
try {
map.put("majorVersion", String.valueOf(database.getDatabaseMajorVersion()));
map.put("minorVersion", String.valueOf(database.getDatabaseMinorVersion()));
map.put("productVersion", database.getDatabaseProductVersion());
map.put("user", database.getConnection().getConnectionUserName());
} catch (DatabaseException e) {
//ok
}
return map;
default:
throw new UnexpectedLiquibaseException("Unknown field: " + field);
}
}
@Override
public SerializationType getSerializableFieldType(String field) {
switch (field) {
case "snapshotControl":
case "objects":
case "referencedObjects":
return SerializationType.NESTED_OBJECT;
default:
throw new UnexpectedLiquibaseException("Unknown field: " + field);
}
}
public Database getDatabase() {
return database;
}
public ResultSetCache getResultSetCache(String key) {
if (!resultSetCaches.containsKey(key)) {
resultSetCaches.put(key, new ResultSetCache());
}
return resultSetCaches.get(key);
}
/**
* Include the object described by the passed example object in this snapshot. Returns the object snapshot or null
* if the object does not exist in the database. If the same object was returned by an earlier include() call,
* the same object instance will be returned.
*/
protected T include(T example) throws DatabaseException, InvalidExampleException {
if (example == null) {
return null;
}
if (database.isSystemObject(example)) {
return null;
}
if ((example instanceof Schema) && (example.getName() == null) && ((((Schema) example).getCatalog() == null)
|| (((Schema) example).getCatalogName() == null))) {
CatalogAndSchema catalogAndSchema = ((Schema) example).toCatalogAndSchema().customize(database);
example = (T) new Schema(catalogAndSchema.getCatalogName(), catalogAndSchema.getSchemaName());
}
if (!snapshotControl.shouldInclude(example)) {
LOGGER.fine("Excluding " + example);
return example;
}
T existing = get(example);
if (existing != null) {
return existing;
}
if (isKnownNull(example)) {
return null;
}
SnapshotListener snapshotListener = snapshotControl.getSnapshotListener();
SnapshotGeneratorChain chain = createGeneratorChain(example.getClass(), database);
if (snapshotListener != null) {
snapshotListener.willSnapshot(example, database);
}
T object = chain.snapshot(example, this);
if (object == null) {
Set collection = knownNull.computeIfAbsent(example.getClass(), k -> new HashSet<>());
collection.add(example);
if (example instanceof Schema) {
if (snapshotControl.isWarnIfObjectNotFound())
Scope.getCurrentScope().getLog(getClass()).warning("Did not find schema '" + example + "' to snapshot");
}
if (example instanceof Catalog) {
if (snapshotControl.isWarnIfObjectNotFound())
Scope.getCurrentScope().getLog(getClass()).warning("Did not find catalog '" + example + "' to snapshot");
}
} else {
allFound.add(object);
try {
includeNestedObjects(object);
} catch (ReflectiveOperationException e) {
throw new UnexpectedLiquibaseException(e);
}
}
if (snapshotListener != null) {
snapshotListener.finishedSnapshot(example, object, database);
}
return object;
}
private void includeNestedObjects(DatabaseObject object) throws DatabaseException, InvalidExampleException, ReflectiveOperationException {
for (String field : new HashSet<>(object.getAttributes())) {
Object fieldValue = object.getAttribute(field, Object.class);
//
// Make sure we always assign a snapshot ID when we handle index columns
// in the case where one of the columns is descending
//
if ("columns".equals(field) && ((object.getClass() == PrimaryKey.class) || (object.getClass() == Index
.class) || (object.getClass() == UniqueConstraint.class))) {
if ((fieldValue != null) && !((Collection>) fieldValue).isEmpty()) {
if (descendingColumnExists((Collection)fieldValue)) {
List columns = (List)fieldValue;
for (Column col : columns) {
if (col.getSnapshotId() != null) {
continue;
}
col.setSnapshotId(SnapshotIdService.getInstance().generateId());
includeNestedObjects(col);
referencedObjects.add(col);
}
continue;
}
}
}
Object newFieldValue = replaceObject(fieldValue);
if (newFieldValue == null) { //sometimes an object references a non-snapshotted object. Leave it with the unsnapshotted example
if (((object instanceof UniqueConstraint) || (object instanceof PrimaryKey) || (object instanceof
ForeignKey)) && "backingIndex".equals(field)) { //unless it is the backing index, that is handled a bit strange and we need to handle the case where there is no backing index (disabled PK on oracle)
object.setAttribute(field, null);
}
} else if (fieldValue != newFieldValue) {
object.setAttribute(field, newFieldValue);
}
}
}
//
// Check for existence of a descending column within the Collection
//
private boolean descendingColumnExists(Collection fieldValue) {
for (Column column : fieldValue) {
String columnName = column.getName().toLowerCase();
if (BooleanUtil.isTrue(column.getDescending()) ||
columnName.endsWith(" asc") ||
columnName.endsWith(" desc") ||
columnName.endsWith(" random")) {
return true;
}
}
return false;
}
private Object replaceObject(Object fieldValue) throws DatabaseException, InvalidExampleException, ReflectiveOperationException {
if (fieldValue == null) {
return null;
}
if (fieldValue instanceof DatabaseObject) {
if (((DatabaseObject) fieldValue).getSnapshotId() != null) { //already been replaced
return fieldValue;
}
if (!snapshotControl.shouldInclude(((DatabaseObject) fieldValue).getClass())) {
return fieldValue;
}
if (!(fieldValue instanceof Catalog) && isWrongSchema(((DatabaseObject) fieldValue))) {
DatabaseObject savedFieldValue = referencedObjects.get((DatabaseObject) fieldValue, schemaComparisons);
if (savedFieldValue == null) {
savedFieldValue = (DatabaseObject) fieldValue;
savedFieldValue.setSnapshotId(SnapshotIdService.getInstance().generateId());
includeNestedObjects(savedFieldValue);
referencedObjects.add(savedFieldValue);
}
return savedFieldValue;
}
if ((fieldValue instanceof Catalog) && isWrongCatalog(((DatabaseObject) fieldValue))) {
DatabaseObject savedFieldValue = referencedObjects.get((DatabaseObject) fieldValue, schemaComparisons);
if (savedFieldValue == null) {
savedFieldValue = (DatabaseObject) fieldValue;
savedFieldValue.setSnapshotId(SnapshotIdService.getInstance().generateId());
referencedObjects.add(savedFieldValue);
}
return savedFieldValue;
}
if (((DatabaseObject) fieldValue).getSnapshotId() == null) {
return include((DatabaseObject) fieldValue);
} else {
return fieldValue;
}
// } else if (Set.class.isAssignableFrom(field.getType())) {
// field.setAccessible(true);
// Set fieldValue = field.get(object);
// for (Object val : fieldValue) {
//
// }
} else if (fieldValue instanceof Collection) {
Iterator fieldValueIterator = new CopyOnWriteArrayList((Collection) fieldValue).iterator();
List