Invert enforcement patterns when needed 95/68895/3
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 28 Feb 2018 15:03:09 +0000 (16:03 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 28 Feb 2018 23:11:39 +0000 (00:11 +0100)
RFC7950 and yang-model-api defines pattern modifier invert-match,
which needs to be taken into account when validating incoming strings.

Mutate the pattern we expose to the codegen such that it captures
the inversion operation. This code is closely related to generated
code and string formats used therein, hence the beef of the
implementation lives in BindingMapping.

JIRA: MDSAL-314
Change-Id: Ie29745d3343f565ac6b1b5716b1ec38dd0f09bc9
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
binding/mdsal-binding-generator-impl/src/main/java/org/opendaylight/mdsal/binding/yang/types/TypeProviderImpl.java
binding/yang-binding/src/main/java/org/opendaylight/yangtools/yang/binding/BindingMapping.java
binding2/mdsal-binding2-generator-impl/src/main/java/org/opendaylight/mdsal/binding/javav2/generator/yang/types/TypeGenHelper.java
binding2/mdsal-binding2-util/src/main/java/org/opendaylight/mdsal/binding/javav2/util/BindingMapping.java

index db446a86c9a5ebde9ba50e999bbd0522129e1c6e..b3f60616563612048922f02777745f8fe884b442 100644 (file)
@@ -78,6 +78,7 @@ import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.ModifierKind;
 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
@@ -1044,7 +1045,8 @@ public final class TypeProviderImpl implements TypeProvider {
      *            parent Schema Node for Extended Subtype
      *
      */
-    private void resolveExtendedSubtypeAsUnion(final GeneratedTOBuilder parentUnionGenTOBuilder, final TypeDefinition<?> unionSubtype, final List<String> regularExpressions, final SchemaNode parentNode) {
+    private void resolveExtendedSubtypeAsUnion(final GeneratedTOBuilder parentUnionGenTOBuilder,
+            final TypeDefinition<?> unionSubtype, final List<String> regularExpressions, final SchemaNode parentNode) {
         final String unionTypeName = unionSubtype.getQName().getLocalName();
         final Type genTO = findGenTO(unionTypeName, unionSubtype);
         if (genTO != null) {
@@ -1258,7 +1260,14 @@ public final class TypeProviderImpl implements TypeProvider {
 
         final List<String> regExps = new ArrayList<>(patternConstraints.size());
         for (final PatternConstraint patternConstraint : patternConstraints) {
-            final String regEx = patternConstraint.getJavaPatternString();
+            String regEx = patternConstraint.getJavaPatternString();
+
+            // The pattern can be inverted
+            final Optional<ModifierKind> optModifier = patternConstraint.getModifier();
+            if (optModifier.isPresent()) {
+                regEx = applyModifier(optModifier.get(), regEx);
+            }
+
             final String modifiedRegEx = StringEscapeUtils.escapeJava(regEx);
             regExps.add(modifiedRegEx);
         }
@@ -1266,6 +1275,16 @@ public final class TypeProviderImpl implements TypeProvider {
         return regExps;
     }
 
+    private static String applyModifier(final ModifierKind modifier, final String pattern) {
+        switch (modifier) {
+            case INVERT_MATCH:
+                return BindingMapping.negatePatternString(pattern);
+            default:
+                LOG.warn("Ignoring unhandled modifier {}", modifier);
+                return pattern;
+        }
+    }
+
     /**
      *
      * Adds to the <code>genTOBuilder</code> the constant which contains regular
index c97171376309c54d4f9e59625efda2c451b770d1..afd2ce91d79bd00485d43c27f2ae1333d603b30e 100644 (file)
@@ -53,6 +53,9 @@ public final class BindingMapping {
     public static final String RPC_INPUT_SUFFIX = "Input";
     public static final String RPC_OUTPUT_SUFFIX = "Output";
 
+    private static final String NEGATED_PATTERN_PREFIX = "^(?!";
+    private static final String NEGATED_PATTERN_SUFFIX = ").*$";
+
     private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
 
     private BindingMapping() {
@@ -199,6 +202,68 @@ public final class BindingMapping {
         }
     }
 
+    /**
+     * Create a {@link Pattern} expression which performs inverted match to the specified pattern. The input pattern
+     * is expected to be a valid regular expression passing {@link Pattern#compile(String)} and to have both start and
+     * end of string anchors as the first and last characters.
+     *
+     * @param pattern Pattern regular expression to negate
+     * @return Negated regular expression
+     * @throws IllegalArgumentException if the pattern does not conform to expected structure
+     * @throws NullPointerException if pattern is null
+     */
+    public static String negatePatternString(final String pattern) {
+        checkArgument(pattern.charAt(0) == '^' && pattern.charAt(pattern.length() - 1) == '$',
+                "Pattern '%s' does not have expected format", pattern);
+
+        /*
+         * Converting the expression into a negation is tricky. For example, when we have:
+         *
+         *   pattern "a|b" { modifier invert-match; }
+         *
+         * this gets escaped into either "^a|b$" or "^(?:a|b)$". Either format can occur, as the non-capturing group
+         * strictly needed only in some cases. From that we want to arrive at:
+         *   "^(?!(?:a|b)$).*$".
+         *
+         *           ^^^         original expression
+         *        ^^^^^^^^       tail of a grouped expression (without head anchor)
+         *    ^^^^        ^^^^   inversion of match
+         *
+         * Inversion works by explicitly anchoring at the start of the string and then:
+         * - specifying a negative lookahead until the end of string
+         * - matching any string
+         * - anchoring at the end of the string
+         */
+        final boolean hasGroup = pattern.startsWith("^(?:") && pattern.endsWith(")$");
+        final int len = pattern.length();
+        final StringBuilder sb = new StringBuilder(len + (hasGroup ? 7 : 11)).append(NEGATED_PATTERN_PREFIX);
+
+        if (hasGroup) {
+            sb.append(pattern, 1, len);
+        } else {
+            sb.append("(?:").append(pattern, 1, len - 1).append(")$");
+        }
+        return sb.append(NEGATED_PATTERN_SUFFIX).toString();
+    }
+
+    /**
+     * Check if the specified {@link Pattern} is the result of {@link #negatePatternString(String)}. This method
+     * assumes the pattern was not hand-coded but rather was automatically-generated, such that its non-automated
+     * parts come from XSD regular expressions. If this constraint is violated, this method may result false positives.
+     *
+     * @param pattern Pattern to check
+     * @return True if this pattern is a negation.
+     * @throws NullPointerException if pattern is null
+     * @throws IllegalArgumentException if the pattern does not conform to expected structure
+     */
+    public static boolean isNegatedPattern(final Pattern pattern) {
+        return isNegatedPattern(pattern.toString());
+    }
+
+    private static boolean isNegatedPattern(final String pattern) {
+        return pattern.startsWith(NEGATED_PATTERN_PREFIX) && pattern.endsWith(NEGATED_PATTERN_SUFFIX);
+    }
+
     /**
      * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character. This
      * function is null-safe.
index 406a0a18358bbd5e0d29093f4fa1d22ee6a6d017..3795a238a241053d2196e2348718b501cf080d03 100644 (file)
@@ -58,15 +58,19 @@ import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 import org.opendaylight.yangtools.yang.model.api.Status;
 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.ModifierKind;
 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Auxiliary util class for {@link TypeProviderImpl} class
  */
 @Beta
 final class TypeGenHelper {
+    private static final Logger LOG = LoggerFactory.getLogger(TypeGenHelper.class);
 
     private TypeGenHelper() {
         throw new UnsupportedOperationException("Util class");
@@ -227,7 +231,14 @@ final class TypeGenHelper {
 
         final List<String> regExps = new ArrayList<>(patternConstraints.size());
         for (final PatternConstraint patternConstraint : patternConstraints) {
-            final String regEx = patternConstraint.getJavaPatternString();
+            String regEx = patternConstraint.getJavaPatternString();
+
+            // The pattern can be inverted
+            final Optional<ModifierKind> optModifier = patternConstraint.getModifier();
+            if (optModifier.isPresent()) {
+                regEx = applyModifier(optModifier.get(), regEx);
+            }
+
             final String modifiedRegEx = StringEscapeUtils.escapeJava(regEx);
             regExps.add(modifiedRegEx);
         }
@@ -235,6 +246,16 @@ final class TypeGenHelper {
         return regExps;
     }
 
+    private static String applyModifier(final ModifierKind modifier, final String pattern) {
+        switch (modifier) {
+            case INVERT_MATCH:
+                return BindingMapping.negatePatternString(pattern);
+            default:
+                LOG.warn("Ignoring unhandled modifier {}", modifier);
+                return pattern;
+        }
+    }
+
     /**
      * Finds out for each type definition how many immersion (depth) is
      * necessary to get to the base type. Every type definition is inserted to
index a42b001efaed39bcedefee50173ec43e097d1c36..8500f5e452d135a31af059dc1d5db49e891b5193 100644 (file)
@@ -43,8 +43,6 @@ public final class BindingMapping {
      */
     public static final String PACKAGE_PREFIX = "org.opendaylight.mdsal.gen.javav2";
 
-    private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
-    private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
     public static final String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
     public static final String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
     public static final String PATTERN_CONSTANT_NAME = "PATTERN_CONSTANTS";
@@ -52,6 +50,11 @@ public final class BindingMapping {
     public static final String RPC_INPUT_SUFFIX = "Input";
     public static final String RPC_OUTPUT_SUFFIX = "Output";
 
+    private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
+    private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
+    private static final String NEGATED_PATTERN_PREFIX = "^(?!";
+    private static final String NEGATED_PATTERN_SUFFIX = ").*$";
+
     private BindingMapping() {
         throw new UnsupportedOperationException("Utility class");
     }
@@ -119,6 +122,68 @@ public final class BindingMapping {
         return packageNameBuilder.toString();
     }
 
+    /**
+     * Create a {@link Pattern} expression which performs inverted match to the specified pattern. The input pattern
+     * is expected to be a valid regular expression passing {@link Pattern#compile(String)} and to have both start and
+     * end of string anchors as the first and last characters.
+     *
+     * @param pattern Pattern regular expression to negate
+     * @return Negated regular expression
+     * @throws IllegalArgumentException if the pattern does not conform to expected structure
+     * @throws NullPointerException if pattern is null
+     */
+    public static String negatePatternString(final String pattern) {
+        checkArgument(pattern.charAt(0) == '^' && pattern.charAt(pattern.length() - 1) == '$',
+                "Pattern '%s' does not have expected format", pattern);
+
+        /*
+         * Converting the expression into a negation is tricky. For example, when we have:
+         *
+         *   pattern "a|b" { modifier invert-match; }
+         *
+         * this gets escaped into either "^a|b$" or "^(?:a|b)$". Either format can occur, as the non-capturing group
+         * strictly needed only in some cases. From that we want to arrive at:
+         *   "^(?!(?:a|b)$).*$".
+         *
+         *           ^^^         original expression
+         *        ^^^^^^^^       tail of a grouped expression (without head anchor)
+         *    ^^^^        ^^^^   inversion of match
+         *
+         * Inversion works by explicitly anchoring at the start of the string and then:
+         * - specifying a negative lookahead until the end of string
+         * - matching any string
+         * - anchoring at the end of the string
+         */
+        final boolean hasGroup = pattern.startsWith("^(?:") && pattern.endsWith(")$");
+        final int len = pattern.length();
+        final StringBuilder sb = new StringBuilder(len + (hasGroup ? 7 : 11)).append(NEGATED_PATTERN_PREFIX);
+
+        if (hasGroup) {
+            sb.append(pattern, 1, len);
+        } else {
+            sb.append("(?:").append(pattern, 1, len - 1).append(")$");
+        }
+        return sb.append(NEGATED_PATTERN_SUFFIX).toString();
+    }
+
+    /**
+     * Check if the specified {@link Pattern} is the result of {@link #negatePatternString(String)}. This method
+     * assumes the pattern was not hand-coded but rather was automatically-generated, such that its non-automated
+     * parts come from XSD regular expressions. If this constraint is violated, this method may result false positives.
+     *
+     * @param pattern Pattern to check
+     * @return True if this pattern is a negation.
+     * @throws NullPointerException if pattern is null
+     * @throws IllegalArgumentException if the pattern does not conform to expected structure
+     */
+    public static boolean isNegatedPattern(final Pattern pattern) {
+        return isNegatedPattern(pattern.toString());
+    }
+
+    private static boolean isNegatedPattern(final String pattern) {
+        return pattern.startsWith(NEGATED_PATTERN_PREFIX) && pattern.endsWith(NEGATED_PATTERN_SUFFIX);
+    }
+
     //TODO: further implementation of static util methods...
 
 }