
org.jsimpledb.OnChangeScanner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jsimpledb-main Show documentation
Show all versions of jsimpledb-main Show documentation
JSimpleDB classes that map Java model classes onto the core API.
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package org.jsimpledb;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.reflect.TypeToken;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import org.jsimpledb.annotation.OnChange;
import org.jsimpledb.change.FieldChange;
import org.jsimpledb.change.ListFieldAdd;
import org.jsimpledb.change.ListFieldClear;
import org.jsimpledb.change.ListFieldRemove;
import org.jsimpledb.change.ListFieldReplace;
import org.jsimpledb.change.MapFieldAdd;
import org.jsimpledb.change.MapFieldClear;
import org.jsimpledb.change.MapFieldRemove;
import org.jsimpledb.change.MapFieldReplace;
import org.jsimpledb.change.SetFieldAdd;
import org.jsimpledb.change.SetFieldClear;
import org.jsimpledb.change.SetFieldRemove;
import org.jsimpledb.change.SimpleFieldChange;
import org.jsimpledb.core.ListField;
import org.jsimpledb.core.MapField;
import org.jsimpledb.core.ObjId;
import org.jsimpledb.core.SetField;
import org.jsimpledb.core.SimpleField;
import org.jsimpledb.core.Transaction;
/**
* Scans for {@link OnChange @OnChange} annotations.
*/
class OnChangeScanner extends AnnotationScanner {
OnChangeScanner(JClass jclass) {
super(jclass, OnChange.class);
}
@Override
protected boolean includeMethod(Method method, OnChange annotation) {
this.checkReturnType(method, void.class);
if (this.getParameterTypeTokens(method).size() > 1)
throw new IllegalArgumentException(this.getErrorPrefix(method) + "method is required to take zero or one parameter");
return true; // we do further parameter type check in ChangeMethodInfo
}
@Override
protected ChangeMethodInfo createMethodInfo(Method method, OnChange annotation) {
return new ChangeMethodInfo(method, annotation);
}
// ChangeMethodInfo
class ChangeMethodInfo extends MethodInfo {
final boolean isStatic;
final HashMap> paths;
@SuppressWarnings("unchecked")
ChangeMethodInfo(Method method, OnChange annotation) {
super(method, annotation);
// Get start type
this.isStatic = (method.getModifiers() & Modifier.STATIC) != 0;
Class> startType = method.getDeclaringClass();
if (annotation.startType() != void.class) {
if (!this.isStatic) {
throw new IllegalArgumentException(OnChangeScanner.this.getErrorPrefix(method)
+ "startType() may only be used for annotations on static methods");
}
if (annotation.startType().isPrimitive() || annotation.startType().isArray()) {
throw new IllegalArgumentException(OnChangeScanner.this.getErrorPrefix(method)
+ "invalid startType() " + annotation.startType());
}
startType = annotation.startType();
}
// Replace empty reference path list with "all fields in this object"
List stringPaths = Arrays.asList(annotation.value());
final boolean wildcard = stringPaths.isEmpty();
if (wildcard) {
stringPaths = Lists.transform(Lists.newArrayList(OnChangeScanner.this.jclass.jfields.values()),
new Function() {
@Override
public String apply(JField jfield) {
return jfield.name + "#" + jfield.storageId;
}
});
}
// Get method parameter type (generic and raw), if any
final TypeToken> genericParameterType;
final Class extends FieldChange> rawParameterType;
switch (method.getParameterTypes().length) {
case 1:
rawParameterType = (Class extends FieldChange>)method.getParameterTypes()[0];
genericParameterType = OnChangeScanner.this.getParameterTypeTokens(method).get(0);
break;
case 0:
rawParameterType = null;
genericParameterType = null;
break;
default:
throw new RuntimeException("internal error");
}
// Parse reference paths
boolean anyFieldsFound = false;
this.paths = new HashMap>(stringPaths.size());
for (int i = 0; i < stringPaths.size(); i++) {
final String stringPath = stringPaths.get(i);
// Parse reference path
final ReferencePath path;
try {
path = OnChangeScanner.this.jclass.jdb.parseReferencePath(startType, stringPath, false);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(OnChangeScanner.this.getErrorPrefix(method) + e.getMessage(), e);
}
// Validate the parameter type against possible changes
if (rawParameterType != null) {
// Get all (concrete) change types emitted by the target field
final ArrayList> changeParameterTypes = new ArrayList>();
try {
path.targetFieldInfo.addChangeParameterTypes(changeParameterTypes, path.targetType);
} catch (UnsupportedOperationException e) {
if (wildcard)
continue;
throw new IllegalArgumentException(OnChangeScanner.this.getErrorPrefix(method) + "path `" + stringPath
+ "' is invalid because change notifications are not supported for " + path.targetFieldInfo, e);
}
anyFieldsFound = true;
// Check whether method parameter type accepts as least one of them; must do so consistently raw vs. generic
boolean anyChangeMatch = false;
for (TypeToken> possibleChangeType : changeParameterTypes) {
final boolean matchesGeneric = genericParameterType.isAssignableFrom(possibleChangeType);
final boolean matchesRaw = rawParameterType.isAssignableFrom(possibleChangeType.getRawType());
if (matchesGeneric != matchesRaw) {
throw new IllegalArgumentException(OnChangeScanner.this.getErrorPrefix(method)
+ "method parameter type " + genericParameterType + " will match changes emitted from `"
+ stringPath + "' at runtime" + " due to type erasure, but has incompatible generic type "
+ genericParameterType + "; parameter type should be compatible with "
+ (changeParameterTypes.size() != 1 ?
"one of: " + changeParameterTypes : changeParameterTypes.get(0)));
}
if (matchesGeneric) {
anyChangeMatch = true;
break;
}
}
// If not wildcard match, then at least one change type must match method
if (!anyChangeMatch) {
if (wildcard)
continue;
throw new IllegalArgumentException(OnChangeScanner.this.getErrorPrefix(method) + "path `" + stringPath
+ "' is invalid because no changes emitted by " + path.targetFieldInfo + " match the method's"
+ " parameter type " + genericParameterType + "; the emitted change type is "
+ (changeParameterTypes.size() != 1 ? "one of: " + changeParameterTypes : changeParameterTypes.get(0)));
}
}
// Determine storage ID's corresponding to matching target types; this filters out obsolete types from old versions
final HashSet storageIds = new HashSet();
for (JClass> jclass : OnChangeScanner.this.jclass.jdb.getJClasses(path.targetType))
storageIds.add(jclass.storageId);
// Match
this.paths.put(path, storageIds);
}
// No matching fields?
if (this.paths.isEmpty()) { // must be wildcard
if (!anyFieldsFound) {
throw new IllegalArgumentException(OnChangeScanner.this.getErrorPrefix(method)
+ "there are no fields that will generate change events");
}
throw new IllegalArgumentException(OnChangeScanner.this.getErrorPrefix(method) + "no changes emitted by any field"
+ " will match the method's parameter type " + genericParameterType);
}
}
// Register listeners for this method
void registerChangeListener(JTransaction jtx) {
final ChangeMethodListener listener = new ChangeMethodListener(jtx, this.getMethod());
for (Map.Entry> entry : this.paths.entrySet()) {
final ReferencePath path = entry.getKey();
final HashSet objectTypeStorageIds = entry.getValue();
path.targetFieldInfo.registerChangeListener(jtx.tx, path.getReferenceFields(), objectTypeStorageIds, listener);
}
}
}
// ChangeMethodListener
static class ChangeMethodListener implements AllChangesListener {
private final JTransaction jtx;
private final Method method;
private final Class>[] genericTypes;
ChangeMethodListener(JTransaction jtx, Method method) {
Preconditions.checkArgument(jtx != null, "null jtx");
Preconditions.checkArgument(method != null, "null method");
this.jtx = jtx;
this.method = method;
// Extract generic types from method's FieldChange> parameter
switch (this.method.getParameterTypes().length) {
case 1:
final ArrayList> genericTypeList = new ArrayList<>(3);
for (Type type : ((ParameterizedType)this.method.getGenericParameterTypes()[0]).getActualTypeArguments())
genericTypeList.add(TypeToken.of(type).getRawType());
this.genericTypes = genericTypeList.toArray(new Class>[genericTypeList.size()]);
break;
case 0:
this.genericTypes = null;
break;
default:
throw new RuntimeException("internal error");
}
}
// SimpleFieldChangeListener
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void onSimpleFieldChange(Transaction tx, ObjId id,
SimpleField field, int[] path, NavigableSet referrers, T oldValue, T newValue) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final Object joldValue = this.jtx.convertCoreValue(field, oldValue);
final Object jnewValue = this.jtx.convertCoreValue(field, newValue);
final JObject jobj = this.checkTypes(SimpleFieldChange.class, id, joldValue, jnewValue);
if (jobj == null)
return;
this.invoke(referrers, new SimpleFieldChange(jobj, field.getStorageId(), field.getName(), joldValue, jnewValue));
}
// SetFieldChangeListener
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void onSetFieldAdd(Transaction tx, ObjId id,
SetField field, int[] path, NavigableSet referrers, E value) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final Object jvalue = this.jtx.convertCoreValue(field.getElementField(), value);
final JObject jobj = this.checkTypes(SetFieldAdd.class, id, jvalue);
if (jobj == null)
return;
this.invoke(referrers, new SetFieldAdd(jobj, field.getStorageId(), field.getName(), jvalue));
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void onSetFieldRemove(Transaction tx, ObjId id,
SetField field, int[] path, NavigableSet referrers, E value) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final Object jvalue = this.jtx.convertCoreValue(field.getElementField(), value);
final JObject jobj = this.checkTypes(SetFieldRemove.class, id, jvalue);
if (jobj == null)
return;
this.invoke(referrers, new SetFieldRemove(jobj, field.getStorageId(), field.getName(), jvalue));
}
@Override
public void onSetFieldClear(Transaction tx, ObjId id, SetField> field, int[] path, NavigableSet referrers) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final JObject jobj = this.checkTypes(SetFieldClear.class, id);
if (jobj == null)
return;
this.invoke(referrers, new SetFieldClear(jobj, field.getStorageId(), field.getName()));
}
// ListFieldChangeListener
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void onListFieldAdd(Transaction tx, ObjId id,
ListField field, int[] path, NavigableSet referrers, int index, E value) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final Object jvalue = this.jtx.convertCoreValue(field.getElementField(), value);
final JObject jobj = this.checkTypes(ListFieldAdd.class, id, jvalue);
if (jobj == null)
return;
this.invoke(referrers, new ListFieldAdd(jobj, field.getStorageId(), field.getName(), index, jvalue));
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void onListFieldRemove(Transaction tx, ObjId id,
ListField field, int[] path, NavigableSet referrers, int index, E value) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final Object jvalue = this.jtx.convertCoreValue(field.getElementField(), value);
final JObject jobj = this.checkTypes(ListFieldRemove.class, id, jvalue);
if (jobj == null)
return;
this.invoke(referrers, new ListFieldRemove(jobj, field.getStorageId(), field.getName(), index, jvalue));
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void onListFieldReplace(Transaction tx, ObjId id,
ListField field, int[] path, NavigableSet referrers, int index, E oldValue, E newValue) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final Object joldValue = this.jtx.convertCoreValue(field.getElementField(), oldValue);
final Object jnewValue = this.jtx.convertCoreValue(field.getElementField(), newValue);
final JObject jobj = this.checkTypes(ListFieldReplace.class, id, joldValue, jnewValue);
if (jobj == null)
return;
this.invoke(referrers, new ListFieldReplace(jobj, field.getStorageId(), field.getName(), index, joldValue, jnewValue));
}
@Override
public void onListFieldClear(Transaction tx, ObjId id, ListField> field, int[] path, NavigableSet referrers) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final JObject jobj = this.checkTypes(ListFieldClear.class, id);
if (jobj == null)
return;
this.invoke(referrers, new ListFieldClear(jobj, field.getStorageId(), field.getName()));
}
// MapFieldChangeListener
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void onMapFieldAdd(Transaction tx, ObjId id,
MapField field, int[] path, NavigableSet referrers, K key, V value) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final Object jkey = this.jtx.convertCoreValue(field.getKeyField(), key);
final Object jvalue = this.jtx.convertCoreValue(field.getValueField(), value);
final JObject jobj = this.checkTypes(MapFieldAdd.class, id, jkey, jvalue);
if (jobj == null)
return;
this.invoke(referrers, new MapFieldAdd(jobj, field.getStorageId(), field.getName(), jkey, jvalue));
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void onMapFieldRemove(Transaction tx, ObjId id,
MapField field, int[] path, NavigableSet referrers, K key, V value) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final Object jkey = this.jtx.convertCoreValue(field.getKeyField(), key);
final Object jvalue = this.jtx.convertCoreValue(field.getValueField(), value);
final JObject jobj = this.checkTypes(MapFieldRemove.class, id, jkey, jvalue);
if (jobj == null)
return;
this.invoke(referrers, new MapFieldRemove(jobj, field.getStorageId(), field.getName(), jkey, jvalue));
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void onMapFieldReplace(Transaction tx, ObjId id,
MapField field, int[] path, NavigableSet referrers, K key, V oldValue, V newValue) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final Object jkey = this.jtx.convertCoreValue(field.getKeyField(), key);
final Object joldValue = this.jtx.convertCoreValue(field.getValueField(), oldValue);
final Object jnewValue = this.jtx.convertCoreValue(field.getValueField(), newValue);
final JObject jobj = this.checkTypes(MapFieldReplace.class, id, jkey, joldValue, jnewValue);
if (jobj == null)
return;
this.invoke(referrers, new MapFieldReplace(jobj, field.getStorageId(), field.getName(), jkey, joldValue, jnewValue));
}
@Override
public void onMapFieldClear(Transaction tx, ObjId id, MapField, ?> field, int[] path, NavigableSet referrers) {
if (this.genericTypes == null) {
this.invoke(referrers);
return;
}
final JObject jobj = this.checkTypes(MapFieldClear.class, id);
if (jobj == null)
return;
this.invoke(referrers, new MapFieldClear(jobj, field.getStorageId(), field.getName()));
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (obj == null || obj.getClass() != this.getClass())
return false;
final ChangeMethodListener that = (ChangeMethodListener)obj;
return this.method.equals(that.method) && this.jtx.equals(that.jtx);
}
@Override
public int hashCode() {
return this.method.hashCode() ^ this.jtx.hashCode();
}
// Internal methods
private JObject checkTypes(Class /*extends FieldChange>*/> changeType, ObjId id, Object... values) {
// Check method parameter type
if (!method.getParameterTypes()[0].isAssignableFrom(changeType))
return null;
// Check first generic type parameter which is the JObject corresponding to id
final JObject jobj = this.jtx.get(id);
if (!this.genericTypes[0].isInstance(jobj))
return null;
// Check other generic type parameter(s)
for (int i = 1; i < this.genericTypes.length; i++) {
final Object value = values[Math.min(i, values.length) - 1];
if (value != null && !this.genericTypes[i].isInstance(value))
return null;
}
// OK types agree
return jobj;
}
// Used when @OnChange method takes zero parameters
private void invoke(NavigableSet referrers) {
if ((this.method.getModifiers() & Modifier.STATIC) != 0)
Util.invoke(this.method, null);
else {
for (ObjId id : referrers) {
final JObject target = this.jtx.get(id); // type of 'id' should always be found
// Avoid invoking subclass's @OnChange method on superclass instance;
// this can happen when the field is in superclass but wildcard @OnChange is in the subclass
if (this.method.getDeclaringClass().isInstance(target))
Util.invoke(this.method, target);
}
}
}
// Used when @OnChange method takes one parameter
private void invoke(NavigableSet referrers, FieldChange change) {
assert change != null;
if ((this.method.getModifiers() & Modifier.STATIC) != 0)
Util.invoke(this.method, null, change);
else {
for (ObjId id : referrers) {
final JObject target = this.jtx.get(id); // type of 'id' should always be found
// Avoid invoking subclass's @OnChange method on superclass instance;
// this can happen when the field is in superclass but wildcard @OnChange is in the subclass
if (this.method.getDeclaringClass().isInstance(target))
Util.invoke(this.method, target, change);
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy