All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.java.com.amazon.ionpathextraction.FsmMatcherBuilder Maven / Gradle / Ivy

Go to download

Ion Path Extraction API aims to combine the convenience of a DOM API with the speed of a streaming API.

The newest version!
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at:
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */

package com.amazon.ionpathextraction;

import com.amazon.ion.IonReader;
import com.amazon.ionpathextraction.internal.Annotations;
import com.amazon.ionpathextraction.pathcomponents.Index;
import com.amazon.ionpathextraction.pathcomponents.PathComponent;
import com.amazon.ionpathextraction.pathcomponents.Text;
import com.amazon.ionpathextraction.pathcomponents.Wildcard;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Builds a root FsmMatcher for a set of SearchPaths.
 * 
* One key principle in the implementation is to close over as much branching as possible at build time. * For example: for a case-insensitive field lookup, lower case the field names once, at build time. *
* The second key principle is that there should be at-most-one Matcher state for a given reader context. * So any combination of different paths which could both be active for the same reader context are disallowed. * For example: allowing a mix of field names and ordinal positions for a given sub-path. *
* Beyond that, there are some usage patterns which could be included, such as annotations filtering on * field names or ordinals, but for which there was no observed usage. */ class FsmMatcherBuilder { private final PathTreeNode root = new PathTreeNode(); private final boolean caseInsensitiveAll; private final boolean caseInsensitiveFields; FsmMatcherBuilder(final boolean caseInsensitiveAll, final boolean caseInsensitiveFields) { this.caseInsensitiveAll = caseInsensitiveAll; this.caseInsensitiveFields = caseInsensitiveFields; } /** * Incorporate the searchPath into the matcher tree to be built. * * @throws UnsupportedPathExpression if the SearchPath is not supported. */ void accept(final SearchPath searchPath) { List steps = searchPath.getNormalizedPath(); PathTreeNode currentNode = root; for (PathComponent step : steps) { currentNode = currentNode.acceptStep(step); } currentNode.setCallback(searchPath.getCallback()); } /** * Build the FsmMatcher for the set of paths. * * @throws UnsupportedPathExpression if the combination of SearchPaths is not supported. */ FsmMatcher build() { return root.buildMatcher(); } /** * Mutable builder node to model the path tree before building into a FsmMatcher. */ private class PathTreeNode { BiFunction callback; PathTreeNode wildcard; Map annotatedSplats = new HashMap<>(); Map fields = new HashMap<>(); Map indexes = new HashMap<>(); /** * Find or create a new PathTreeNode for the child step. * * @return the new or existing node. * @throws UnsupportedPathExpression if the step contains path components that are not supported */ private PathTreeNode acceptStep(final PathComponent step) { if (step.hasAnnotations() && caseInsensitiveAll) { throw new UnsupportedPathExpression( "Case Insensitive Matching of Annotations is not yet supported by this matcher.\n" + "Use the legacy matcher or the withMatchFieldNamesCaseInsensitive option instead."); } PathTreeNode child; if (step instanceof Wildcard) { if (step.hasAnnotations()) { child = annotatedSplats.computeIfAbsent(step.getAnnotations(), a -> new PathTreeNode()); } else { if (wildcard == null) { wildcard = new PathTreeNode(); } child = wildcard; } } else { if (step.hasAnnotations()) { // this is not too bad to do, but it takes care to do without impacting the non-annotated case // which is the majority of usage. one would also want to mind the principle to avoid multiple // distinct match paths for a given reader context and only allow either annotated or not // for a given field name or index ordinal. throw new UnsupportedPathExpression("Annotations are only supported on wildcards!"); } if (step instanceof Text) { String fieldName = caseInsensitiveFields ? ((Text) step).getFieldName().toLowerCase() : ((Text) step).getFieldName(); child = fields.computeIfAbsent(fieldName, f -> new PathTreeNode()); } else if (step instanceof Index) { child = indexes.computeIfAbsent(((Index) step).getOrdinal(), i -> new PathTreeNode()); } else { throw new IllegalArgumentException("step of unknown runtime type: " + step.getClass()); } } return child; } private void setCallback(final BiFunction callback) { if (this.callback == null) { this.callback = callback; } else { // this would actually be pretty simple to do: just create a ComposedCallback of BiFunctions. throw new UnsupportedPathExpression("Cannot set multiple callbacks for same path!"); } } private FsmMatcher buildMatcher() { List> matchers = new ArrayList<>(); if (wildcard != null) { matchers.add(new SplatMatcher<>(wildcard.buildMatcher(), callback)); } if (!annotatedSplats.isEmpty()) { List> children = new ArrayList<>(annotatedSplats.size()); List annotations = new ArrayList<>(annotatedSplats.size()); for (Map.Entry entry : annotatedSplats.entrySet()) { children.add(entry.getValue().buildMatcher()); annotations.add(entry.getKey().getAnnotations()); } matchers.add(new AnnotationsMatcher<>(annotations, children)); } if (!fields.isEmpty()) { Map> children = fields.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, (e) -> e.getValue().buildMatcher())); FsmMatcher fieldMatcher = caseInsensitiveFields ? new CaseInsensitiveFieldMatcher<>(children, callback) : new FieldMatcher<>(children, callback); matchers.add(fieldMatcher); } if (!indexes.isEmpty()) { Map> children = indexes.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, (e) -> e.getValue().buildMatcher())); matchers.add(new IndexMatcher<>(children, callback)); } if (matchers.isEmpty()) { return new TerminalMatcher<>(callback); } else if (matchers.size() == 1) { return matchers.get(0); } else { // the main issue with allowing more than one is that it means that any given match context // may produce multiple matches, and search path writers may become reliant on the order // in which callbacks for such cases are called. And in the general case, that might mean // a crazy mix between the different types of matching, which devolves to the for-each loop // we see in the PathExtractorImpl. // That seems like a lot of complexity for a usage pattern of questionable value. // So if you're reading this, and you think "oh this is a silly restriction", then take // the time to understand why it's important to the path writer and reconsider accordingly. throw new UnsupportedPathExpression( "Only one variant of wildcard, annotated wildcard, field names, or ordinals is supported!"); } } } private static class SplatMatcher extends FsmMatcher { FsmMatcher child; SplatMatcher( final FsmMatcher child, final BiFunction callback) { this.child = child; this.callback = callback; } @Override FsmMatcher transition(final String fieldName, final int position, final Supplier annotations) { return child; } } private static class FieldMatcher extends FsmMatcher { Map> fields; FieldMatcher( final Map> fields, final BiFunction callback) { this.fields = fields; this.callback = callback; } @Override FsmMatcher transition(final String fieldName, final int position, final Supplier annotations) { return fields.get(fieldName); } } private static class CaseInsensitiveFieldMatcher extends FieldMatcher { CaseInsensitiveFieldMatcher( final Map> fields, final BiFunction callback) { super(fields, callback); } @Override FsmMatcher transition(final String fieldName, final int position, final Supplier annotations) { return fields.get(fieldName.toLowerCase()); } } private static class IndexMatcher extends FsmMatcher { Map> indexes; IndexMatcher( final Map> indexes, final BiFunction callback) { this.indexes = indexes; this.callback = callback; } @Override FsmMatcher transition(final String fieldName, final int position, final Supplier annotations) { return indexes.get(position); } } private static class TerminalMatcher extends FsmMatcher { TerminalMatcher(final BiFunction callback) { this.callback = callback; this.terminal = true; } @Override FsmMatcher transition(final String fieldName, final int position, final Supplier annotations) { return null; } } private static class AnnotationsMatcher extends FsmMatcher { List candidates; List> matchers; AnnotationsMatcher(final List candidates, final List> matchers) { this.candidates = candidates; this.matchers = matchers; } @Override FsmMatcher transition(final String fieldName, final int position, final Supplier annotations) { for (int i = 0; i < candidates.size(); i++) { if (Arrays.equals(candidates.get(i), annotations.get())) { return matchers.get(i); } } return null; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy