/*
* Copyright (c) 2013 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.db.common.diff;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlTransient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.model.valid.EnumType;
import com.emc.storageos.db.client.model.DbKeyspace;
import com.emc.storageos.db.client.model.UpgradeAllowed;
import com.emc.storageos.db.client.upgrade.CustomMigrationCallback;
import com.emc.storageos.db.common.schema.AnnotationType;
import com.emc.storageos.db.common.schema.AnnotationValue;
import com.emc.storageos.db.common.schema.DbSchema;
import com.emc.storageos.db.common.schema.FieldInfo;
import com.emc.storageos.db.common.schema.Key;
import com.emc.storageos.db.common.schema.SchemaObject;
public class CollectionChangeTracker<T extends SchemaObject, S extends Diff> extends Diff {
private static final Logger log = LoggerFactory.getLogger(CollectionChangeTracker.class);
private Class<T> clazz;
private List<T> newList = new ArrayList<T>();
private List<T> removedList = new ArrayList<T>();
private List<T> duplicateList = new ArrayList<T>();
private List<S> diffs = new ArrayList<S>();
public static <T extends SchemaObject, S extends Diff> CollectionChangeTracker<T, S> newInstance(
Class<T> schemaClass, Class<S> diffClass, List<T> srcList, List<T> tgtList) {
if (srcList == null && tgtList == null) {
return null;
}
if (srcList != null && srcList.equals(tgtList)) {
return null;
}
CollectionChangeTracker<T, S> ret = new CollectionChangeTracker<T, S>(schemaClass,
diffClass, srcList, tgtList);
if (ret.isChanged()) {
return ret;
} else {
return null;
}
}
private CollectionChangeTracker() {
}
private CollectionChangeTracker(Class<T> schemaClass, Class<S> diffClass,
List<T> srcList, List<T> tgtList) {
clazz = schemaClass;
if (srcList == null || srcList.isEmpty()) {
newList = tgtList;
return;
}
if (tgtList == null || tgtList.isEmpty()) {
removedList = srcList;
return;
}
// We want to catch whether there are duplicate keys in the tgt list.
// This is primarily for CFs. Now let's assume that the src list is safe.
Map<String, T> tgtMap = new HashMap<String, T>();
for (T tgt : tgtList) {
String tgtKey = getKey(tgt);
if (tgtMap.containsKey(tgtKey)) {
duplicateList.add(tgt);
} else {
tgtMap.put(tgtKey, tgt);
}
}
tgtMap.clear();
// initialize newList
for (T tgt : tgtList) {
String tgtKey = getKey(tgt);
boolean found = false;
for (T src : srcList) {
String srcKey = getKey(src);
if (tgtKey.equals(srcKey)) {
found = true;
if (!tgt.equals(src)) {
try {
S diff = diffClass.getConstructor(schemaClass,
schemaClass).newInstance(src, tgt);
if (diff.isChanged()) {
diffs.add(diff);
}
} catch (NoSuchMethodException | InstantiationException |
IllegalAccessException | InvocationTargetException e) {
log.error("Failed to instantiate {}", diffClass, e);
}
}
}
}
if (!found) {
newList.add(tgt);
}
}
// initialize removedList
for (T src : srcList) {
String srcKey = getKey(src);
boolean found = false;
for (T tgt : tgtList) {
String tgtKey = getKey(tgt);
if (tgtKey.equals(srcKey)) {
found = true;
}
}
if (!found) {
removedList.add(src);
}
}
}
@XmlElements({
@XmlElement(name = "new_annotation", type = AnnotationType.class),
@XmlElement(name = "new_annotation_value", type = AnnotationValue.class),
@XmlElement(name = "new_schema", type = DbSchema.class),
@XmlElement(name = "new_field", type = FieldInfo.class)
})
public List<T> getNewList() {
return newList;
}
@XmlElements({
@XmlElement(name = "removed_annotation", type = AnnotationType.class),
@XmlElement(name = "removed_annotation_value", type = AnnotationValue.class),
@XmlElement(name = "removed_schema", type = DbSchema.class),
@XmlElement(name = "removed_field", type = FieldInfo.class)
})
public List<T> getRemovedList() {
return removedList;
}
@XmlElements({
@XmlElement(name = "duplicate_annotation", type = AnnotationType.class),
@XmlElement(name = "duplicate_annotation_value", type = AnnotationValue.class),
@XmlElement(name = "duplicate_schema", type = DbSchema.class),
@XmlElement(name = "duplicate_field", type = FieldInfo.class)
})
public List<T> getDuplicateList() {
return duplicateList;
}
@XmlElements({
@XmlElement(name = "changed_annotation", type = AnnotationTypeDiff.class),
@XmlElement(name = "changed_annotation_value", type = AnnotationValueDiff.class),
@XmlElement(name = "changed_schema", type = DbSchemaDiff.class),
@XmlElement(name = "changed_field", type = FieldInfoDiff.class)
})
public List<S> getDiff() {
return diffs;
}
@XmlTransient
public Class<T> getSchemaClass() {
return clazz;
}
private String getKey(T value) {
Method[] methods = clazz.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].isAnnotationPresent(Key.class)) {
try {
return (String) methods[i].invoke(value);
} catch (Exception e) {
log.error("Failed to get key", e);
}
}
}
return null;
}
public boolean isUpgradable() {
boolean returnVal = true;
if (!removedList.isEmpty()) {
for (T schema : removedList) {
// CustomMigrationCallback can be removed
if (clazz.equals(AnnotationType.class)) {
AnnotationType at = (AnnotationType) schema;
// since it has been removed from the target schema, there's no runtime
// type associated with it. Do string comparison instead.
if (CustomMigrationCallback.class.getCanonicalName().equals(at.getType())) {
log.info("CustomMigrationCallback {} has been removed", at.describe());
continue;
} else if (EnumType.class.getCanonicalName().equals(at.getType())) {
log.info("EnumType {} has been removed", at.describe());
continue;
}
if (at.canBeIgnore()) {
continue;
}
}
log.warn("An unsupported schema change has been made. {} has been removed.",
schema.describe());
returnVal = false;
}
}
if (!duplicateList.isEmpty()) {
for (T schema : duplicateList) {
log.warn("An unsupported schema change has been made. Duplicate {} has been added",
schema.describe());
}
returnVal = false;
}
for (S diff : diffs) {
if (!diff.isUpgradable()) {
returnVal = false;
}
}
if (clazz.equals(AnnotationType.class)) {
for (T element : newList) {
AnnotationType at = (AnnotationType) element;
Class cfClass = at.getCfClass();
// refuse adding any annotation (including index) on existing field
if (cfClass.isAnnotationPresent(DbKeyspace.class)) {
DbKeyspace keyspaceType = (DbKeyspace) cfClass.getAnnotation(DbKeyspace.class);
if (DbKeyspace.Keyspaces.GLOBAL.equals(keyspaceType.value())) {
log.warn("An unsupported geo schema change has been made. {} has been added",
at.describe());
returnVal = false;
break;
}
}
// check UpgradeAllowed annotation for new annotations
if (!CustomMigrationCallback.class.isAssignableFrom(at.getAnnoClass()) &&
!at.getAnnoClass().isAnnotationPresent(UpgradeAllowed.class)) {
log.warn("An unsupported schema change has been made. {} has been added",
at.describe());
returnVal = false;
break;
}
}
}
return returnVal;
}
public boolean isChanged() {
if (!newList.isEmpty()) {
return true;
}
if (!removedList.isEmpty()) {
return true;
}
if (!duplicateList.isEmpty()) {
return true;
}
for (S diff : diffs) {
if (diff.isChanged()) {
return true;
}
}
return false;
}
}