xapi.model.api.ModelModule Maven / Gradle / Ivy
Show all versions of xapi-dev Show documentation
/**
*
*/
package xapi.model.api;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.Map.Entry;
import java.util.Objects;
import xapi.collect.X_Collect;
import xapi.collect.api.IntTo;
import xapi.collect.api.StringTo;
import xapi.dev.source.CharBuffer;
import xapi.inject.X_Inject;
import xapi.model.X_Model;
import xapi.model.impl.ClusteringPrimitiveDeserializer;
import xapi.model.impl.ClusteringPrimitiveSerializer;
import xapi.source.api.CharIterator;
import xapi.source.impl.StringCharIterator;
import xapi.util.X_Debug;
import xapi.util.api.Digester;
/**
* This class represents a module of model types; it contains the {@link ModelManifest}s for all types within this module.
* In practice, this is used for servers to be able keep in sync with multiple clients; by having the client transmit
* the strong hash of the ModelModule, the server can load the correct serialization policy to be able to understand
* any number of clients.
*
* Whenever the server loads the ModelModule, it should save a copy into its datastore, so that, if updated,
* clients who present a previous strong hash can still function correctly. The server will generally be deployed
* with a module for each client compilation, which it can save while it is still on the classpath; then, should a
* new version be deployed, the policy on the classpath will be stale, but can be loaded from the datastore.
*
* Using this technique will allow the server to understand version-skewed clients; however, care must be taken
* that a breaking change across versions will not lead to server errors. Using generic .getAllProperties() methods
* will help avoid interface-based changes, but there will still be a problem when an incompatible change is made.
*
* To mitigate this, we will (in the future) first create the ability to invalidate serialization policies that
* are too old; this can be done with an annatation, @BreakingChange, which signals to the serialization policy
* builder that the given type cannot be safely used before the current version. This annotaiton will accept
* an optional hash key to state that "all versions at or before the specified key should be invalidated".
*
* Once we are able to safely prevent breaking changes from causing server errors (we can just send back an
* error message telling the client to update), then we will build a @MigrationStrategy, which will include
* instructions on how to transform a stale model into an acceptable format. This annotation will point to
* helper classes that exist which can transform a given model.
*
* Using a migration strategy will easily facilitate simple field renames or removals, but for more complex situations
* like adding a field that must be populated from external sources, the {@link ModelMigration} interface will take
* an extra context variable (like an HttpSession) to help the server fill in any data that is not within scope.
*
* When it is not possible to migrate a field, then a @BreakingChange should be used to force the client to update.
*
* @author James X. Nelson ([email protected], @james)
*
*/
public class ModelModule implements Serializable {
private static final long serialVersionUID = 977462783892289984L;
private final StringTo manifests;
private final IntTo strongNames;
private String uuid;
private String moduleName;
private transient String serialized;
public ModelModule() {
manifests = X_Collect.newStringMap(ModelManifest.class);
strongNames = X_Collect.newList(String.class);
}
public ModelModule addManifest(final ModelManifest manifest) {
manifests.put(manifest.getType(), manifest);
return this;
}
public ModelModule addStrongName(final String strongName) {
strongNames.add(strongName);
return this;
}
public ModelManifest getManifest(final String modelType) {
return manifests.get(modelType);
}
public String[] getStrongNames() {
return strongNames.toArray();
}
/**
* @return -> uuid
*/
public String getUuid() {
if (uuid == null) {
uuid = computeUuid(X_Inject.instance(PrimitiveSerializer.class));
}
return uuid;
}
protected String computeUuid(final PrimitiveSerializer primitives) {
// Compute the checksum of the policy itself. That checksum will become our UUID,
// which will then be appended before the policy itself.
final String result = calculateSerialization(primitives);
final Digester digest = X_Inject.instance(Digester.class);
byte[] asBytes;
try {
asBytes = result.getBytes("UTF-8");
} catch (final UnsupportedEncodingException e) {
throw X_Debug.rethrow(e);
}
final byte[] uuid = digest.digest(asBytes);
return digest.toString(uuid);
}
/**
* @param uuid -> set uuid
*/
public void setUuid(final String uuid) {
this.uuid = uuid;
}
public static String serialize(final ModelModule module) {
final CharBuffer buffer = new CharBuffer();
serialize(buffer, module, X_Inject.instance(PrimitiveSerializer.class));
return buffer.toString();
}
public static CharBuffer serialize(final CharBuffer into, final ModelModule module, final PrimitiveSerializer primitives) {
// Append the uuid, as a string (using a leading size for deserialization purposes)
into.append(primitives.serializeString(module.getUuid()));
into.append(primitives.serializeInt(module.strongNames.size()));
for (final String strongName : module.strongNames.forEach()) {
into.append(primitives.serializeString(strongName));
}
into.append(module.calculateSerialization(primitives));
return into;
}
protected String calculateSerialization(PrimitiveSerializer primitives) {
if (serialized != null) {
return serialized;
}
// We will build our policy in our own buffer, so we can safely use it to calculate our strong hash later
final CharBuffer policy = new CharBuffer();
policy.append(primitives.serializeString(getModuleName()));
primitives = new ClusteringPrimitiveSerializer(primitives, policy);
// Directly append the policy to the result (the string is not wrapped),
// however, we do append the size of the manifests so we know when to stop deserializing
policy.append(primitives.serializeInt(manifests.size()));
for (final ModelManifest manifest : manifests.values()) {
// TODO: collect up reused strings like classnames, and append them into a "classpool",
// to reduce the total size of the policy
ModelManifest.serialize(policy, manifest, primitives);
}
serialized = policy.toString();
return serialized;
}
public static ModelModule deserialize(final String chars) {
final StringCharIterator iter = new StringCharIterator(chars);
return deserialize(iter, X_Inject.instance(PrimitiveSerializer.class));
}
public static ModelModule deserialize(final CharIterator chars, PrimitiveSerializer primitives) {
final ModelModule module = new ModelModule();
module.setUuid(primitives.deserializeString(chars));
int numStrongNames = primitives.deserializeInt(chars);
while (numStrongNames --> 0) {
module.strongNames.add(primitives.deserializeString(chars));
}
module.setModuleName(primitives.deserializeString(chars));
primitives = new ClusteringPrimitiveDeserializer(primitives, chars) {
@Override
@SuppressWarnings("unchecked")
public Class deserializeClass(final CharIterator c) {
final Class cls = super.deserializeClass(c);
if (Model.class.isAssignableFrom(cls)) {
X_Model.getService().register(Class.class.cast(cls));
} else if (cls.isArray()) {
Class component = cls.getComponentType();
while (component.isArray()) {
component = cls.getComponentType();
}
if (Model.class.isAssignableFrom(component)) {
X_Model.getService().register(Class.class.cast(component));
}
}
return cls;
}
};
int manifests = primitives.deserializeInt(chars);
while (manifests --> 0) {
final ModelManifest manifest = ModelManifest.deserialize(chars, primitives);
module.manifests.put(manifest.getType(), manifest);
}
return module;
}
@Override
public int hashCode() {
return getUuid().hashCode();
}
/**
* This implementation is EXTREMELY INEFFICIENT, and should only be used for unit tests;
* this object should not be treated as a map key, but if you absolutely must, you should
* consider using a mapping structure that allows you to provide a more efficient equality check.
*/
@Override
public boolean equals(final Object obj) {
if (obj instanceof ModelModule) {
final ModelModule other = (ModelModule) obj;
if (manifests.size() != other.manifests.size()) {
return false;
}
if (!Objects.equals(moduleName, other.moduleName)) {
return false;
}
if (!Objects.equals(getUuid(), other.getUuid())) {
return false;
}
for (final Entry entry : manifests.entries()) {
final ModelManifest compare = other.manifests.get(entry.getKey());
if (!Objects.equals(compare, entry.getValue())) {
return false;
}
}
return true;
}
return false;
}
/**
* @return -> moduleName
*/
public String getModuleName() {
return moduleName;
}
/**
* @param moduleName -> set moduleName
*/
public void setModuleName(final String moduleName) {
this.moduleName = moduleName;
}
}