diff --git a/src/com/massivecraft/massivecore/store/Coll.java b/src/com/massivecraft/massivecore/store/Coll.java index e665bbf4..dcf54ab8 100644 --- a/src/com/massivecraft/massivecore/store/Coll.java +++ b/src/com/massivecraft/massivecore/store/Coll.java @@ -26,6 +26,8 @@ import com.massivecraft.massivecore.comparator.ComparatorNaturalOrder; import com.massivecraft.massivecore.mixin.MixinModification; import com.massivecraft.massivecore.predicate.Predicate; import com.massivecraft.massivecore.predicate.PredicateEqualsIgnoreCase; +import com.massivecraft.massivecore.store.migration.VersionMigrationUtil; +import com.massivecraft.massivecore.util.ReflectionUtil; import com.massivecraft.massivecore.util.Txt; import com.massivecraft.massivecore.xlib.gson.Gson; import com.massivecraft.massivecore.xlib.gson.JsonElement; @@ -203,6 +205,10 @@ public class Coll> extends CollAbstract return entity.isDefault(); } + // What entity version do we want? + protected final int entityTargetVersion; + @Override public int getEntityTargetVersion() { return this.entityTargetVersion; } + // -------------------------------------------- // // COPY AND CREATE // -------------------------------------------- // @@ -508,7 +514,17 @@ public class Coll> extends CollAbstract if ( ! this.remoteEntryIsOk(id, remoteEntry)) return; JsonObject raw = remoteEntry.getKey(); Long mtime = remoteEntry.getValue(); - + + int version = VersionMigrationUtil.getVersion(raw); + if (version > this.getEntityTargetVersion()) + { + logLoadError(id, String.format("Cannot load entity of entity version %d", version)); + return; + } + + // Migrate if another version is wanted + boolean migrated = VersionMigrationUtil.migrate(this.getEntityClass(), raw, this.getEntityTargetVersion()); + // Calculate temp but handle raw cases. E temp; @@ -547,6 +563,9 @@ public class Coll> extends CollAbstract entity.setLastRaw(raw); entity.setLastMtime(mtime); entity.setLastDefault(false); + + // Now the loading is done. If it was migrated we will have to save it to remote again. + if (migrated) this.putIdentifiedModificationFixed(id, Modification.LOCAL_ALTER); } public boolean remoteEntryIsOk(String id, Entry remoteEntry) @@ -947,7 +966,7 @@ public class Coll> extends CollAbstract // -------------------------------------------- // // CONSTRUCT // -------------------------------------------- // - + public Coll(String id, Class entityClass, Db db, MassivePlugin plugin) { // Plugin @@ -980,6 +999,18 @@ public class Coll> extends CollAbstract // Collections this.id2entity = new ConcurrentHashMap(); this.identifiedModifications = new ConcurrentHashMap(); + + // Migration + int version = 0; + try + { + version = ReflectionUtil.getField(this.getEntityClass(), VersionMigrationUtil.VERSION_FIELD_NAME, this.createNewInstance()); + } + catch (Exception ex) + { + // The field was not there + } + this.entityTargetVersion = version; // Tasks this.tickTask = new Runnable() @@ -1025,7 +1056,7 @@ public class Coll> extends CollAbstract if (ret == null) throw new RuntimeException("plugin could not be calculated"); return ret; } - + @SuppressWarnings("unchecked") public Class calculateEntityClass() { @@ -1034,7 +1065,7 @@ public class Coll> extends CollAbstract Type[] typeArguments = superType.getActualTypeArguments(); return (Class) typeArguments[0]; } - + public String calculateId() { return this.getPlugin().getDescription().getName().toLowerCase() + "_" + this.getEntityClass().getSimpleName().toLowerCase(); @@ -1064,6 +1095,8 @@ public class Coll> extends CollAbstract // TODO: Clean up this stuff below. It branches too late. if (active) { + VersionMigrationUtil.validateMigratorsPresent(entityClass, 0, this.getEntityTargetVersion()); + if (this.supportsPusher()) { this.getPusher().init(); diff --git a/src/com/massivecraft/massivecore/store/CollInterface.java b/src/com/massivecraft/massivecore/store/CollInterface.java index 5534f896..691b4d5c 100644 --- a/src/com/massivecraft/massivecore/store/CollInterface.java +++ b/src/com/massivecraft/massivecore/store/CollInterface.java @@ -97,6 +97,8 @@ public interface CollInterface> extends Named, Active, Ident // A default entity will not be saved. // This is often used together with creative collections to save disc space. public boolean isDefault(E entity); + + public int getEntityTargetVersion(); // -------------------------------------------- // // COPY AND CREATE diff --git a/src/com/massivecraft/massivecore/store/Entity.java b/src/com/massivecraft/massivecore/store/Entity.java index 133f02ec..7ebd6f55 100644 --- a/src/com/massivecraft/massivecore/store/Entity.java +++ b/src/com/massivecraft/massivecore/store/Entity.java @@ -56,7 +56,7 @@ public class Entity> implements Identified this.lastMtime = 0; this.lastDefault = false; } - + // -------------------------------------------- // // ATTACH AND DETACH // -------------------------------------------- // diff --git a/src/com/massivecraft/massivecore/store/migration/VersionMigrationUtil.java b/src/com/massivecraft/massivecore/store/migration/VersionMigrationUtil.java new file mode 100644 index 00000000..39d487ef --- /dev/null +++ b/src/com/massivecraft/massivecore/store/migration/VersionMigrationUtil.java @@ -0,0 +1,155 @@ +package com.massivecraft.massivecore.store.migration; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.massivecraft.massivecore.collections.MassiveList; +import com.massivecraft.massivecore.collections.MassiveMap; +import com.massivecraft.massivecore.store.Entity; +import com.massivecraft.massivecore.util.Txt; +import com.massivecraft.massivecore.xlib.gson.JsonElement; +import com.massivecraft.massivecore.xlib.gson.JsonObject; + +public class VersionMigrationUtil +{ + // -------------------------------------------- // + // CONSTANTS + // -------------------------------------------- // + + public static final String VERSION_FIELD_NAME = "version"; + + // -------------------------------------------- // + // REGISTRY + // -------------------------------------------- // + + private static Map>, Map> migrators = new HashMap<>(); + + public static boolean isActive(VersionMigratorRoot migrator) + { + return getMigratorMap(migrator).get(migrator.getVersion()) == migrator; + } + + // ADD + public static void addMigrator(VersionMigratorRoot migrator) + { + VersionMigratorRoot old = getMigratorMap(migrator).put(migrator.getVersion(), migrator); + + // If there was an old one and it wasn't this one already: deactivate. + if (old != null && old != migrator) old.setActive(false); + } + + // REMOVE + public static void removeMigrator(VersionMigratorRoot migrator) + { + VersionMigratorRoot current = getMigratorMap(migrator).get(migrator.getVersion()); + + // If there wasn't a new one already: remove + if (current == migrator) getMigratorMap(migrator).remove(migrator.getVersion()); + } + + // GET + public static VersionMigratorRoot getMigrator(Class> entityClass, int version) + { + Map migratorMap = getMigratorMap(entityClass); + + VersionMigratorRoot migrator = migratorMap.get(version); + if (migrator == null) + { + throw new RuntimeException(String.format("No VersionMigrator found for %s version %d", entityClass.getName(), version)); + } + + return migrator; + } + + // GET MAP + private static Map getMigratorMap(VersionMigratorRoot migrator) + { + return getMigratorMap(migrator.getEntityClass()); + } + + private static Map getMigratorMap(Class> entityClass) + { + Map ret = migrators.get(entityClass); + if (ret == null) + { + ret = new MassiveMap<>(); + migrators.put(entityClass, ret); + } + return ret; + } + + // -------------------------------------------- // + // MIGRATE + // -------------------------------------------- // + + public static boolean migrate(Class> entityClass, JsonObject entity, int targetVersion) + { + if (entityClass == null) throw new NullPointerException("entityClass"); + if (entity == null) throw new NullPointerException("entity"); + + int entityVersion = getVersion(entity); + if (entityVersion == targetVersion) return false; + + validateMigratorsPresent(entityClass, entityVersion, targetVersion); + + for (; entityVersion < targetVersion; entityVersion++) + { + // When upgrading we need the migrator we are updating to. + // When downgrading we need the migrator we are downgrading from. + // This is done to preserve the same logic within the same class. + // That is why when updating we don't use entityVersion and when downgrading we do. + VersionMigrator migrator = getMigrator(entityClass, entityVersion+1); + migrator.migrate(entity); + } + + return true; + } + + // -------------------------------------------- // + // MISSING MIGRATORS + // -------------------------------------------- // + + public static void validateMigratorsPresent(Class> entityClass, int from, int to) + { + List missingMigrators = VersionMigrationUtil.getMissingMigratorVersions(entityClass, from, to); + if (missingMigrators.isEmpty()) return; + + String versions = Txt.implodeCommaAndDot(missingMigrators); + String name = entityClass.getName(); + throw new IllegalStateException(String.format("Could not find migrators for %s for versions: %s", name, versions)); + } + + public static List getMissingMigratorVersions(Class> entityClass, int from, int to) + { + if (from == to) return Collections.emptyList(); + if (from > to) throw new IllegalArgumentException(String.format("from: %d to: %d", from, to)); + Map migrators = getMigratorMap(entityClass); + + // We need not the from but we need the to. + from++; + to++; + + List ret = new MassiveList<>(); + for (int i = from; i < to; i++) + { + if (migrators.containsKey(i)) continue; + ret.add(i); + } + return ret; + } + + // -------------------------------------------- // + // UTIL + // -------------------------------------------- // + + public static int getVersion(JsonObject entity) + { + if (entity == null) throw new NullPointerException("entity"); + JsonElement element = entity.get(VERSION_FIELD_NAME); + if (element == null) return 0; + return element.getAsInt(); + } + +} diff --git a/src/com/massivecraft/massivecore/store/migration/VersionMigrator.java b/src/com/massivecraft/massivecore/store/migration/VersionMigrator.java new file mode 100644 index 00000000..a5a2d155 --- /dev/null +++ b/src/com/massivecraft/massivecore/store/migration/VersionMigrator.java @@ -0,0 +1,13 @@ +package com.massivecraft.massivecore.store.migration; + +import com.massivecraft.massivecore.xlib.gson.JsonObject; + +public interface VersionMigrator +{ + // -------------------------------------------- // + // MIGRATION + // -------------------------------------------- // + + public void migrate(JsonObject entity); + +} diff --git a/src/com/massivecraft/massivecore/store/migration/VersionMigratorField.java b/src/com/massivecraft/massivecore/store/migration/VersionMigratorField.java new file mode 100644 index 00000000..402fce16 --- /dev/null +++ b/src/com/massivecraft/massivecore/store/migration/VersionMigratorField.java @@ -0,0 +1,54 @@ +package com.massivecraft.massivecore.store.migration; + +import com.massivecraft.massivecore.xlib.gson.JsonElement; +import com.massivecraft.massivecore.xlib.gson.JsonNull; +import com.massivecraft.massivecore.xlib.gson.JsonObject; +import com.massivecraft.massivecore.xlib.gson.JsonPrimitive; + +public abstract class VersionMigratorField implements VersionMigrator +{ + // -------------------------------------------- // + // FIELDS + // -------------------------------------------- // + + private final String fieldName; + public String getFieldName() { return fieldName; } + + // -------------------------------------------- // + // CONSTRUCT + // -------------------------------------------- // + + public VersionMigratorField(String fieldName) + { + if (fieldName == null) throw new NullPointerException("fieldName"); + this.fieldName = fieldName; + } + + // -------------------------------------------- // + // OVERRIDE + // -------------------------------------------- // + + @Override + public void migrate(JsonObject entity) + { + JsonElement value = entity.get(this.getFieldName()); + if (value == null) value = JsonNull.INSTANCE; + value = getElement(migrateInner(value)); + entity.add(this.getFieldName(), value); + } + + private static JsonElement getElement(Object object) + { + if (object instanceof JsonElement) return (JsonElement) object; + else if (object instanceof String) return new JsonPrimitive((String) object); + else if (object instanceof Boolean) return new JsonPrimitive((Boolean) object); + else if (object instanceof Character) return new JsonPrimitive((Character) object); + else if (object instanceof Number) return new JsonPrimitive((Number) object); + + throw new IllegalArgumentException("Unvalid JsonElement: " + object); + } + + // Must return JsonElement or a primitive + public abstract Object migrateInner(JsonElement element); + +} diff --git a/src/com/massivecraft/massivecore/store/migration/VersionMigratorRename.java b/src/com/massivecraft/massivecore/store/migration/VersionMigratorRename.java new file mode 100644 index 00000000..4c66e7e5 --- /dev/null +++ b/src/com/massivecraft/massivecore/store/migration/VersionMigratorRename.java @@ -0,0 +1,46 @@ +package com.massivecraft.massivecore.store.migration; + +import com.massivecraft.massivecore.xlib.gson.JsonElement; +import com.massivecraft.massivecore.xlib.gson.JsonObject; + +public class VersionMigratorRename implements VersionMigrator +{ + // -------------------------------------------- // + // FIELDS + // -------------------------------------------- // + + private final String from; + public String getFrom() { return from; } + + private final String to; + public String getTo() { return to; } + + // -------------------------------------------- // + // CONSTRUCT + // -------------------------------------------- // + + public static VersionMigratorRename get(String from, String to) { return new VersionMigratorRename(from, to); } + public VersionMigratorRename(String from, String to) + { + if (from == null) throw new NullPointerException("from"); + if (to == null) throw new NullPointerException("to"); + + this.from = from; + this.to = to; + } + + // -------------------------------------------- // + // OVERRIDE + // -------------------------------------------- // + + @Override + public void migrate(JsonObject entity) + { + String from = this.getFrom(); + String to = this.getTo(); + + JsonElement element = entity.remove(from); + if (element != null) entity.add(to, element); + } + +} diff --git a/src/com/massivecraft/massivecore/store/migration/VersionMigratorRoot.java b/src/com/massivecraft/massivecore/store/migration/VersionMigratorRoot.java new file mode 100644 index 00000000..64a21469 --- /dev/null +++ b/src/com/massivecraft/massivecore/store/migration/VersionMigratorRoot.java @@ -0,0 +1,139 @@ +package com.massivecraft.massivecore.store.migration; + +import java.util.List; +import java.util.regex.Matcher; + +import com.massivecraft.massivecore.Active; +import com.massivecraft.massivecore.MassivePlugin; +import com.massivecraft.massivecore.collections.MassiveList; +import com.massivecraft.massivecore.store.Entity; +import com.massivecraft.massivecore.util.Txt; +import com.massivecraft.massivecore.xlib.gson.JsonObject; + +public class VersionMigratorRoot implements VersionMigrator, Active +{ + // -------------------------------------------- // + // FIELDS + // -------------------------------------------- // + + private List innerMigrators = new MassiveList<>(); + public List getInnerMigrators() { return innerMigrators; } + public void setInnerMigrators(List innerMigrators) { this.innerMigrators = new MassiveList<>(innerMigrators); } + public void addInnerMigrator(VersionMigrator innerMigrator) + { + this.innerMigrators.add(innerMigrator); + } + + private final Class> entityClass; + public Class> getEntityClass() { return entityClass; } + + // -------------------------------------------- // + // CONSTRUCT + // -------------------------------------------- // + + public VersionMigratorRoot(Class> entityClass) + { + this.entityClass = entityClass; + } + + // -------------------------------------------- // + // OVERRIDE: ACTIVE + // -------------------------------------------- // + + // Boolean + @Override + public boolean isActive() + { + return VersionMigrationUtil.isActive(this); + } + + @Override + public void setActive(boolean active) + { + if (active) + { + VersionMigrationUtil.addMigrator(this); + } + else + { + VersionMigrationUtil.removeMigrator(this); + } + } + + // Plugin + private MassivePlugin activePlugin = null; + + @Override + public MassivePlugin setActivePlugin(MassivePlugin activePlugin) + { + MassivePlugin ret = this.activePlugin; + this.activePlugin = activePlugin; + return ret; + } + + @Override + public MassivePlugin getActivePlugin() + { + return this.activePlugin; + } + + @Override + public void setActive(MassivePlugin plugin) + { + this.setActivePlugin(plugin); + this.setActive(plugin != null); + } + + // -------------------------------------------- // + // VERSION + // -------------------------------------------- // + + public int getVersion() + { + String name = this.getClass().getSimpleName(); + if (!name.startsWith("V")) throw new UnsupportedOperationException(String.format("Name of %s doesn't start with \"V\".", name)); + if (!Character.isDigit(name.charAt(1))) throw new IllegalStateException(String.format("Second character of %s isn't a digit", name)); + + Matcher matcher = Txt.PATTERN_NUMBER.matcher(name); + if (!matcher.find()) throw new UnsupportedOperationException(String.format("%s doesn't have a version number.", name)); + String number = matcher.group(); + + return Integer.parseInt(number); + } + + // -------------------------------------------- // + // MIGRATE + // -------------------------------------------- // + + @Override + public void migrate(JsonObject entity) + { + if (entity == null) throw new NullPointerException("entity"); + + // Get entity version and the expected entity version ... + int entityVersion = VersionMigrationUtil.getVersion(entity); + int expectedEntityVersion = this.getVersion() - 1; + + // ... make sure they match ... + if (entityVersion != expectedEntityVersion) throw new IllegalArgumentException(String.format("Entiy version: %d Expected: %d", entityVersion, expectedEntityVersion)); + + // ... do the migration. + this.migrateInner(entity); + this.migrateVersion(entity); + } + + private void migrateVersion(JsonObject entity) + { + entity.addProperty(VersionMigrationUtil.VERSION_FIELD_NAME, this.getVersion()); + } + + public void migrateInner(JsonObject entity) + { + // Look over all inner migrators. + for (VersionMigrator migrator : this.getInnerMigrators()) + { + migrator.migrate(entity); + } + } + +} diff --git a/src/com/massivecraft/massivecore/util/ReflectionUtil.java b/src/com/massivecraft/massivecore/util/ReflectionUtil.java index b813e99d..4ff50c78 100644 --- a/src/com/massivecraft/massivecore/util/ReflectionUtil.java +++ b/src/com/massivecraft/massivecore/util/ReflectionUtil.java @@ -3,13 +3,11 @@ package com.massivecraft.massivecore.util; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.GenericDeclaration; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; import java.util.ArrayList; import java.util.List; @@ -246,7 +244,7 @@ public class ReflectionUtil } return fallback; } - + // -------------------------------------------- // // ANNOTATION // -------------------------------------------- //