Merge pull request #12 from apache/SLING-9867
SLING-9867 : Add extension to provide metadata about configuration api
diff --git a/.gitignore b/.gitignore
index 5b783ed..38f5ca4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@
.vlt
.DS_Store
jcr.log
+.vscode
atlassian-ide-plugin.xml
diff --git a/pom.xml b/pom.xml
index 7bc0bec..a980994 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,7 +56,7 @@
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.feature</artifactId>
- <version>1.2.14</version>
+ <version>1.2.15-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandler.java b/src/main/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandler.java
new file mode 100644
index 0000000..b7d6b68
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandler.java
@@ -0,0 +1,100 @@
+/*
+ * 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;
+
+import java.util.Map;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.HandlerContext;
+import org.apache.sling.feature.builder.MergeHandler;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FactoryConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FrameworkPropertyDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.Region;
+
+/**
+ * Merge the configuration api extension
+ */
+public class ConfigurationApiMergeHandler implements MergeHandler {
+
+ @Override
+ public boolean canMerge(final Extension extension) {
+ return ConfigurationApi.EXTENSION_NAME.equals(extension.getName());
+ }
+
+ @Override
+ public void merge(final HandlerContext context,
+ final Feature targetFeature,
+ final Feature sourceFeature,
+ final Extension targetExtension,
+ final Extension sourceExtension) {
+
+ if ( targetExtension == null ) {
+ // no target available yet, just copy source
+ final Extension ext = new Extension(ExtensionType.JSON, ConfigurationApi.EXTENSION_NAME, sourceExtension.getState());
+ ext.setJSON(sourceExtension.getJSON());
+ targetFeature.getExtensions().add(ext);
+ } else {
+ final ConfigurationApi sourceApi = ConfigurationApi.getConfigurationApi(sourceExtension);
+ final ConfigurationApi targetApi = ConfigurationApi.getConfigurationApi(targetExtension);
+
+ // region merging
+ if ( context.isInitialMerge() ) {
+ targetApi.setRegion(sourceApi.getRegion());
+ } else {
+ // region merging is different for prototypes
+ if ( sourceApi.getRegion() != targetApi.getRegion() ) {
+ if ( context.isPrototypeMerge() ) {
+ if ( sourceApi.getRegion() != null ) {
+ targetApi.setRegion(sourceApi.getRegion());
+ }
+ } else {
+ targetApi.setRegion(Region.GLOBAL);
+ }
+ }
+ }
+
+ // merge - but throw on duplicates
+ for(final Map.Entry<String, ConfigurationDescription> entry : sourceApi.getConfigurationDescriptions().entrySet()) {
+ if ( targetApi.getConfigurationDescriptions().containsKey(entry.getKey())) {
+ throw new IllegalStateException("Duplicate configuration description " + entry.getKey());
+ }
+ targetApi.getConfigurationDescriptions().put(entry.getKey(), entry.getValue());
+ }
+ for(final Map.Entry<String, FactoryConfigurationDescription> entry : sourceApi.getFactoryConfigurationDescriptions().entrySet()) {
+ if ( targetApi.getFactoryConfigurationDescriptions().containsKey(entry.getKey())) {
+ throw new IllegalStateException("Duplicate factory configuration description " + entry.getKey());
+ }
+ targetApi.getFactoryConfigurationDescriptions().put(entry.getKey(), entry.getValue());
+ }
+ for(final Map.Entry<String, FrameworkPropertyDescription> entry : sourceApi.getFrameworkPropertyDescriptions().entrySet()) {
+ if ( targetApi.getFrameworkPropertyDescriptions().containsKey(entry.getKey())) {
+ throw new IllegalStateException("Duplicate framework property description " + entry.getKey());
+ }
+ targetApi.getFrameworkPropertyDescriptions().put(entry.getKey(), entry.getValue());
+ }
+ targetApi.getInternalConfigurations().addAll(sourceApi.getInternalConfigurations());
+ targetApi.getInternalFactoryConfigurations().addAll(sourceApi.getInternalFactoryConfigurations());
+ targetApi.getInternalFrameworkProperties().addAll(sourceApi.getInternalFrameworkProperties());
+
+ ConfigurationApi.setConfigurationApi(targetFeature, targetApi);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckConfigurationApi.java b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckConfigurationApi.java
new file mode 100644
index 0000000..20a5d6c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckConfigurationApi.java
@@ -0,0 +1,88 @@
+/*
+ * 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.analyser;
+
+import java.util.Map;
+
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi;
+import org.apache.sling.feature.extension.apiregions.api.config.validation.ConfigurationValidationResult;
+import org.apache.sling.feature.extension.apiregions.api.config.validation.FeatureValidationResult;
+import org.apache.sling.feature.extension.apiregions.api.config.validation.FeatureValidator;
+import org.apache.sling.feature.extension.apiregions.api.config.validation.PropertyValidationResult;
+
+
+public class CheckConfigurationApi implements AnalyserTask{
+
+ @Override
+ public String getId() {
+ return "configuration-api";
+ }
+
+ @Override
+ public String getName() {
+ return "Configuration API analyser task";
+ }
+
+ @Override
+ public void execute(final AnalyserTaskContext context) throws Exception {
+ final FeatureValidator validator = new FeatureValidator();
+ validator.setFeatureProvider(context.getFeatureProvider());
+
+ final ConfigurationApi api = ConfigurationApi.getConfigurationApi(context.getFeature());
+ if ( api == null ) {
+ context.reportExtensionWarning(ConfigurationApi.EXTENSION_NAME, "Configuration api is not specified, unable to validate feature");
+ } else {
+ final FeatureValidationResult result = validator.validate(context.getFeature(), api);
+ if ( !result.isValid() ) {
+ for(final Map.Entry<String, PropertyValidationResult> entry : result.getFrameworkPropertyResults().entrySet()) {
+ for(final String warn : entry.getValue().getWarnings()) {
+ context.reportWarning("Framework property " + entry.getKey() + " : " + warn);
+ }
+ if ( !entry.getValue().isValid() ) {
+ for(final String err : entry.getValue().getErrors()) {
+ context.reportError("Framework property " + entry.getKey() + " : " + err);
+ }
+ }
+ }
+ for(final Map.Entry<String, ConfigurationValidationResult> entry : result.getConfigurationResults().entrySet()) {
+ for(final String warn : entry.getValue().getWarnings()) {
+ context.reportWarning("Configuration " + entry.getKey() + " : " + warn);
+ }
+ for(final Map.Entry<String, PropertyValidationResult> propEntry : entry.getValue().getPropertyResults().entrySet()) {
+ for(final String warn : propEntry.getValue().getWarnings()) {
+ context.reportWarning("Configuration " + entry.getKey() + "." + propEntry.getKey() + " : " + warn);
+ }
+ }
+ if ( !entry.getValue().isValid() ) {
+ for(final String err : entry.getValue().getGlobalErrors()) {
+ context.reportError("Configuration " + entry.getKey() + " : " + err);
+ }
+ for(final Map.Entry<String, PropertyValidationResult> propEntry : entry.getValue().getPropertyResults().entrySet()) {
+ if ( !propEntry.getValue().isValid() ) {
+ for(final String err : propEntry.getValue().getErrors()) {
+ context.reportWarning("Configuration " + entry.getKey() + "." + propEntry.getKey() + " : " + err);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntity.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntity.java
new file mode 100644
index 0000000..31210f8
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntity.java
@@ -0,0 +1,187 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+import org.apache.felix.cm.json.Configurations;
+
+/**
+ * Abstract class used by all entities which allow additional attributes to be stored.
+ */
+public abstract class AttributeableEntity {
+
+ /** The additional attributes */
+ private final Map<String, JsonValue> attributes = new LinkedHashMap<>();
+
+ /**
+ * Clear the object and reset to defaults
+ */
+ public void clear() {
+ this.attributes.clear();
+ }
+
+ /**
+ * Convert this object into JSON
+ *
+ * @return The json object
+ * @throws IOException If generating the JSON fails
+ */
+ public JsonObject toJSONObject() throws IOException {
+ final JsonObjectBuilder objectBuilder = this.createJson();
+ return objectBuilder.build();
+ }
+
+ /**
+ * Extract the metadata from the JSON object.
+ * This method first calls {@link #clear()}
+ *
+ * @param jsonObj The JSON Object
+ * @throws IOException If JSON parsing fails
+ */
+ public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+ this.clear();
+ try {
+ for(final Map.Entry<String, JsonValue> entry : jsonObj.entrySet()) {
+ this.getAttributes().put(entry.getKey(), entry.getValue());
+ }
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the attributes
+ * @return Mutable map of attributes, by attribute name
+ */
+ public Map<String, JsonValue> getAttributes() {
+ return this.attributes;
+ }
+
+ /**
+ * Convert this object into JSON
+ *
+ * @return The json object builder
+ * @throws IOException If generating the JSON fails
+ */
+ JsonObjectBuilder createJson() throws IOException {
+ final JsonObjectBuilder objectBuilder = Json.createObjectBuilder();
+
+ for(final Map.Entry<String, JsonValue> entry : this.getAttributes().entrySet()) {
+ objectBuilder.add(entry.getKey(), entry.getValue());
+ }
+
+ return objectBuilder;
+ }
+
+ /**
+ * Helper method to get a string value from a JsonValue
+ * @param jsonValue The json value
+ * @return The string value or {@code null}.
+ */
+ String getString(final JsonValue jsonValue) {
+ final Object obj = Configurations.convertToObject(jsonValue);
+ if ( obj != null ) {
+ return obj.toString();
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to get a string value from an attribute
+ * @param attributeName The attribute name
+ * @return The string value or {@code null}.
+ */
+ String getString(final String attributeName) {
+ final JsonValue val = this.getAttributes().remove(attributeName);
+ if ( val != null ) {
+ final Object obj = Configurations.convertToObject(val);
+ if ( obj != null ) {
+ return obj.toString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to get a number value from an attribute
+ * @param attributeName The attribute name
+ * @return The string value or {@code null}.
+ * @throws IOException If the attribute value is not of type boolean
+ */
+ Number getNumber(final String attributeName) throws IOException {
+ final JsonValue val = this.getAttributes().remove(attributeName);
+ if ( val != null ) {
+ final Object obj = Configurations.convertToObject(val);
+ if ( obj instanceof Number ) {
+ return (Number)obj;
+ }
+ throw new IOException("Invalid type for number value " + attributeName + " : " + val.getValueType().name());
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to set a string value
+ */
+ void setString(final JsonObjectBuilder builder, final String attributeName, final String value) {
+ if ( value != null ) {
+ builder.add(attributeName, value);
+ }
+ }
+
+ /**
+ * Helper method to get a integer value from an attribute
+ * @param attributeName The attribute name
+ * @param defaultValue default value
+ * @return The integer value or the default value
+ */
+ int getInteger(final String attributeName, final int defaultValue) {
+ final String val = this.getString(attributeName);
+ if ( val != null ) {
+ return Integer.parseInt(val);
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Helper method to get a boolean value from an attribute
+ * @param attributeName The attribute name
+ * @param defaultValue default value
+ * @return The boolean value or the default value
+ * @throws IOException If the attribute value is not of type boolean
+ */
+ boolean getBoolean(final String attributeName, final boolean defaultValue) throws IOException {
+ final JsonValue val = this.getAttributes().remove(attributeName);
+ if ( val != null ) {
+ final Object obj = Configurations.convertToObject(val);
+ if ( obj instanceof Boolean ) {
+ return ((Boolean)obj).booleanValue();
+ }
+ throw new IOException("Invalid type for boolean value " + attributeName + " : " + val.getValueType().name());
+ }
+ return defaultValue;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntity.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntity.java
new file mode 100644
index 0000000..6f25d4d
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntity.java
@@ -0,0 +1,95 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+/**
+ * A configurable entity has properties
+ */
+public abstract class ConfigurableEntity extends DescribableEntity {
+
+ /** The properties */
+ private final Map<String, PropertyDescription> properties = new LinkedHashMap<>();
+
+ /**
+ * Clear the object and reset to defaults
+ */
+ public void clear() {
+ super.clear();
+ this.properties.clear();
+ }
+
+ /**
+ * Extract the metadata from the JSON object.
+ * This method first calls {@link #clear()}
+ *
+ * @param jsonObj The JSON Object
+ * @throws IOException If JSON parsing fails
+ */
+ public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+ super.fromJSONObject(jsonObj);
+ try {
+ final JsonValue val = this.getAttributes().remove(InternalConstants.KEY_PROPERTIES);
+ if ( val != null ) {
+ for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.fromJSONObject(innerEntry.getValue().asJsonObject());
+ this.getPropertyDescriptions().put(innerEntry.getKey(), prop);
+ }
+ }
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the properties
+ * @return Mutable map of properties by property name
+ */
+ public Map<String, PropertyDescription> getPropertyDescriptions() {
+ return this.properties;
+ }
+
+ /**
+ * Convert this object into JSON
+ *
+ * @return The json object builder
+ * @throws IOException If generating the JSON fails
+ */
+ JsonObjectBuilder createJson() throws IOException {
+ final JsonObjectBuilder objBuilder = super.createJson();
+
+ if ( !this.getPropertyDescriptions().isEmpty() ) {
+ final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+ for(final Map.Entry<String, PropertyDescription> entry : this.getPropertyDescriptions().entrySet()) {
+ propBuilder.add(entry.getKey(), entry.getValue().createJson());
+ }
+ objBuilder.add(InternalConstants.KEY_PROPERTIES, propBuilder);
+ }
+
+ return objBuilder;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApi.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApi.java
new file mode 100644
index 0000000..2d7af47
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApi.java
@@ -0,0 +1,332 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+
+/**
+ * A configuration api describes the set of supported OSGi
+ * configurations and framework properties. This object can be
+ * stored as an extension inside a feature model.
+ */
+public class ConfigurationApi extends AttributeableEntity {
+
+ /** The name of the api regions extension. */
+ public static final String EXTENSION_NAME = "configuration-api";
+
+ /**
+ * Get the configuration api from the feature - if it exists.
+ *
+ * @param feature The feature
+ * @return The configuration api or {@code null}.
+ * @throws IllegalArgumentException If the extension is wrongly formatted
+ */
+ public static ConfigurationApi getConfigurationApi(final Feature feature) {
+ final Extension ext = feature == null ? null : feature.getExtensions().getByName(EXTENSION_NAME);
+ return getConfigurationApi(ext);
+ }
+
+ /**
+ * Get the configuration api from the extension.
+ *
+ * @param ext The extension
+ * @return The configuration api or {@code null} if the extension is {@code null}.
+ * @throws IllegalArgumentException If the extension is wrongly formatted
+ */
+ public static ConfigurationApi getConfigurationApi(final Extension ext) {
+ if ( ext == null ) {
+ return null;
+ }
+ if ( ext.getType() != ExtensionType.JSON ) {
+ throw new IllegalArgumentException("Extension " + ext.getName() + " must have JSON type");
+ }
+ try {
+ final ConfigurationApi result = new ConfigurationApi();
+ result.fromJSONObject(ext.getJSONStructure().asJsonObject());
+ return result;
+ } catch ( final IOException ioe) {
+ throw new IllegalArgumentException(ioe.getMessage(), ioe);
+ }
+ }
+
+ /**
+ * Set the configuration api as an extension to the feature
+ * @param feature The feature
+ * @param api The configuration api
+ * @throws IllegalStateException If the feature has already an extension of a wrong type
+ * @throws IllegalArgumentException If the api configuration can't be serialized to JSON
+ */
+ public static void setConfigurationApi(final Feature feature, final ConfigurationApi api) {
+ Extension ext = feature.getExtensions().getByName(EXTENSION_NAME);
+ if ( api == null ) {
+ if ( ext != null ) {
+ feature.getExtensions().remove(ext);
+ }
+ } else {
+ if ( ext == null ) {
+ ext = new Extension(ExtensionType.JSON, EXTENSION_NAME, ExtensionState.OPTIONAL);
+ feature.getExtensions().add(ext);
+ }
+ try {
+ ext.setJSONStructure(api.toJSONObject());
+ } catch ( final IOException ioe) {
+ throw new IllegalArgumentException(ioe);
+ }
+ }
+ }
+
+ /** The map of configurations */
+ private final Map<String, ConfigurationDescription> configurations = new LinkedHashMap<>();
+
+ /** The map of factory configurations */
+ private final Map<String, FactoryConfigurationDescription> factories = new LinkedHashMap<>();
+
+ /** The map of framework properties */
+ private final Map<String, FrameworkPropertyDescription> frameworkProperties = new LinkedHashMap<>();
+
+ /** The list of internal configuration names */
+ private final List<String> internalConfigurations = new ArrayList<>();
+
+ /** The list of internal factory configuration names */
+ private final List<String> internalFactories = new ArrayList<>();
+
+ /** The list of internal framework property names */
+ private final List<String> internalFrameworkProperties = new ArrayList<>();
+
+ /** The configuration region of this feature */
+ private Region region;
+
+ /**
+ * Clear the object and reset to defaults
+ */
+ public void clear() {
+ super.clear();
+ this.configurations.clear();
+ this.factories.clear();
+ this.frameworkProperties.clear();
+ this.internalConfigurations.clear();
+ this.internalFactories.clear();
+ this.internalFrameworkProperties.clear();
+ this.setRegion(null);
+ }
+
+ /**
+ * Extract the metadata from the JSON object.
+ * This method first calls {@link #clear()}.
+ *
+ * @param jsonObj The JSON Object
+ * @throws IOException If JSON parsing fails
+ */
+ public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+ super.fromJSONObject(jsonObj);
+ try {
+ final String typeVal = this.getString(InternalConstants.KEY_REGION);
+ if ( typeVal != null ) {
+ this.setRegion(Region.valueOf(typeVal.toUpperCase()));
+ }
+
+ JsonValue val;
+ val = this.getAttributes().remove(InternalConstants.KEY_CONFIGURATIONS);
+ if ( val != null ) {
+ for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+ final ConfigurationDescription cfg = new ConfigurationDescription();
+ cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+ this.getConfigurationDescriptions().put(innerEntry.getKey(), cfg);
+ }
+ }
+
+ val = this.getAttributes().remove(InternalConstants.KEY_FACTORIES);
+ if ( val != null ) {
+ for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+ final FactoryConfigurationDescription cfg = new FactoryConfigurationDescription();
+ cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+ this.getFactoryConfigurationDescriptions().put(innerEntry.getKey(), cfg);
+ }
+ }
+
+ val = this.getAttributes().remove(InternalConstants.KEY_FWK_PROPERTIES);
+ if ( val != null ) {
+ for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+ final FrameworkPropertyDescription cfg = new FrameworkPropertyDescription();
+ cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+ this.getFrameworkPropertyDescriptions().put(innerEntry.getKey(), cfg);
+ }
+ }
+
+ val = this.getAttributes().remove(InternalConstants.KEY_INTERNAL_CONFIGURATIONS);
+ if ( val != null ) {
+ for(final JsonValue innerVal : val.asJsonArray()) {
+ this.getInternalConfigurations().add(getString(innerVal));
+ }
+ }
+
+ val = this.getAttributes().remove(InternalConstants.KEY_INTERNAL_FACTORIES);
+ if ( val != null ) {
+ for(final JsonValue innerVal : val.asJsonArray()) {
+ this.getInternalFactoryConfigurations().add(getString(innerVal));
+ }
+ }
+
+ val = this.getAttributes().remove(InternalConstants.KEY_INTERNAL_FWK_PROPERTIES);
+ if ( val != null ) {
+ for(final JsonValue innerVal : val.asJsonArray()) {
+ this.getInternalFrameworkProperties().add(getString(innerVal));
+ }
+ }
+
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the configuration descriptions
+ * @return Mutable map of configuration descriptions by pid
+ */
+ public Map<String, ConfigurationDescription> getConfigurationDescriptions() {
+ return configurations;
+ }
+
+ /**
+ * Get the factory configuration descriptions
+ * @return Mutable map of factory descriptions by factory pid
+ */
+ public Map<String, FactoryConfigurationDescription> getFactoryConfigurationDescriptions() {
+ return factories;
+ }
+
+ /**
+ * Get the framework properties
+ * @return Mutable map of framework properties
+ */
+ public Map<String, FrameworkPropertyDescription> getFrameworkPropertyDescriptions() {
+ return frameworkProperties;
+ }
+
+ /**
+ * Get the internal configuration pids
+ * @return Mutable list of internal configuration pids
+ */
+ public List<String> getInternalConfigurations() {
+ return internalConfigurations;
+ }
+
+ /**
+ * Get the internal factory pids
+ * @return Mutable list of internal factory configuration pids
+ */
+ public List<String> getInternalFactoryConfigurations() {
+ return internalFactories;
+ }
+
+ /**
+ * Get the internal framework property names
+ * @return Mutable list of internal framework property names
+ */
+ public List<String> getInternalFrameworkProperties() {
+ return internalFrameworkProperties;
+ }
+
+ /**
+ * Get the api configuration region
+ * @return The region or {@code null}
+ */
+ public Region getRegion() {
+ return this.region;
+ }
+
+ /**
+ * Set the api configuration region
+ * @param value The region to set
+ */
+ public void setRegion(final Region value) {
+ this.region = value;
+ }
+
+ /**
+ * Convert this object into JSON
+ *
+ * @return The json object builder
+ * @throws IOException If generating the JSON fails
+ */
+ JsonObjectBuilder createJson() throws IOException {
+ final JsonObjectBuilder objBuilder = super.createJson();
+ if ( this.getRegion() != null ) {
+ objBuilder.add(InternalConstants.KEY_REGION, this.getRegion().name());
+ }
+ if ( !this.getConfigurationDescriptions().isEmpty() ) {
+ final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+ for(final Map.Entry<String, ConfigurationDescription> entry : this.getConfigurationDescriptions().entrySet()) {
+ propBuilder.add(entry.getKey(), entry.getValue().createJson());
+ }
+ objBuilder.add(InternalConstants.KEY_CONFIGURATIONS, propBuilder);
+ }
+ if ( !this.getFactoryConfigurationDescriptions().isEmpty() ) {
+ final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+ for(final Map.Entry<String, FactoryConfigurationDescription> entry : this.getFactoryConfigurationDescriptions().entrySet()) {
+ propBuilder.add(entry.getKey(), entry.getValue().createJson());
+ }
+ objBuilder.add(InternalConstants.KEY_FACTORIES, propBuilder);
+ }
+ if ( !this.getFrameworkPropertyDescriptions().isEmpty() ) {
+ final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+ for(final Map.Entry<String, FrameworkPropertyDescription> entry : this.getFrameworkPropertyDescriptions().entrySet()) {
+ propBuilder.add(entry.getKey(), entry.getValue().createJson());
+ }
+ objBuilder.add(InternalConstants.KEY_FWK_PROPERTIES, propBuilder);
+ }
+ if ( !this.getInternalConfigurations().isEmpty() ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String n : this.getInternalConfigurations()) {
+ arrayBuilder.add(n);
+ }
+ objBuilder.add(InternalConstants.KEY_INTERNAL_CONFIGURATIONS, arrayBuilder);
+ }
+ if ( !this.getInternalFactoryConfigurations().isEmpty() ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String n : this.getInternalFactoryConfigurations()) {
+ arrayBuilder.add(n);
+ }
+ objBuilder.add(InternalConstants.KEY_INTERNAL_FACTORIES, arrayBuilder);
+ }
+ if ( !this.getInternalFrameworkProperties().isEmpty() ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String n : this.getInternalFrameworkProperties()) {
+ arrayBuilder.add(n);
+ }
+ objBuilder.add(InternalConstants.KEY_INTERNAL_FWK_PROPERTIES, arrayBuilder);
+ }
+
+ return objBuilder;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationDescription.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationDescription.java
new file mode 100644
index 0000000..0e2c7e9
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationDescription.java
@@ -0,0 +1,24 @@
+/*
+ * 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 description of an OSGi configuration
+ */
+public class ConfigurationDescription extends ConfigurableEntity {
+
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntity.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntity.java
new file mode 100644
index 0000000..b2617d4
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntity.java
@@ -0,0 +1,131 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+
+/**
+ * Abstract class for all describable entities, having an optional title,
+ * description and deprecation info.
+ */
+public abstract class DescribableEntity extends AttributeableEntity {
+
+ /** The title */
+ private String title;
+
+ /** The description */
+ private String description;
+
+ /** The optional deprecation text */
+ private String deprecated;
+
+ /**
+ * Clear the object and reset to defaults
+ */
+ public void clear() {
+ super.clear();
+ this.setTitle(null);
+ this.setDescription(null);
+ this.setDeprecated(null);
+ }
+
+ /**
+ * Extract the metadata from the JSON object.
+ * This method first calls {@link #clear()}
+ *
+ * @param jsonObj The JSON Object
+ * @throws IOException If JSON parsing fails
+ */
+ public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+ super.fromJSONObject(jsonObj);
+ try {
+ this.setTitle(this.getString(InternalConstants.KEY_TITLE));
+ this.setDescription(this.getString(InternalConstants.KEY_DESCRIPTION));
+ this.setDeprecated(this.getString(InternalConstants.KEY_DEPRECATED));
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the title
+ * @return The title or {@code null}
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Set the title
+ * @param title the title to set
+ */
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+
+ /**
+ * Get the description
+ * @return the description or {@code null}
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Set the description
+ * @param description the description to set
+ */
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ /**
+ * Get the deprecation text
+ * @return the deprecation text or {@code null}
+ */
+ public String getDeprecated() {
+ return deprecated;
+ }
+
+ /**
+ * Set the deprecation text
+ * @param deprecated the deprecation text to set
+ */
+ public void setDeprecated(final String deprecated) {
+ this.deprecated = deprecated;
+ }
+
+ /**
+ * Convert this object into JSON
+ *
+ * @return The json object builder
+ * @throws IOException If generating the JSON fails
+ */
+ JsonObjectBuilder createJson() throws IOException {
+ final JsonObjectBuilder objectBuilder = super.createJson();
+
+ this.setString(objectBuilder, InternalConstants.KEY_TITLE, this.getTitle());
+ this.setString(objectBuilder, InternalConstants.KEY_DESCRIPTION, this.getDescription());
+ this.setString(objectBuilder, InternalConstants.KEY_DEPRECATED, this.getDeprecated());
+
+ return objectBuilder;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescription.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescription.java
new file mode 100644
index 0000000..9c8d7fa
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescription.java
@@ -0,0 +1,135 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+/**
+ * Description of an OSGi factory configuration
+ */
+public class FactoryConfigurationDescription extends ConfigurableEntity {
+
+ private final Set<Operation> operations = new HashSet<>();
+
+ private final List<String> internalNames = new ArrayList<>();
+
+ public FactoryConfigurationDescription() {
+ this.setDefaults();
+ }
+
+ void setDefaults() {
+ this.getOperations().add(Operation.CREATE);
+ this.getOperations().add(Operation.UPDATE);
+ }
+
+ /**
+ * Clear the object and set the defaults
+ */
+ public void clear() {
+ super.clear();
+ this.setDefaults();
+ this.internalNames.clear();
+ }
+
+ /**
+ * Extract the metadata from the JSON object.
+ * This method first calls {@link #clear()}
+ *
+ * @param jsonObj The JSON Object
+ * @throws IOException If JSON parsing fails
+ */
+ public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+ super.fromJSONObject(jsonObj);
+ try {
+ JsonValue val;
+ val = this.getAttributes().remove(InternalConstants.KEY_OPERATIONS);
+ if ( val != null ) {
+ this.getOperations().clear();
+ for(final JsonValue innerVal : val.asJsonArray()) {
+ final String v = getString(innerVal).toUpperCase();
+ this.getOperations().add(Operation.valueOf(v));
+ }
+ if ( this.getOperations().isEmpty() ) {
+ throw new IOException("Operations must not be empty");
+ }
+ }
+
+ val = this.getAttributes().remove(InternalConstants.KEY_INTERNAL_NAMES);
+ if ( val != null ) {
+ for(final JsonValue innerVal : val.asJsonArray()) {
+ this.getInternalNames().add(getString(innerVal));
+ }
+ }
+
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the operations
+ * @return Mutable set of operations
+ */
+ public Set<Operation> getOperations() {
+ return operations;
+ }
+
+ /**
+ * Get the internal factory configuration name
+ * @return Mutable list of internal names
+ */
+ public List<String> getInternalNames() {
+ return internalNames;
+ }
+
+ /**
+ * Convert this object into JSON
+ *
+ * @return The json object builder
+ * @throws IOException If generating the JSON fails
+ */
+ JsonObjectBuilder createJson() throws IOException {
+ final JsonObjectBuilder objBuilder = super.createJson();
+
+ if ( !this.getOperations().isEmpty() && this.getOperations().size() != 2 ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final Operation op : this.getOperations()) {
+ arrayBuilder.add(op.name());
+ }
+ objBuilder.add(InternalConstants.KEY_OPERATIONS, arrayBuilder);
+ }
+ if ( !this.getInternalNames().isEmpty() ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String n : this.getInternalNames()) {
+ arrayBuilder.add(n);
+ }
+ objBuilder.add(InternalConstants.KEY_INTERNAL_NAMES, arrayBuilder);
+ }
+ return objBuilder;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkPropertyDescription.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkPropertyDescription.java
new file mode 100644
index 0000000..df9a3b0
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkPropertyDescription.java
@@ -0,0 +1,24 @@
+/*
+ * 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 framework property description
+ */
+public class FrameworkPropertyDescription extends PropertyDescription {
+
+}
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
new file mode 100644
index 0000000..2fff4a3
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/InternalConstants.java
@@ -0,0 +1,73 @@
+/*
+ * 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;
+
+/**
+ * Constants used in this implementation
+ */
+abstract class InternalConstants {
+
+ static final String KEY_TITLE = "title";
+
+ static final String KEY_DESCRIPTION = "description";
+
+ static final String KEY_DEPRECATED = "deprecated";
+
+ static final String KEY_PROPERTIES = "properties";
+
+ static final String KEY_CONFIGURATIONS = "configurations";
+
+ static final String KEY_FACTORIES = "factory-configurations";
+
+ static final String KEY_FWK_PROPERTIES = "framework-properties";
+
+ static final String KEY_INTERNAL_CONFIGURATIONS = "internal-configurations";
+
+ static final String KEY_INTERNAL_FACTORIES = "internal-factory-configurations";
+
+ static final String KEY_INTERNAL_FWK_PROPERTIES = "internal-framework-properties";
+
+ static final String KEY_TYPE = "type";
+
+ static final String KEY_CARDINALITY = "cardinality";
+
+ static final String KEY_VARIABLE = "variable";
+
+ static final String KEY_RANGE = "range";
+
+ static final String KEY_MIN = "min";
+
+ static final String KEY_MAX = "max";
+
+ static final String KEY_INCLUDES = "includes";
+
+ static final String KEY_EXCLUDES = "excludes";
+
+ static final String KEY_OPTIONS = "options";
+
+ static final String KEY_REGEX = "regex";
+
+ static final String KEY_VALUE = "value";
+
+ static final String KEY_REQUIRED = "required";
+
+ static final String KEY_OPERATIONS = "operations";
+
+ static final String KEY_INTERNAL_NAMES = "internal-names";
+
+ static final String KEY_REGION = "region";
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Operation.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Operation.java
new file mode 100644
index 0000000..312c2b2
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Operation.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+/**
+ * Operations for factory configurations
+ */
+public enum Operation {
+
+ CREATE, // allowed to create a factory configuration
+ UPDATE // allowed to update a factory configuration
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Option.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Option.java
new file mode 100644
index 0000000..41d2c23
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Option.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+
+/**
+ * Option for a property value
+ */
+public class Option extends DescribableEntity {
+
+ /** The value for the option */
+ private String value;
+
+ /**
+ * Clear the object and reset to defaults
+ */
+ public void clear() {
+ super.clear();
+ this.setValue(null);
+ }
+
+ /**
+ * Extract the metadata from the JSON object.
+ * This method first calls {@link #clear()}
+ * @param jsonObj The JSON Object
+ * @throws IOException If JSON parsing fails
+ */
+ public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+ super.fromJSONObject(jsonObj);
+ try {
+ this.setValue(this.getString(InternalConstants.KEY_VALUE));
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the value for the option
+ * @return the value or {@code null}
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Set the value for the option
+ * @param value the value to set
+ */
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ /**
+ * Convert this object into JSON
+ *
+ * @return The json object builder
+ * @throws IOException If generating the JSON fails
+ */
+ JsonObjectBuilder createJson() throws IOException {
+ final JsonObjectBuilder objectBuilder = super.createJson();
+
+ this.setString(objectBuilder, InternalConstants.KEY_VALUE, this.getValue());
+
+ return objectBuilder;
+ }
+}
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
new file mode 100644
index 0000000..4323937
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescription.java
@@ -0,0 +1,350 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+/**
+ * Instances of this class represent a single configuration property
+ */
+public class PropertyDescription extends DescribableEntity {
+
+ /** The property type */
+ private PropertyType type;
+
+ /** The property cardinality */
+ private int cardinality;
+
+ /** The optional variable */
+ private String variable;
+
+ /** The optional range */
+ private Range range;
+
+ /** The required includes for an array/collection (optional) */
+ private String[] includes;
+
+ /** The required excludes for an array/collection (optional) */
+ private String[] excludes;
+
+ /** The optional list of options for the value */
+ private List<Option> options;
+
+ /** The optional regex */
+ private Pattern pattern;
+
+ /** Required? */
+ private boolean required;
+
+ /**
+ * Create a new description
+ */
+ public PropertyDescription() {
+ this.setDefaults();
+ }
+
+ void setDefaults() {
+ this.setType(PropertyType.STRING);
+ this.setCardinality(1);
+ this.setRequired(false);
+ }
+
+ /**
+ * Clear the object and reset to defaults
+ */
+ public void clear() {
+ super.clear();
+ this.setDefaults();
+ this.setVariable(null);
+ this.setRange(null);
+ this.setIncludes(null);
+ this.setExcludes(null);
+ this.setOptions(null);
+ this.setRegex(null);
+ }
+
+ /**
+ * Extract the metadata from the JSON object.
+ * This method first calls {@link #clear()}
+ * @param jsonObj The JSON Object
+ * @throws IOException If JSON parsing fails
+ */
+ public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+ super.fromJSONObject(jsonObj);
+ try {
+ this.setVariable(this.getString(InternalConstants.KEY_VARIABLE));
+ this.setCardinality(this.getInteger(InternalConstants.KEY_CARDINALITY, this.getCardinality()));
+ this.setRequired(this.getBoolean(InternalConstants.KEY_REQUIRED, this.isRequired()));
+
+ final String typeVal = this.getString(InternalConstants.KEY_TYPE);
+ if ( typeVal != null ) {
+ this.setType(PropertyType.valueOf(typeVal.toUpperCase()));
+ }
+ final JsonValue rangeVal = this.getAttributes().remove(InternalConstants.KEY_RANGE);
+ if ( rangeVal != null ) {
+ final Range range = new Range();
+ range.fromJSONObject(rangeVal.asJsonObject());
+ this.setRange(range);
+ }
+ final JsonValue incs = this.getAttributes().remove(InternalConstants.KEY_INCLUDES);
+ if ( incs != null ) {
+ final List<String> list = new ArrayList<>();
+ for(final JsonValue innerVal : incs.asJsonArray()) {
+ list.add(getString(innerVal));
+ }
+ this.setIncludes(list.toArray(new String[list.size()]));
+ }
+ final JsonValue excs = this.getAttributes().remove(InternalConstants.KEY_EXCLUDES);
+ if ( excs != null ) {
+ final List<String> list = new ArrayList<>();
+ for(final JsonValue innerVal : excs.asJsonArray()) {
+ list.add(getString(innerVal));
+ }
+ this.setExcludes(list.toArray(new String[list.size()]));
+ }
+ final JsonValue opts = this.getAttributes().remove(InternalConstants.KEY_OPTIONS);
+ if ( opts != null ) {
+ final List<Option> list = new ArrayList<>();
+ for(final JsonValue innerVal : opts.asJsonArray()) {
+ final Option o = new Option();
+ o.fromJSONObject(innerVal.asJsonObject());
+ list.add(o);
+ }
+ this.setOptions(list);
+ }
+ this.setRegex(this.getString(InternalConstants.KEY_REGEX));
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Convert this object into JSON
+ *
+ * @return The json object builder
+ * @throws IOException If generating the JSON fails
+ */
+ JsonObjectBuilder createJson() throws IOException {
+ final JsonObjectBuilder objectBuilder = super.createJson();
+
+ if ( this.getType() != null && this.getType() != PropertyType.STRING ) {
+ this.setString(objectBuilder, InternalConstants.KEY_TYPE, this.getType().name());
+ }
+ if ( this.getCardinality() != 1 ) {
+ objectBuilder.add(InternalConstants.KEY_CARDINALITY, this.getCardinality());
+ }
+ if ( this.isRequired() ) {
+ objectBuilder.add(InternalConstants.KEY_REQUIRED, this.isRequired());
+ }
+ this.setString(objectBuilder, InternalConstants.KEY_VARIABLE, this.getVariable());
+
+ if ( this.range != null ) {
+ objectBuilder.add(InternalConstants.KEY_RANGE, this.range.toJSONObject());
+ }
+ if ( this.includes != null && this.includes.length > 0 ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String v : this.includes) {
+ arrayBuilder.add(v);
+ }
+ objectBuilder.add(InternalConstants.KEY_INCLUDES, arrayBuilder);
+ }
+ if ( this.excludes != null && this.excludes.length > 0 ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String v : this.excludes) {
+ arrayBuilder.add(v);
+ }
+ objectBuilder.add(InternalConstants.KEY_EXCLUDES, arrayBuilder);
+ }
+ if ( this.options != null && !this.options.isEmpty()) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final Option o : this.options) {
+ arrayBuilder.add(o.toJSONObject());
+ }
+ objectBuilder.add(InternalConstants.KEY_OPTIONS, arrayBuilder);
+ }
+ this.setString(objectBuilder, InternalConstants.KEY_REGEX, this.getRegex());
+
+ return objectBuilder;
+ }
+
+ /**
+ * Get the property type
+ * @return the type
+ */
+ public PropertyType getType() {
+ return type;
+ }
+
+ /**
+ * Set the property type
+ * @param type the type to set
+ */
+ public void setType(final PropertyType type) {
+ this.type = type == null ? PropertyType.STRING : type;
+ }
+
+ /**
+ * Get the cardinality
+ * @return the cardinality
+ */
+ public int getCardinality() {
+ return cardinality;
+ }
+
+ /**
+ * Set the cardinality
+ * @param cardinality the cardinality to set
+ */
+ public void setCardinality(final int cardinality) {
+ this.cardinality = cardinality;
+ }
+
+ /**
+ * Get the variable
+ * @return the variable or {@code null}
+ */
+ public String getVariable() {
+ return variable;
+ }
+
+ /**
+ * Set the variable
+ * @param variable the variable to set
+ */
+ public void setVariable(final String variable) {
+ this.variable = variable;
+ }
+
+ /**
+ * Get the range
+ * @return the range or {@code null}
+ */
+ public Range getRange() {
+ return range;
+ }
+
+ /**
+ * Set the range
+ * @param range the range to set
+ */
+ public void setRange(final Range range) {
+ this.range = range;
+ }
+
+ /**
+ * Get the includes
+ * @return the includes or {@code null}
+ */
+ public String[] getIncludes() {
+ return includes;
+ }
+
+ /**
+ * Set the includes
+ * @param includes the includes to set
+ */
+ public void setIncludes(final String[] includes) {
+ this.includes = includes;
+ }
+
+ /**
+ * Get the excludes
+ * @return the excludes or {@code null}
+ */
+ public String[] getExcludes() {
+ return excludes;
+ }
+
+ /**
+ * Set the excludes
+ * @param excludes the excludes to set
+ */
+ public void setExcludes(final String[] excludes) {
+ this.excludes = excludes;
+ }
+
+ /**
+ * Get the list of options
+ * @return the options or {@code null}
+ */
+ public List<Option> getOptions() {
+ return options;
+ }
+
+ /**
+ * Set the list of options
+ * @param options the options to set
+ */
+ public void setOptions(final List<Option> options) {
+ this.options = options;
+ }
+
+ /**
+ * Get the regex
+ * @return the regex or {@code null}
+ */
+ public String getRegex() {
+ return pattern == null ? null : pattern.pattern();
+ }
+
+ /**
+ * Set the regex
+ * @param regex the regex to set
+ * @throws IllegalArgumentException If the pattern is not valid
+ */
+ public void setRegex(final String regex) {
+ if ( regex == null ) {
+ this.pattern = null;
+ } else {
+ this.pattern = Pattern.compile(regex);
+ }
+ }
+
+ /**
+ * Get the regex pattern
+ * @return The pattern or {@code null}
+ */
+ public Pattern getRegexPattern() {
+ return this.pattern;
+ }
+
+ /**
+ * Is this property required?
+ * @return {@code true} if it is required
+ */
+ public boolean isRequired() {
+ return this.required;
+ }
+
+ /**
+ * Set whether this property is required
+ * @param flag The new value
+ */
+ public void setRequired(final boolean flag) {
+ this.required = flag;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyType.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyType.java
new file mode 100644
index 0000000..899f93a
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyType.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/**
+ * Property type
+ */
+public enum PropertyType {
+
+ STRING,
+ LONG,
+ INTEGER,
+ SHORT,
+ CHARACTER,
+ BYTE,
+ DOUBLE,
+ FLOAT,
+ BOOLEAN,
+ PASSWORD,
+ URL,
+ EMAIL,
+ PATH;
+
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Range.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Range.java
new file mode 100644
index 0000000..f5daf55
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Range.java
@@ -0,0 +1,121 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+
+import org.apache.felix.cm.json.Configurations;
+
+/**
+ * A numerical value range
+ */
+public class Range extends AttributeableEntity {
+
+ /** The optional min value */
+ private Number min;
+
+ /** The optional max value */
+ private Number max;
+
+ /**
+ * Clear the object and reset to defaults
+ */
+ public void clear() {
+ super.clear();
+ this.setMax(null);
+ this.setMin(null);
+ }
+
+ /**
+ * Extract the metadata from the JSON object.
+ * This method first calls {@link #clear()}
+ * @param jsonObj The JSON Object
+ * @throws IOException If JSON parsing fails
+ */
+ public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+ super.fromJSONObject(jsonObj);
+ try {
+ this.setMin(this.getNumber(InternalConstants.KEY_MIN));
+ this.setMax(this.getNumber(InternalConstants.KEY_MAX));
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the min value
+ * @return the min or {@code null}
+ */
+ public Number getMin() {
+ return min;
+ }
+
+ /**
+ * Set the min value
+ * @param min the min to set
+ */
+ public void setMin(final Number min) {
+ this.min = min;
+ }
+
+ /**
+ * Get the max value
+ * @return the max or {@code null}
+ */
+ public Number getMax() {
+ return max;
+ }
+
+ /**
+ * Set the max value
+ * @param max the max to set
+ */
+ public void setMax(final Number max) {
+ this.max = max;
+ }
+
+ /**
+ * Convert this object into JSON
+ *
+ * @return The json object builder
+ * @throws IOException If generating the JSON fails
+ */
+ JsonObjectBuilder createJson() throws IOException {
+ final JsonObjectBuilder objectBuilder = super.createJson();
+
+ if ( this.getMin() != null ) {
+ objectBuilder.add(InternalConstants.KEY_MIN, Configurations.convertToJsonValue(this.getMin()));
+ }
+ if ( this.getMax() != null ) {
+ objectBuilder.add(InternalConstants.KEY_MAX, Configurations.convertToJsonValue(this.getMax()));
+ }
+
+ return objectBuilder;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "Range [min=" + getMax() + ", max=" + getMax() + "]";
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Region.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Region.java
new file mode 100644
index 0000000..d4c14f4
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Region.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+public enum Region {
+
+ GLOBAL,
+ INTERNAL;
+
+}
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
new file mode 100644
index 0000000..b67f74d
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.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/ConfigurationValidationResult.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidationResult.java
new file mode 100644
index 0000000..f1cb9f4
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidationResult.java
@@ -0,0 +1,60 @@
+/*
+ * 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.validation;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ConfigurationValidationResult {
+
+ private final Map<String, PropertyValidationResult> propertyResults = new HashMap<>();
+
+ private final List<String> globalErrors = new ArrayList<>();
+
+ private final List<String> warnings = new ArrayList<>();
+
+ public boolean isValid() {
+ boolean valid = globalErrors.isEmpty();
+ if ( valid ) {
+ for(final PropertyValidationResult r : this.propertyResults.values()) {
+ if ( !r.isValid() ) {
+ valid = false;
+ break;
+ }
+ }
+ }
+ return valid;
+ }
+
+ public List<String> getGlobalErrors() {
+ return this.globalErrors;
+ }
+
+ public Map<String, PropertyValidationResult> getPropertyResults() {
+ return propertyResults;
+ }
+
+ /**
+ * Return the list of warnings
+ * @return The list of warnings - might be empty
+ */
+ public List<String> getWarnings() {
+ return this.warnings;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidator.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidator.java
new file mode 100644
index 0000000..a7e505d
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidator.java
@@ -0,0 +1,104 @@
+/*
+ * 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.validation;
+
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurableEntity;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FactoryConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.PropertyDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.Region;
+import org.osgi.framework.Constants;
+
+/**
+ * Validator to validate a configuration
+ */
+public class ConfigurationValidator {
+
+ /**
+ * List of properties which are always allowed
+ */
+ public static final List<String> ALLOWED_PROPERTIES = Arrays.asList(Constants.SERVICE_DESCRIPTION,
+ Constants.SERVICE_VENDOR,
+ Constants.SERVICE_RANKING);
+
+
+ private final PropertyValidator propertyValidator = new PropertyValidator();
+
+ /**
+ * Validate a configuration
+ * @param config The OSGi configuration
+ * @param desc The configuration description
+ * @param region The optional region for the configuration
+ * @return The result
+ */
+ public ConfigurationValidationResult validate(final Configuration config, final ConfigurableEntity desc, final Region region) {
+ final ConfigurationValidationResult result = new ConfigurationValidationResult();
+ if ( config.isFactoryConfiguration() ) {
+ if ( !(desc instanceof FactoryConfigurationDescription) ) {
+ result.getGlobalErrors().add("Factory configuration cannot be validated against non factory configuration description");
+ } else {
+ validateProperties(desc, config, result.getPropertyResults(), region);
+ }
+ } else {
+ if ( !(desc instanceof ConfigurationDescription) ) {
+ result.getGlobalErrors().add("Configuration cannot be validated against factory configuration description");
+ } else {
+ validateProperties(desc, config, result.getPropertyResults(), region);
+ }
+ }
+
+ if ( desc.getDeprecated() != null ) {
+ result.getWarnings().add(desc.getDeprecated());
+ }
+ return result;
+ }
+
+ void validateProperties(final ConfigurableEntity desc,
+ final Configuration configuration,
+ final Map<String, PropertyValidationResult> results,
+ final Region region) {
+ final Dictionary<String, Object> properties = configuration.getConfigurationProperties();
+ for(final Map.Entry<String, PropertyDescription> propEntry : desc.getPropertyDescriptions().entrySet()) {
+ final Object value = properties.get(propEntry.getKey());
+ final PropertyValidationResult result = propertyValidator.validate(value, propEntry.getValue());
+ results.put(propEntry.getKey(), result);
+ }
+ final Enumeration<String> keyEnum = properties.keys();
+ while ( keyEnum.hasMoreElements() ) {
+ final String propName = keyEnum.nextElement();
+ if ( !desc.getPropertyDescriptions().containsKey(propName) ) {
+ final PropertyValidationResult result = new PropertyValidationResult();
+ results.put(propName, result);
+ if ( Constants.SERVICE_RANKING.equals(propName) ) {
+ final Object value = properties.get(propName);
+ if ( !(value instanceof Integer) ) {
+ result.getErrors().add("service.ranking must be of type Integer");
+ }
+ } else if ( !ALLOWED_PROPERTIES.contains(propName) && region != Region.INTERNAL ) {
+ result.getErrors().add("Property is not allowed");
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidationResult.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidationResult.java
new file mode 100644
index 0000000..d757845
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidationResult.java
@@ -0,0 +1,69 @@
+/*
+ * 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.validation;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Validation result for a feature
+ */
+public class FeatureValidationResult {
+
+ private final Map<String, ConfigurationValidationResult> configurationResults = new HashMap<>();
+
+ private final Map<String, PropertyValidationResult> frameworkPropertyResults = new HashMap<>();
+
+ /**
+ * Is the configuration of the feature valid?
+ * @return {@code true} if it is valid
+ */
+ public boolean isValid() {
+ boolean valid = true;
+ for(final ConfigurationValidationResult r : this.configurationResults.values()) {
+ if ( !r.isValid() ) {
+ valid = false;
+ break;
+ }
+ }
+ if ( valid ) {
+ for(final PropertyValidationResult r : this.frameworkPropertyResults.values()) {
+ if ( !r.isValid() ) {
+ valid = false;
+ break;
+ }
+ }
+ }
+ return valid;
+ }
+
+ /**
+ * Get the confiugration validation results.
+ * @return The results keyed by configuration PIDs
+ */
+ public Map<String, ConfigurationValidationResult> getConfigurationResults() {
+ return this.configurationResults;
+ }
+
+ /**
+ * Get the framework property validation results
+ * @return The results keyed by framework property name
+ */
+ public Map<String, PropertyValidationResult> getFrameworkPropertyResults() {
+ return this.frameworkPropertyResults;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidator.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidator.java
new file mode 100644
index 0000000..63cdc37
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidator.java
@@ -0,0 +1,212 @@
+/*
+ * 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.validation;
+
+import java.util.List;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.FeatureProvider;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FactoryConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FrameworkPropertyDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.Operation;
+import org.apache.sling.feature.extension.apiregions.api.config.Region;
+
+/**
+ * Validator to validate a feature
+ */
+public class FeatureValidator {
+
+ private final ConfigurationValidator configurationValidator = new ConfigurationValidator();
+
+ private final PropertyValidator propertyValidator = new PropertyValidator();
+
+ private FeatureProvider featureProvider;
+
+ /**
+ * Get the current feature provider
+ * @return the feature provider or {@code null}
+ */
+ public FeatureProvider getFeatureProvider() {
+ return featureProvider;
+ }
+
+ /**
+ * Set the feature provider
+ * @param featureProvider the feature provider to set
+ */
+ public void setFeatureProvider(final FeatureProvider provider) {
+ this.featureProvider = provider;
+ }
+
+ /**
+ * Validate the feature against the configuration API
+ * @param feature The feature
+ * @param api The configuration API
+ * @return A {@code FeatureValidationResult}
+ * @throws IllegalArgumentException If api is {@code null}
+ */
+ public FeatureValidationResult validate(final Feature feature, final ConfigurationApi api) {
+ final FeatureValidationResult result = new FeatureValidationResult();
+ if ( api == null ) {
+ throw new IllegalArgumentException();
+ }
+
+ for(final Configuration config : feature.getConfigurations()) {
+ final RegionInfo regionInfo = getRegionInfo(feature, config);
+
+ if ( regionInfo == null ) {
+ final ConfigurationValidationResult cvr = new ConfigurationValidationResult();
+ cvr.getGlobalErrors().add("Unable to properly validate configuration, region info cannot be determined");
+ result.getConfigurationResults().put(config.getPid(), cvr);
+ } else {
+ if ( config.isFactoryConfiguration() ) {
+ final FactoryConfigurationDescription desc = api.getFactoryConfigurationDescriptions().get(config.getFactoryPid());
+ if ( desc != null ) {
+ final ConfigurationValidationResult r = configurationValidator.validate(config, desc, regionInfo.region);
+ result.getConfigurationResults().put(config.getPid(), r);
+ if ( regionInfo.region != Region.INTERNAL ) {
+ if ( desc.getOperations().isEmpty() ) {
+ r.getGlobalErrors().add("No operations allowed for factory configuration");
+ } else {
+ if ( regionInfo.isUpdate && !desc.getOperations().contains(Operation.UPDATE)) {
+ r.getGlobalErrors().add("Updating of factory configuration is not allowed");
+ } else if ( !regionInfo.isUpdate && !desc.getOperations().contains(Operation.CREATE)) {
+ r.getGlobalErrors().add("Creation of factory configuration is not allowed");
+ }
+ }
+ if ( desc.getInternalNames().contains(config.getName())) {
+ r.getGlobalErrors().add("Factory configuration with name is not allowed");
+ }
+ }
+
+ } else if ( regionInfo.region != Region.INTERNAL && api.getInternalFactoryConfigurations().contains(config.getFactoryPid())) {
+ final ConfigurationValidationResult cvr = new ConfigurationValidationResult();
+ cvr.getGlobalErrors().add("Factory configuration is not allowed");
+ result.getConfigurationResults().put(config.getPid(), cvr);
+ }
+ } else {
+ final ConfigurationDescription desc = api.getConfigurationDescriptions().get(config.getPid());
+ if ( desc != null ) {
+ final ConfigurationValidationResult r = configurationValidator.validate(config, desc, regionInfo.region);
+ result.getConfigurationResults().put(config.getPid(), r);
+ } else if ( regionInfo.region!= Region.INTERNAL && api.getInternalConfigurations().contains(config.getPid())) {
+ final ConfigurationValidationResult cvr = new ConfigurationValidationResult();
+ cvr.getGlobalErrors().add("Configuration is not allowed");
+ result.getConfigurationResults().put(config.getPid(), cvr);
+ }
+ }
+ }
+
+ // make sure a result exists
+ result.getConfigurationResults().computeIfAbsent(config.getPid(), id -> new ConfigurationValidationResult());
+ }
+
+ for(final String frameworkProperty : feature.getFrameworkProperties().keySet()) {
+ final RegionInfo regionInfo = getRegionInfo(feature, frameworkProperty);
+ if ( regionInfo == null ) {
+ final PropertyValidationResult pvr = new PropertyValidationResult();
+ pvr.getErrors().add("Unable to properly validate framework property, region info cannot be determined");
+ result.getFrameworkPropertyResults().put(frameworkProperty, pvr);
+ } else {
+ final FrameworkPropertyDescription fpd = api.getFrameworkPropertyDescriptions().get(frameworkProperty);
+ if ( fpd != null ) {
+ final PropertyValidationResult pvr = propertyValidator.validate(feature.getFrameworkProperties().get(frameworkProperty), fpd);
+ result.getFrameworkPropertyResults().put(frameworkProperty, pvr);
+ } else if ( regionInfo.region != Region.INTERNAL && api.getInternalFrameworkProperties().contains(frameworkProperty) ) {
+ final PropertyValidationResult pvr = new PropertyValidationResult();
+ pvr.getErrors().add("Framework property is not allowed");
+ result.getFrameworkPropertyResults().put(frameworkProperty, pvr);
+ }
+ }
+ // make sure a result exists
+ result.getFrameworkPropertyResults().computeIfAbsent(frameworkProperty, id -> new PropertyValidationResult());
+ }
+
+ return result;
+ }
+
+ static final class RegionInfo {
+
+ public Region region;
+
+ public boolean isUpdate;
+ }
+
+ RegionInfo getRegionInfo(final Feature feature, final Configuration cfg) {
+ final FeatureProvider provider = this.getFeatureProvider();
+ final RegionInfo result = new RegionInfo();
+
+ final List<ArtifactId> list = cfg.getFeatureOrigins();
+ if ( !list.isEmpty() ) {
+ boolean global = false;
+ for(final ArtifactId id : list) {
+ final Feature f = provider == null ? null : provider.provide(id);
+ if ( f == null ) {
+ return null;
+ }
+ final ConfigurationApi api = ConfigurationApi.getConfigurationApi(f);
+ if ( api == null || api.getRegion() != Region.INTERNAL ) {
+ global = true;
+ break;
+ }
+ }
+ result.region = global ? Region.GLOBAL : Region.INTERNAL;
+ result.isUpdate = list.size() > 1;
+ } else {
+ final ConfigurationApi api = ConfigurationApi.getConfigurationApi(feature);
+ if ( api == null || api.getRegion() == null || api.getRegion() == Region.GLOBAL ) {
+ result.region = Region.GLOBAL;
+ } else {
+ result.region = Region.INTERNAL;
+ }
+ result.isUpdate = false;
+ }
+ return result;
+ }
+
+ RegionInfo getRegionInfo(final Feature feature, final String frameworkProperty) {
+ final FeatureProvider provider = this.getFeatureProvider();
+
+ final List<ArtifactId> list = feature.getFeatureOrigins(feature.getFrameworkPropertyMetadata(frameworkProperty));
+ boolean global = false;
+ for(final ArtifactId id : list) {
+ Feature found = null;
+ if ( feature.getId().equals(id) ) {
+ found = feature;
+ } else {
+ found = provider == null ? null : provider.provide(id);
+ }
+ if ( found == null ) {
+ return null;
+ }
+ final ConfigurationApi api = ConfigurationApi.getConfigurationApi(found);
+ if ( api == null || api.getRegion() != Region.INTERNAL ) {
+ global = true;
+ break;
+ }
+ }
+ final RegionInfo result = new RegionInfo();
+ result.region = global ? Region.GLOBAL : Region.INTERNAL;
+ result.isUpdate = list.size() > 1;
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidationResult.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidationResult.java
new file mode 100644
index 0000000..b68ef2e
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidationResult.java
@@ -0,0 +1,52 @@
+/*
+ * 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.validation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PropertyValidationResult {
+
+ private final List<String> errors = new ArrayList<>();
+
+ private final List<String> warnings = new ArrayList<>();
+
+ /**
+ * Is the property value valid?
+ * @return {@code true} if the value is valid
+ */
+ public boolean isValid() {
+ return errors.isEmpty();
+ }
+
+ /**
+ * If {@link #isValid()} returns {@code false} this returns
+ * a list of human readable errors.
+ * @return A list of errors - empty if {@link #isValid()} returns {@code true}
+ */
+ public List<String> getErrors() {
+ return errors;
+ }
+
+ /**
+ * Return the list of warnings
+ * @return The list of warnings - might be empty
+ */
+ public List<String> getWarnings() {
+ return this.warnings;
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..82f7a1c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidator.java
@@ -0,0 +1,381 @@
+/*
+ * 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.validation;
+
+import java.lang.reflect.Array;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.sling.feature.extension.apiregions.api.config.Option;
+import org.apache.sling.feature.extension.apiregions.api.config.PropertyDescription;
+
+/**
+ * Validate a configuration property
+ */
+public class PropertyValidator {
+
+ /**
+ * Validate the value against the property definition
+ * @return A property validation result
+ */
+ public PropertyValidationResult validate(final Object value, final PropertyDescription prop) {
+ final PropertyValidationResult result = new PropertyValidationResult();
+ if ( value == null ) {
+ if ( prop.isRequired() ) {
+ result.getErrors().add("No value provided");
+ }
+ } else {
+ final List<Object> values;
+ if ( value.getClass().isArray() ) {
+ // array
+ values = new ArrayList<>();
+ for(int i=0;i<Array.getLength(value);i++) {
+ values.add(Array.get(value, i));
+ }
+ } else if ( value instanceof Collection ) {
+ // collection
+ values = new ArrayList<>();
+ final Collection<?> c = (Collection<?>)value;
+ for(final Object o : c) {
+ values.add(o);
+ }
+ } else {
+ // single value
+ values = null;
+ validateValue(prop, value, result.getErrors());
+ }
+
+ if ( values != null ) {
+ // array or collection
+ for(final Object val : values) {
+ validateValue(prop, val, result.getErrors());
+ }
+ validateList(prop, values, result.getErrors());
+ }
+
+ if ( prop.getDeprecated() != null ) {
+ result.getWarnings().add(prop.getDeprecated());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Validate a multi value
+ * @param prop The property description
+ * @param values The values
+ * @param messages The messages to record errors
+ */
+ void validateList(final PropertyDescription prop, final List<Object> values, final List<String> messages) {
+ if ( prop.getCardinality() > 0 && values.size() > prop.getCardinality() ) {
+ messages.add("Array/collection contains too many elements, only " + prop.getCardinality() + " allowed");
+ }
+ if ( prop.getIncludes() != null ) {
+ for(final String inc : prop.getIncludes()) {
+ boolean found = false;
+ for(final Object val : values) {
+ if ( inc.equals(val.toString())) {
+ found = true;
+ break;
+ }
+ }
+ if ( !found ) {
+ messages.add("Required included value " + inc + " not found");
+ }
+ }
+ }
+ if ( prop.getExcludes() != null ) {
+ for(final String exc : prop.getExcludes()) {
+ boolean found = false;
+ for(final Object val : values) {
+ if ( exc.equals(val.toString())) {
+ found = true;
+ break;
+ }
+ }
+ if ( found ) {
+ messages.add("Required excluded value " + exc + " found");
+ }
+ }
+ }
+ }
+
+ void validateValue(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( value != null ) {
+ switch ( prop.getType() ) {
+ case BOOLEAN : validateBoolean(prop, value, messages);
+ break;
+ case BYTE : validateByte(prop, value, messages);
+ break;
+ case CHARACTER : validateCharacter(prop, value, messages);
+ break;
+ case DOUBLE : validateDouble(prop, value, messages);
+ break;
+ case FLOAT : validateFloat(prop, value, messages);
+ break;
+ case INTEGER : validateInteger(prop, value, messages);
+ break;
+ case LONG : validateLong(prop, value, messages);
+ break;
+ case SHORT : validateShort(prop, value, messages);
+ break;
+ case STRING : // no special validation for string
+ break;
+ case EMAIL : validateEmail(prop, value, messages);
+ break;
+ case PASSWORD : validatePassword(prop, value, messages);
+ break;
+ case URL : validateURL(prop, value, messages);
+ break;
+ case PATH : validatePath(prop, value, messages);
+ break;
+ default : messages.add("Unable to validate value - unknown property type : " + prop.getType());
+ }
+ validateRegex(prop, value, messages);
+ validateOptions(prop, value, messages);
+ } else {
+ messages.add("Null value provided for validation");
+ }
+ }
+
+ void validateBoolean(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( ! (value instanceof Boolean) ) {
+ if ( value instanceof String ) {
+ final String v = (String)value;
+ if ( ! v.equalsIgnoreCase("true") && !v.equalsIgnoreCase("false") ) {
+ messages.add("Boolean value must either be true or false, but not " + value);
+ }
+ } else {
+ messages.add("Boolean value must either be of type Boolean or String : " + value);
+ }
+ }
+ }
+
+ void validateByte(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( ! (value instanceof Byte) ) {
+ if ( value instanceof String ) {
+ final String v = (String)value;
+ try {
+ validateRange(prop, Byte.valueOf(v), messages);
+ } catch ( final NumberFormatException nfe ) {
+ messages.add("Value is not a valid Byte : " + value);
+ }
+ } else if ( value instanceof Number ) {
+ validateRange(prop, ((Number)value).byteValue(), messages);
+ } else {
+ messages.add("Byte value must either be of type Byte or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Byte)value, messages);
+ }
+ }
+
+ void validateShort(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( ! (value instanceof Short) ) {
+ if ( value instanceof String ) {
+ final String v = (String)value;
+ try {
+ validateRange(prop, Short.valueOf(v), messages);
+ } catch ( final NumberFormatException nfe ) {
+ messages.add("Value is not a valid Short : " + value);
+ }
+ } else if ( value instanceof Number ) {
+ validateRange(prop, ((Number)value).shortValue(), messages);
+ } else {
+ messages.add("Short value must either be of type Short or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Short)value, messages);
+ }
+ }
+
+ void validateInteger(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( ! (value instanceof Integer) ) {
+ if ( value instanceof String ) {
+ final String v = (String)value;
+ try {
+ validateRange(prop, Integer.valueOf(v), messages);
+ } catch ( final NumberFormatException nfe ) {
+ messages.add("Value is not a valid Integer : " + value);
+ }
+ } else if ( value instanceof Number ) {
+ validateRange(prop, ((Number)value).intValue(), messages);
+ } else {
+ messages.add("Integer value must either be of type Integer or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Integer)value, messages);
+ }
+ }
+
+ void validateLong(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( ! (value instanceof Long) ) {
+ if ( value instanceof String ) {
+ final String v = (String)value;
+ try {
+ validateRange(prop, Long.valueOf(v), messages);
+ } catch ( final NumberFormatException nfe ) {
+ messages.add("Value is not a valid Long : " + value);
+ }
+ } else if ( value instanceof Number ) {
+ validateRange(prop, ((Number)value).longValue(), messages);
+ } else {
+ messages.add("Long value must either be of type Long or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Long)value, messages);
+ }
+ }
+
+ void validateFloat(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( ! (value instanceof Float) ) {
+ if ( value instanceof String ) {
+ final String v = (String)value;
+ try {
+ validateRange(prop, Float.valueOf(v), messages);
+ } catch ( final NumberFormatException nfe ) {
+ messages.add("Value is not a valid Float : " + value);
+ }
+ } else if ( value instanceof Number ) {
+ validateRange(prop, ((Number)value).floatValue(), messages);
+ } else {
+ messages.add("Float value must either be of type Float or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Float)value, messages);
+ }
+ }
+
+ void validateDouble(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( ! (value instanceof Double) ) {
+ if ( value instanceof String ) {
+ final String v = (String)value;
+ try {
+ validateRange(prop, Double.valueOf(v), messages);
+ } catch ( final NumberFormatException nfe ) {
+ messages.add("Value is not a valid Double : " + value);
+ }
+ } else if ( value instanceof Number ) {
+ validateRange(prop, ((Number)value).doubleValue(), messages);
+ } else {
+ messages.add("Double value must either be of type Double or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Double)value, messages);
+ }
+ }
+
+ void validateCharacter(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( ! (value instanceof Character) ) {
+ if ( value instanceof String ) {
+ final String v = (String)value;
+ if ( v.length() > 1 ) {
+ messages.add("Value is not a valid Character : " + value);
+ }
+ } else {
+ messages.add("Character value must either be of type Character or String : " + value);
+ }
+ }
+ }
+
+ void validateURL(final PropertyDescription prop, final Object value, final List<String> messages) {
+ final String val = value.toString();
+ try {
+ new URL(val);
+ } catch ( final MalformedURLException mue) {
+ messages.add("Value is not a valid URL : " + val);
+ }
+ }
+
+ void validateEmail(final PropertyDescription prop, final Object value, final List<String> messages) {
+ final String val = value.toString();
+ // poor man's validation (should probably use InternetAddress)
+ if ( !val.contains("@") ) {
+ messages.add("Not a valid email address " + val);
+ }
+ }
+
+ void validatePassword(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( prop.getVariable() == null ) {
+ messages.add("Value for a password must use a variable");
+ }
+ }
+
+ void validatePath(final PropertyDescription prop, final Object value, final List<String> messages) {
+ final String val = value.toString();
+ // poor man's validation
+ if ( !val.startsWith("/") ) {
+ messages.add("Not a valid path " + val);
+ }
+ }
+
+ void validateRange(final PropertyDescription prop, final Number value, final List<String> messages) {
+ if ( prop.getRange() != null ) {
+ if ( prop.getRange().getMin() != null ) {
+ if ( value instanceof Float || value instanceof Double ) {
+ final double min = prop.getRange().getMin().doubleValue();
+ if ( value.doubleValue() < min ) {
+ messages.add("Value " + value + " is too low; should not be lower than " + prop.getRange().getMin());
+ }
+ } else {
+ final long min = prop.getRange().getMin().longValue();
+ if ( value.longValue() < min ) {
+ messages.add("Value " + value + " is too low; should not be lower than " + prop.getRange().getMin());
+ }
+ }
+ }
+ if ( prop.getRange().getMax() != null ) {
+ if ( value instanceof Float || value instanceof Double ) {
+ final double max = prop.getRange().getMax().doubleValue();
+ if ( value.doubleValue() > max ) {
+ messages.add("Value " + value + " is too high; should not be higher than " + prop.getRange().getMax());
+ }
+ } else {
+ final long max = prop.getRange().getMax().longValue();
+ if ( value.longValue() > max ) {
+ messages.add("Value " + value + " is too high; should not be higher than " + prop.getRange().getMax());
+ }
+ }
+ }
+ }
+ }
+
+ void validateRegex(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( prop.getRegexPattern() != null ) {
+ if ( !prop.getRegexPattern().matcher(value.toString()).matches() ) {
+ messages.add("Value " + value + " does not match regex " + prop.getRegex());
+ }
+ }
+ }
+
+ void validateOptions(final PropertyDescription prop, final Object value, final List<String> messages) {
+ if ( prop.getOptions() != null ) {
+ boolean found = false;
+ for(final Option opt : prop.getOptions()) {
+ if ( opt.getValue().equals(value.toString() ) ) {
+ found = true;
+ }
+ }
+ if ( !found ) {
+ messages.add("Value " + value + " does not match provided options");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/package-info.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/package-info.java
new file mode 100644
index 0000000..247f925
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.feature.extension.apiregions.api.config.validation;
+
+
diff --git a/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask b/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
index 0744c72..c2f9f10 100644
--- a/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
+++ b/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
@@ -4,3 +4,4 @@
org.apache.sling.feature.extension.apiregions.analyser.CheckApiRegionsOrder
org.apache.sling.feature.extension.apiregions.analyser.CheckApiRegionsBundleExportsImports
org.apache.sling.feature.extension.apiregions.analyser.CheckApiRegionsCrossFeatureDups
+org.apache.sling.feature.extension.apiregions.analyser.CheckConfigurationApi
\ No newline at end of file
diff --git a/src/main/resources/META-INF/services/org.apache.sling.feature.builder.MergeHandler b/src/main/resources/META-INF/services/org.apache.sling.feature.builder.MergeHandler
index ec6db93..caa2534 100644
--- a/src/main/resources/META-INF/services/org.apache.sling.feature.builder.MergeHandler
+++ b/src/main/resources/META-INF/services/org.apache.sling.feature.builder.MergeHandler
@@ -1 +1,2 @@
org.apache.sling.feature.extension.apiregions.APIRegionMergeHandler
+org.apache.sling.feature.extension.apiregions.ConfigurationApiMergeHandler
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandlerTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandlerTest.java
new file mode 100644
index 0000000..7c95589
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandlerTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Prototype;
+import org.apache.sling.feature.builder.BuilderContext;
+import org.apache.sling.feature.builder.FeatureBuilder;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi;
+import org.apache.sling.feature.extension.apiregions.api.config.Region;
+import org.junit.Test;
+
+public class ConfigurationApiMergeHandlerTest {
+
+ @Test public void testPrototypeRegionMerge() {
+ final Feature prototype = new Feature(ArtifactId.parse("g:p:1"));
+ final ConfigurationApi prototypeApi = new ConfigurationApi();
+ ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+
+ // always return prototype
+ final BuilderContext context = new BuilderContext(id -> prototype);
+ context.addMergeExtensions(new ConfigurationApiMergeHandler());
+
+ final Feature feature = new Feature(ArtifactId.parse("g:f:1"));
+ feature.setPrototype(new Prototype(prototype.getId()));
+ final ConfigurationApi featureApi = new ConfigurationApi();
+ ConfigurationApi.setConfigurationApi(feature, featureApi);
+
+ // no region
+ Feature result = FeatureBuilder.assemble(feature, context);
+ ConfigurationApi api = ConfigurationApi.getConfigurationApi(result);
+ assertNotNull(api);
+ assertNull(api.getRegion());
+
+ // prototype has region
+ prototypeApi.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+ result = FeatureBuilder.assemble(feature, context);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.INTERNAL, api.getRegion());
+
+ prototypeApi.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+ result = FeatureBuilder.assemble(feature, context);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+
+ // feature has region
+ prototypeApi.setRegion(null);
+ ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+ featureApi.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(feature, featureApi);
+ result = FeatureBuilder.assemble(feature, context);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.INTERNAL, api.getRegion());
+
+ featureApi.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(feature, featureApi);
+ result = FeatureBuilder.assemble(feature, context);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+
+ // both have region
+ prototypeApi.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+ featureApi.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(feature, featureApi);
+ result = FeatureBuilder.assemble(feature, context);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.INTERNAL, api.getRegion());
+
+ prototypeApi.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+ featureApi.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(feature, featureApi);
+ result = FeatureBuilder.assemble(feature, context);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.INTERNAL, api.getRegion());
+
+ prototypeApi.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+ featureApi.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(feature, featureApi);
+ result = FeatureBuilder.assemble(feature, context);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+
+ prototypeApi.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+ featureApi.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(feature, featureApi);
+ result = FeatureBuilder.assemble(feature, context);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+ }
+
+ @Test public void testRegionMerge() {
+ // always return prototype
+ final BuilderContext context = new BuilderContext(id -> null);
+ context.addMergeExtensions(new ConfigurationApiMergeHandler());
+
+ final Feature featureA = new Feature(ArtifactId.parse("g:a:1"));
+ final ConfigurationApi apiA = new ConfigurationApi();
+ ConfigurationApi.setConfigurationApi(featureA, apiA);
+
+ final Feature featureB = new Feature(ArtifactId.parse("g:b:1"));
+ final ConfigurationApi apiB = new ConfigurationApi();
+ ConfigurationApi.setConfigurationApi(featureB, apiB);
+
+ // no region
+ final ArtifactId id = ArtifactId.parse("g:m:1");
+ Feature result = FeatureBuilder.assemble(id, context, featureA, featureB);
+ ConfigurationApi api = ConfigurationApi.getConfigurationApi(result);
+ assertNotNull(api);
+ assertNull(api.getRegion());
+
+ // only A has region
+ apiA.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(featureA, apiA);
+ result = FeatureBuilder.assemble(id, context, featureA, featureB);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+
+ apiA.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(featureA, apiA);
+ result = FeatureBuilder.assemble(id, context, featureA, featureB);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+
+ // only B has region
+ apiA.setRegion(null);
+ ConfigurationApi.setConfigurationApi(featureA, apiA);
+ apiB.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(featureB, apiB);
+ result = FeatureBuilder.assemble(id, context, featureA, featureB);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+
+ apiB.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(featureB, apiB);
+ result = FeatureBuilder.assemble(id, context, featureA, featureB);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+
+ // both have region
+ apiA.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(featureA, apiA);
+ apiB.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(featureB, apiB);
+ result = FeatureBuilder.assemble(id, context, featureA, featureB);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.INTERNAL, api.getRegion());
+
+ apiA.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(featureA, apiA);
+ apiB.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(featureB, apiB);
+ result = FeatureBuilder.assemble(id, context, featureA, featureB);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+
+ apiA.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(featureA, apiA);
+ apiB.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(featureB, apiB);
+ result = FeatureBuilder.assemble(id, context, featureA, featureB);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+
+ apiA.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(featureA, apiA);
+ apiB.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(featureB, apiB);
+ result = FeatureBuilder.assemble(id, context, featureA, featureB);
+ api = ConfigurationApi.getConfigurationApi(result);
+ assertEquals(Region.GLOBAL, api.getRegion());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntityTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntityTest.java
new file mode 100644
index 0000000..165a5ed
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntityTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+
+import javax.json.Json;
+import javax.json.JsonValue;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class AttributeableEntityTest {
+
+ public static class AE extends AttributeableEntity {
+ // AttributeableEntity is abstract, therefore subclassing for testing
+ }
+
+ @Test public void testClear() {
+ final AE entity = new AE();
+ entity.getAttributes().put("a", Json.createValue(5));
+ entity.clear();
+ assertTrue(entity.getAttributes().isEmpty());
+ }
+
+ @Test public void testFromJSONObject() throws IOException {
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"a\" : 1, \"b\" : \"2\"}");
+
+ final AE entity = new AE();
+ entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+ assertEquals(2, entity.getAttributes().size());
+ assertEquals(Json.createValue(1), entity.getAttributes().get("a"));
+ assertEquals(Json.createValue("2"), entity.getAttributes().get("b"));
+ }
+
+ @Test public void testToJSONObject() throws IOException {
+ final AE entity = new AE();
+ entity.getAttributes().put("a", Json.createValue(1));
+ entity.getAttributes().put("b", Json.createValue("2"));
+
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"a\" : 1, \"b\" : \"2\"}");
+
+ assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+ }
+
+ @Test public void testGetString() {
+ final AE entity = new AE();
+ assertNull(entity.getString("foo"));
+ entity.getAttributes().put("foo", Json.createValue("bar"));
+ assertEquals("bar", entity.getString("foo"));
+ assertTrue(entity.getAttributes().isEmpty());
+ }
+
+ @Test public void testGetBoolean() throws IOException {
+ final AE entity = new AE();
+ assertTrue(entity.getBoolean("foo", true));
+
+ entity.getAttributes().put("foo", JsonValue.FALSE);
+ assertEquals(false, entity.getBoolean("foo", true));
+ assertTrue(entity.getAttributes().isEmpty());
+
+
+ try {
+ entity.getAttributes().put("foo", Json.createValue(1.0));
+ entity.getBoolean("foo", false);
+ fail();
+ } catch ( final IOException expected) {
+ // this is expected
+ }
+ }
+
+ @Test public void testGetInteger() throws IOException {
+ final AE entity = new AE();
+ assertEquals(7, entity.getInteger("foo", 7));
+
+ entity.getAttributes().put("foo", Json.createValue(9));
+ assertEquals(9, entity.getInteger("foo", 7));
+ assertTrue(entity.getAttributes().isEmpty());
+
+ entity.getAttributes().put("foo", Json.createValue("9"));
+ assertEquals(9, entity.getInteger("foo", 7));
+ assertTrue(entity.getAttributes().isEmpty());
+ }
+
+ @Test public void testGetNumber() throws IOException {
+ final AE entity = new AE();
+ assertNull(entity.getNumber("foo"));
+
+ entity.getAttributes().put("foo", Json.createValue(5));
+ assertEquals(5L, entity.getNumber("foo"));
+ assertTrue(entity.getAttributes().isEmpty());
+
+
+ try {
+ entity.getAttributes().put("foo", Json.createValue("a"));
+ entity.getNumber("foo");
+ fail();
+ } catch ( final IOException expected) {
+ // this is expected
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntityTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntityTest.java
new file mode 100644
index 0000000..896afc0
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntityTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class ConfigurableEntityTest {
+
+ public static class CE extends ConfigurableEntity {
+ // ConfigurableEntity is abstract, therefore subclassing for testing
+ }
+
+ @Test public void testClear() {
+ final CE entity = new CE();
+ entity.getAttributes().put("a", Json.createValue(5));
+ entity.setDeprecated("d");
+ entity.setTitle("t");
+ entity.setDescription("x");
+ entity.getPropertyDescriptions().put("a", new PropertyDescription());
+ entity.clear();
+ assertTrue(entity.getAttributes().isEmpty());
+ assertNull(entity.getDeprecated());
+ assertNull(entity.getTitle());
+ assertNull(entity.getDescription());
+ assertTrue(entity.getPropertyDescriptions().isEmpty());
+ }
+
+ @Test public void testFromJSONObject() throws IOException {
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"properties\" : { \"a\" : {}, \"b\" : {}}}");
+
+ final CE entity = new CE();
+ entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+ assertEquals(2, entity.getPropertyDescriptions().size());
+ assertNotNull(entity.getPropertyDescriptions().get("a"));
+ assertNotNull(entity.getPropertyDescriptions().get("b"));
+ }
+
+ @Test public void testToJSONObject() throws IOException {
+ final CE entity = new CE();
+ entity.getPropertyDescriptions().put("a", new PropertyDescription());
+ entity.getPropertyDescriptions().put("b", new PropertyDescription());
+
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"properties\" : { \"a\" : {}, \"b\" : {}}}");
+
+ assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApiTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApiTest.java
new file mode 100644
index 0000000..a362501
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApiTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.junit.Test;
+
+public class ConfigurationApiTest {
+
+ @Test public void testNullFeature() {
+ assertNull(ConfigurationApi.getConfigurationApi((Feature)null));
+ }
+
+ @Test public void testNullExtension() {
+ assertNull(ConfigurationApi.getConfigurationApi((Extension)null));
+ final Feature f = new Feature(ArtifactId.parse("g:a:1.0"));
+ assertNull(ConfigurationApi.getConfigurationApi(f));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testWrongExtensionType() {
+ final Feature f = new Feature(ArtifactId.parse("g:a:1.0"));
+ final Extension e = new Extension(ExtensionType.TEXT, ConfigurationApi.EXTENSION_NAME, ExtensionState.OPTIONAL);
+ f.getExtensions().add(e);
+ ConfigurationApi.getConfigurationApi(f);
+ }
+
+ @Test public void testSetConfigurationApi() {
+ final ConfigurationApi api = new ConfigurationApi();
+ final Feature f = new Feature(ArtifactId.parse("g:a:1"));
+
+ assertNull(f.getExtensions().getByName(ConfigurationApi.EXTENSION_NAME));
+
+ ConfigurationApi.setConfigurationApi(f, api);
+ assertNotNull(f.getExtensions().getByName(ConfigurationApi.EXTENSION_NAME));
+ assertNotNull(ConfigurationApi.getConfigurationApi(f));
+
+ ConfigurationApi.setConfigurationApi(f, null);
+ assertNull(f.getExtensions().getByName(ConfigurationApi.EXTENSION_NAME));
+ }
+
+ @Test public void testClear() {
+ final ConfigurationApi entity = new ConfigurationApi();
+ entity.getAttributes().put("a", Json.createValue(5));
+ entity.getConfigurationDescriptions().put("pid", new ConfigurationDescription());
+ entity.getFactoryConfigurationDescriptions().put("factory", new FactoryConfigurationDescription());
+ entity.getFrameworkPropertyDescriptions().put("prop", new FrameworkPropertyDescription());
+ entity.getInternalConfigurations().add("ipid");
+ entity.getInternalFactoryConfigurations().add("ifactory");
+ entity.getInternalFrameworkProperties().add("iprop");
+ entity.setRegion(Region.GLOBAL);
+ entity.clear();
+ assertTrue(entity.getAttributes().isEmpty());
+ assertTrue(entity.getConfigurationDescriptions().isEmpty());
+ assertTrue(entity.getFactoryConfigurationDescriptions().isEmpty());
+ assertTrue(entity.getFrameworkPropertyDescriptions().isEmpty());
+ assertTrue(entity.getInternalConfigurations().isEmpty());
+ assertTrue(entity.getInternalFactoryConfigurations().isEmpty());
+ assertTrue(entity.getInternalFrameworkProperties().isEmpty());
+ assertNull(entity.getRegion());
+ }
+
+ @Test public void testFromJSONObject() throws IOException {
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"a\" : 5, \"configurations\" : { \"pid\": {}}, " +
+ "\"factory-configurations\" : { \"factory\" : {}}," +
+ "\"framework-properties\" : { \"prop\" : { \"type\" : \"STRING\"}}," +
+ "\"internal-configurations\" : [\"ipid\"],"+
+ "\"internal-factory-configurations\" : [\"ifactory\"],"+
+ "\"internal-framework-properties\" : [\"iprop\"],"+
+ "\"region\" : \"INTERNAL\"}");
+
+ final ConfigurationApi entity = new ConfigurationApi();
+ entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+ assertEquals(1, entity.getConfigurationDescriptions().size());
+ assertEquals(1, entity.getFactoryConfigurationDescriptions().size());
+ assertEquals(1, entity.getFrameworkPropertyDescriptions().size());
+ assertEquals(1, entity.getInternalConfigurations().size());
+ assertEquals(1, entity.getInternalFactoryConfigurations().size());
+ assertEquals(1, entity.getInternalFrameworkProperties().size());
+ assertTrue(entity.getConfigurationDescriptions().containsKey("pid"));
+ assertTrue(entity.getFactoryConfigurationDescriptions().containsKey("factory"));
+ assertTrue(entity.getFrameworkPropertyDescriptions().containsKey("prop"));
+ assertTrue(entity.getInternalConfigurations().contains("ipid"));
+ assertTrue(entity.getInternalFactoryConfigurations().contains("ifactory"));
+ assertTrue(entity.getInternalFrameworkProperties().contains("iprop"));
+ assertEquals(Region.INTERNAL, entity.getRegion());
+ }
+
+ @Test public void testToJSONObject() throws IOException {
+ final ConfigurationApi entity = new ConfigurationApi();
+ entity.getAttributes().put("a", Json.createValue(5));
+ entity.getConfigurationDescriptions().put("pid", new ConfigurationDescription());
+ entity.getFactoryConfigurationDescriptions().put("factory", new FactoryConfigurationDescription());
+ entity.getFrameworkPropertyDescriptions().put("prop", new FrameworkPropertyDescription());
+ entity.getInternalConfigurations().add("ipid");
+ entity.getInternalFactoryConfigurations().add("ifactory");
+ entity.getInternalFrameworkProperties().add("iprop");
+ entity.setRegion(Region.INTERNAL);
+
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"a\" : 5, \"configurations\" : { \"pid\": {}}, " +
+ "\"factory-configurations\" : { \"factory\" : {}}," +
+ "\"framework-properties\" : { \"prop\" : {}}," +
+ "\"internal-configurations\" : [\"ipid\"],"+
+ "\"internal-factory-configurations\" : [\"ifactory\"],"+
+ "\"internal-framework-properties\" : [\"iprop\"],"+
+ "\"region\" : \"INTERNAL\"}");
+
+ assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+ }
+}
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntityTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntityTest.java
new file mode 100644
index 0000000..f2ad633
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntityTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class DescribableEntityTest {
+
+ public static class DE extends DescribableEntity {
+ // DescribableEntity is abstract, therefore subclassing for testing
+ }
+
+ @Test public void testClear() {
+ final DE entity = new DE();
+ entity.getAttributes().put("a", Json.createValue(5));
+ entity.setDeprecated("d");
+ entity.setTitle("t");
+ entity.setDescription("x");
+ entity.clear();
+ assertTrue(entity.getAttributes().isEmpty());
+ assertNull(entity.getDeprecated());
+ assertNull(entity.getTitle());
+ assertNull(entity.getDescription());
+ }
+
+ @Test public void testFromJSONObject() throws IOException {
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"a\" : 1, \"b\" : \"2\", \"title\" : \"t\", \"description\" : \"desc\", \"deprecated\" : \"depr\"}");
+
+ final DE entity = new DE();
+ entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+ assertEquals(2, entity.getAttributes().size());
+ assertEquals(Json.createValue(1), entity.getAttributes().get("a"));
+ assertEquals(Json.createValue("2"), entity.getAttributes().get("b"));
+ assertEquals("t", entity.getTitle());
+ assertEquals("desc", entity.getDescription());
+ assertEquals("depr", entity.getDeprecated());
+ }
+
+ @Test public void testToJSONObject() throws IOException {
+ final DE entity = new DE();
+ entity.getAttributes().put("a", Json.createValue(1));
+ entity.getAttributes().put("b", Json.createValue("2"));
+ entity.setTitle("t");
+ entity.setDescription("desc");
+ entity.setDeprecated("depr");
+
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"a\" : 1, \"b\" : \"2\", \"title\" : \"t\", \"description\" : \"desc\", \"deprecated\" : \"depr\"}");
+
+ assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescriptionTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescriptionTest.java
new file mode 100644
index 0000000..d396a71
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescriptionTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class FactoryConfigurationDescriptionTest {
+
+ @Test public void testClear() {
+ final FactoryConfigurationDescription entity = new FactoryConfigurationDescription();
+ entity.getAttributes().put("a", Json.createValue(5));
+ entity.setDeprecated("d");
+ entity.setTitle("t");
+ entity.setDescription("x");
+ entity.getPropertyDescriptions().put("a", new PropertyDescription());
+ entity.getOperations().add(Operation.CREATE);
+ entity.getInternalNames().add("internal");
+ entity.clear();
+ assertTrue(entity.getAttributes().isEmpty());
+ assertNull(entity.getDeprecated());
+ assertNull(entity.getTitle());
+ assertNull(entity.getDescription());
+ assertTrue(entity.getPropertyDescriptions().isEmpty());
+ assertEquals(2, entity.getOperations().size());
+ assertTrue(entity.getInternalNames().isEmpty());
+ }
+
+ @Test public void testFromJSONObject() throws IOException {
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"internal-names\" : [ \"a\", \"b\"], \"operations\" : [\"create\"]}");
+
+ final FactoryConfigurationDescription entity = new FactoryConfigurationDescription();
+ entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+ assertEquals(2, entity.getInternalNames().size());
+ assertTrue(entity.getInternalNames().contains("a"));
+ assertTrue(entity.getInternalNames().contains("b"));
+ assertEquals(1, entity.getOperations().size());
+ assertEquals(Operation.CREATE, entity.getOperations().iterator().next());
+ }
+
+ @Test public void testToJSONObject() throws IOException {
+ final FactoryConfigurationDescription entity = new FactoryConfigurationDescription();
+ entity.getInternalNames().add("a");
+ entity.getInternalNames().add("b");
+ entity.getOperations().clear();
+ entity.getOperations().add(Operation.UPDATE);
+
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"internal-names\" : [ \"a\", \"b\"], \"operations\" : [\"UPDATE\"]}");
+
+ assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/OptionTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/OptionTest.java
new file mode 100644
index 0000000..924c2b0
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/OptionTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class OptionTest {
+
+ @Test public void testClear() {
+ final Option entity = new Option();
+ entity.getAttributes().put("a", Json.createValue(5));
+ entity.setDeprecated("d");
+ entity.setTitle("t");
+ entity.setDescription("x");
+ entity.setValue("foo");
+ entity.clear();
+ assertTrue(entity.getAttributes().isEmpty());
+ assertNull(entity.getDeprecated());
+ assertNull(entity.getTitle());
+ assertNull(entity.getDescription());
+ assertNull(entity.getValue());
+ }
+
+ @Test public void testFromJSONObject() throws IOException {
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"a\" : 1, \"b\" : \"2\", \"title\" : \"t\", \"description\" : \"desc\", \"value\" : \"v\" }");
+
+ final Option entity = new Option();
+ entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+ assertEquals(2, entity.getAttributes().size());
+ assertEquals(Json.createValue(1), entity.getAttributes().get("a"));
+ assertEquals(Json.createValue("2"), entity.getAttributes().get("b"));
+ assertEquals("t", entity.getTitle());
+ assertEquals("desc", entity.getDescription());
+ assertEquals("v", entity.getValue());
+ }
+
+ @Test public void testToJSONObject() throws IOException {
+ final Option entity = new Option();
+ entity.getAttributes().put("a", Json.createValue(1));
+ entity.getAttributes().put("b", Json.createValue("2"));
+ entity.setTitle("t");
+ entity.setDescription("desc");
+ entity.setValue("v");
+
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"a\" : 1, \"b\" : \"2\", \"title\" : \"t\", \"description\" : \"desc\", \"value\" : \"v\" }");
+
+ assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..d9b50c2
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class PropertyDescriptionTest {
+
+ @Test public void testClear() {
+ final PropertyDescription entity = new PropertyDescription();
+ entity.getAttributes().put("a", Json.createValue(5));
+ entity.setDeprecated("d");
+ entity.setTitle("t");
+ entity.setDescription("x");
+ entity.setCardinality(5);
+ entity.setExcludes(new String[] {"ex"});
+ entity.setIncludes(new String[] {"in"});
+ entity.setOptions(Arrays.asList(new Option()));
+ entity.setRange(new Range());
+ entity.setRegex(".");
+ entity.setRequired(true);
+ entity.setVariable("var");
+ entity.setType(PropertyType.BYTE);
+ entity.clear();
+ assertTrue(entity.getAttributes().isEmpty());
+ assertNull(entity.getDeprecated());
+ assertNull(entity.getTitle());
+ assertNull(entity.getDescription());
+ assertEquals(1, entity.getCardinality());
+ assertNull(entity.getExcludes());
+ assertNull(entity.getIncludes());
+ assertNull(entity.getOptions());
+ assertNull(entity.getRange());
+ assertNull(entity.getRegex());
+ assertNull(entity.getRegexPattern());
+ assertNull(entity.getVariable());
+ assertFalse(entity.isRequired());
+ assertEquals(PropertyType.STRING, entity.getType());
+ }
+
+ @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\": \".\"}");
+
+ final PropertyDescription entity = new PropertyDescription();
+ entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+
+ assertEquals(5, entity.getCardinality());
+ assertEquals(PropertyType.BYTE, entity.getType());
+ assertTrue(entity.isRequired());
+ assertEquals("var", entity.getVariable());
+ assertNotNull(entity.getRange());
+ assertArrayEquals(new String[] {"ex"}, entity.getExcludes());
+ assertArrayEquals(new String[] {"in"}, entity.getIncludes());
+ assertEquals(1, entity.getOptions().size());
+ assertEquals(".", entity.getRegex());
+ assertNotNull(entity.getRegexPattern());
+
+ // test defaults and empty values
+ ext.setJSON("{ \"variable\" : \"var\", \"regex\": \".\"}");
+ entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+
+ assertEquals(1, entity.getCardinality());
+ assertEquals(PropertyType.STRING, entity.getType());
+ assertFalse(entity.isRequired());
+ assertEquals("var", entity.getVariable());
+ assertNull(entity.getRange());
+ assertNull(entity.getExcludes());
+ assertNull(entity.getIncludes());
+ assertNull(entity.getOptions());
+ assertEquals(".", entity.getRegex());
+ assertNotNull(entity.getRegexPattern());
+ }
+
+ @Test public void testToJSONObject() throws IOException {
+ final PropertyDescription entity = new PropertyDescription();
+ entity.setCardinality(5);
+ entity.setExcludes(new String[] {"ex"});
+ entity.setIncludes(new String[] {"in"});
+ entity.setOptions(Arrays.asList(new Option()));
+ entity.setRange(new Range());
+ entity.setRegex(".");
+ entity.setRequired(true);
+ entity.setVariable("var");
+ entity.setType(PropertyType.BYTE);
+
+ 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\": \".\"}");
+
+ assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+
+ // test defaults and empty values
+ entity.setCardinality(1);
+ entity.setType(null);
+ entity.setRequired(false);
+ entity.setRange(null);
+ entity.setOptions(null);
+ entity.setExcludes(null);
+ entity.setIncludes(null);
+
+ ext.setJSON("{ \"variable\" : \"var\", \"regex\": \".\"}");
+
+ assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/RangeTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/RangeTest.java
new file mode 100644
index 0000000..b894daf
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/RangeTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class RangeTest {
+
+ @Test public void testClear() {
+ final Range entity = new Range();
+ entity.getAttributes().put("a", Json.createValue(5));
+ entity.setMax(5);
+ entity.setMin(20.1);
+ entity.clear();
+ assertTrue(entity.getAttributes().isEmpty());
+ assertNull(entity.getMax());
+ assertNull(entity.getMin());
+ }
+
+ @Test public void testFromJSONObject() throws IOException {
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"min\" : 5, \"max\" : 20.1 }");
+
+ final Range entity = new Range();
+ entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+ assertEquals(5L, entity.getMin());
+ assertEquals(20.1, entity.getMax());
+ }
+
+ @Test public void testToJSONObject() throws IOException {
+ final Range entity = new Range();
+ entity.setMin(5);
+ entity.setMax(20.1);
+
+ final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+ ext.setJSON("{ \"min\" : 5, \"max\" : 20.1 }");
+
+ assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidatorTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidatorTest.java
new file mode 100644
index 0000000..46c1f57
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidatorTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.validation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FactoryConfigurationDescription;
+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.Region;
+import org.junit.Test;
+import org.osgi.framework.Constants;
+
+public class ConfigurationValidatorTest {
+
+ private final ConfigurationValidator validator = new ConfigurationValidator();
+
+ @Test public void testWrongDescriptionTypeForConfiguration() {
+ final Configuration cfg = new Configuration("org.apache");
+ final FactoryConfigurationDescription fcd = new FactoryConfigurationDescription();
+
+ final ConfigurationValidationResult result = validator.validate(cfg, fcd, null);
+ assertFalse(result.isValid());
+ assertEquals(1, result.getGlobalErrors().size());
+ }
+
+ @Test public void testWrongDescriptionTypeForFactoryConfiguration() {
+ final Configuration cfg = new Configuration("org.apache~foo");
+ final ConfigurationDescription fcd = new ConfigurationDescription();
+
+ final ConfigurationValidationResult result = validator.validate(cfg, fcd, null);
+ assertFalse(result.isValid());
+ assertEquals(1, result.getGlobalErrors().size());
+ }
+
+ @Test public void testDeprecated() {
+ final Configuration cfg = new Configuration("org.apache");
+ final ConfigurationDescription cd = new ConfigurationDescription();
+
+ ConfigurationValidationResult result = validator.validate(cfg, cd, null);
+ assertTrue(result.isValid());
+ assertTrue(result.getWarnings().isEmpty());
+
+ cd.setDeprecated("this is deprecated");
+ result = validator.validate(cfg, cd, null);
+ assertTrue(result.isValid());
+ assertFalse(result.getWarnings().isEmpty());
+ assertEquals("this is deprecated", result.getWarnings().get(0));
+ }
+
+ @Test public void testServiceRanking() {
+ final Configuration cfg = new Configuration("org.apache");
+ final ConfigurationDescription cd = new ConfigurationDescription();
+ cfg.getProperties().put(Constants.SERVICE_RANKING, 5);
+
+ ConfigurationValidationResult result = validator.validate(cfg, cd, null);
+ assertTrue(result.isValid());
+
+ cfg.getProperties().put(Constants.SERVICE_RANKING, "5");
+ result = validator.validate(cfg, cd, null);
+ assertFalse(result.isValid());
+ }
+
+ @Test public void testAllowedProperties() {
+ final Configuration cfg = new Configuration("org.apache");
+ final ConfigurationDescription cd = new ConfigurationDescription();
+ cfg.getProperties().put(Constants.SERVICE_DESCRIPTION, "desc");
+ cfg.getProperties().put(Constants.SERVICE_VENDOR, "vendor");
+
+ ConfigurationValidationResult result = validator.validate(cfg, cd, null);
+ assertTrue(result.isValid());
+ }
+
+ @Test public void testAdditionalProperties() {
+ final Configuration cfg = new Configuration("org.apache");
+ cfg.getProperties().put("a", "desc");
+
+ final ConfigurationDescription cd = new ConfigurationDescription();
+ final PropertyDescription prop = new PropertyDescription();
+ cd.getPropertyDescriptions().put("a", prop);
+
+ ConfigurationValidationResult result = validator.validate(cfg, cd, Region.GLOBAL);
+ assertTrue(result.isValid());
+ assertEquals(1, result.getPropertyResults().size());
+ assertTrue(result.getPropertyResults().get("a").isValid());
+
+ cfg.getProperties().put("b", "vendor");
+ result = validator.validate(cfg, cd, Region.GLOBAL);
+ assertFalse(result.isValid());
+ assertEquals(2, result.getPropertyResults().size());
+ assertTrue(result.getPropertyResults().get("a").isValid());
+ assertFalse(result.getPropertyResults().get("b").isValid());
+
+ // allowed if internal
+ result = validator.validate(cfg, cd, Region.INTERNAL);
+ assertTrue(result.isValid());
+ assertEquals(2, result.getPropertyResults().size());
+ }
+
+ @Test public void testInvalidProperty() {
+ final Configuration cfg = new Configuration("org.apache");
+ cfg.getProperties().put("a", "desc");
+ cfg.getProperties().put("b", "vendor");
+
+ final ConfigurationDescription cd = new ConfigurationDescription();
+ final PropertyDescription propA = new PropertyDescription();
+ cd.getPropertyDescriptions().put("a", propA);
+ final PropertyDescription propB = new PropertyDescription();
+ propB.setType(PropertyType.INTEGER);
+ cd.getPropertyDescriptions().put("b", propB);
+
+ ConfigurationValidationResult result = validator.validate(cfg, cd, null);
+ assertFalse(result.isValid());
+ assertEquals(2, result.getPropertyResults().size());
+ assertTrue(result.getPropertyResults().get("a").isValid());
+ assertFalse(result.getPropertyResults().get("b").isValid());
+ }
+}
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidatorTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidatorTest.java
new file mode 100644
index 0000000..8d4130a
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidatorTest.java
@@ -0,0 +1,635 @@
+/*
+ * 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.validation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.FeatureProvider;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FactoryConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FrameworkPropertyDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.Operation;
+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.Region;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FeatureValidatorTest {
+
+ private static final String PID = "org.apache.sling";
+
+ private static final String FACTORY_PID = "org.apache.sling.factory";
+
+ private final FeatureValidator validator = new FeatureValidator();
+
+ @Before public void setup() {
+ this.validator.setFeatureProvider(null);
+ }
+
+ private Feature createFeature(final String id) {
+ final Feature f= new Feature(ArtifactId.parse(id));
+ final Configuration c = new Configuration(PID);
+ c.getProperties().put("prop", "a");
+ f.getConfigurations().add(c);
+
+ final Configuration fc = new Configuration(FACTORY_PID.concat("~print"));
+ fc.getProperties().put("key", "value");
+ f.getConfigurations().add(fc);
+
+ f.getFrameworkProperties().put("prop", "1");
+
+ return f;
+ }
+
+ private ConfigurationApi createApi() {
+ final ConfigurationApi api = new ConfigurationApi();
+
+ final ConfigurationDescription cd = new ConfigurationDescription();
+ cd.getPropertyDescriptions().put("prop", new PropertyDescription());
+
+ api.getConfigurationDescriptions().put(PID, cd);
+
+ final FactoryConfigurationDescription fd = new FactoryConfigurationDescription();
+ fd.getPropertyDescriptions().put("key", new PropertyDescription());
+
+ api.getFactoryConfigurationDescriptions().put(FACTORY_PID, fd);
+
+ final FrameworkPropertyDescription fpd = new FrameworkPropertyDescription();
+ fpd.setType(PropertyType.INTEGER);
+ api.getFrameworkPropertyDescriptions().put("prop", fpd);
+
+ return api;
+ }
+
+ @Test public void testGetRegionInfoConfigurationNoOrigin() {
+ final Feature f1 = createFeature("g:a:1");
+ final Configuration cfg = f1.getConfigurations().getConfiguration(PID);
+
+ // no api set
+ FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // empty region in api
+ final ConfigurationApi api = createApi();
+ ConfigurationApi.setConfigurationApi(f1, api);
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // global region in api
+ api.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // internal region in api
+ api.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.INTERNAL, info.region);
+ assertFalse(info.isUpdate);
+ }
+
+ @Test public void testGetRegionInfoConfigurationSingleOrigin() {
+ final Feature f1 = createFeature("g:a:1");
+ final Configuration cfg = f1.getConfigurations().getConfiguration(PID);
+
+ final Feature f2 = createFeature("g:b:1");
+ cfg.setFeatureOrigins(Collections.singletonList(f2.getId()));
+
+ // set feature provider to always provide f2
+ this.validator.setFeatureProvider(id -> f2);
+ // no api in origin
+ FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // no region in api
+ final ConfigurationApi api2 = new ConfigurationApi();
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // global in api
+ api2.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // internal in api
+ api2.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.INTERNAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // unknown id
+ this.validator.setFeatureProvider(id -> null);
+ cfg.setFeatureOrigins(Collections.singletonList(ArtifactId.parse("g:xy:1")));
+ info = validator.getRegionInfo(f1, cfg);
+ assertNull(info);
+ }
+
+ @Test public void testGetRegionInfoConfigurationMultipleOrigins() {
+ final Feature f1 = createFeature("g:a:1");
+ final Configuration cfg = f1.getConfigurations().getConfiguration(PID);
+
+ final Feature f2 = createFeature("g:b:1");
+ final Feature f3 = createFeature("g:c:1");
+ cfg.setFeatureOrigins(Arrays.asList(f2.getId(), f3.getId()));
+
+ final FeatureProvider provider = new FeatureProvider() {
+
+ @Override
+ public Feature provide(final ArtifactId id) {
+ if ( f1.getId().equals(id) ) {
+ return f1;
+ } else if ( f2.getId().equals(id)) {
+ return f2;
+ } else if ( f3.getId().equals(id)) {
+ return f3;
+ }
+ return null;
+ }
+
+ };
+
+ this.validator.setFeatureProvider(provider);
+
+ // no api in origins
+ FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertTrue(info.isUpdate);
+
+ // global-internal
+ final ConfigurationApi api2 = new ConfigurationApi();
+ final ConfigurationApi api3 = new ConfigurationApi();
+ api2.setRegion(Region.GLOBAL);
+ api3.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ ConfigurationApi.setConfigurationApi(f3, api3);
+
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertTrue(info.isUpdate);
+
+ // global-global
+ api2.setRegion(Region.GLOBAL);
+ api3.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ ConfigurationApi.setConfigurationApi(f3, api3);
+
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertTrue(info.isUpdate);
+
+ // internal-internal
+ api2.setRegion(Region.INTERNAL);
+ api3.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ ConfigurationApi.setConfigurationApi(f3, api3);
+
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.INTERNAL, info.region);
+ assertTrue(info.isUpdate);
+
+ // internal-global
+ api2.setRegion(Region.INTERNAL);
+ api3.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ ConfigurationApi.setConfigurationApi(f3, api3);
+
+ info = validator.getRegionInfo(f1, cfg);
+ assertEquals(Region.GLOBAL, info.region);
+ assertTrue(info.isUpdate);
+ }
+
+ @Test public void testGetRegionInfoFrameworkPropertyNoOrigin() {
+ final Feature f1 = createFeature("g:a:1");
+
+ // no api set
+ FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // empty region in api
+ final ConfigurationApi api = createApi();
+ ConfigurationApi.setConfigurationApi(f1, api);
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // global region in api
+ api.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // internal region in api
+ api.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.INTERNAL, info.region);
+ assertFalse(info.isUpdate);
+ }
+
+ @Test public void testGetRegionInfoFrameworkPropertySingleOrigin() {
+ final Feature f1 = createFeature("g:a:1");
+
+ final Feature f2 = createFeature("g:b:1");
+ f1.setFeatureOrigins(f1.getFrameworkPropertyMetadata("prop"), Collections.singletonList(f2.getId()));
+
+ // set feature provider to always provide f2
+ this.validator.setFeatureProvider(id -> f2);
+ // no api in origin
+ FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // no region in api
+ final ConfigurationApi api2 = new ConfigurationApi();
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // global in api
+ api2.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // internal in api
+ api2.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.INTERNAL, info.region);
+ assertFalse(info.isUpdate);
+
+ // unknown id
+ this.validator.setFeatureProvider(id -> null);
+ f1.setFeatureOrigins(f1.getFrameworkPropertyMetadata("prop"), Collections.singletonList(ArtifactId.parse("g:xy:1")));
+ info = validator.getRegionInfo(f1, "prop");
+ assertNull(info);
+ }
+
+ @Test public void testGetRegionInfoFrameworkPropertyMultipleOrigins() {
+ final Feature f1 = createFeature("g:a:1");
+
+ final Feature f2 = createFeature("g:b:1");
+ final Feature f3 = createFeature("g:c:1");
+ f1.setFeatureOrigins(f1.getFrameworkPropertyMetadata("prop"), Arrays.asList(f2.getId(), f3.getId()));
+
+ final FeatureProvider provider = new FeatureProvider() {
+
+ @Override
+ public Feature provide(final ArtifactId id) {
+ if ( f1.getId().equals(id) ) {
+ return f1;
+ } else if ( f2.getId().equals(id)) {
+ return f2;
+ } else if ( f3.getId().equals(id)) {
+ return f3;
+ }
+ return null;
+ }
+
+ };
+
+ this.validator.setFeatureProvider(provider);
+
+ // no api in origins
+ FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertTrue(info.isUpdate);
+
+ // global-internal
+ final ConfigurationApi api2 = new ConfigurationApi();
+ final ConfigurationApi api3 = new ConfigurationApi();
+ api2.setRegion(Region.GLOBAL);
+ api3.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ ConfigurationApi.setConfigurationApi(f3, api3);
+
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertTrue(info.isUpdate);
+
+ // global-global
+ api2.setRegion(Region.GLOBAL);
+ api3.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ ConfigurationApi.setConfigurationApi(f3, api3);
+
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertTrue(info.isUpdate);
+
+ // internal-internal
+ api2.setRegion(Region.INTERNAL);
+ api3.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ ConfigurationApi.setConfigurationApi(f3, api3);
+
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.INTERNAL, info.region);
+ assertTrue(info.isUpdate);
+
+ // internal-global
+ api2.setRegion(Region.INTERNAL);
+ api3.setRegion(Region.GLOBAL);
+ ConfigurationApi.setConfigurationApi(f2, api2);
+ ConfigurationApi.setConfigurationApi(f3, api3);
+
+ info = validator.getRegionInfo(f1, "prop");
+ assertEquals(Region.GLOBAL, info.region);
+ assertTrue(info.isUpdate);
+ }
+
+ @Test public void testSingleConfigurationValidation() {
+ final Feature f1 = createFeature("g:a:1");
+ final ConfigurationApi api = createApi();
+ ConfigurationApi.setConfigurationApi(f1, api);
+
+ FeatureValidationResult result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ // add property
+ f1.getConfigurations().getConfiguration(PID).getProperties().put("b", "x");
+ result = validator.validate(f1, api);
+ assertFalse(result.isValid());
+ }
+
+ @Test public void testInternalConfiguration() {
+ final Feature f1 = createFeature("g:a:1");
+ final ConfigurationApi api = new ConfigurationApi();
+ ConfigurationApi.setConfigurationApi(f1, api);
+
+ // global region
+ FeatureValidationResult result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ // mark configurations as internal
+ api.getInternalConfigurations().add(PID);
+ api.getInternalFactoryConfigurations().add(FACTORY_PID);
+ ConfigurationApi.setConfigurationApi(f1, api);
+
+ // global region
+ result = validator.validate(f1, api);
+ assertFalse(result.isValid());
+ assertFalse(result.getConfigurationResults().get(PID).isValid());
+ assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+ // internal region
+ api.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+ }
+
+ @Test public void testInternalFactoryNames() {
+ final Feature f1 = createFeature("g:a:1");
+
+ final Configuration fa = new Configuration(FACTORY_PID.concat("~a"));
+ fa.getProperties().put("key", "value");
+ f1.getConfigurations().add(fa);
+
+ final Configuration fb = new Configuration(FACTORY_PID.concat("~b"));
+ fb.getProperties().put("key", "value");
+ f1.getConfigurations().add(fb);
+
+ final ConfigurationApi api = createApi();
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getInternalNames().add("a");
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getInternalNames().add("b");
+ ConfigurationApi.setConfigurationApi(f1, api);
+
+ FeatureValidationResult result = validator.validate(f1, api);
+ assertFalse(result.isValid());
+ assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~a")).isValid());
+ assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~b")).isValid());
+ assertTrue(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+ // internal region
+ api.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+ }
+
+ @Test public void testFactoryConfigurationOperationsWithCreate() {
+ final Feature f1 = createFeature("g:a:1");
+ final ConfigurationApi api = createApi();
+
+ // no operation -> fail
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ ConfigurationApi.setConfigurationApi(f1, api);
+ FeatureValidationResult result = validator.validate(f1, api);
+ assertFalse(result.isValid());
+ assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+ // only update -> fail
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertFalse(result.isValid());
+ assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+ // only create -> success
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ // update, create -> success
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ // internal region -> always success
+ api.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+ }
+
+ @Test public void testFactoryConfigurationOperationsWithUpdate() {
+ final Feature f1 = createFeature("g:a:1");
+ final ConfigurationApi api = createApi();
+
+ final Configuration cfg = f1.getConfigurations().getConfiguration(FACTORY_PID.concat("~print"));
+
+ final Feature f2 = createFeature("g:b:1");
+ final Feature f3 = createFeature("g:c:1");
+ cfg.setFeatureOrigins(Arrays.asList(f2.getId(), f3.getId()));
+
+ final FeatureProvider provider = new FeatureProvider() {
+
+ @Override
+ public Feature provide(final ArtifactId id) {
+ if ( f1.getId().equals(id) ) {
+ return f1;
+ } else if ( f2.getId().equals(id)) {
+ return f2;
+ } else if ( f3.getId().equals(id)) {
+ return f3;
+ }
+ return null;
+ }
+
+ };
+
+ this.validator.setFeatureProvider(provider);
+
+ // no operation -> fail
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ ConfigurationApi.setConfigurationApi(f1, api);
+ FeatureValidationResult result = validator.validate(f1, api);
+ assertFalse(result.isValid());
+ assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+ // only update -> success
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ // only create -> fail
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertFalse(result.isValid());
+ assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+ // update, create -> success
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ // internal region -> always success
+ api.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ ConfigurationApi.setConfigurationApi(f2, api);
+ ConfigurationApi.setConfigurationApi(f3, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+ }
+
+ @Test public void testInternalFrameworkProperty() {
+ final Feature f1 = createFeature("g:a:1");
+ final ConfigurationApi api = new ConfigurationApi();
+ ConfigurationApi.setConfigurationApi(f1, api);
+
+ // global region
+ FeatureValidationResult result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ // mark framework property as internal
+ api.getInternalFrameworkProperties().add("prop");
+ ConfigurationApi.setConfigurationApi(f1, api);
+
+ // global region
+ result = validator.validate(f1, api);
+ assertFalse(result.isValid());
+ assertFalse(result.getFrameworkPropertyResults().get("prop").isValid());
+
+ // internal region
+ api.setRegion(Region.INTERNAL);
+ ConfigurationApi.setConfigurationApi(f1, api);
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+ }
+
+ @Test public void testFrameworkProperty() {
+ final Feature f1 = createFeature("g:a:1");
+ final ConfigurationApi api = createApi();
+ ConfigurationApi.setConfigurationApi(f1, api);
+
+ // value is valid
+ FeatureValidationResult result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ // no value -> valid
+ f1.getFrameworkProperties().remove("prop");
+ result = validator.validate(f1, api);
+ assertTrue(result.isValid());
+
+ // invalid value
+ f1.getFrameworkProperties().put("prop", "foo");
+ result = validator.validate(f1, api);
+ assertFalse(result.isValid());
+ assertFalse(result.getFrameworkPropertyResults().get("prop").isValid());
+ }
+}
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
new file mode 100644
index 0000000..acbffa5
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidatorTest.java
@@ -0,0 +1,413 @@
+/*
+ * 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.validation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sling.feature.extension.apiregions.api.config.Option;
+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;
+import org.junit.Test;
+
+public class PropertyValidatorTest {
+
+ private final PropertyValidator validator = new PropertyValidator();
+
+ @Test public void testValidateWithNull() {
+ final PropertyDescription prop = new PropertyDescription();
+
+ // prop not required - no error
+ assertTrue(validator.validate(null, prop).getErrors().isEmpty());
+ assertTrue(validator.validate(null, prop).isValid());
+
+ // prop required - error
+ prop.setRequired(true);
+ assertEquals(1, validator.validate(null, prop).getErrors().size());
+ assertFalse(validator.validate(null, prop).isValid());
+ }
+
+ @Test public void testValidateBoolean() {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.setType(PropertyType.BOOLEAN);
+
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateBoolean(prop, Boolean.TRUE, messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateBoolean(prop, Boolean.FALSE, messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateBoolean(prop, "TRUE", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateBoolean(prop, "FALSE", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateBoolean(prop, "yes", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ validator.validateBoolean(prop, 1, messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+ }
+
+ @Test public void testValidateByte() {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.setType(PropertyType.BYTE);
+
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateByte(prop, (byte)1, messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateByte(prop, "1", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateByte(prop, "yes", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ validator.validateByte(prop, 1, messages);
+ assertTrue(messages.isEmpty());
+ }
+
+ @Test public void testValidateShort() {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.setType(PropertyType.SHORT);
+
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateShort(prop, (short)1, messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateShort(prop, "1", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateShort(prop, "yes", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ validator.validateShort(prop, 1, messages);
+ assertTrue(messages.isEmpty());
+ }
+
+ @Test public void testValidateInteger() {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.setType(PropertyType.INTEGER);
+
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateInteger(prop, 1, messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateInteger(prop, "1", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateInteger(prop, "yes", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ validator.validateInteger(prop, 1, messages);
+ assertTrue(messages.isEmpty());
+ }
+
+ @Test public void testValidateLong() {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.setType(PropertyType.LONG);
+
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateLong(prop, 1L, messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateLong(prop, "1", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateLong(prop, "yes", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ validator.validateLong(prop, 1, messages);
+ assertTrue(messages.isEmpty());
+ }
+
+ @Test public void testValidateFloat() {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.setType(PropertyType.FLOAT);
+
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateFloat(prop, 1.1, messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateFloat(prop, "1.1", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateFloat(prop, "yes", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ validator.validateFloat(prop, 1, messages);
+ assertTrue(messages.isEmpty());
+ }
+
+ @Test public void testValidateDouble() {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.setType(PropertyType.DOUBLE);
+
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateDouble(prop, 1.1d, messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateDouble(prop, "1.1", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateDouble(prop, "yes", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ validator.validateDouble(prop, 1, messages);
+ assertTrue(messages.isEmpty());
+ }
+
+ @Test public void testValidateChar() {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.setType(PropertyType.CHARACTER);
+
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateCharacter(prop, 'x', messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateCharacter(prop, "y", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateCharacter(prop, "yes", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+ }
+
+ @Test public void testValidateUrl() {
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateURL(null, "https://sling.apache.org/documentation", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateURL(null, "hello world", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+ }
+
+ @Test public void testValidateEmail() {
+ final List<String> messages = new ArrayList<>();
+
+ validator.validateEmail(null, "a@b.com", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateEmail(null, "hello world", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+ }
+
+ @Test public void testValidatePassword() {
+ final PropertyDescription prop = new PropertyDescription();
+ final List<String> messages = new ArrayList<>();
+
+ validator.validatePassword(prop, null, messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ prop.setVariable("secret");
+ validator.validatePassword(prop, null, messages);
+ assertTrue(messages.isEmpty());
+ }
+
+ @Test public void testValidatePath() {
+ final List<String> messages = new ArrayList<>();
+
+ validator.validatePath(null, "/a/b/c", messages);
+ assertTrue(messages.isEmpty());
+
+ validator.validateEmail(null, "hello world", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+ }
+
+ @Test public void testValidateRange() {
+ final List<String> messages = new ArrayList<>();
+ final PropertyDescription prop = new PropertyDescription();
+
+ // no range set
+ validator.validateRange(prop, 2, messages);
+ assertTrue(messages.isEmpty());
+
+ // empty range set
+ prop.setRange(new Range());
+ validator.validateRange(prop, 2, messages);
+ assertTrue(messages.isEmpty());
+
+ // min set
+ prop.getRange().setMin(5);
+ validator.validateRange(prop, 5, messages);
+ assertTrue(messages.isEmpty());
+ validator.validateRange(prop, 6, messages);
+ assertTrue(messages.isEmpty());
+ validator.validateRange(prop, 4, messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ validator.validateRange(prop, 5.0, messages);
+ assertTrue(messages.isEmpty());
+ validator.validateRange(prop, 6.0, messages);
+ assertTrue(messages.isEmpty());
+ validator.validateRange(prop, 4.0, messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ // max set
+ prop.getRange().setMax(6);
+ validator.validateRange(prop, 5, messages);
+ assertTrue(messages.isEmpty());
+ validator.validateRange(prop, 6, messages);
+ assertTrue(messages.isEmpty());
+ validator.validateRange(prop, 7, messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ validator.validateRange(prop, 5.0, messages);
+ assertTrue(messages.isEmpty());
+ validator.validateRange(prop, 6.0, messages);
+ assertTrue(messages.isEmpty());
+ validator.validateRange(prop, 7.0, messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+ }
+
+ @Test public void testValidateRegex() {
+ final List<String> messages = new ArrayList<>();
+ final PropertyDescription prop = new PropertyDescription();
+
+ // no regex
+ validator.validateRegex(prop, "hello world", messages);
+ validator.validateRegex(prop, "world", messages);
+ assertTrue(messages.isEmpty());
+
+ // regex
+ prop.setRegex("h(.*)");
+ validator.validateRegex(prop, "hello world", messages);
+ assertTrue(messages.isEmpty());
+ validator.validateRegex(prop, "world", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+ }
+
+ @Test public void testValidateOptions() {
+ final List<String> messages = new ArrayList<>();
+ final PropertyDescription prop = new PropertyDescription();
+
+ // no options
+ validator.validateOptions(prop, "foo", messages);
+ validator.validateOptions(prop, "bar", messages);
+ assertTrue(messages.isEmpty());
+
+ // options
+ final List<Option> options = new ArrayList<>();
+ final Option o1 = new Option();
+ o1.setValue("foo");
+ final Option o2 = new Option();
+ o2.setValue("7");
+ options.add(o1);
+ options.add(o2);
+ prop.setOptions(options);
+ validator.validateOptions(prop, "foo", messages);
+ assertTrue(messages.isEmpty());
+ validator.validateOptions(prop, "bar", messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+ validator.validateOptions(prop, 7, messages);
+ assertTrue(messages.isEmpty());
+ }
+
+ @Test public void testValidateList() {
+ final List<String> messages = new ArrayList<>();
+ final PropertyDescription prop = new PropertyDescription();
+
+ final List<Object> values = new ArrayList<>();
+ values.add("a");
+ values.add("b");
+ values.add("c");
+
+ // default cardinality - no excludes/includes
+ validator.validateList(prop, values, messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ // cardinality 3 - no excludes/includes
+ prop.setCardinality(3);
+ validator.validateList(prop, values, messages);
+ assertTrue(messages.isEmpty());
+
+ values.add("d");
+ validator.validateList(prop, values, messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ // excludes
+ prop.setExcludes(new String[] {"d", "e"});
+ validator.validateList(prop, values, messages);
+ assertEquals(2, messages.size()); // cardinality and exclude
+ messages.clear();
+
+ values.remove("d");
+ validator.validateList(prop, values, messages);
+ assertTrue(messages.isEmpty());
+
+ // includes
+ prop.setIncludes(new String[] {"b"});
+ validator.validateList(prop, values, messages);
+ assertTrue(messages.isEmpty());
+
+ prop.setIncludes(new String[] {"x"});
+ validator.validateList(prop, values, messages);
+ assertEquals(1, messages.size());
+ messages.clear();
+
+ values.add("x");
+ values.remove("a");
+ validator.validateList(prop, values, messages);
+ assertTrue(messages.isEmpty());
+ }
+
+ @Test public void testDeprecation() {
+ final PropertyDescription prop = new PropertyDescription();
+ prop.setDeprecated("This is deprecated");
+
+ final PropertyValidationResult result = validator.validate("foo", prop);
+ assertTrue(result.isValid());
+ assertEquals(1, result.getWarnings().size());
+ assertEquals("This is deprecated", result.getWarnings().get(0));
+ }
+}