io.permazen.ReferencePath Maven / Gradle / Ivy
Show all versions of permazen-main Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen;
import com.google.common.base.Preconditions;
import com.google.common.primitives.Ints;
import io.permazen.annotation.OnChange;
import io.permazen.annotation.OnDelete;
import io.permazen.core.ObjId;
import io.permazen.kv.KeyRange;
import io.permazen.kv.KeyRanges;
import io.permazen.util.TypeTokens;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Permazen reference paths.
*
*
*
*
*
* Overview
*
*
* A reference path defines a bi-directional path of object references, starting from some starting object type(s)
* and ending up at some target object type(s) by hopping from object to object.
* Because reference fields are always indexed, given a set of starting or target instances Permazen can efficiently
* compute the set of objects at the other end of the path. This calculation includes automatic elimination
* of duplicates caused by loops or multiple paths.
*
*
* The reference fields in the path may be simple fields or sub-fields of complex fields (i.e., list element, set element,
* map key, or map value), and each field may be traversed in either the forward or inverse direction. In short, a
* {@link ReferencePath} consists of a set of starting object types, a list of reference fields, and a boolean flag
* for each field that determines the direction the field should be traversed.
*
*
* When stepping forward through a complex field, or backward through any field, the number of reachable objects can increase.
* In general, the number of target objects can be vastly different than the number of starting objects, depending on the
* fan-in/fan-out of the reference fields traversed. This should be kept in mind when considering the use of reference paths.
* A {@link ReferencePath} containing only forward simple reference fields is termed {@linkplain #isSingular singular}.
*
*
Type Pruning
*
*
* At each step in the path, there is a set of possible current object types: at the initial step, this set
* is just the starting object types, and after the final step, this set becomes the target object types. At each step,
* some of the current object types may get pruned because they are incompatible with the next field in the path. This
* happens when the field is only defined in some of the types (for forward steps), or when the field can only refer to
* some of the types (for inverse steps). In these cases, the search ends for any pruned types and continues for the
* remaining types. It is an error if, at any step, all types are pruned, as this would imply that no objects could
* ever be found.
*
*
String Form
*
*
* Reference paths are denoted in {@link String} form as a concatenation of zero or more reference steps:
*
* - Forward reference steps are denoted by either {@code "->fieldName"} or (rarely) {@code "->TypeName.fieldName"}.
* The latter form is only needed to disambiguate when two or more of the current object types define incompatible
* fields with the same name.
*
- Inverse reference steps are denoted {@code "<-TypeName.fieldName"} where {@code fieldName} is the name
* of a reference field defined in {@code TypeName} (or some sub-type(s) therein).
*
*
*
* To parse a reference path into a {@link ReferencePath} instance, the path must be interpreted in the context of
* some starting object types (see {@link Permazen#parseReferencePath Permazen.parseReferencePath()}).
*
*
Type Names
*
*
* The {@code TypeName} is either the name of an object type in the schema (usually the unqualified name of the corresponding
* Java model class), or else the fully-qualified name of any Java class or interface.
*
*
Field Names
*
*
* For simple reference fields, the {@code fieldName} is just the field name. For reference fields that are sub-fields
* of complex fields, {@code fieldName} must specify both the parent field and the sub-field:
*
* - For {@code Map} fields, specify the sub-field via either {@code myfield.key} or {@code myfield.value}.
*
- For {@code List} and {@code Set} fields, the only sub-field is {@code element}, so you can specify
* {@code myfield.element} or abbreviate as {@code myfield}.
*
*
* Examples
*
*
* Consider the following model classes:
*
*
* @PermazenType
* public interface Animal<T extends Animal<T>> {
*
* T getParent();
* void setParent(T parent);
*
* Set<Animal<?>> getEnemies();
* }
*
* @PermazenType
* public interface Elephant extends Animal<Elephant> {
*
* Elephant getFriend();
* void setFriend(Elephant friend);
* }
*
* @PermazenType
* public interface Giraffe extends Animal<Giraffe> {
*
* Giraffe getFriend();
* void setFriend(Giraffe friend);
* }
*
*
* Then the reference paths below would have the following meanings:
*
*
*
* Reference Path Examples
*
* Start Type
* Path
* Target Types
* Description
*
*
* {@code Elephant}
* {@code ""}
* {@code Elephant}
* The starting {@code Elephant}
*
*
* {@code Elephant}
* {@code "->parent"}
* {@code Elephant}
* The {@code Elephant}'s parent
*
*
* {@code Giraffe}
* {@code "->parent"}
* {@code Giraffe}
* The {@code Giraffe}'s parent
*
*
* {@code Animal}
* {@code "->parent"}
* {@code Elephant}, {@code Giraffe}
* The {@code Animals}'s parent
*
*
* {@code Elephant}
* {@code "<-Elephant.enemies"}
* {@code Elephant}
* All {@code Elephant}s for whom the original {@code Elephant} is an enemy
*
*
* {@code Elephant}
* {@code "<-Animal.enemies"}
* {@code Elephant}, {@code Giraffe}
* All {@code Animal}s for whom the original {@code Elephant} is an enemy
*
*
* {@code Elephant}
* {@code "<-friend"}
* {@code Elephant}
* All {@code Elephant}s for whom the original {@code Elephant} is their friend
*
*
* {@code Animal}
* {@code "<-friend"}
* {@code Elephant}, {@code Giraffe}
* All {@code Animal}s for whom the original {@code Animal} is their friend
*
*
* {@code Elephant}
* {@code "->friend<-Giraffe.enemies"}
* {@code Giraffe}
* All {@code Giraffe}s for whom the original {@code Elephant}'s friend is an enemy
*
*
* {@code Elephant}
* {@code "->enemies<-Giraffe.friend}
{@code ->enemies<-Elephant.friend"}
* {@code Elephant}
* All {@code Elephant}s who's friend is an enemy of some {@code Giraffe} for whom one of the original
* {@code Elephant}'s enemies is their friend
*
*
* {@code Elephant}
* {@code "<-Giraffe.friend"}
* N/A
* Invalid - it's not possible for an {@code Elephant} to be a {@code Giraffe}'s friend
*
*
*
*
* Using Reference Paths
*
*
* Reference paths may be explicitly created via {@link Permazen#parseReferencePath Permazen.parseReferencePath()}
* and traversed in the forward direction via
* {@link PermazenTransaction#followReferencePath PermazenTransaction.followReferencePath()}
* or in the inverse direction via {@link PermazenTransaction#invertReferencePath PermazenTransaction.invertReferencePath()}.
*
*
* The {@link io.permazen.annotation.ReferencePath @ReferencePath} annotation can be used to auto-generate methods
* that traverse reference paths.
*
*
* Reference paths are also used by {@link OnChange @OnChange} and {@link OnDelete @OnDelete} annotations to specify
* non-local objects for monitoring.
*
* @see Permazen#parseReferencePath Permazen.parseReferencePath()
* @see PermazenTransaction#followReferencePath PermazenTransaction.followReferencePath()
* @see PermazenTransaction#invertReferencePath PermazenTransaction.invertReferencePath()
* @see io.permazen.annotation.ReferencePath @ReferencePath
* @see OnChange @OnChange
* @see OnDelete @OnDelete
*/
public class ReferencePath {
private static final String FWD_PREFIX = "->";
private static final String REV_PREFIX = "<-";
final Permazen pdb;
final String path;
final boolean singular;
final int[] storageIds;
final ArrayList>> currentTypesList;
private final Logger log = LoggerFactory.getLogger(this.getClass());
private volatile KeyRanges[] pathKeyRanges;
/**
* Constructor.
*
* @param pdb {@link Permazen} against which to resolve object and field names
* @param startTypes starting model types for the path, with null meaning {@link UntypedPermazenObject}
* @param path reference path in string form
* @throws IllegalArgumentException if {@code startTypes} is empty
* @throws IllegalArgumentException if {@code path} is invalid
* @throws IllegalArgumentException if any parameter is null
*/
ReferencePath(Permazen pdb, Collection> startTypes, String path) {
// Sanity check
Preconditions.checkArgument(pdb != null, "null pdb");
Preconditions.checkArgument(startTypes != null, "null startTypes");
Preconditions.checkArgument(!startTypes.isEmpty(), "empty startTypes");
Preconditions.checkArgument(path != null, "null path");
this.pdb = pdb;
this.path = path;
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: START path=\"{}\" startTypes={}", path, debugFormat(startTypes));
// Split the path into steps, each starting with "->" or "<-"
final List steps = new ArrayList<>(); // steps without prefixes
final List inverses = new ArrayList<>(); // true = inverse, false = forward
final Pattern prefixPattern = Pattern.compile(String.format("(%s|%s)",
Pattern.quote(FWD_PREFIX), Pattern.quote(REV_PREFIX)));
final String errorPrefix = "invalid path \"" + path + "\"";
while (!path.isEmpty()) {
Matcher matcher = prefixPattern.matcher(path);
if (!matcher.lookingAt()) {
throw new IllegalArgumentException(String.format(
"%s: steps must start with either \"%s\" or \"%s\"", errorPrefix, FWD_PREFIX, REV_PREFIX));
}
final String prefix = matcher.group();
final boolean inverse = prefix.equals(REV_PREFIX);
path = path.substring(prefix.length());
final String step = (matcher = prefixPattern.matcher(path)).find() ? path.substring(0, matcher.start()) : path;
if (step.isEmpty())
throw new IllegalArgumentException(String.format("%s: invalid empty step \"%s\"", errorPrefix, prefix));
steps.add(step);
inverses.add(inverse);
path = path.substring(step.length());
}
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: steps={}", steps);
// Initialize cursors
HashSet cursors = new HashSet<>();
for (PermazenClass> startType : startTypes)
cursors.add(new Cursor(startType));
// Advance cursors until all steps are completed or they all peter out
for (int i = 0; i < steps.size(); i++) {
final String step = steps.get(i);
final boolean inverse = inverses.get(i);
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: STEP {} inverse={} cursors={}", step, inverse, debugFormat(cursors));
// Advance the remaining cursors
final HashSet newCursors = new HashSet<>();
IllegalArgumentException error = null;
for (Cursor cursor : cursors) {
// Debug
if (this.log.isTraceEnabled())
this.log.trace("RefPath: processing cursor {}", cursor);
// Try to advance this cursor
final Set advancedCursors;
try {
advancedCursors = cursor.advance(inverse, step);
} catch (IllegalArgumentException e) {
if (this.log.isTraceEnabled())
this.log.trace("RefPath: advance({}, \"{}\") on {} failed: {}", inverse, step, cursor, e.getMessage());
if (error == null || cursor.pclass != null)
error = e;
continue;
}
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath: advance({}, \"{}\") on {} succeeded: advancedCursors={}",
inverse, step, cursor, debugFormat(advancedCursors));
}
// Add new cursors to next cursor set
newCursors.addAll(advancedCursors);
}
// Any cursors remaining?
if ((cursors = newCursors).isEmpty())
throw error;
}
if (this.log.isTraceEnabled())
this.log.trace("RefPath: final cursors={}", debugFormat(cursors));
// Verify all cursors stepped through the same fields
final Iterator cursortIterator = cursors.iterator();
this.storageIds = cursortIterator.next().getStorageIds();
cursortIterator.forEachRemaining(cursor -> {
if (!Arrays.equals(cursor.getStorageIds(), storageIds)) {
throw new IllegalArgumentException(String.format(
"%s: path is ambiguous due to traversal of different fields in different types", errorPrefix));
}
});
// Record the current types at each step
this.currentTypesList = new ArrayList<>(steps.size() + 1);
for (int i = 0; i <= steps.size(); i++) {
// Combine current types at this step from all cursors
final int i2 = i;
final Set> currentTypes = cursors.stream()
.map(Cursor::getCurrentTypesList)
.map(list -> list.get(i2))
.collect(Collectors.toSet());
// If UntypedObject is possible, then all model types should be possible
assert !currentTypes.contains(null) || currentTypes.containsAll(this.pdb.pclasses);
// Add to current types to list
if (this.log.isTraceEnabled())
this.log.trace("RefPath: currentTypesList[{}] = {}", this.currentTypesList.size(), currentTypes);
this.currentTypesList.add(Collections.unmodifiableSet(currentTypes));
}
// Record whether path is singular
this.singular = cursors.iterator().next().isSingular(); // all cursors should be the same on this question
// Logging
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath: DONE: singular={} fields={} currentTypesList={}",
singular, Ints.asList(this.storageIds), debugFormat(this.currentTypesList));
}
}
private String debugFormat(Collection> items) {
if (items == null)
return "null";
if (items.isEmpty())
return "EMPTY";
return "\n " + items.stream().map(String::valueOf).collect(Collectors.joining("\n "));
}
/**
* Get the possible model object types for the objects at the start of this path.
*
*
* This method returns the first set in the list returned by {@link #getCurrentTypesList}.
* If this path has zero length, then this method returns the same set as {@link #getTargetTypes}.
*
*
* The returned set will contain a null element when the target object can possibly be an {@link UntypedPermazenObject}.
*
* @return non-empty set of model object types at which this reference path starts, possibly including null
*/
public Set> getStartingTypes() {
return this.currentTypesList.get(0);
}
/**
* Get the narrowest possible Java type of the object(s) at which this path starts.
*
*
* 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 "starting type", none of which is a sub-type of any other.
* To retrieve all such starting types, use {@link #getStartingTypes}; this method just invokes
* {@link TypeTokens#findLowestCommonAncestorOfClasses TypeTokens.findLowestCommonAncestorOfClasses()} on that result.
*
* @return the Java type at which this reference path starts
*/
public Class> getStartingType() {
return TypeTokens.findLowestCommonAncestorOfClasses(ReferencePath.toClasses(this.getStartingTypes())).getRawType();
}
/**
* Get the possible model object types for the objects at the end of this path.
*
*
* This method returns the last set in the list returned by {@link #getCurrentTypesList}.
* If this path has zero length, then this method returns the same set as {@link #getStartingTypes}.
*
*
* The returned set will contain a null element when the target object can possibly be an {@link UntypedPermazenObject}.
*
* @return non-empty set of model object types at which this reference path ends, possibly including null
*/
public Set> getTargetTypes() {
return this.currentTypesList.get(this.currentTypesList.size() - 1);
}
/**
* Get the narrowest possible Java type of the object(s) 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 TypeTokens#findLowestCommonAncestorOfClasses TypeTokens.findLowestCommonAncestorOfClasses()} on that result.
*
* @return the Java type at which this reference path ends
*/
public Class> getTargetType() {
return TypeTokens.findLowestCommonAncestorOfClasses(ReferencePath.toClasses(this.getTargetTypes())).getRawType();
}
/**
* Get the current 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 the {@linkplain #getStartingTypes starting object types}
* and the last element contains the {@linkplain #getTargetTypes target types}.
*
*
* A set in the list will contain a null element if an object at that step can possibly be an {@link UntypedPermazenObject}.
*
* @return list of the possible {@link PermazenClass}'s appearing at each step in this path, each of which is
* non-empty and may include null
*/
public List>> getCurrentTypesList() {
return Collections.unmodifiableList(this.currentTypesList);
}
/**
* Get the storage IDs of the reference fields in this path in the order they occur.
*
*
* Storage ID's will be negated to indicate reference fields traversed in the inverse direction.
*
*
* The result will be empty if this path is empty.
*
* @return zero or more possibly negated reference field storage IDs
*/
public int[] getReferenceFields() {
return this.storageIds.clone();
}
/**
* Determine whether traversing this path can result in only one object being found.
*
*
* An empty path is always singular - it always returns just the starting object.
*
* @return true if this path only includes forward simple field references, otherwise false
*/
public boolean isSingular() {
return this.singular;
}
/**
* Get the number of steps in this reference path.
*
* @return the length of this path
*/
public int size() {
return this.storageIds.length;
}
/**
* Determine whether this path is empty, i.e., contains zero steps.
*
* @return true if this path is empty
*/
public boolean isEmpty() {
return this.size() == 0;
}
// Object
/**
* Get the {@link String} form of the path associated with this instance.
*/
@Override
public String toString() {
return this.path;
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (obj == null || obj.getClass() != this.getClass())
return false;
final ReferencePath that = (ReferencePath)obj;
return this.pdb.equals(that.pdb)
&& this.path.equals(that.path);
// the other fields are derived from the above
}
@Override
public int hashCode() {
return this.getClass().hashCode()
^ this.pdb.hashCode()
^ this.path.hashCode();
}
// Package Methods
KeyRanges[] getPathKeyRanges() {
if (this.pathKeyRanges == null) {
final int numPClasses = this.pdb.pclasses.size();
final KeyRanges[] array = new KeyRanges[this.currentTypesList.size()];
for (int i = 0; i < this.currentTypesList.size(); i++) {
// Get the current types at this step
final Set> pclasses = this.currentTypesList.get(i);
// If every model type plus UntypedPermazenObject is possible, then no filter is needed
if (pclasses.size() > numPClasses)
continue;
// Restrict to the specific PermazenClass's ranges
final ArrayList ranges = new ArrayList<>(pclasses.size());
for (PermazenClass> pclass : pclasses)
ranges.add(ObjId.getKeyRange(pclass.storageId));
// Build filter
array[i] = new KeyRanges(ranges);
}
this.pathKeyRanges = array;
}
return this.pathKeyRanges;
}
// Utility methods
private static Stream> toClasses(Set> pclasses) {
return pclasses.stream()
.map(pclass -> pclass != null ? pclass.type : UntypedPermazenObject.class);
}
// Cursor
/**
* A cursor position in a partially parsed reference path.
*
*
* Instances are immutable.
*/
final class Cursor {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final int stepsSoFar; // number of previous steps/cursors
private final Cursor previousCursor; // previous step's cursor (null if none)
private final int previousStorageId; // previous step's storage ID (zero if none)
private final PermazenClass> pclass; // current step's PermazenClass, or null for UntypedPermazenObject
private final boolean singular; // true if path is singular so far
// Constructor for initial cursors
private Cursor(PermazenClass> pclass) {
this(null, 0, pclass, true);
}
// Constructor for additional cursors
private Cursor(Cursor previousCursor, int previousStorageId, PermazenClass> pclass, boolean singular) {
this.stepsSoFar = previousCursor != null ? previousCursor.stepsSoFar + 1 : 0;
this.previousCursor = previousCursor;
this.previousStorageId = previousStorageId;
this.pclass = pclass;
this.singular = singular;
}
// Get the number of previous steps/cursors
public int getStepsSoFar() {
return this.stepsSoFar;
}
// Get the reference field storage ID's corresponding to all previous steps, negated for inverse steps
public int[] getStorageIds() {
return Stream.concat(this.streamPrevious(), Stream.of(this))
.skip(1)
.mapToInt(cursor -> cursor.previousStorageId)
.toArray();
}
// Get the current object type for all previous steps plus this next one
public List> getCurrentTypesList() {
return Stream.concat(this.streamPrevious().map(cursor -> cursor.pclass), Stream.of(this.pclass))
.collect(Collectors.toList());
}
// Does this path only return ever a single object?
public boolean isSingular() {
return this.singular;
}
// Stream the previous cursors in order
private Stream streamPrevious() {
final Cursor[] cursors = new Cursor[this.stepsSoFar];
int index = this.stepsSoFar;
for (Cursor cursor = this.previousCursor; cursor != null; cursor = cursor.previousCursor)
cursors[--index] = cursor;
return Stream.of(cursors);
}
/**
* Advance through the given step and return the resulting new cursors.
*
* @param inverse true for inverse step
* @param step step to advance without prefix
* @return resulting cursors
* @throws IllegalArgumentException if step is invalid
*/
public Set advance(boolean inverse, String step) {
// Sanity check
Preconditions.checkArgument(step != null, "null step");
final String errorPrefix = String.format("invalid %s step \"%s\"", inverse ? "inverse" : "forward", step);
// Parse next step
if (this.log.isTraceEnabled())
this.log.trace("RefPath.advance(): next step: {} \"{}\"", inverse ? "inverse" : "forward", step);
// Resolve the next step
FieldResolution nextStep = this.resolveStep(errorPrefix, inverse, step);
final Set> nextPClasses = nextStep.types();
final PermazenReferenceField nextPField = nextStep.field();
// Get the storage ID representing this step, negated for inverse steps
final int storageId = inverse ? -nextPField.storageId : nextPField.storageId;
// Can the new cursor produce multiple objects? Yes unless we've only seen forward simple references so far.
final boolean nextSingular = this.singular && !inverse && nextPField.getParentField() == null;
// Debug
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath.advance(): nextPField={} storageId={} nextSingular={} nextPClasses={}",
nextPField, storageId, nextSingular, debugFormat(nextPClasses));
}
// Create new cursor(s)
final HashSet newCursors = new HashSet<>(3);
for (PermazenClass> nextPClass : nextPClasses)
newCursors.add(new Cursor(this, storageId, nextPClass, nextSingular));
assert !newCursors.isEmpty();
// Done
if (this.log.isTraceEnabled())
this.log.trace("RefPath.advance(): result={}", debugFormat(newCursors));
return newCursors;
}
// Resolve the next step. This is complicated because there are lots of possibilities because a dot "." can separate
// two Java package/class name components, a type name and a field name, or a field name and a sub-field name.
private FieldResolution resolveStep(String errorPrefix, final boolean inverse, final String step) {
// Are there any dots? If not, this is easy.
final int dot1 = step.lastIndexOf('.'); // the very last dot
if (dot1 == -1) {
// Inverse steps must be qualified
if (inverse)
throw new IllegalArgumentException(errorPrefix);
// It must be an unqualified forward step, i.e., simple field name
return this.resolveUnqualifiedStep(errorPrefix, step);
}
// If there is only one dot, this is a forward step, and the step matches an unqualified field name, then use it
final int dot2 = step.lastIndexOf('.', dot1 - 1);
if (dot2 == -1 && !inverse) {
try {
return this.resolveUnqualifiedStep(errorPrefix, step);
} catch (IllegalArgumentException e) {
// Nope, so from now on assume forward steps are qualified
}
}
// What is the current type?
final Class> currentType = Optional.ofNullable(this.pclass)
.>map(PermazenClass::getType)
.orElse(UntypedPermazenObject.class);
// For forward steps, the TypeName qualifier must be restricted to the current type
final Class> upperBound = !inverse ? currentType : null;
// Resolve this qualified step
FieldResolution nextStep = this.resolveQualifiedStep(errorPrefix, step, upperBound);
// For inverse steps, verify that the reference field can actually refer to the current type
if (inverse && !nextStep.field().typeToken.getRawType().isAssignableFrom(currentType)) {
throw new IllegalArgumentException(String.format(
"%s: %s can't refer to %s", errorPrefix, nextStep.field(), currentType));
}
// For forward steps, the next set of current types derives from what types the reference field can point to
if (!inverse)
nextStep = new FieldResolution(ReferencePath.this.pdb, nextStep.field());
// Done
return nextStep;
}
// Resolve an unqualified step like "[fieldName]", where "fieldName" is a simple field, or possibly a complex sub-field.
private FieldResolution resolveUnqualifiedStep(String errorPrefix, final String fieldName) {
// Handle the UntypedPermazenObject case
if (this.pclass == null) {
throw new IllegalArgumentException(String.format(
"%s: field \"%s\" not found in %s", errorPrefix, fieldName, UntypedPermazenObject.class));
}
// Find the named field
final PermazenReferenceField field;
try {
field = (PermazenReferenceField)Util.findSimpleField(this.pclass, fieldName);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(String.format("%s: %s", errorPrefix, e.getMessage()), e);
} catch (ClassCastException e) {
throw new IllegalArgumentException(String.format(
"%s: field \"%s\" in %s is not a reference field", errorPrefix, fieldName, this.pclass));
}
if (field == null) {
throw new IllegalArgumentException(String.format(
"%s: field \"%s\" not found in %s", errorPrefix, fieldName, this.pclass));
}
// Done
return new FieldResolution(ReferencePath.this.pdb, field);
}
// Resolve a qualified step like "[typeName].[fieldName]", where "typeName" could be either a schema object
// name or fully qualified Java class name, and "fieldName" is a simple field, or possibly a complex sub-field.
private FieldResolution resolveQualifiedStep(String errorPrefix, final String step, Class> upperBound) {
// Find the last dot
final int dot1 = step.lastIndexOf('.');
if (dot1 == -1) {
throw new IllegalArgumentException(String.format(
"%s: field name must be qualified with a type name", errorPrefix));
}
// Parse into TypeName and fieldName, but note the ambiguity: "foo.bar.key" could
// mean either { type="foo", field="bar.key" } or { type="foo.bar", field="key" }.
// If field name has three or more components, try interpreting it both ways.
final int dot2 = step.lastIndexOf('.', dot1 - 1);
final FieldResolution try1 = this.resolveQualifiedStep(errorPrefix,
step.substring(0, dot1), step.substring(dot1 + 1), upperBound);
final FieldResolution try2 = dot2 != -1 ?
this.resolveQualifiedStep(errorPrefix, step.substring(0, dot2), step.substring(dot2 + 1), upperBound) : null;
if (this.log.isTraceEnabled())
this.log.trace("RefPath.advance(): qualified candidates: try1={} try2={} bound={}", try1, try2, upperBound);
// Anything found?
if (try1 == null && try2 == null) {
throw new IllegalArgumentException(String.format(
"%s: no such type and/or reference field found%s",
errorPrefix, upperBound != null ? String.format(" in the context of %s", upperBound) : ""));
}
// Verify two different interpretations are consistent (unlikely!)
if (try1 != null && try2 != null && !try1.isConsistentWith(try2)) {
throw new IllegalArgumentException(String.format(
"%s: ambiguous reference; matched %s in %s and %s in %s",
errorPrefix, try1.field(), try1.types(), try2.field(), try2.types()));
}
// Done
return try1 != null ? try1 : try2;
}
// Resolve a qualified step "[typeName].[fieldName]" where "typeName" and "fieldName" are given.
// The upperBound, if any, applies to the type name.
private FieldResolution resolveQualifiedStep(String errorPrefix, String typeName, String fieldName, Class> upperBound) {
// Try resolving type name as a schema model object type
final Class> modelType = Optional.of(ReferencePath.this.pdb.pclassesByName)
.map(map -> map.get(typeName))
.map(PermazenClass::getType)
.orElse(null);
// Try resolving type name as a regular Java class
Class> javaType = null;
try {
javaType = Class.forName(typeName, false, ReferencePath.this.pdb.loader.getParent());
} catch (ClassNotFoundException e) {
// ignore
}
// Anything matched?
if (modelType == null && javaType == null)
return null;
// Check for ambiguity
if (modelType != null && javaType != null && modelType != javaType) {
throw new IllegalArgumentException(String.format(
"%s: ambiguous type name \"{}\" matches both %s and %s", errorPrefix, typeName, modelType, javaType));
}
// Get the type we found
if (javaType == null)
javaType = modelType;
// Find the field, gather matching PermazenClass's, and verify field appears consistently
PermazenReferenceField field = null;
final HashSet> pclasses = new HashSet<>();
for (PermazenClass> nextPClass : ReferencePath.this.pdb.getPermazenClasses(javaType)) {
// Apply upper bound, if any
if (upperBound != null && !upperBound.isAssignableFrom(nextPClass.getType()))
continue;
// Find the reference field
final PermazenReferenceField candidateField;
try {
candidateField = (PermazenReferenceField)Util.findSimpleField(nextPClass, fieldName);
} catch (IllegalArgumentException | ClassCastException e) {
continue;
}
if (candidateField == null)
continue;
// Check for ambiguity
if (field == null)
field = candidateField;
else if (!candidateField.getSchemaId().equals(field.getSchemaId())) {
throw new IllegalArgumentException(String.format(
"%s: ambiguous field \"%s\" in %s matches both %s and %s",
errorPrefix, fieldName, javaType, field, candidateField));
}
// Add pclass
pclasses.add(nextPClass);
}
// Return what we found, if anything
return field != null ? new FieldResolution(pclasses, field) : null;
}
// FieldResolution
private record FieldResolution(Set> types, PermazenReferenceField field) {
FieldResolution(Permazen pdb, PermazenReferenceField field) {
this(new HashSet<>(pdb.getPermazenClasses(field.typeToken.getRawType())), field);
}
boolean isConsistentWith(FieldResolution that) {
return this.types().equals(that.types()) && this.field().getSchemaId().equals(that.field().getSchemaId());
}
}
// 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 Objects.equals(this.previousCursor, that.previousCursor)
&& this.previousStorageId == that.previousStorageId
&& Objects.equals(this.pclass, that.pclass)
&& this.singular == that.singular;
}
@Override
public int hashCode() {
return this.getClass().hashCode()
^ Objects.hashCode(this.previousCursor)
^ Integer.hashCode(this.previousStorageId)
^ Objects.hashCode(this.pclass)
^ Boolean.hashCode(this.singular);
}
@Override
public String toString() {
return "Cursor"
+ "[stepsSoFar=" + this.stepsSoFar
+ (this.previousCursor != null ? ",previousStorageId=" + this.previousStorageId : "")
+ ",pclass=" + this.pclass
+ ",singular=" + this.singular
+ "]";
}
}
}