org.jsimpledb.ReferencePath Maven / Gradle / Ivy
/*
* 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.Iterables;
import com.google.common.reflect.TypeToken;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.jsimpledb.util.CastFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Reference paths.
*
*
* A Reference path consists of:
*
* - A {@linkplain #getStartType starting Java object type},
* - A final {@linkplain #getTargetType target object type} and {@linkplain #getTargetField target field}
* within that type, and
* - An intermediate chain of zero or more {@linkplain #getReferenceFields reference fields} through which an
* instance of the target type can be reached from an instance of the starting type.
*
*
*
* Reference paths can be described by the combination of (a) the starting Java object type, and (b) the path of reference
* fields and final target field in a {@link String} form consisting of field names separated by period ({@code "."}) characters.
* All of the fields except the last must be reference fields.
*
*
* For example, path {@code "parent.age"} starting from object type {@code Person} might refer to the age of
* the parent of a {@code Person}.
*
*
* When a complex field appears in a reference path, both the name of the complex field and the specific sub-field
* being traversed should appear, e.g., {@code "mymap.key.somefield"}. For set and list fields, the (only) sub-field is
* named {@code "element"}, while for map fields the sub-fields are named {@code "key"} and {@code "value"}.
*
*
* Fields of Sub-Types
*
*
* In some cases, a field may not exist in a Java object type, but it does exist in a some sub-type of that type. For example:
*
*
* @JSimpleClass(storageId = 10)
* public class Person {
*
* @JSimpleSetField(storageId = 11)
* public abstract Set<Person> getFriends();
*
* @OnChange("friends.element.name")
* private void friendNameChanged(SimpleFieldChange<NamedPerson, String> change) {
* // ... do whatever
* }
* }
*
* @JSimpleClass(storageId = 20)
* public class NamedPerson extends Person {
*
* @JSimpleField(storageId = 21)
* public abstract String getName();
* public abstract void setName(String name);
* }
*
* Here the path {@code "friends.element.name"} is technically incorrect because {@code "friends.element"}
* has type {@code Person}, and {@code "name"} is a field of {@code NamedPerson}, not {@code Person}. However, this will
* still work as long as there is no ambiguity, i.e., in this example, there are no other sub-types of {@code Person}
* with a field named {@code "name"}. Note also in the example above the
* {@link org.jsimpledb.change.SimpleFieldChange} parameter to the method
* {@code friendNameChanged()} necessarily has generic type {@code NamedPerson}, not {@code Person}.
*
*
* In 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"}.
*
*
* Reference paths are created via {@link JSimpleDB#parseReferencePath JSimpleDB.parseReferencePath()}.
*
* @see JSimpleDB#parseReferencePath JSimpleDB.parseReferencePath()
*/
public class ReferencePath {
final Class> startType;
final Class> targetType;
final JFieldInfo targetFieldInfo;
final JComplexFieldInfo targetSuperFieldInfo;
final ArrayList referenceFieldInfos = new ArrayList<>();
final String path;
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 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 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 path} is invalid
*/
ReferencePath(JSimpleDB jdb, Class> startType, String path, 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 + "': ";
if (path.length() == 0)
throw new IllegalArgumentException(errorPrefix + "path is empty");
this.startType = startType;
this.path = path;
// Split the path into field names
final ArrayDeque fieldNames = new ArrayDeque<>();
while (true) {
final int dot = path.indexOf('.');
if (dot == -1) {
if (path.length() == 0)
throw new IllegalArgumentException(errorPrefix + "ends in `.'");
fieldNames.add(path);
break;
}
if (dot == 0)
throw new IllegalArgumentException(errorPrefix + "contains an empty path component");
fieldNames.add(path.substring(0, dot));
path = path.substring(dot + 1);
}
// Initialize loop state
Class> currentType = this.startType;
JFieldInfo fieldInfo = null;
JComplexFieldInfo superFieldInfo = null;
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath: START: startType=" + this.startType + " path=" + fieldNames
+ " lastIsSubField=" + lastIsSubField);
}
// Parse field names
assert !fieldNames.isEmpty();
while (!fieldNames.isEmpty()) {
final String fieldName = fieldNames.removeFirst();
String description = "field `" + fieldName + "' in type " + currentType;
// Get 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(errorPrefix + "invalid field name `" + fieldName + "'");
}
searchName = fieldName.substring(0, hash);
} else
searchName = fieldName;
// Logging
if (this.log.isTraceEnabled())
this.log.trace("RefPath: [" + searchName + "] currentType=" + currentType + " storageId=" + explicitStorageId);
// Find all JFields matching 'fieldName' in some JClass whose type matches 'currentType'
final HashMap, JField> matchingFields = new HashMap<>();
for (JClass> jclass : jdb.getJClasses(currentType)) {
final JField jfield = jclass.jfieldsByName.get(searchName);
if (jfield == null)
continue;
if (explicitStorageId != 0 && jfield.storageId != explicitStorageId)
continue;
matchingFields.put(jclass, jfield);
}
// Logging
if (this.log.isTraceEnabled())
this.log.trace("RefPath: matching fields: " + matchingFields);
// Check matching fields and verify they all have the same storage ID
if (matchingFields.isEmpty()) {
throw new IllegalArgumentException(errorPrefix + "there is no field named `" + searchName + "'"
+ (explicitStorageId != 0 ? " with storage ID " + explicitStorageId : "")
+ " in (any sub-type of) " + currentType);
}
final int fieldStorageId = matchingFields.values().iterator().next().storageId;
for (JField jfield : matchingFields.values()) {
if (jfield.storageId != fieldStorageId) {
throw new IllegalArgumentException(errorPrefix + "there are multiple, non-equal fields named `"
+ fieldName + "' in sub-types of type " + currentType);
}
}
fieldInfo = jdb.getJFieldInfo(fieldStorageId, JFieldInfo.class);
// Get common supertype of all types containing the field
currentType = Util.findLowestCommonAncestorOfClasses(
Iterables.transform(matchingFields.keySet(), new JClassTypeFunction())).getRawType();
// Logging
if (this.log.isTraceEnabled())
this.log.trace("RefPath: updated currentType=" + currentType + " fieldType=" + fieldInfo.getTypeToken(currentType));
// Handle complex fields
superFieldInfo = null;
if (fieldInfo instanceof JComplexFieldInfo) {
final JComplexFieldInfo complexFieldInfo = (JComplexFieldInfo)fieldInfo;
// Last field?
if (fieldNames.isEmpty()) {
if (lastIsSubField != null && lastIsSubField) {
throw new IllegalArgumentException(errorPrefix + "path may not end on complex " + description
+ "; a sub-field of must be specified");
}
break;
}
superFieldInfo = complexFieldInfo;
// Get sub-field
final String subFieldName = fieldNames.removeFirst();
description = "sub-field `" + subFieldName + "' of complex " + description;
try {
fieldInfo = complexFieldInfo.getSubFieldInfo(subFieldName);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(errorPrefix + "invalid " + description + ": " + e.getMessage(), e);
}
// Update matching fields to match sub-field
for (Map.Entry, JField> entry : matchingFields.entrySet())
entry.setValue(((JComplexField)entry.getValue()).getSubField(subFieldName));
// Logging
if (this.log.isTraceEnabled())
this.log.trace("RefPath: [" + searchName + "." + subFieldName + "] new matching fields=" + matchingFields);
}
// Last field?
if (fieldNames.isEmpty()) {
if (superFieldInfo != null && Boolean.FALSE.equals(lastIsSubField)) {
throw new IllegalArgumentException(errorPrefix + "path may not end on " + description
+ "; specify the complex field itself instead");
}
break;
}
// Field is not last, so it must be a reference field
if (!(fieldInfo instanceof JReferenceFieldInfo))
throw new IllegalArgumentException(errorPrefix + description + " is not a reference field");
final JReferenceFieldInfo referenceFieldInfo = (JReferenceFieldInfo)fieldInfo;
// Add reference field to the reference field list
this.referenceFieldInfos.add(referenceFieldInfo);
// Advance through the reference, using the narrowest type possible
currentType = Util.findLowestCommonAncestor(Iterables.transform(
Iterables.transform(matchingFields.values(), new CastFunction(JReferenceField.class)),
new JReferenceFieldTypeFunction())).getRawType();
// Logging
if (this.log.isTraceEnabled())
this.log.trace("RefPath: step through reference, new currentType=" + currentType);
}
// Done
this.targetType = currentType;
this.targetFieldInfo = fieldInfo;
this.targetSuperFieldInfo = superFieldInfo;
// Logging
if (this.log.isTraceEnabled()) {
this.log.trace("RefPath: DONE: targetType=" + this.targetType + " targetFieldInfo=" + this.targetFieldInfo
+ " targetSuperFieldInfo=" + this.targetSuperFieldInfo + " targetFieldType=" + this.getTargetFieldType()
+ " references=" + this.referenceFieldInfos);
}
}
/**
* 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.
*
*
* If there are zero {@linkplain #getReferenceFields reference fields} in this path, then this will
* equal 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.
*
* @return the Java type at which this reference path ends
*/
public Class> getTargetType() {
return this.targetType;
}
/**
* Get the Java type corresponding to the field at which this path ends.
*
* @return the type of the field at which this reference path ends
*/
public TypeToken> getTargetFieldType() {
return this.targetFieldInfo.getTypeToken(this.targetType);
}
/**
* Get the storage ID associated with the target field 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
*/
public int getTargetField() {
return this.targetFieldInfo.storageId;
}
/**
* Get the storage ID associated with the complex field containing the {@linkplain #getTargetField target field}
* a sub-field, 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
*/
public int getTargetSuperField() {
return this.targetSuperFieldInfo != null ? this.targetSuperFieldInfo.storageId : 0;
}
/**
* Get the storage IDs of the reference fields in this path.
*
*
* 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 reference field storage IDs
*/
public int[] getReferenceFields() {
final int[] storageIds = new int[this.referenceFieldInfos.size()];
for (int i = 0; i < storageIds.length; i++)
storageIds[i] = this.referenceFieldInfos.get(i).storageId;
return storageIds;
}
/**
* Get the {@link String} form of the path associated with this instance.
*/
@Override
public String toString() {
return this.path;
}
// Functions
private static class JReferenceFieldTypeFunction implements Function> {
@Override
public TypeToken> apply(JReferenceField jfield) {
return jfield.typeToken;
}
}
private static class JClassTypeFunction implements Function, Class>> {
@Override
public Class> apply(JClass> jclass) {
return jclass.type;
}
}
}