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

software.amazon.smithy.model.pattern.SmithyPattern Maven / Gradle / Ivy

/*
 * Copyright 2019 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 software.amazon.smithy.model.pattern;

import static java.lang.String.format;

import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import software.amazon.smithy.model.shapes.ShapeId;

/**
 * Represents a contained pattern.
 *
 * 

A pattern is a series of segments, some of which may be labels. * *

Labels may appear in the pattern in the form of "{label}". Labels must * not be repeated, must not contain other labels (e.g., "{fo{bar}oo}"), * and the label name must match the regex "^[a-zA-Z0-9_]+$". No labels can * appear after the query string. * *

Greedy labels, a specialized type of label, may be specified using * "{label+}". Only a single greedy label may appear in a pattern, and it * must be the last label in a pattern. Greedy labels may be disabled for a * pattern as part of the builder construction. */ public class SmithyPattern { private final String pattern; private final List segments; protected SmithyPattern(Builder builder) { pattern = Objects.requireNonNull(builder.pattern); segments = Objects.requireNonNull(builder.segments); checkForDuplicateLabels(); if (!builder.allowsGreedyLabels && segments.stream().anyMatch(Segment::isGreedyLabel)) { throw new InvalidPatternException("Pattern must not contain a greedy label. Found " + pattern); } } /** * Gets all segments, in order. * * @return All segments, in order, in an unmodifiable list. */ public final List getSegments() { return Collections.unmodifiableList(segments); } /** * Get a list of all segments that are labels. * * @return Label segments in an unmodifiable list. */ public final List getLabels() { return Collections.unmodifiableList( segments.stream().filter(Segment::isLabel).collect(Collectors.toList())); } /** * Get a label by case-insensitive name. * * @param name Name of the label to retrieve. * @return An optionally found label. */ public final Optional getLabel(String name) { String searchKey = name.toLowerCase(Locale.US); return segments.stream() .filter(Segment::isLabel) .filter(label -> label.getContent().toLowerCase(Locale.US).equals(searchKey)) .findFirst(); } /** * Gets the greedy label of the pattern, if present. * * @return Returns the optionally found segment that is a greedy label. */ public final Optional getGreedyLabel() { return segments.stream().filter(Segment::isGreedyLabel).findFirst(); } @Override public String toString() { return pattern; } @Override public boolean equals(Object other) { return other instanceof SmithyPattern && pattern.equals(((SmithyPattern) other).pattern); } /** * Gets a map of explicitly conflicting label segments between this * pattern and another. * * @param otherPattern SmithyPattern to check against. * @return A map of Segments where each pair represents a conflict * and where the key is a segment from this pattern. This map is * ordered so segments that appear first in this pattern appear * first when iterating the map. */ public Map getConflictingLabelSegmentsMap(SmithyPattern otherPattern) { Map conflictingSegments = new LinkedHashMap<>(); List segments = getSegments(); List otherSegments = otherPattern.getSegments(); int minSize = Math.min(segments.size(), otherSegments.size()); for (int i = 0; i < minSize; i++) { Segment thisSegment = segments.get(i); Segment otherSegment = otherSegments.get(i); if (thisSegment.isLabel() != otherSegment.isLabel()) { // The segments conflict if one is a literal and the other // is a label. conflictingSegments.put(thisSegment, otherSegment); } else if (thisSegment.isGreedyLabel() != otherSegment.isGreedyLabel()) { // The segments conflict if a greedy label is introduced at // or before segments in the other pattern. conflictingSegments.put(thisSegment, otherSegment); } else if (!thisSegment.isLabel()) { // Both are literals. They can only conflict if they are the // same exact string. if (!thisSegment.getContent().equals(otherSegment.getContent())) { return conflictingSegments; } } } return conflictingSegments; } @Override public int hashCode() { return pattern.hashCode(); } private void checkForDuplicateLabels() { Set labels = new HashSet<>(); segments.forEach(segment -> { if (segment.isLabel() && !labels.add(segment.getContent().toLowerCase(Locale.US))) { throw new InvalidPatternException(format("Label `%s` is defined more than once in pattern: %s", segment.getContent(), pattern)); } }); } /** * @return Returns a builder used to create a SmithyPattern. */ public static Builder builder() { return new Builder(); } /** * Builder used to create a SmithyPattern. */ public static final class Builder { private boolean allowsGreedyLabels = true; private String pattern; private List segments; private Builder() {} public Builder allowsGreedyLabels(boolean allowsGreedyLabels) { this.allowsGreedyLabels = allowsGreedyLabels; return this; } public Builder pattern(String pattern) { this.pattern = pattern; return this; } public Builder segments(List segments) { this.segments = segments; return this; } public SmithyPattern build() { return new SmithyPattern(this); } } /** * Segment within a SmithyPattern. */ public static final class Segment { public enum Type { LITERAL, LABEL, GREEDY_LABEL } private final String asString; private final String content; private final Type segmentType; public Segment(String content, Type segmentType) { this(content, segmentType, null); } public Segment(String content, Type segmentType, Integer offset) { this.content = Objects.requireNonNull(content); this.segmentType = segmentType; checkForInvalidContents(offset); if (segmentType == Type.GREEDY_LABEL) { asString = "{" + content + "+}"; } else if (segmentType == Type.LABEL) { asString = "{" + content + "}"; } else { asString = content; } } private void checkForInvalidContents(Integer offset) { String offsetString = ""; if (offset != null) { offsetString += " at index " + offset; } if (segmentType == Type.LITERAL) { if (content.isEmpty()) { throw new InvalidPatternException("Segments must not be empty" + offsetString); } else if (content.contains("{") || content.contains("}")) { throw new InvalidPatternException( "Literal segments must not contain `{` or `}` characters. Found segment `" + content + "`" + offsetString); } } else if (content.isEmpty()) { throw new InvalidPatternException("Empty label declaration in pattern" + offsetString + "."); } else if (!ShapeId.isValidIdentifier(content)) { throw new InvalidPatternException( "Invalid label name in pattern: '" + content + "'" + offsetString + ". Labels must contain value identifiers."); } } /** * Parse a segment from the given offset. * * @param content Content of the segment. * @param offset Character offset where the segment starts in the containing pattern. * @return Returns the created segment. * @throws InvalidPatternException if the segment is invalid. */ public static Segment parse(String content, int offset) { if (content.length() >= 2 && content.charAt(0) == '{' && content.charAt(content.length() - 1) == '}') { Type labelType = content.charAt(content.length() - 2) == '+' ? Type.GREEDY_LABEL : Type.LABEL; content = labelType == Type.GREEDY_LABEL ? content.substring(1, content.length() - 2) : content.substring(1, content.length() - 1); return new Segment(content, labelType, offset); } else { return new Segment(content, Type.LITERAL, offset); } } /** * Get the content of the segment. * *

The return value contains the segment in its entirety for * non-labels, and the label name for both labels and greedy labels. * For example, given a segment of "{label+}", the return value of * getContent would be "label". * * @return Content of the segment. */ public String getContent() { return content; } /** * @return True if the segment is a non-label literal. */ public boolean isLiteral() { return segmentType == Type.LITERAL; } /** * @return True if the segment is a label regardless of whether is greedy or not. */ public boolean isLabel() { return segmentType != Type.LITERAL; } /** * @return True if the segment is a non-greedy label. */ public boolean isNonGreedyLabel() { return segmentType == Type.LABEL; } /** * @return True if the segment is a greedy label. */ public boolean isGreedyLabel() { return segmentType == Type.GREEDY_LABEL; } /** * Get the segment as a literal value to be used in a pattern. * *

Unlike the result of {@link #getContent}, the return value * of {@code toString} includes braces for labels and "+" for * greedy labels. * * @return The literal segment. */ @Override public String toString() { return asString; } @Override public boolean equals(Object other) { return other instanceof Segment && asString.equals(((Segment) other).asString); } @Override public int hashCode() { return asString.hashCode(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy