package tc.oc.pgm.features; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NavigableSet; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Ordering; import com.google.common.reflect.TypeToken; import gnu.trove.list.TIntList; import org.jdom2.Element; import tc.oc.commons.core.ListUtils; import tc.oc.commons.core.collection.CountingStringMap; import tc.oc.commons.core.inspect.Inspectable; import tc.oc.commons.core.reflect.Methods; import tc.oc.commons.core.reflect.Types; import tc.oc.commons.core.util.CacheUtils; import tc.oc.commons.core.util.CachingMethodHandleInvoker; import tc.oc.commons.core.util.Comparables; import tc.oc.commons.core.util.Optionals; import tc.oc.commons.core.util.ProxyUtils; import tc.oc.commons.core.util.ThrowingRunnable; import tc.oc.commons.core.util.Utils; import tc.oc.pgm.map.inject.MapScoped; import tc.oc.pgm.utils.XMLUtils; import tc.oc.pgm.xml.InvalidXMLException; import tc.oc.pgm.xml.Node; import tc.oc.pgm.xml.validate.Validatable; import tc.oc.pgm.xml.validate.Validation; import static com.google.common.base.Preconditions.*; import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowConsumer; import static tc.oc.pgm.features.FeatureDefinition.getFeatureName; /** * Store of {@link FeatureDefinition}s, supporting lookup by ID and type, and forward references through dynamic proxies. * * Every feature in a map is added to this context, with or without an ID. The right way to retrieve all features * of a particular type is through the {@link #all(Class)} method on this class. * * Features are added to the context through one of the {@link #define} methods. This can optionally include a * source {@link Element} which may be used for debugging and error messages. * * Features with an ID can be accessed through forward references using one of the {@link #reference} methods. * These methods can be called before the feature is defined. They will return a dynamic proxy of the specified * interface type that will delegate to the feature once it is defined. * * Validations can be registered through the {@link #validate} method, which accepts defined features or forward * reference proxies. * * After parsing, the {@link #postParse} method is called, which checks that all references resolve to a defined * feature that implements the interface specified by the reference. Then, all validations run. * * A feature with an ID is *always* accessed through a dynamic proxy. When an ID is given, the {@link #define} * method returns a proxy, just like {@link #reference}, and that proxy should replace the original object for * all purposes. This is necessary so that equality testing works in all cases. Referenced features must be * tested for equality using only their IDs, since that is the only thing known about them when a forward * reference is created. But {@link FeatureDefinition}s in general do not now their own ID, so even when the * feature is defined, it needs to be wrapped in a proxy that can lookup its ID in the context. * * This also means that equality testing on features *must* be done through {@link #equals}, and never with * the == operator. Proxies for the same feature might compare equal with == or they might not. * * Every method that returns a feature instance from the context requires the return interface type to be * specified explicitly. If the feature has an ID, then this type must be an interface, so that it can be * proxied. Due to forward references, the context may have to create proxies for features with only vague * knowledge of their final type e.g. GoalDefinition. This is why the type needs to be specified. * * Proxies always implement {@link FeatureProxy}, in addition to the requested type. This can be used to * detect them, and to get their ID. * * Trying to define a feature multiple times (i.e. passing the same object to {@link #define} multiple times) * will generate an exception. As such, it is important that parsers never try to reuse feature objects. * Every definition must be a unique object instance, even if they are otherwise identical. */ @MapScoped public class FeatureDefinitionContext implements FeatureValidationContext, Comparator<FeatureDefinition> { // List of all Records of any type private final Set<Record<?>> records = new HashSet<>(); // Defined records ordered lexically (i.e. order of their source Elements) private final NavigableSet<Record<?>> lexical = new TreeSet<>(); // All records, indexed by definition. An IdentityHashMap is used to bypass // the equality logic used by proxies. private final Map<FeatureDefinition, Record> byDefinition = new IdentityHashMap<>(); // IdentifiedRecords, indexed by ID private final Map<String, IdentifiedRecord> byId = new HashMap<>(); // IdentifiedRecords and SluggedFeatureDefinition records, indexed by final slug, // which may be altered to make it unique. private final CountingStringMap<Record> bySlug = new CountingStringMap<>(1000, "--"); private final List<Validatable> validatables = new ArrayList<>(); @Inject private FeatureDefinitionContext() {} public enum Phase { PARSE, VALIDATE, FINISHED } private Phase phase = Phase.PARSE; public Phase phase() { return phase; } private void assertParsingComplete() { if(!Comparables.greaterThan(phase, Phase.PARSE)) { throw new IllegalStateException("Cannot perform this operation before parsing is complete"); } } private @Nullable Record recordFor(FeatureDefinition feature) { return feature instanceof FeatureProxy ? byId.get(((FeatureProxy) feature).getId()) : byDefinition.get(feature); } private Record needRecord(FeatureDefinition feature) { final Record record = recordFor(feature); if(record == null) { throw new IllegalStateException("No record of feature " + feature); } return record; } public @Nullable <T extends FeatureDefinition> T get(String id, Class<T> type) { return get(id, TypeToken.of(type)); } /** * Return a feature with the given ID and type, or null if no such feature exists. * If the ID exists but is the wrong type, this method will still return null. */ public @Nullable <T extends FeatureDefinition> T get(String id, TypeToken<T> type) { final IdentifiedRecord record = byId.get(id); if(record != null && record.assignableTo(type)) { return (T) record.direct(); } return null; } public Stream<? extends FeatureDefinition> all() { return lexical.stream().map(Record::direct); } /** * Return all defined features of the given type, in the order they were defined. */ public <T extends FeatureDefinition> Stream<T> all(Class<T> type) { return all(TypeToken.of(type)); } public <T extends FeatureDefinition> Stream<T> all(TypeToken<T> type) { return lexical.stream() .filter(record -> record.assignableTo(type)) .map(record -> (T) record.direct()); } /** * Test if the context contains any features of the given type */ public boolean containsAny(Class<? extends FeatureDefinition> type) { return lexical.stream() .anyMatch(record -> record.assignableTo(TypeToken.of(type))); } /** * Get a guaranteed unique slug for the given feature. * * Must not be called until after parsing. */ public String slug(SluggedFeatureDefinition feature) { assertParsingComplete(); final String slug = needRecord(feature).slug(); if(slug == null) { throw new IllegalStateException("Cannot generate a slug for " + feature.identify()); } return slug; } /** * Return the XML source element that defines the given feature, * or null if the feature was defined without a source element. * * @throws IllegalStateException if given a proxy for an undefined feature */ public @Nullable Element definitionNode(FeatureDefinition feature) { final Record record = recordFor(feature); return record == null ? null : record.definitionNode(); } public @Nullable Node sourceNode(FeatureDefinition feature) { return feature instanceof FeatureReference ? ((FeatureReference) feature).referenceNode() : Node.fromNullable(definitionNode(feature)); } public String describeWithLocation(FeatureDefinition feature) { final Element node = definitionNode(feature); if(node == null) { return feature.getFeatureName(); } else { return feature.getFeatureName() + " [" + new Node(node).describeWithLocation() + "]"; } } @Override public int compare(FeatureDefinition a, FeatureDefinition b) { return needRecord(a).compareTo(needRecord(b)); } @Override public <T extends Validatable> T validate(T validatable) throws InvalidXMLException { switch(phase) { case PARSE: validatables.add(validatable); break; case VALIDATE: validatable.validate(); break; default: throw new IllegalStateException("Cannot validate in " + phase + " phase"); } return validatable; } @Override public <T extends FeatureDefinition> T validate(T feature, @Nullable Node source, Stream<Validation<? super T>> validations) { final Node node = source != null ? source : sourceNode(feature); validations.forEach(rethrowConsumer(validation -> { final Validatable validatable = validation.bind(node).compose(() -> (T) feature.getDefinition()); validate(validatable); })); return feature; } /** * Define the given feature with no source element or ID, and return it. * * Since the feature has no ID, a proxy is never returned. */ public FeatureDefinition define(FeatureDefinition definition) throws InvalidXMLException { return define((Element) null, definition); } public <T extends FeatureDefinition> T define(Class<T> type, T definition) throws InvalidXMLException { return define(null, type, definition); } /** * Define the given feature with an optional ID, and no source node. * * This method does nothing if the given feature is a proxy, or is already defined. * * @return a proxy which must be used to access the feature */ public FeatureDefinition define(@Nullable String id, FeatureDefinition definition) throws InvalidXMLException { return define(null, id, definition).direct(); } /** * Define the given feature with an optional source element, and return it as a {@link FeatureDefinition} * (which may be a proxy, if the source element has an ID attribute). */ public FeatureDefinition define(@Nullable Element source, FeatureDefinition definition) throws InvalidXMLException { return define(source, FeatureDefinition.class, definition); } /** * Define the given feature with an optional source element. * * The ID of the feature is parsed from the 'id' attribute of the given element, if present. * * An object of the given type is returned, which should be used to access the feature from now on. * If the feature has an ID, the type must be an interface, and the returned object will be a proxy. * * If the type is not an interface, and the source element has an ID element, then an * {@link InvalidXMLException} will be thrown. * * This method does nothing if the given feature is a proxy, or is already defined. */ public <T extends FeatureDefinition> T define(@Nullable Element source, Class<T> type, T impl) throws InvalidXMLException { return define(source, source == null ? null : source.getAttributeValue("id"), type, impl); } public <T extends FeatureDefinition> T define(@Nullable Element source, @Nullable String id, Class<T> type, T impl) throws InvalidXMLException { if(id != null && !type.isInterface()) { // Can't proxy a non-interface type, so no references and no IDs throw new IllegalArgumentException("Cannot assign an ID to a " + impl.getFeatureName()); } return define(source, id, impl).direct(); } private <T extends FeatureDefinition> Record<T> define(@Nullable Element source, @Nullable String id, T definition) throws InvalidXMLException { if(definition instanceof FeatureProxy) { throw new IllegalArgumentException("Attempt to define proxy " + definition); } final Record<?> existing = byDefinition.get(definition); if(existing != null) { // Allow redefinition only with identical parameters if(Objects.equals(id, existing.id()) && Objects.equals(source, existing.definitionNode())) return (Record<T>) existing; throw new IllegalArgumentException("Attempted redefinition of " + definition); } final Record<T> record; if(id == null) { // If feature has no ID, return a new anonymous record record = new AnonymousRecord<>(source, definition); } else { validateId(Node.fromNullable(source), id); // If there is an ID, get or create an identified record final IdentifiedRecord identified = byId.computeIfAbsent(id, IdentifiedRecord::new); record = (Record<T>) identified; // If the record is not defined yet, do that now, otherwise verify that // the new definition is the same as the existing one. if(!identified.isDefined()) { identified.define(source, definition); } else if(identified.definition() != definition) { throw new InvalidXMLException("The ID '" + id + "' is already in use by a " + identified.featureName(), source); } } // Ensure all of the feature's dependencies are also defined. If any have not // yet been defined, they will be defined now with no ID or source Element, and // any later attempt to redefine them with either of those will cause an error. definition.dependencies().forEach(rethrowConsumer(dep -> { if(!(dep instanceof FeatureProxy || byDefinition.containsKey(dep))) { define(dep); } })); // Let the feature register validations definition.validate(this); // And register the feature itself if it is a Validatable if(definition instanceof Validatable) { validate((Validatable) definition); } return record; } /** * Return a proxy for the feature with the given ID and type, which must be an interface. * * The returned object will implement the given interface, as well as {@link FeatureProxy}. * This can be used to distinguish proxies from real features. * * The proxy generally must not be accessed until after the post-parse phase, * when references are resolved. However, it is safe to call {@link #equals} * {@link #hashCode}, and {@link #toString}. * * @param source The XML node that references the feature * @param type The type required by the reference * @return An object implementing the requested interface, that delegates to * the feature, once it is defined */ public <T extends FeatureDefinition> T reference(Node source, String id, Class<T> type) throws InvalidXMLException { return byId.computeIfAbsent(checkNotNull(id), IdentifiedRecord::new) .reference(type, checkNotNull(source)); } public <T extends FeatureDefinition> T reference(Node source, Class<T> type) throws InvalidXMLException { return reference(source, source.getValueNormalize(), type); } private void detectDependencyCycles(FeatureDefinition feature, List<FeatureDefinition> stack) throws InvalidXMLException { final int i = stack.indexOf(feature); if(i >= 0) { throw new InvalidXMLException("Dependency cycle detected: " + Stream.concat(stack.subList(i, stack.size()).stream(), Stream.of(feature)) .map(this::describeWithLocation) .collect(Collectors.joining(" -> ")), definitionNode(feature)); } final List<FeatureDefinition> subStack = ListUtils.append(stack, feature); feature.dependencies(FeatureDefinition.class).forEach(rethrowConsumer(dep -> detectDependencyCycles(dep, subStack))); } /** * Resolve all references and run all validations */ public Collection<InvalidXMLException> postParse() { phase = Phase.VALIDATE; final List<InvalidXMLException> errors = new ArrayList<>(); try { records.forEach(r -> r.validateReferences(errors)); if(!errors.isEmpty()) return errors; records.forEach(r -> r.validateDependencies(errors)); if(!errors.isEmpty()) return errors; collectErrors(errors, null, () -> { for(Validatable validatable : validatables) { validate(validatable); } }); if(!errors.isEmpty()) return errors; records.forEach(Record::validateSlug); return errors; } finally { phase = Phase.FINISHED; } } private void collectErrors(Collection<InvalidXMLException> errors, @Nullable FeatureDefinition feature, ThrowingRunnable<InvalidXMLException> runnable) { try { runnable.runThrows(); } catch(InvalidXMLException e) { if(e.getNode() == null && feature != null) { e.offerNode(sourceNode(feature)); } errors.add(e); } } private void validateId(@Nullable Node source, String id) throws InvalidXMLException { if(id.length() == 0) { throw new InvalidXMLException("ID cannot be blank", source); } } private abstract class Record<F extends FeatureDefinition> implements Comparable<Record<?>> { @Nullable F definition; @Nullable String slug; @Nullable Element source; @Nullable TIntList path; public Record() { records.add(this); } abstract @Nullable String id(); abstract F direct(); abstract @Nullable String defaultSlug(); F definition() { assertDefined(); return definition; } boolean isDefined() { return definition != null; } void assertDefined() { if(!isDefined()) { final String id = id(); throw new IllegalStateException("Cannot access undefined " + featureName() + (id == null ? "" : " with ID " + id)); } } @Nullable String slug() { return slug; } void define(@Nullable Element source, F definition) throws InvalidXMLException { checkState(!isDefined()); this.definition = checkNotNull(definition); if(source != null) this.source = source; // Find the lexical path of the source Element if(this.source != null) { this.path = XMLUtils.indexPath(this.source); } // Index by definition and source location byDefinition.put(definition, this); lexical.add(this); } /** * Records are sorted by lexical position of their definition Element. * Records without an Element are ordered before those with an Element, * and two different records NEVER compare equal. */ @Override public int compareTo(Record<?> that) { assertDefined(); if(this == that) return 0; if(this.path == null) { if(that.path == null) { return Ordering.arbitrary().compare(this, that); } else { return -1; } } else { if(that.path == null) { return 1; } else { return ListUtils.lexicalCompare(this.path, that.path); } } } @Nullable Element definitionNode() { assertDefined(); return source; } boolean assignableTo(TypeToken<? extends FeatureDefinition> type) { return isDefined() && type.getRawType().isInstance(definition()); } String featureName() { return definition().getFeatureName(); } Class<? extends FeatureDefinition> featureType() { return definition().getFeatureType(); } /** * Run validations that do not try to access any other records */ void validateReferences(Collection<InvalidXMLException> errors) {} /** * Run the remaining validations that were not run in {@link #validateReferences(Collection)} * * {@link #validateReferences(Collection)} is called on ALL records before this method is called * on ANY of them, and if any errors are generated during the former phase, the latter is not run * at all. */ void validateDependencies(Collection<InvalidXMLException> errors) { final F definition = definition(); collectErrors(errors, definition, () -> detectDependencyCycles(definition, ImmutableList.of())); } void validateSlug() { // Resolve the slug only after parsing, in case it needs to access other features final String defaultSlug = defaultSlug(); if(defaultSlug != null) { slug = bySlug.putReturningKey(defaultSlug, this); } } } /** * Record created for features defined without an ID. These features can never * be referenced, so they don't need proxies or anything fancy like that. */ private class AnonymousRecord<F extends FeatureDefinition> extends Record<F> { private AnonymousRecord(@Nullable Element source, F definition) throws InvalidXMLException { define(source, definition); } @Override @Nullable String id() { return null; } @Override @Nullable String defaultSlug() { return definition() instanceof SluggedFeatureDefinition ? ((SluggedFeatureDefinition) definition()).defaultSlug() : null; } @Override F direct() { return definition; } } /** * Record created for features defined with an ID, or for forward references. */ private class IdentifiedRecord extends Record<FeatureDefinition> { final String id; @Nullable FeatureProxy definitionProxy; final LoadingCache<ReferenceKey, Reference> referenceCache = CacheUtils.newCache(Reference::new); IdentifiedRecord(String id) { this.id = checkNotNull(id); } @Override String id() { return id; } @Override String defaultSlug() { return id(); } @Override void define(@Nullable Element source, FeatureDefinition definition) throws InvalidXMLException { super.define(source, definition); // Set the identity delegate of the feature if(!(definition instanceof FeatureDefinition.Impl)) { throw new IllegalArgumentException("Cannot assign an ID to " + definition.getClass().getName() + " because it does not extend " + FeatureDefinition.Impl.class.getName()); } final FeatureDefinition.Impl impl = (FeatureDefinition.Impl) definition; if(impl.identityDelegate != null) { throw new IllegalStateException("Identity delegate is already set for " + definition + ", probably because something called equals() or hashCode() on it" + " before it was registered with the " + FeatureDefinitionContext.class.getSimpleName()); } impl.identityDelegate = new Delegate(); } Collection<Reference> references() { return referenceCache.asMap().values(); } String featureName() { return isDefined() ? super.featureName() : FeatureDefinition.getFeatureName(featureType()); } Class<? extends FeatureDefinition> featureType() { return isDefined() ? super.featureType() : Types.commonAncestor(FeatureDefinition.class, references().stream().map(ref -> ref.key.type)).get(); } <T extends FeatureDefinition> T reference(Class<T> type, @Nullable Node source) { if(source == null && definition != null) { // If we have a definition and there is no source node, we can just // return the definition proxy instead of creating a new reference. checkArgument(type.isInstance(definition)); return (T) direct(); } else { return (T) referenceCache.getUnchecked(new ReferenceKey(type, source)).proxy; } } @Override FeatureDefinition direct() { assertDefined(); if(definitionProxy == null) { definitionProxy = ProxyUtils.newProxy(FeatureProxy.class, Types.minimalInheritedInterfaces(definition.getClass()), new Delegate()); } return (FeatureDefinition) definitionProxy; } @Override void validateReferences(Collection<InvalidXMLException> errors) { final Iterator<Reference> iter = references().iterator(); if(iter.hasNext() && !isDefined()) { // Only generate one missing reference error per ID errors.add(new InvalidXMLException("Missing " + featureName() + " with ID '" + id + "'", iter.next().referenceNode())); return; } for(Reference reference : references()) { if(!reference.referenceType().isInstance(definition())) { errors.add(new InvalidXMLException("Wrong type for ID '" + id + "': expected a " + getFeatureName(reference.referenceType()) + " rather than a " + featureName(), reference.referenceNode())); } } super.validateReferences(errors); } /** * This object is mixed-in with all proxies, and handles any method calls * that it implements, which includes all the methods of {@link FeatureProxy} * and {@link Object}. Any calls that it does not implement are forwarded * to the feature definition. */ class Delegate extends CachingMethodHandleInvoker implements FeatureProxy { @Override protected Object targetFor(Method method) { return Methods.respondsTo(this, method) ? this : definition(); } @Override @Inspect public String getId() { return id; } @Override public FeatureDefinition getDefinition() { return definition(); } @Override @Inspect(inline=true) // Append properties from definition public Optional<FeatureDefinition> tryDefinition() { return Optionals.getIf(isDefined(), IdentifiedRecord.this::definition); } @Override public String getFeatureName() { return featureName(); } @Override public Class<? extends FeatureDefinition> getFeatureType() { return featureType(); } @Override public Class<? extends FeatureDefinition> getDefinitionType() { return isDefined() ? definition().getClass() : null; } @Override public boolean isDefined() { return IdentifiedRecord.this.isDefined(); } @Override public void assertDefined() throws IllegalStateException { IdentifiedRecord.this.assertDefined(); } @Override public String inspectType() { return isDefined() ? definition().inspectType() : featureType().getSimpleName(); } @Override public Optional<String> inspectIdentity() { return Optional.of(tryDefinition().flatMap(Inspectable::inspectIdentity) .map(def -> def + ":" + id) .orElse(id)); } @Override public String toString() { return inspect(); } @Override public FeatureDefinitionContext context() { return FeatureDefinitionContext.this; } @Override public boolean equals(Object that) { if(that instanceof FeatureProxy) { // Proxies are equal if they have the same parent context and ID final FeatureProxy thatDelegate = (FeatureProxy) that; return this.context().equals(thatDelegate.context()) && this.getId().equals(thatDelegate.getId()); } else { // If the other object is not a proxy, the only other way it can // be equal is if it is the definition for this record. In that // case, it will have its identityDelegate set to some Delegate // (possibly this one) that will compare equal to this Delegate. return that != null && that.equals(this); } } @Override public int hashCode() { return Objects.hash(context(), getId()); } } class Reference extends Delegate implements FeatureReference { final ReferenceKey key; final FeatureReference proxy; Reference(ReferenceKey key) { this.key = checkNotNull(key); this.proxy = ProxyUtils.newProxy(FeatureReference.class, ImmutableSet.of(key.type), this); } @Override @Inspect(name="reference") public Node referenceNode() { return key.source; } @Override public Class<? extends FeatureDefinition> referenceType() { return key.type; } } } private static class ReferenceKey { final Class<? extends FeatureDefinition> type; final @Nullable Node source; private ReferenceKey(Class<? extends FeatureDefinition> type, @Nullable Node source) { this.type = checkNotNull(type); this.source = source; checkArgument(type.isInterface()); } @Override public int hashCode() { return Objects.hash(type, source); } @Override public boolean equals(Object obj) { return Utils.equals(ReferenceKey.class, this, obj, that -> this.type.equals(that.type) && Objects.equals(this.source, that.source) ); } } }