SLING-10194 : Allow property configuration for placeholder handling
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/InternalConstants.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/InternalConstants.java
index a99f32b..8954115 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/InternalConstants.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/InternalConstants.java
@@ -76,4 +76,6 @@
     static final String KEY_DEFAULT = "default";
 
     static final String KEY_MODE = "mode";
+
+    static final String KEY_PLACEHOLDER_POLICY = "placeholder-policy";
 }
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PlaceholderPolicy.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PlaceholderPolicy.java
new file mode 100644
index 0000000..8b11aaa
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PlaceholderPolicy.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.sling.feature.extension.apiregions.api.config;
+
+/**
+ * A policy for using placeholders in property values.
+ * @since 1.3
+ */
+public enum PlaceholderPolicy {
+ 
+    /** Default policy defined by the property type. */
+    DEFAULT,
+    /** Allow placeholders in the value. */
+    ALLOW,
+    /** Require a placeholder in the value. */
+    REQUIRE,
+    /** Do not allow a placeholder in the value. */
+    DENY;
+ 
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescription.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescription.java
index 9b1d4cf..1be609f 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescription.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescription.java
@@ -76,6 +76,12 @@
     private Mode mode;
 
     /**
+     * The placeholder policy
+     * @since 1.3
+     */
+    private PlaceholderPolicy placeholderPolicy;
+
+    /**
      * Create a new description
      */
     public PropertyDescription() {
@@ -86,6 +92,7 @@
 		this.setType(PropertyType.STRING);
         this.setCardinality(1);
         this.setRequired(false);
+        this.setPlaceholderPolicy(PlaceholderPolicy.DEFAULT);
     }
 
     /**
@@ -164,6 +171,10 @@
 			if ( modeVal != null ) {
                 this.setMode(Mode.valueOf(modeVal.toUpperCase()));				
 			}
+			final String policyVal = this.getString(InternalConstants.KEY_PLACEHOLDER_POLICY);
+			if ( policyVal != null ) {
+                this.setPlaceholderPolicy(PlaceholderPolicy.valueOf(policyVal.toUpperCase()));
+			}
  		} catch (final JsonException | IllegalArgumentException e) {
             throw new IOException(e);
         }
@@ -221,6 +232,9 @@
         if ( this.getMode() != null ) {
             objectBuilder.add(InternalConstants.KEY_MODE, this.getMode().name());
         }
+        if ( this.getPlaceholderPolicy() != PlaceholderPolicy.DEFAULT ) {
+            objectBuilder.add(InternalConstants.KEY_PLACEHOLDER_POLICY, this.getPlaceholderPolicy().name());
+        }
 
         return objectBuilder;
 	}
@@ -425,4 +439,22 @@
     public void setMode(final Mode value) {
         this.mode = value;
     }
- }
+ 
+    /**
+     * Get the placeholder policy.
+     * @return The policy
+     * @since 1.3
+     */
+    public PlaceholderPolicy getPlaceholderPolicy() {
+        return this.placeholderPolicy;
+    }
+
+    /**
+     * Set the placeholder policy
+     * @param policy The new policy
+     * @since 1.3
+     */
+    public void setPlaceholderPolicy(final PlaceholderPolicy policy) {
+        this.placeholderPolicy = policy == null ? PlaceholderPolicy.DEFAULT : policy;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/package-info.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/package-info.java
index 6ac6f43..b8a44cf 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/package-info.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@org.osgi.annotation.versioning.Version("1.2.0")
+@org.osgi.annotation.versioning.Version("1.3.0")
 package org.apache.sling.feature.extension.apiregions.api.config;
 
 
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidator.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidator.java
index 6cfc4f1..0e3f5be 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidator.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidator.java
@@ -26,6 +26,7 @@
 
 import org.apache.sling.feature.extension.apiregions.api.config.Mode;
 import org.apache.sling.feature.extension.apiregions.api.config.Option;
+import org.apache.sling.feature.extension.apiregions.api.config.PlaceholderPolicy;
 import org.apache.sling.feature.extension.apiregions.api.config.PropertyDescription;
 import org.apache.sling.feature.extension.apiregions.api.config.PropertyType;
 
@@ -195,7 +196,10 @@
                     default : context.result.getErrors().add("Unable to validate value - unknown property type : " + context.description.getType());
                 }
                 validateRegex(context, value);
-                validateOptions(context, value);                
+                validateOptions(context, value);
+                if ( context.description.getType() != PropertyType.PASSWORD ) {
+                    validatePlaceholderPolicy(context, value, false);              
+                }
             } else {
                 // placeholder is present
                 if ( context.description.getType() == PropertyType.PASSWORD ) {
@@ -208,6 +212,9 @@
                 } else {
                     context.result.markSkipped();
                 }
+                if ( context.description.getType() != PropertyType.PASSWORD ) {
+                    validatePlaceholderPolicy(context, value, true);              
+                }
             }
         } else {
 			setResult(context, "Null value provided for validation");
@@ -381,7 +388,7 @@
 	}
 
 	void validatePassword(final Context context, final Object value, final boolean hasPlaceholder) {
-        if ( !hasPlaceholder ) {
+        if ( !hasPlaceholder && context.description.getPlaceholderPolicy() != PlaceholderPolicy.DENY ) {
             setResult(context, "Value for a password must use a placeholder");
         }
 	}
@@ -447,6 +454,15 @@
         }
     }
 
