package tc.oc.pgm.xml.parser; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import java.util.stream.Stream; import javax.inject.Provider; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.inject.Binder; import com.google.inject.Key; import org.jdom2.Element; import tc.oc.commons.core.ListUtils; import tc.oc.commons.core.reflect.Types; import tc.oc.commons.core.util.AmbiguousElementException; import tc.oc.commons.core.util.DuplicateElementException; import tc.oc.pgm.features.FeatureDefinition; import tc.oc.pgm.features.FeatureParser; import tc.oc.pgm.xml.InvalidXMLException; import tc.oc.pgm.xml.Node; import tc.oc.pgm.xml.NodeSplitter; import tc.oc.pgm.xml.finder.Attributes; import tc.oc.pgm.xml.finder.EmptyChildren; import tc.oc.pgm.xml.finder.Grandchildren; import tc.oc.pgm.xml.finder.NodeFinder; import tc.oc.pgm.xml.validate.Validatable; import tc.oc.pgm.xml.validate.Validation; import tc.oc.pgm.xml.validate.ValidationContext; import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowConsumer; import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowFunction; /** * Parses a named property from an {@link Element}. * * The inner type {@link I} can be anything. The provided {@link Parser} * is used to parse instances of this type. * * The outer type {@link O} is what the parser returns. It can be the same as * the inner type, or some container of the inner type. The provided {@link Aggregator} * is used to construct the outer type. */ public class PropertyParser<O, I> { public static final List<Class<? extends NodeFinder>> DEFAULT_NODE_FINDERS = ImmutableList.of( Attributes.class, EmptyChildren.class, Grandchildren.class ); private final Provider<? extends Parser<I>> innerParser; private final Provider<ValidationContext> validationContext; private final String name; private final List<String> aliases; private final Set<String> names; private final List<Provider<? extends NodeFinder>> finders; private final Provider<? extends NodeSplitter> splitter; private final Aggregator<O, I> aggregator; private final List<Provider<Validation<? super I>>> validations; /** * @param binder Required so that this class can request injection for itself. * (we need to instantiate this at configuration time, so we can't expect to have an Injector) * @param name Name of the property (only used for error messages, does not affect parsing) * @param aliases * @param finders Used to find the {@link Node}s that {@link I} instances will be parsed from * @param splitter Used to split the value of each {@link Node}s into multiple strings that are * parsed as {@link I} instances (this only works with a {@link PrimitiveParser}) * @param aggregator Used to combine {@link I} instances into an {@link O} instance * @param validations Validations to run on each {@link I} instance */ public PropertyParser(Binder binder, String name, Iterable<String> aliases, List<Class<? extends NodeFinder>> finders, Class<? extends NodeSplitter> splitter, Aggregator<O, I> aggregator, List<Class<Validation<? super I>>> validations) { Types.assertFullySpecified(aggregator.outerTypeToken()); Types.assertFullySpecified(aggregator.innerTypeToken()); this.name = name; this.aliases = ImmutableList.copyOf(aliases); this.names = ImmutableSet.<String>builder().add(name).addAll(aliases).build(); this.aggregator = aggregator; this.finders = ListUtils.transformedCopyOf(finders, binder::getProvider); this.splitter = binder.getProvider(splitter); this.validations = ListUtils.transformedCopyOf(validations, binder::getProvider); // Figure out which base parser type we need final Class<? extends Parser> parserBase; if(Types.isAssignable(FeatureDefinition.class, aggregator.innerTypeToken())) { // If I is a FeatureDefinition, get a FeatureParser so we can parse references parserBase = FeatureParser.class; } else if(!NodeSplitter.Atom.class.isAssignableFrom(splitter)) { // If the splitter is anything besides Atom, we will need a PrimitiveParser parserBase = PrimitiveParser.class; } else { // Otherwise, get a Parser<I> parserBase = Parser.class; } this.innerParser = binder.getProvider(Key.get(Types.parameterizedTypeLiteral(parserBase, aggregator.innerTypeLiteral()))); this.validationContext = binder.getProvider(ValidationContext.class); } public String name() { return name; } public List<String> aliases() { return aliases; } public Set<String> names() { return names; } public Stream<Node> findNodes(Element parent) throws InvalidXMLException { return finders.stream().map(Provider::get).flatMap( finder -> names.stream().flatMap( name -> finder.findNodes(parent, name) ) ); } private I validate(I value, Node node) throws InvalidXMLException { final ValidationContext validationContext = this.validationContext.get(); if(value instanceof Validatable) { validationContext.validate(((Validatable) value).offeringNode(node)); } validations.forEach(rethrowConsumer(validation -> validationContext.validate(validation.get().bind(value, node)))); return value; } public O parse(Element parent, Stream<Node> nodes) throws InvalidXMLException { final Parser<I> innerParser = this.innerParser.get(); final Stream<I> values; if(innerParser instanceof PrimitiveParser) { // For PrimitiveParsers, split the text of attributes and children final PrimitiveParser<I> primitiveParser = (PrimitiveParser<I>) innerParser; final NodeSplitter splitter = this.splitter.get(); values = nodes.flatMap(node -> splitter.split(node).map(rethrowFunction( text -> validate(primitiveParser.parse(node, text), node) ))); } else { values = nodes.map(rethrowFunction( node -> validate(innerParser.parse(node), node) )); } try { return aggregator.aggregateElements(values); } catch(NoSuchElementException e) { throw new InvalidXMLException("Missing value for property '" + name() + "'", parent); } catch(AmbiguousElementException e) { throw new InvalidXMLException("Multiple values for property '" + name() + "'", parent); } catch(DuplicateElementException e) { throw new InvalidXMLException("Duplicate values for property '" + name() + "'", parent); } } }