Add version migration system to the database

This commit is contained in:
Magnus Ulf Jørgensen 2017-03-06 16:45:29 +01:00
parent 5433468509
commit 9abccd50b0
9 changed files with 448 additions and 8 deletions

View File

@ -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<E extends Entity<E>> extends CollAbstract<E>
return entity.isDefault();
}
// What entity version do we want?
protected final int entityTargetVersion;
@Override public int getEntityTargetVersion() { return this.entityTargetVersion; }
// -------------------------------------------- //
// COPY AND CREATE
// -------------------------------------------- //
@ -509,6 +515,16 @@ public class Coll<E extends Entity<E>> extends CollAbstract<E>
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<E extends Entity<E>> extends CollAbstract<E>
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<JsonObject, Long> remoteEntry)
@ -981,6 +1000,18 @@ public class Coll<E extends Entity<E>> extends CollAbstract<E>
this.id2entity = new ConcurrentHashMap<String, E>();
this.identifiedModifications = new ConcurrentHashMap<String, Modification>();
// 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()
{
@ -1064,6 +1095,8 @@ public class Coll<E extends Entity<E>> extends CollAbstract<E>
// TODO: Clean up this stuff below. It branches too late.
if (active)
{
VersionMigrationUtil.validateMigratorsPresent(entityClass, 0, this.getEntityTargetVersion());
if (this.supportsPusher())
{
this.getPusher().init();

View File

@ -98,6 +98,8 @@ public interface CollInterface<E extends Entity<E>> extends Named, Active, Ident
// This is often used together with creative collections to save disc space.
public boolean isDefault(E entity);
public int getEntityTargetVersion();
// -------------------------------------------- //
// COPY AND CREATE
// -------------------------------------------- //

View File

@ -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<Class<? extends Entity<?>>, Map<Integer, VersionMigratorRoot>> 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<? extends Entity<?>> entityClass, int version)
{
Map<Integer, VersionMigratorRoot> 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<Integer, VersionMigratorRoot> getMigratorMap(VersionMigratorRoot migrator)
{
return getMigratorMap(migrator.getEntityClass());
}
private static Map<Integer, VersionMigratorRoot> getMigratorMap(Class<? extends Entity<?>> entityClass)
{
Map<Integer, VersionMigratorRoot> ret = migrators.get(entityClass);
if (ret == null)
{
ret = new MassiveMap<>();
migrators.put(entityClass, ret);
}
return ret;
}
// -------------------------------------------- //
// MIGRATE
// -------------------------------------------- //
public static boolean migrate(Class<? extends Entity<?>> 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<? extends Entity<?>> entityClass, int from, int to)
{
List<Integer> 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<Integer> getMissingMigratorVersions(Class<? extends Entity<?>> 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<Integer, VersionMigratorRoot> migrators = getMigratorMap(entityClass);
// We need not the from but we need the to.
from++;
to++;
List<Integer> 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();
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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<VersionMigrator> innerMigrators = new MassiveList<>();
public List<VersionMigrator> getInnerMigrators() { return innerMigrators; }
public void setInnerMigrators(List<VersionMigrator> innerMigrators) { this.innerMigrators = new MassiveList<>(innerMigrators); }
public void addInnerMigrator(VersionMigrator innerMigrator)
{
this.innerMigrators.add(innerMigrator);
}
private final Class<? extends Entity<?>> entityClass;
public Class<? extends Entity<?>> getEntityClass() { return entityClass; }
// -------------------------------------------- //
// CONSTRUCT
// -------------------------------------------- //
public VersionMigratorRoot(Class<? extends Entity<?>> 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);
}
}
}

View File

@ -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;