+    void validatePlaceholderPolicy(final Context context, final Object value, final boolean hasPlaceholder) {
+        // for policy default and allow nothing needs to be validated
+        if ( context.description.getPlaceholderPolicy() == PlaceholderPolicy.DENY && hasPlaceholder ) {
+            setResult(context, "Placeholder in value is not allowed");
+        }  else if ( context.description.getPlaceholderPolicy() == PlaceholderPolicy.REQUIRE && !hasPlaceholder ) {
+            setResult(context, "Value must use a placeholder");
+        }
+    }         
+
     static final class Context {
 
         public final PropertyValidationResult result = new PropertyValidationResult();
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java
index 2e90b3b..ed5f02f 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java
@@ -53,6 +53,7 @@
         entity.setType(PropertyType.BYTE);        
         entity.setDefaultValue("default");
         entity.setMode(Mode.SILENT);
+        entity.setPlaceholderPolicy(PlaceholderPolicy.ALLOW);
         entity.clear();
         assertTrue(entity.getAttributes().isEmpty());
         assertNull(entity.getDeprecated());
@@ -70,13 +71,14 @@
         assertEquals(PropertyType.STRING, entity.getType());
         assertNull(entity.getDefaultValue());
         assertNull(entity.getMode());
+        assertEquals(PlaceholderPolicy.DEFAULT, entity.getPlaceholderPolicy());
     }
 
     @Test public void testFromJSONObject() throws IOException {
         final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
         ext.setJSON("{ \"type\" : \"BYTE\", \"cardinality\": 5, \"required\" : true, \"variable\" : \"var\"," +
         "\"range\" : {}, \"includes\" : [\"in\"], \"excludes\" : [\"ex\"] , \"options\": [{}], \"regex\": \".\"," +
-        "\"default\" : \"def\"}");
+        "\"default\" : \"def\", \"placeholder-policy\" : \"DENY\"}");
 
         final PropertyDescription entity = new PropertyDescription();
         entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
@@ -92,6 +94,7 @@
         assertEquals(".", entity.getRegex());
         assertNotNull(entity.getRegexPattern());
         assertEquals("def", entity.getDefaultValue());
+        assertEquals(PlaceholderPolicy.DENY, entity.getPlaceholderPolicy());
 
         // test defaults and empty values
         ext.setJSON("{ \"variable\" : \"var\", \"regex\": \".\"}");
@@ -107,6 +110,7 @@
         assertNull(entity.getOptions());
         assertEquals(".", entity.getRegex());
         assertNotNull(entity.getRegexPattern());
+        assertEquals(PlaceholderPolicy.DEFAULT, entity.getPlaceholderPolicy());
    }
 
     @Test public void testToJSONObject() throws IOException {
@@ -121,11 +125,12 @@
         entity.setVariable("var");
         entity.setType(PropertyType.BYTE);
         entity.setDefaultValue("def");
+        entity.setPlaceholderPolicy(PlaceholderPolicy.DENY);
 
         final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
         ext.setJSON("{ \"type\" : \"BYTE\", \"cardinality\": 5, \"required\" : true, \"variable\" : \"var\"," +
             "\"range\" : {}, \"includes\" : [\"in\"], \"excludes\" : [\"ex\"] , \"options\": [{}], \"regex\": \".\"," +
-            "\"default\" : \"def\"}");
+            "\"default\" : \"def\", \"placeholder-policy\" : \"DENY\"}");
 
         assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
 
@@ -138,6 +143,7 @@
         entity.setExcludes(null);
         entity.setIncludes(null);
         entity.setDefaultValue(null);
+        entity.setPlaceholderPolicy(null);
         
         ext.setJSON("{ \"variable\" : \"var\", \"regex\": \".\"}");
 
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidatorTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidatorTest.java
index f62b9e9..2ad6fab 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidatorTest.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidatorTest.java
@@ -27,6 +27,7 @@
 
 import org.apache.sling.feature.extension.apiregions.api.config.Mode;
 import org.apache.sling.feature.extension.apiregions.api.config.Option;
+import org.apache.sling.feature.extension.apiregions.api.config.PlaceholderPolicy;
 import org.apache.sling.feature.extension.apiregions.api.config.PropertyDescription;
 import org.apache.sling.feature.extension.apiregions.api.config.PropertyType;
 import org.apache.sling.feature.extension.apiregions.api.config.Range;
@@ -477,4 +478,34 @@
         assertFalse(result.isValid());
         assertTrue(result.isSkipped());
     }
-}
+
+    @Test public void testPlaceholderPolicyRequire() {
+        final PropertyDescription desc = new PropertyDescription();
+        desc.setPlaceholderPolicy(PlaceholderPolicy.REQUIRE);
+
+        PropertyValidationResult result = null;
+
+        result = validator.validate("$[env:variable]", desc);
+        assertTrue(result.isValid());
+        assertFalse(result.isSkipped());
+
+        result = validator.validate("hello", desc);
+        assertFalse(result.isValid());
+        assertFalse(result.isSkipped());
+    }
+
+    @Test public void testPlaceholderPolicyDeny() {
+        final PropertyDescription desc = new PropertyDescription();
+        desc.setPlaceholderPolicy(PlaceholderPolicy.DENY);
+
+        PropertyValidationResult result = null;
+
+        result = validator.validate("$[env:variable]", desc);
+        assertFalse(result.isValid());
+        assertFalse(result.isSkipped());
+
+        result = validator.validate("hello", desc);
+        assertTrue(result.isValid());
+        assertFalse(result.isSkipped());
+    }
+}
\ No newline at end of file