org.jsimpledb.ReferencePath Maven / Gradle / Ivy
Show all versions of jsimpledb-main Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package org.jsimpledb;
import com.google.common.base.Preconditions;
import com.google.common.primitives.Ints;
import com.google.common.reflect.TypeToken;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.jsimpledb.core.ObjId;
import org.jsimpledb.kv.KeyRange;
import org.jsimpledb.kv.KeyRanges;
import org.jsimpledb.schema.SchemaObjectType;
import org.jsimpledb.util.ParseContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Reference paths.
*
*
* A reference path is a {@link String} specifying a path from some implicit starting object type, through zero
* or more reference fields, ending up at some target object type(s).
* In other words, given some starting object(s), a reference path takes you through a path of references to the target object(s).
* Note that the number of target objects can be vastly different than the number of starting objects, depending on
* the fan-in/fan-out of the references traversed.
*
*
Specifying Fields
*
*
* The starting object type is always implicit from the context in which the path is used. The {@link String} path describes
* the reference fields, i.e., the "steps" in the path that travel from the starting object type to the target object type.
* The steps are separated by dot ({@code "."}) characters.
*
*
* The reference fields in the path may be traversed in either the forward or inverse direction:
* to traverse a field in the forward direction, specify name of the field; to traverse a field in the inverse direction,
* specify the name of the field and its containing type using the syntax {@code ^Type:field^}.
*
*
* For complex fields, specify both the field and sub-field: for {@link Set} and {@link java.util.List} fields, the sub-field is
* always {@code "element"}, while for {@link java.util.Map} fields the sub-field is either {@code "key"} or {@code "value"}.
* For example, to traverse a map field's {@code key} sub-field, specify {@code "mymap.key"}.
*
*
Target Fields
*
*
* A reference path may optionally have a target field appended to the end of the path. The target field does not have
* to be a reference field. If a target field is specified, the target object types
* are restricted to those types containing the target field.
*
*
* Note that to avoid ambiguity it must be known at the time the path is parsed whether the path contains a target field: for
* example, consider the path {@code "parent.parent"} in the context of a {@code Child} object: if there is a target field,
* the target object is the child's parent and the target field is the {@code "parent"} field of the child's parent
* (which just happens to also be a reference field), but if there is no target field, the path simply refers to the child's
* grandparent.
*
*
Examples
*
*
* Considering the following model classes:
*
*
* @JSimpleClass
* public class Employee {
*
* public abstract String getName();
* public abstract void setName(String name);
*
* public abstract Employee getManager();
* public abstract void setManager(Employee manager);
*
* public abstract Set<Asset> getAssets();
* }
*
* @JSimpleClass
* public class Asset {
*
* public abstract int getAssetId();
* public abstract void setAssetId(int assetId);
*
* public abstract String getName();
* public abstract void setName(String name);
* }
*
*
* Then these paths have the following meanings:
*
*
*
*
* Start Type
* Path
* Has Target Field?
* Description
*
*
* {@code Employee}
* {@code ""}
* No
* The {@code Employee}
*
*
* {@code Employee}
* {@code ""}
* Yes
* Invalid - no target field specified
*
*
* {@code Employee}
* {@code "name"}
* Yes
* The {@code Employee}'s name
*
*
* {@code Employee}
* {@code "name"}
* No
* Invalid - {@code "name"} is not a reference field
*
*
* {@code Asset}
* {@code "name"}
* Yes
* The {@code Asset}'s name
*
*
* {@code Object}
* {@code "name"}
* Yes
* The {@code Employee}'s or {@code Asset}'s name
*
*
* {@code Employee}
* {@code "manager"}
* No
* The {@code Employee}'s manager
*
*
* {@code Employee}
* {@code "manager"}
* Yes
* The {@code "manager"} field of the {@code Employee}
*
*
* {@code Employee}
* {@code "manager.name"}
* Yes
* The {@code Employee}'s manager's name
*
*
* {@code Employee}
* {@code "^Employee:manager^.name"}
* Yes
* The names of all of the {@code Employee}'s direct reports
*
*
* {@code Employee}
* {@code "manager.assets.description"}
* Yes
* The descriptions of the {@code Employee}'s manager's assets
*
*
* {@code Employee}
* {@code "manager.^Employee:manager^"}
* No
* All of the {@code Employee}'s manager's direct reports
*
*
* {@code Asset}
* {@code "^Employee:assets.element^"}
* No
* The employee owning the {@code Asset}
*
*
* {@code Asset}
* {@code "^Employee:assets.element^"}
* Yes
* Invalid - no target field specified
*
*
* {@code Asset}
* {@code "^Employee:assets.element^}
{@code .manager.^Employee:manager^.asset.assetId"}
* Yes
* ID's of all {@code Asset}s owned by direct reports of the manager of the {@code Employee} owning the original
* {@code Asset}
*
*
*
*
* Fields of Sub-Types
*
*
* The same field can appear in multiple types, e.g., {@code "name"} in the example above appears in both {@code Employee}
* and {@code Asset}. The set of all possible object types is recalculated at each step in the reference path, including
* at the last step, which gives the target object type(s). At each intermediate step, as long as the Java types do not
* contain incompatible definitions for the named field, the step is valid.
*
*
* In rare cases where multiple sub-types of a common super-type type have fields with the same name but different storage IDs,
* the storage ID may be explicitly specified as a suffix, for example, {@code "name#123"}.
*
*
Using Reference Paths
*
*
* Reference paths may be explicitly created via {@link JSimpleDB#parseReferencePath JSimpleDB.parseReferencePath()}
* and traversed in the forward direction via {@link JTransaction#followReferencePath JTransaction.followReferencePath()}
* or in the inverse direction via {@link JTransaction#invertReferencePath JTransaction.invertReferencePath()}.
*
*
* Reference paths are also used implicitly by {@link org.jsimpledb.annotation.OnChange @OnChange} annotations to
* specify non-local objects for change monitoring, and by {@link org.jsimpledb.annotation.FollowPath @FollowPath}
* annotations.
*
* @see JSimpleDB#parseReferencePath JSimpleDB.parseReferencePath()
* @see JTransaction#followReferencePath JTransaction.followReferencePath()
* @see JTransaction#invertReferencePath JTransaction.invertReferencePath()
* @see org.jsimpledb.annotation.OnChange @OnChange
*/
public class ReferencePath {
private static final String IDENT = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*";
private static final String IDENT_ID = IDENT + "(?:#[0-9]+)?";
private static final String IDENT_ID_1OR2 = IDENT_ID + "(?:\\." + IDENT_ID + ")?";
private static final String IDENTS = IDENT + "(?:\\." + IDENT + ")*";
private static final String FWD_STEP = "(" + IDENT_ID + ")";
private static final String REV_STEP = "\\^((" + IDENTS + "):(" + IDENT_ID_1OR2 + "))\\^";
final JSimpleDB jdb;
final Class> startType;
final ArrayList>> pathTypes;
final Set> targetFieldTypes;
final int targetFieldStorageId;
final int targetSuperFieldStorageId;
final int[] referenceFieldStorageIds;
final Set cursors;
final String path;
private final Logger log = LoggerFactory.getLogger(this.getClass());
private volatile KeyRanges[] pathKeyRanges;
/**
* Constructor.
*
* @param jdb {@link JSimpleDB} against which to resolve object and field names
* @param startType starting Java type for the path
* @param path dot-separated path of zero or more reference fields, followed by a target field
* @param withTargetField true if this path has a target field
* @param lastIsSubField true if the last field can be a complex sub-field but not a complex field, false for the reverse,
* or null for don't care
* @throws IllegalArgumentException if {@code jdb}, {@code startType}, or {@code path} is null
* @throws IllegalArgumentException if {@code startType} is not compatible with any Java model types
* @throws IllegalArgumentException if {@code path} is invalid
*/
ReferencePath(JSimpleDB jdb, Class> startType, String path, boolean withTargetField, Boolean lastIsSubField) {
// Sanity check
Preconditions.checkArgument(jdb != null, "null jdb");
Preconditions.checkArgument(startType != null, "null startType");
Preconditions.checkArgument(path != null, "null path");
final String errorPrefix = "invalid path `" + path + "': ";
this.jdb = jdb;
this.startType = startType;
this.path = path;
// Debug
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath: START startType=" + startType.getName() + " path=\"" + path
+ "\" withTargetField=" + withTargetField + " lastIsSubField=" + lastIsSubField);
}
// Split the path into field names
final ArrayDeque fieldNames = new ArrayDeque<>();
final ParseContext ctx = new ParseContext(path);
while (!ctx.isEOF()) {
// Gobble separator
if (ctx.getIndex() > 0)
ctx.expect('.');
// Parse next step (either forward or reverse)
Matcher matcher = ctx.tryPattern(FWD_STEP);
if (matcher == null && (matcher = ctx.tryPattern(REV_STEP)) == null) {
throw new IllegalArgumentException(errorPrefix + "invalid path starting at `"
+ ParseContext.truncate(ctx.getInput(), 32) + "'");
}
fieldNames.add(matcher.group());
}
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: fieldNames=" + fieldNames);
// Get starting types
final List extends JClass>> startJClasses = this.jdb.getJClasses(this.startType);
if (startJClasses.isEmpty()) {
throw new IllegalArgumentException(errorPrefix
+ "no model type is an instance of path start type " + this.startType.getName());
}
// Initialize cursors
final HashSet remainingCursors = new HashSet<>();
startJClasses.stream()
.map(jclass -> new Cursor(jclass, jclass.getType(), fieldNames))
.forEach(remainingCursors::add);
if (this.startType.isAssignableFrom(UntypedJObject.class))
remainingCursors.add(new Cursor(null, UntypedJObject.class, fieldNames));
// Recursively advance cursors
IllegalArgumentException error = null;
final HashSet completedCursors = new HashSet<>();
while (!remainingCursors.isEmpty()) {
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: remainingCursors=" + remainingCursors);
// Advance outstanding cursors
final HashSet previouslyRemainingCursors = new HashSet<>(remainingCursors);
remainingCursors.clear();
for (Cursor cursor : previouslyRemainingCursors) {
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: processing remainingCursor " + cursor);
// Cursors that have no remaining fields, and for which no target field is required, are completed
if (!withTargetField && !cursor.hasMoreFieldNames()) {
if (this.log.isTraceEnabled())
this.log.trace("RefPath: remainingCursor " + cursor + " is completed");
completedCursors.add(cursor);
continue;
}
// Try to identify the next field in the path
final Set newCursors;
try {
newCursors = cursor.identifyNextField(withTargetField, lastIsSubField);
} catch (IllegalArgumentException e) {
if (this.log.isTraceEnabled())
this.log.trace("RefPath: identifyNextField() on " + cursor + " failed: " + e.getMessage());
error = e;
continue;
}
if (this.log.isTraceEnabled())
this.log.trace("RefPath: identifyNextField() on " + cursor + " succeeded: newCursors=" + newCursors);
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: after identifyNextField(), newCursors=" + newCursors);
// Remove cursors for which we have reached the end of the path
for (Iterator i = newCursors.iterator(); i.hasNext(); ) {
final Cursor newCursor = i.next();
if (withTargetField && !newCursor.hasMoreFieldNames()) {
if (newCursor.isReverseStep()) // target field cannot be a reverse step
throw new IllegalArgumentException("Invalid reference path: missing target field");
if (this.log.isTraceEnabled())
this.log.trace("RefPath: newCursor " + cursor + " is completed");
completedCursors.add(newCursor);
i.remove();
}
}
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: after identifyNextField(), remaining newCursors=" + newCursors);
// Advance the unfinished cursors through the next reference field
for (Cursor newCursor : newCursors) {
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: invoking stepThroughReference() on " + newCursor);
// "Dereference" the reference field, possibly branching to create multiple new cursors
final Set dereferencedCursors;
try {
dereferencedCursors = newCursor.stepThroughReference();
} catch (IllegalArgumentException e) {
error = e;
continue;
}
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: stepThroughReference() returned " + dereferencedCursors);
remainingCursors.addAll(dereferencedCursors);
}
}
}
// Check for error
if (this.log.isTraceEnabled())
this.log.trace("RefPath: remainingCursors=" + remainingCursors + " completedCursors=" + completedCursors);
if (completedCursors.isEmpty())
throw error;
// Check that all cursors took the same path
final ArrayList referenceFieldList = completedCursors.iterator().next().getReferenceFields();
for (Cursor cursor : completedCursors) {
if (!cursor.getReferenceFields().equals(referenceFieldList)) {
throw new IllegalArgumentException(errorPrefix
+ "path is ambiguous due to traversal of fields with different types");
}
}
// Get target field info
if (withTargetField) {
// Sanity check target field has a consistent storage ID and parent storage ID
final Set targetFieldStorageIds = completedCursors.stream()
.map(Cursor::getField)
.map(JField::getStorageId)
.collect(Collectors.toSet());
final Set targetSuperFieldStorageIds = completedCursors.stream()
.map(Cursor::getSuperField)
.map(sf -> sf != null ? sf.storageId : 0)
.collect(Collectors.toSet());
if (targetFieldStorageIds.size() != 1 || targetSuperFieldStorageIds.size() != 1) {
throw new IllegalArgumentException(errorPrefix + "the target field `" + fieldNames.pollLast()
+ "' is ambiguous: " + completedCursors.stream().map(Cursor::getField).collect(Collectors.toSet()));
}
// Calculate all possible target field types
final Set> targetFieldTypesSet = completedCursors.stream()
.map(Cursor::getField)
.map(JField::getTypeToken)
.collect(Collectors.toSet());
// Initialize
this.targetFieldTypes = Collections.unmodifiableSet(targetFieldTypesSet);
this.targetFieldStorageId = targetFieldStorageIds.iterator().next();
this.targetSuperFieldStorageId = targetSuperFieldStorageIds.iterator().next();
} else {
this.targetFieldTypes = null;
this.targetFieldStorageId = 0;
this.targetSuperFieldStorageId = 0;
}
// Gather and minimize path types
this.pathTypes = new ArrayList<>(referenceFieldList.size() + 1);
for (int i = 0; i <= referenceFieldList.size(); i++) {
final HashSet> types = new HashSet<>();
for (Cursor cursor : completedCursors) {
final Class> type = cursor.getPathTypes().get(i);
types.add(type);
if (this.log.isTraceEnabled())
this.log.trace("RefPath: added " + type + " to pathTypes[" + i + "] from " + cursor);
}
this.pathTypes.add(ReferencePath.minimizeAndSeal(types));
}
assert this.pathTypes.size() == referenceFieldList.size() + 1;
// Done
this.referenceFieldStorageIds = Ints.toArray(referenceFieldList);
this.cursors = completedCursors;
// Logging
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath: DONE: targetFieldStorageId=" + this.targetFieldStorageId
+ " targetSuperFieldStorageId=" + this.targetSuperFieldStorageId + " targetFieldTypes=" + this.targetFieldTypes
+ " references=" + referenceFieldList + " cursors=" + this.cursors + " pathTypes=" + this.pathTypes);
}
}
/**
* Get the Java type of the object at which this path starts.
*
*
* If there are zero {@linkplain #getReferenceFields reference fields} in this path, then this will
* equal the {@linkplain #getTargetType target object type}, or possibly a super-type if the target
* field exists only in a sub-type.
*
* @return the Java type at which this reference path starts
*/
public Class> getStartType() {
return this.startType;
}
/**
* Get the Java type of the object at which this path ends.
*
*
* The returned type will be as narrow as possible while still including all possibilities, but note that it's
* possible for there to be multiple candidates for the "target type", none of which is a sub-type of any other.
* To retrieve all such target types, use {@link #getTargetTypes}; this method just invokes
* {@link Util#findLowestCommonAncestorOfClasses Util.findLowestCommonAncestorOfClasses()} on the result.
*
* @return the Java type at which this reference path ends
*/
public Class> getTargetType() {
return Util.findLowestCommonAncestorOfClasses(this.getTargetTypes()).getRawType();
}
/**
* Get the possible Java types of the object at which this path ends.
*
*
* If there are zero {@linkplain #getReferenceFields reference fields} in this path, then this method will
* return only the Java type of the {@linkplain #getStartType starting object type}, or possibly a sub-type
* if the target field exists only in a sub-type.
*
*
* The returned type(s) will be maximally narrow. The set will contain only one element if a unique such
* type exists, otherwise it will contain multiple mutually incompatible supertypes of the object types
* at which this path ends.
*
* @return the Java type(s) at which this reference path ends
*/
public Set> getTargetTypes() {
return this.getPathTypes().get(this.pathTypes.size() - 1);
}
/**
* Get the set of possible model object types at each step in the path.
*
*
* The returned list always has length one more than the length of the array returned by {@link #getReferenceFields},
* such that the set at index i contains all possible types found after the ith step.
* The first element contains type(s) that are all assignable to the {@linkplain #getStartType starting type}
* (possibly only the starting type if it was already maximally narrow), and the last element contains the
* {@linkplain #getTargetTypes target type(s)}.
*
*
* Each set in the returned list will be maximally narrow: it will contain only one element if a unique such type exists,
* otherwise it will contain multiple mutually incompatible supertypes of the object types at that step.
*
* @return list of possible {@link JClass} corresponding to each step
*/
public List>> getPathTypes() {
return Collections.unmodifiableList(this.pathTypes);
}
KeyRanges[] getPathKeyRanges() {
if (this.pathKeyRanges == null) {
final int numJClasses = this.jdb.jclasses.size();
final KeyRanges[] array = new KeyRanges[this.pathTypes.size()];
for (int i = 0; i < this.pathTypes.size(); i++) {
final HashSet> jclasses = new HashSet<>();
final Set> types = this.pathTypes.get(i);
for (Class> type : types)
jclasses.addAll(this.jdb.getJClasses(type));
if (jclasses.size() == numJClasses && this.isAnyAssignableFrom(types, UntypedJObject.class))
continue; // no filter needed
final ArrayList ranges = new ArrayList<>(jclasses.size());
for (JClass> jclass : jclasses)
ranges.add(ObjId.getKeyRange(jclass.storageId));
array[i] = new KeyRanges(ranges);
}
this.pathKeyRanges = array;
}
return this.pathKeyRanges;
}
private boolean isAnyAssignableFrom(Iterable extends Class>> tos, Class> from) {
for (Class> to : tos) {
if (to.isAssignableFrom(from))
return true;
}
return false;
}
/**
* Get the Java type(s) corresponding to the target field at which this path ends, if any.
*
*
* The returned type(s) will be maximally narrow. The set will contain only one element if a unique such
* type exists, otherwise it will contain multiple mutually incompatible supertypes of the object types
* at which this path ends. The latter case can only occur when the field is a reference field, and there
* are multiple Java model classes compatible with the field's type.
*
* @return the type of the field at which this reference path ends, or null if this reference path does
* not have a target field
*/
public Set> getTargetFieldTypes() {
return this.targetFieldTypes;
}
/**
* Get the storage ID associated with the target field, if any, in the {@linkplain #getTargetType target object type}.
*
*
* This is just the storage ID of the last field in the path.
*
* @return the storage ID of the field at which this reference path ends, or zero if this reference path does
* not have a target field
*/
public int getTargetField() {
return this.targetFieldStorageId;
}
/**
* Get the storage ID associated with the complex field containing the {@linkplain #getTargetField target field},
* if any, in the case that the target field is a sub-field of a complex field.
*
* @return target field's complex super-field storage ID, or zero if the target field is not a complex sub-field
* or this reference path does not have a target field
*/
public int getTargetSuperField() {
return this.targetSuperFieldStorageId;
}
/**
* Get the storage IDs of the reference fields in this path.
*
*
* Storage ID's will be negated to indicate reference fields traversed in the reverse direction.
*
*
* The path may be empty, i.e., zero references are traversed in the path.
*
*
* Otherwise, the first field is a field in the {@linkplain #getStartType starting object type} and
* the last field is field in some object type that refers to the {@linkplain #getTargetType target object type}.
*
* @return zero or more possibly negated reference field storage IDs
*/
public int[] getReferenceFields() {
return this.referenceFieldStorageIds.clone();
}
/**
* Get the {@link String} form of the path associated with this instance.
*/
@Override
public String toString() {
return this.path;
}
/**
* Identify the named field at the start of the given field name list in the given {@link JClass}.
*
*
* Either one or two field names will be consumed, depending on whether the field is complex.
*
* @param jclass starting type
* @param fieldNames list of field names
* @param lastIsSubField true if the last field can be a complex sub-field but not a complex field, false for the reverse,
* or null for don't care
* @return resulting {@link JField}
* @throws IllegalArgumentException if something is bogus
*/
private JField findField(JClass> jclass, ArrayDeque fieldNames, Boolean lastIsSubField) {
// Sanity check
Preconditions.checkArgument(jclass != null, "null jclass");
Preconditions.checkArgument(fieldNames != null, "null fieldNames");
Preconditions.checkArgument(!fieldNames.isEmpty(), "empty reference path");
// Logging
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath.findField(): jclass=" + jclass
+ " fieldNames=" + fieldNames + " lastIsSubField=" + lastIsSubField);
}
// Get field name and containing type
final String fieldName = fieldNames.removeFirst();
String description = "field `" + fieldName + "' in " + jclass.getType();
// Parse explicit storage ID, if any
final int hash = fieldName.indexOf('#');
int explicitStorageId = 0;
final String searchName;
if (hash != -1) {
try {
explicitStorageId = Integer.parseInt(fieldName.substring(hash + 1));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid field name `" + fieldName + "'");
}
searchName = fieldName.substring(0, hash);
} else
searchName = fieldName;
// Find the JField matching 'fieldName' in jclass
JField matchingField = jclass.jfieldsByName.get(searchName);
if (matchingField == null || (explicitStorageId != 0 && matchingField.storageId != explicitStorageId)) {
throw new IllegalArgumentException("there is no field named `" + searchName + "'"
+ (explicitStorageId != 0 ? " with storage ID " + explicitStorageId : "") + " in " + jclass.getType());
}
// Logging
if (this.log.isTraceEnabled())
this.log.trace("RefPath.findField(): found field " + matchingField + " in " + jclass.getType());
// Handle complex fields
JComplexField superField = null;
if (matchingField instanceof JComplexField) {
superField = (JComplexField)matchingField;
// Logging
if (this.log.isTraceEnabled())
this.log.trace("RefPath.findField(): field is a complex field");
// Last field?
if (fieldNames.isEmpty()) {
// Verify it's OK to end on a complex field
if (Boolean.TRUE.equals(lastIsSubField)) {
final StringBuilder buf = new StringBuilder();
for (JSimpleField subField : superField.getSubFields()) {
if (buf.length() == 0) {
buf.append("path may not end on complex ")
.append(description)
.append("; a sub-field must be specified (e.g., ");
} else
buf.append(" or ");
buf.append('`').append(matchingField.name).append('.').append(subField.name).append('\'');
}
buf.append(")");
throw new IllegalArgumentException(buf.toString());
}
// Done
if (this.log.isTraceEnabled())
this.log.trace("RefPath.findField(): ended on complex field; result=" + matchingField);
return matchingField;
}
// Get the specified sub-field
final String subFieldName = fieldNames.removeFirst();
description = "sub-field `" + subFieldName + "' of complex " + description;
try {
matchingField = superField.getSubField(subFieldName);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("invalid " + description + ": " + e.getMessage(), e);
}
// Logging
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath.findField(): also stepping through sub-field ["
+ searchName + "." + subFieldName + "] to reach " + matchingField);
}
} else if (this.log.isTraceEnabled()) {
if (matchingField instanceof JSimpleField) {
final JSimpleField simpleField = (JSimpleField)matchingField;
this.log.trace("RefPath.findField(): field is a simple field of type " + simpleField.getTypeToken());
} else
this.log.trace("RefPath.findField(): field is " + matchingField);
}
// Verify it's OK to end on a complex sub-field (if that's what happened)
if (fieldNames.isEmpty() && superField != null && Boolean.FALSE.equals(lastIsSubField)) {
throw new IllegalArgumentException("path may not end on " + description
+ "; instead, specify the complex field itself");
}
// Done
if (this.log.isTraceEnabled())
this.log.trace("RefPath.findField(): result=" + matchingField);
return matchingField;
}
// Utility methods
private static Set> minimizeAndSeal(Set> types) {
final HashSet> minimalTypes = new HashSet<>(types.size());
for (TypeToken> typeToken : Util.findLowestCommonAncestorsOfClasses(types))
minimalTypes.add((Class>)typeToken.getRawType());
return Collections.unmodifiableSet(minimalTypes);
}
private static ArrayList> arrayListOf(Class> type) {
final ArrayList> list = new ArrayList<>(1);
list.add(type);
return list;
}
private static ArrayList copyAndAppend(List original, T elem) {
final ArrayList list = new ArrayList<>(original.size() + 1);
list.addAll(original);
list.add(elem);
return list;
}
// Cursor
/**
* A cursor position in a partially parsed reference path.
*
*
* An instance points to a {@link JClass}, and optionally to a {@link JField} within that class (or within
* some referring class in the case of a reverse step).
* It has a list of references it has already traversed, and a list of field names it has yet to traverse.
* Traversing a field involves two steps: (a) finding the field via {@link #identifyNextField},
* and (if not done yet) (b) dereferencing that (presumably reference) field via {@link #stepThroughReference}.
* Note prior to step (a), {@code this.jfield} is null; prior to step (b), {@code this.jfield} is the field
* we just dereferenced in the previous {@link JClass}.
*
*
* Instances are immutable.
*/
final class Cursor {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final ArrayList referenceFields = new ArrayList<>();
private final ArrayList> pathTypes = new ArrayList<>();
private final JClass> jclass; // null means UntypedJObject
private final JField jfield;
private final ArrayDeque fieldNames;
private final boolean reverseStep;
private Cursor(JClass> jclass, Class> startType, ArrayDeque fieldNames) {
this(new ArrayList<>(0), ReferencePath.arrayListOf(startType), jclass, null, fieldNames, false);
}
private Cursor(ArrayList referenceFields, ArrayList> pathTypes, JClass> jclass,
JField jfield, ArrayDeque fieldNames, boolean reverseStep) {
assert pathTypes.size() == referenceFields.size() + 1;
this.referenceFields.addAll(referenceFields);
this.pathTypes.addAll(pathTypes);
this.jclass = jclass;
this.jfield = jfield;
this.fieldNames = fieldNames.clone();
this.reverseStep = reverseStep;
}
public ArrayList getReferenceFields() {
return this.referenceFields;
}
public ArrayList> getPathTypes() {
return this.pathTypes;
}
public int getNumRefs() {
return this.referenceFields.size();
}
public JClass> getJClass() {
return this.jclass;
}
public JField getField() {
return this.jfield;
}
public JComplexField getSuperField() {
return this.jfield instanceof JSimpleField ? ((JSimpleField)this.jfield).getParentField() : null;
}
public boolean hasMoreFieldNames() {
return !this.fieldNames.isEmpty();
}
public boolean isReverseStep() {
return this.reverseStep;
}
/**
* Get the type associated with the next reference field.
*
* @return referred-to type, or null if field is not a reference field
* @throws IllegalStateException if field not identified yet
*/
public Class> getFieldTargetType() {
Preconditions.checkState(this.jfield != null, "have not yet stepped through field");
return this.reverseStep ? this.jfield.getJClass().type :
this.jfield instanceof JReferenceField ? ((JReferenceField)this.jfield).typeToken.getRawType() : null;
}
/**
* Step through the next field name and return the resulting cursor(s).
*
* @param lastIsSubField true if the last field can be a complex sub-field but not a complex field, false for the reverse,
* or null for don't care
* @return resulting cursor(s)
* @throws IllegalArgumentException if step is bogus
*/
public Set identifyNextField(boolean withTargetField, Boolean lastIsSubField) {
// Sanity check
Preconditions.checkArgument(!this.fieldNames.isEmpty(), "empty reference path");
Preconditions.checkState(this.jfield == null, "already stepped through field");
// Get next field name and containing type
final ArrayDeque remainingFieldNames = this.fieldNames.clone();
final String step = remainingFieldNames.peekFirst();
// Handle forward vs. reverse
final HashSet newCursors = new HashSet<>(3);
final Matcher reverseMatcher = Pattern.compile(REV_STEP).matcher(step);
if (reverseMatcher.matches()) {
// This field cannot be the target field
if (withTargetField && remainingFieldNames.isEmpty()) {
throw new IllegalArgumentException("Invalid reference path: missing target field after last step `"
+ step + "'");
}
// Get type and field names
final String typeName = reverseMatcher.group(2);
final String fieldName = reverseMatcher.group(3);
// Consume step
remainingFieldNames.removeFirst();
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath.identifyNextField(): reverse step `" + step
+ "' -> type `" + typeName + "' field `" + fieldName + "'");
}
// Resolve type into all assignable JClass's, plus null if untyped objects are possible
final Class> type;
final SchemaObjectType schemaType = ReferencePath.this.jdb.getNameIndex().getSchemaObjectType(typeName);
if (schemaType != null)
type = ReferencePath.this.jdb.getJClass(schemaType.getStorageId()).getType();
else {
try {
type = Class.forName(typeName, false, Thread.currentThread().getContextClassLoader());
} catch (Exception e) {
throw new IllegalArgumentException("Unknown type `" + typeName
+ "' in reference path reverse traversal step `" + step + "'");
}
}
List extends JClass>> jclasses = ReferencePath.this.jdb.getJClasses(type);
if (type.isAssignableFrom(UntypedJObject.class)) {
final ArrayList> jclasses2 = new ArrayList<>(jclasses.size() + 1);
jclasses2.addAll(jclasses);
jclasses2.add(null);
jclasses = jclasses2;
}
// Any types found?
if (jclasses.isEmpty()) {
throw new IllegalArgumentException("Invalid type `" + typeName
+ "' in reference path reverse traversal step `" + step
+ "': no schema model types are assignable to `" + typeName + "'");
}
// Find field in each type and create corresponding cursors
for (JClass> nextJClass : jclasses) {
if (nextJClass == null)
continue;
final ArrayDeque stepFieldNames = new ArrayDeque<>(Arrays.asList(fieldName.split("\\.")));
final JField nextJField;
try {
nextJField = ReferencePath.this.findField(nextJClass, stepFieldNames, true);
} catch (IllegalArgumentException e) {
continue;
}
newCursors.add(new Cursor(this.referenceFields,
this.pathTypes, nextJClass, nextJField, remainingFieldNames, true));
}
// Any fields found?
if (newCursors.isEmpty()) {
throw new IllegalArgumentException("Invalid reference path reverse traversal step `" + step
+ "': field `" + fieldName + "' does not exist in "
+ (schemaType == null ? "any model type assignable to " : "") + "`" + typeName + "'");
}
} else {
assert Pattern.compile(FWD_STEP).matcher(step).matches() : "`" + step + "' is not a forward step";
// Resolve field
if (this.jclass != null) {
final JField nextJField = ReferencePath.this.findField(this.jclass, remainingFieldNames, lastIsSubField);
newCursors.add(new Cursor(this.referenceFields,
this.pathTypes, this.jclass, nextJField, remainingFieldNames, false));
}
}
// Done
if (this.log.isTraceEnabled())
this.log.trace("RefPath.identifyNextField(): result=" + newCursors);
return newCursors;
}
/**
* Step through the current reference field to the referred-to types.
*/
public Set stepThroughReference() {
// Logging
if (this.log.isTraceEnabled())
this.log.trace("RefPath.stepThroughReference(): this=" + this);
// Sanity check
Preconditions.checkState(this.jfield != null, "have not yet stepped through field");
assert this.jfield != null;
assert this.reverseStep
|| this.jfield.parent == this.jclass
|| (this.jfield instanceof JSimpleField && ((JSimpleField)this.jfield).getParentField().parent == this.jclass);
Preconditions.checkArgument(this.jfield instanceof JReferenceField, this.jfield + " is not a reference field");
// Append reference field to list
final int stepStorageId = this.reverseStep ? -this.jfield.storageId : this.jfield.storageId;
final ArrayList newReferenceFields = ReferencePath.copyAndAppend(this.referenceFields, stepStorageId);
// Advance through the reference, either forward or inverse
final Class> targetType = this.reverseStep ?
this.jfield.getJClass().type : ((JReferenceField)this.jfield).typeToken.getRawType();
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath.stepThroughReference(): targetType="
+ targetType + " -> " + ReferencePath.this.jdb.getJClasses(targetType));
}
final HashSet newCursors = new HashSet<>();
for (JClass> targetJClass : ReferencePath.this.jdb.getJClasses(targetType)) {
final ArrayList> newPathTypes = ReferencePath.copyAndAppend(this.pathTypes, targetJClass.getType());
newCursors.add(new Cursor(newReferenceFields, newPathTypes, targetJClass, null, this.fieldNames, false));
}
if (targetType.isAssignableFrom(UntypedJObject.class)) {
final ArrayList> newPathTypes = ReferencePath.copyAndAppend(this.pathTypes, UntypedJObject.class);
newCursors.add(new Cursor(newReferenceFields, newPathTypes, null, null, this.fieldNames, false));
}
// Done
if (this.log.isTraceEnabled())
this.log.trace("RefPath.stepThroughReference(): result=" + newCursors);
return newCursors;
}
// Object
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (obj == null || obj.getClass() != this.getClass())
return false;
final Cursor that = (Cursor)obj;
return this.referenceFields.equals(that.referenceFields)
&& this.pathTypes.equals(that.pathTypes)
&& Objects.equals(this.jclass, that.jclass)
&& Objects.equals(this.jfield, that.jfield)
&& this.reverseStep == that.reverseStep;
}
@Override
public int hashCode() {
return this.referenceFields.hashCode()
^ this.pathTypes.hashCode()
^ Objects.hashCode(this.jclass)
^ Objects.hashCode(this.jfield)
^ (this.reverseStep ? 1 : 0);
}
@Override
public String toString() {
return "Cursor"
+ "[jclass=" + this.jclass
+ (this.jfield != null ? ",jfield=" + this.jfield : "")
+ (!this.fieldNames.isEmpty() ? ",fieldNames=" + this.fieldNames : "")
+ (!this.referenceFields.isEmpty() ? ",refs=" + this.referenceFields : "")
+ (!this.pathTypes.isEmpty() ? ",pathTypes=" + this.pathTypes : "")
+ (this.reverseStep ? ",reverseStep" : "")
+ "]";
}
}
}