package tc.oc.pgm.features;
import java.util.Optional;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import tc.oc.commons.core.inspect.Inspectable;
import tc.oc.commons.core.reflect.Types;
import tc.oc.commons.core.util.Streams;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Parseable;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Used as a base-class for all templates of features. Kept around during parsing time
* so that we can easily convert a definition into a Feature for match-time, and persist
* the ID from the XML. Also stored in the {@link FeatureDefinitionContext}
* so that we don't collide IDs.
*
* @implNote FeatureDefinitions should NOT implement {@link #equals(Object)} or {@link #hashCode()}.
* Any FD that is assigned an ID will be wrapped in a {@link FeatureReference}, which implements
* equals/hashCode itself based on the ID alone. As such, FDs must always use object identity
* for equality, so that the semantics are consistent with or without an ID.
*
* FDs are free to implement {@link Comparable}, but trying to compare an unresolved proxy
* object will fail, so don't add FDs to any sorted collections before parsing is complete.
*/
public interface FeatureDefinition extends FeatureBase, Parseable {
@Property
default Optional<String> id() {
return Optional.empty();
}
/**
* Called at match load time
*/
default void load(Match match) {}
/**
* Search the ancestry of the given type for a {@link FeatureInfo} annotation,
* and return the type with the annotation. This is likely the most specific
* type implemented by the feature, and the one that should be used in error
* messages.
*/
static @Nullable Class<? extends FeatureDefinition> findFeatureType(Class<? extends FeatureDefinition> type) {
return Types.findAncestor(type, FeatureDefinition.class, t -> t.getAnnotation(FeatureInfo.class) != null);
}
/**
* Return the most specific type implemented by this feature that can be used to
* describe it to mapmakers i.e. in error mesages.
*/
@Override
default Class<? extends FeatureDefinition> getFeatureType() {
return checkNotNull(findFeatureType(getClass()));
}
@Override
default boolean isDefined() { return true; }
@Override
default void assertDefined() throws IllegalStateException {}
@Override
default FeatureDefinition getDefinition() { return this; }
@Override
default Optional<FeatureDefinition> tryDefinition() {
return Optional.of(this);
}
/**
* Concrete class of the definition object (rather than a proxy).
*
* Returns null if the feature is not defined yet.
*/
@Override
default @Nullable Class<? extends FeatureDefinition> getDefinitionType() {
return getClass();
}
default boolean isInstanceOf(Class<? extends FeatureDefinition> type) {
return type.isAssignableFrom(needDefinitionType());
}
default <T extends FeatureDefinition> T asInstanceOf(Class<T> type) {
return type.cast(getDefinition());
}
/**
* Get a readable name for the feature represented by the given type
*/
static String getFeatureName(Class<? extends FeatureDefinition> type) {
final Class<? extends FeatureDefinition> feature = findFeatureType(type);
if(feature != null) {
return feature.getAnnotation(FeatureInfo.class).name();
} else {
return type.getSimpleName();
}
}
/**
* Get a readable name for the type of this feature
*/
@Override
default String getFeatureName() {
return getFeatureName(getClass());
}
default void validate(FeatureValidationContext context) throws InvalidXMLException {}
default Stream<? extends FeatureDefinition> dependencies() {
return Stream.of();
}
default <T extends FeatureDefinition> Stream<? extends T> dependencies(Class<T> type) {
return Streams.instancesOf(dependencies(), type);
}
default <T extends FeatureDefinition> Stream<? extends T> deepDependencies(Class<T> type) {
Stream<? extends T> stream = dependencies(type).flatMap(dep -> dep.deepDependencies(type));
if(isInstanceOf(type)) {
stream = Stream.concat(Stream.of((T) this), stream);
}
return stream;
}
/**
* Unfortunately, we can't override equals in this interface to implement proxy-awareness,
* so all subclasses that can possibly be proxied must extend {@link FeatureDefinition.Impl}.
*/
abstract class Impl extends Inspectable.Impl implements FeatureDefinition {
@Nullable FeatureBase identityDelegate;
@Override
public final int hashCode() {
if(identityDelegate == null) {
identityDelegate = this;
}
return identityDelegate != this ? identityDelegate.hashCode()
: super.hashCode();
}
@Override
public final boolean equals(Object that) {
if(identityDelegate == null) {
identityDelegate = this;
}
return identityDelegate != this ? identityDelegate.equals(that)
: super.equals(that);
}
}
}