You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by cz...@apache.org on 2020/11/02 07:44:28 UTC
[sling-org-apache-sling-feature-extension-apiregions] branch
SLING-9867 updated: SLING-9867 : Add extension to provide metadata about
configuration api
This is an automated email from the ASF dual-hosted git repository.
cziegeler pushed a commit to branch SLING-9867
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-extension-apiregions.git
The following commit(s) were added to refs/heads/SLING-9867 by this push:
new 3ae2797 SLING-9867 : Add extension to provide metadata about configuration api
3ae2797 is described below
commit 3ae2797bde1d7f7de7884a39e490ffa9adfa73f0
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Mon Nov 2 08:44:02 2020 +0100
SLING-9867 : Add extension to provide metadata about configuration api
---
.gitignore | 1 +
.../apiregions/api/config/AttributeableEntity.java | 149 +++++++++
.../apiregions/api/config/ConfigurableEntity.java | 94 ++++++
.../apiregions/api/config/Configuration.java | 21 ++
.../apiregions/api/config/ConfigurationApi.java | 276 +++++++++++++++++
.../extension/apiregions/api/config/Constants.java | 65 ++++
.../apiregions/api/config/DescribableEntity.java | 126 ++++++++
.../api/config/FactoryConfiguration.java | 120 ++++++++
.../apiregions/api/config/FrameworkProperty.java | 24 ++
.../extension/apiregions/api/config/Operation.java | 26 ++
.../extension/apiregions/api/config/Option.java | 82 +++++
.../extension/apiregions/api/config/Property.java | 301 ++++++++++++++++++
.../apiregions/api/config/PropertyType.java | 37 +++
.../extension/apiregions/api/config/Range.java | 112 +++++++
.../extension/apiregions/api/config/Validator.java | 341 +++++++++++++++++++++
15 files changed, 1775 insertions(+)
diff --git a/.gitignore b/.gitignore
index 5b783ed..38f5ca4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@ maven-eclipse.xml
.vlt
.DS_Store
jcr.log
+.vscode
atlassian-ide-plugin.xml
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..3d421dd
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntity.java
@@ -0,0 +1,149 @@
+/*
+ * 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;
+
+public abstract class AttributeableEntity {
+
+ /** The additional attributes */
+ private final Map<String, JsonValue> attributes = new LinkedHashMap<>();
+
+ /**
+ * Create a new instance and set defaults.
+ */
+ public AttributeableEntity() {
+ this.clear();
+ }
+
+ /**
+ * Clear the object and remove all metadata
+ */
+ 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 The attributes
+ */
+ 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;
+ }
+
+ 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
+ */
+ int getInteger(final String attributeName, final int defaultValue) {
+ final String val = this.getString(attributeName);
+ if ( val != null ) {
+ return Integer.parseInt(val);
+ }
+ 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..936ea1c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntity.java
@@ -0,0 +1,94 @@
+/*
+ * 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, Property> properties = new LinkedHashMap<>();
+
+ /**
+ * Clear the object and remove all metadata
+ */
+ 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(Constants.KEY_PROPERTIES);
+ if ( val != null ) {
+ for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+ final Property prop = new Property();
+ prop.fromJSONObject(innerEntry.getValue().asJsonObject());
+ this.getProperties().put(innerEntry.getKey(), prop);
+ }
+ }
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the properties
+ * @return The properties
+ */
+ public Map<String, Property> getProperties() {
+ 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.getProperties().isEmpty() ) {
+ final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+ for(final Map.Entry<String, Property> entry : this.getProperties().entrySet()) {
+ propBuilder.add(entry.getKey(), entry.getValue().createJson());
+ }
+ objBuilder.add(Constants.KEY_PROPERTIES, propBuilder);
+ }
+
+ return objBuilder;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Configuration.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Configuration.java
new file mode 100644
index 0000000..5f95df2
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Configuration.java
@@ -0,0 +1,21 @@
+/*
+ * 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 class Configuration extends ConfigurableEntity {
+
+}
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..b6ce23f
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApi.java
@@ -0,0 +1,276 @@
+/*
+ * 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.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}.
+ * @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);
+ }
+ }
+
+ /** The map of configurations */
+ private final Map<String, Configuration> configurations = new LinkedHashMap<>();
+
+ /** The map of factory configurations */
+ private final Map<String, FactoryConfiguration> factories = new LinkedHashMap<>();
+
+ /** The map of framework properties */
+ private final Map<String, FrameworkProperty> 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<>();
+
+ /**
+ * Clear the object and remove all metadata
+ */
+ public void clear() {
+ super.clear();
+ this.configurations.clear();
+ this.factories.clear();
+ this.frameworkProperties.clear();
+ this.internalConfigurations.clear();
+ this.internalFactories.clear();
+ this.internalFrameworkProperties.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(Constants.KEY_CONFIGURATIONS);
+ if ( val != null ) {
+ for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+ final Configuration cfg = new Configuration();
+ cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+ this.getConfigurations().put(innerEntry.getKey(), cfg);
+ }
+ }
+
+ val = this.getAttributes().remove(Constants.KEY_FACTORIES);
+ if ( val != null ) {
+ for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+ final FactoryConfiguration cfg = new FactoryConfiguration();
+ cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+ this.getFactories().put(innerEntry.getKey(), cfg);
+ }
+ }
+
+ val = this.getAttributes().remove(Constants.KEY_FWK_PROPERTIES);
+ if ( val != null ) {
+ for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+ final FrameworkProperty cfg = new FrameworkProperty();
+ cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+ this.getFrameworkProperties().put(innerEntry.getKey(), cfg);
+ }
+ }
+
+ val = this.getAttributes().remove(Constants.KEY_INTERNAL_CONFIGURATIONS);
+ if ( val != null ) {
+ for(final JsonValue innerVal : val.asJsonArray()) {
+ this.getInternalConfigurations().add(getString(innerVal));
+ }
+ }
+
+ val = this.getAttributes().remove(Constants.KEY_INTERNAL_FACTORIES);
+ if ( val != null ) {
+ for(final JsonValue innerVal : val.asJsonArray()) {
+ this.getInternalFactories().add(getString(innerVal));
+ }
+ }
+
+ val = this.getAttributes().remove(Constants.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 configurations
+ * @return the configurations
+ */
+ public Map<String, Configuration> getConfigurations() {
+ return configurations;
+ }
+
+ /**
+ * Get the factory configurations
+ * @return the factories
+ */
+ public Map<String, FactoryConfiguration> getFactories() {
+ return factories;
+ }
+
+ /**
+ * Get the framework properties
+ * @return the frameworkProperties
+ */
+ public Map<String, FrameworkProperty> getFrameworkProperties() {
+ return frameworkProperties;
+ }
+
+ /**
+ * Get the internal configuration names
+ * @return the internalConfigurations
+ */
+ public List<String> getInternalConfigurations() {
+ return internalConfigurations;
+ }
+
+ /**
+ * Get the internal factory names
+ * @return the internalFactories
+ */
+ public List<String> getInternalFactories() {
+ return internalFactories;
+ }
+
+ /**
+ * Get the internal framework property names
+ * @return the internalFrameworkProperties
+ */
+ public List<String> getInternalFrameworkProperties() {
+ return internalFrameworkProperties;
+ }
+
+ /**
+ * 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.getConfigurations().isEmpty() ) {
+ final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+ for(final Map.Entry<String, Configuration> entry : this.getConfigurations().entrySet()) {
+ propBuilder.add(entry.getKey(), entry.getValue().createJson());
+ }
+ objBuilder.add(Constants.KEY_CONFIGURATIONS, propBuilder);
+ }
+ if ( !this.getFactories().isEmpty() ) {
+ final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+ for(final Map.Entry<String, FactoryConfiguration> entry : this.getFactories().entrySet()) {
+ propBuilder.add(entry.getKey(), entry.getValue().createJson());
+ }
+ objBuilder.add(Constants.KEY_FACTORIES, propBuilder);
+ }
+ if ( !this.getFrameworkProperties().isEmpty() ) {
+ final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+ for(final Map.Entry<String, FrameworkProperty> entry : this.getFrameworkProperties().entrySet()) {
+ propBuilder.add(entry.getKey(), entry.getValue().createJson());
+ }
+ objBuilder.add(Constants.KEY_FWK_PROPERTIES, propBuilder);
+ }
+ if ( !this.getInternalConfigurations().isEmpty() ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String n : this.getInternalConfigurations()) {
+ arrayBuilder.add(n);
+ }
+ objBuilder.add(Constants.KEY_INTERNAL_CONFIGURATIONS, arrayBuilder);
+ }
+ if ( !this.getInternalFactories().isEmpty() ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String n : this.getInternalFactories()) {
+ arrayBuilder.add(n);
+ }
+ objBuilder.add(Constants.KEY_INTERNAL_FACTORIES, arrayBuilder);
+ }
+ if ( !this.getInternalFrameworkProperties().isEmpty() ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String n : this.getInternalFrameworkProperties()) {
+ arrayBuilder.add(n);
+ }
+ objBuilder.add(Constants.KEY_INTERNAL_FWK_PROPERTIES, arrayBuilder);
+ }
+
+ return objBuilder;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Constants.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Constants.java
new file mode 100644
index 0000000..88f5788
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Constants.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;
+
+/**
+ * Constants used in this implementation
+ */
+public abstract class Constants {
+
+ 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 = "factories";
+
+ static final String KEY_FWK_PROPERTIES = "framework-properties";
+
+ static final String KEY_INTERNAL_CONFIGURATIONS = "internal-configurations";
+
+ static final String KEY_INTERNAL_FACTORIES = "internal-factories";
+
+ 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";
+}
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..69c9e3a
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntity.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+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 remove all metadata
+ */
+ 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(Constants.KEY_TITLE));
+ this.setDescription(this.getString(Constants.KEY_DESCRIPTION));
+ this.setDeprecated(this.getString(Constants.KEY_DEPRECATED));
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the title
+ * @return the title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Set the title
+ * @param title the title to set
+ */
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ /**
+ * Get the description
+ * @return the description
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Set the description
+ * @param description the description to set
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * Get the deprecation text
+ * @return the deprecated
+ */
+ public String getDeprecated() {
+ return deprecated;
+ }
+
+ /**
+ * Set the deprecation text
+ * @param deprecated the deprecated to set
+ */
+ public void setDeprecated(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, Constants.KEY_TITLE, this.getTitle());
+ this.setString(objectBuilder, Constants.KEY_DESCRIPTION, this.getDescription());
+ this.setString(objectBuilder, Constants.KEY_DEPRECATED, this.getDeprecated());
+
+ return objectBuilder;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfiguration.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfiguration.java
new file mode 100644
index 0000000..c2345bb
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfiguration.java
@@ -0,0 +1,120 @@
+/*
+ * 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;
+
+public class FactoryConfiguration extends ConfigurableEntity {
+
+ private static final String KEY_OPERATIONS = "operations";
+
+ private static final String KEY_INTERNAL_NAMES = "internal-names";
+
+ private final Set<Operation> operations = new HashSet<>();
+
+ private final List<String> internalNames = new ArrayList<>();
+
+ /**
+ * Clear the object and remove all metadata
+ */
+ public void clear() {
+ super.clear();
+ this.operations.clear();
+ 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(KEY_OPERATIONS);
+ if ( val != null ) {
+ for(final JsonValue innerVal : val.asJsonArray()) {
+ final String v = getString(innerVal).toUpperCase();
+ this.getOperations().add(Operation.valueOf(v));
+ }
+ }
+
+ val = this.getAttributes().remove(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);
+ }
+ }
+
+ /**
+ * @return the operations
+ */
+ public Set<Operation> getOperations() {
+ return operations;
+ }
+
+ /**
+ * @return the internalNames
+ */
+ 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() ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final Operation op : this.getOperations()) {
+ arrayBuilder.add(op.name());
+ }
+ objBuilder.add(KEY_OPERATIONS, arrayBuilder);
+ }
+ if ( !this.getInternalNames().isEmpty() ) {
+ final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for(final String n : this.getInternalNames()) {
+ arrayBuilder.add(n);
+ }
+ objBuilder.add(KEY_INTERNAL_NAMES, arrayBuilder);
+ }
+ return objBuilder;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkProperty.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkProperty.java
new file mode 100644
index 0000000..320448b
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkProperty.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
+ */
+public class FrameworkProperty extends Property {
+
+}
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..308143b
--- /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,
+ UPDATE
+}
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..12ba1e0
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Option.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+public class Option extends DescribableEntity {
+
+ /** The value for the option */
+ private String value;
+
+ /**
+ * Clear the object and remove all metadata
+ */
+ 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(Constants.KEY_VALUE));
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the value for the option
+ * @return the value
+ */
+ 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, Constants.KEY_VALUE, this.getValue());
+
+ return objectBuilder;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Property.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Property.java
new file mode 100644
index 0000000..b06ed54
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Property.java
@@ -0,0 +1,301 @@
+/*
+ * 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 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 Property 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 String regex;
+
+ /**
+ * Clear the object and remove all metadata
+ */
+ public void clear() {
+ super.clear();
+ this.setType(PropertyType.STRING);
+ this.setCardinality(1);
+ 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(Constants.KEY_VARIABLE));
+ this.setCardinality(this.getInteger(Constants.KEY_CARDINALITY, this.getCardinality()));
+
+ final String typeVal = this.getString(Constants.KEY_TYPE);
+ if ( typeVal != null ) {
+ this.setType(PropertyType.valueOf(typeVal.toUpperCase()));
+ }
+ final JsonValue rangeVal = this.getAttributes().remove(Constants.KEY_RANGE);
+ if ( rangeVal != null ) {
+ final Range range = new Range();
+ range.fromJSONObject(rangeVal.asJsonObject());
+ this.setRange(range);
+ }
+ final JsonValue incs = this.getAttributes().remove(Constants.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(Constants.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(Constants.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(Constants.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.setString(objectBuilder, Constants.KEY_TYPE, this.getType().name());
+ }
+ if ( this.getCardinality() != 1 ) {
+ objectBuilder.add(Constants.KEY_CARDINALITY, this.getCardinality());
+ }
+ this.setString(objectBuilder, Constants.KEY_VARIABLE, this.getVariable());
+
+ if ( this.range != null ) {
+ objectBuilder.add(Constants.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(Constants.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(Constants.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(Constants.KEY_OPTIONS, arrayBuilder);
+ }
+ this.setString(objectBuilder, Constants.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;
+ }
+
+ /**
+ * 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
+ */
+ 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
+ */
+ public String getRegex() {
+ return regex;
+ }
+
+ /**
+ * Set the regex
+ * @param regex the regex to set
+ */
+ public void setRegex(final String regex) {
+ this.regex = regex;
+ }
+}
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..e0d2889
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyType.java
@@ -0,0 +1,37 @@
+/*
+ * 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,
+ INT,
+ SHORT,
+ CHAR,
+ BYTE,
+ DOUBLE,
+ FLOAT,
+ BOOLEAN,
+ PASSWORD,
+ URL,
+ EMAIL;
+
+}
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..01a474f
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Range.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+public class Range extends AttributeableEntity {
+
+ /** The optional min value */
+ private String min;
+
+ /** The optional max value */
+ private String max;
+
+ /**
+ * Clear the object and remove all metadata
+ */
+ 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.getString(Constants.KEY_MIN));
+ this.setMax(this.getString(Constants.KEY_MAX));
+ } catch (final JsonException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Get the min value
+ * @return the min
+ */
+ public String getMin() {
+ return min;
+ }
+
+ /**
+ * Set the min value
+ * @param min the min to set
+ */
+ public void setMin(final String min) {
+ this.min = min;
+ }
+
+ /**
+ * Get the max value
+ * @return the max
+ */
+ public String getMax() {
+ return max;
+ }
+
+ /**
+ * Set the max value
+ * @param max the max to set
+ */
+ public void setMax(final String 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();
+
+ this.setString(objectBuilder, Constants.KEY_MIN, this.getMin());
+ this.setString(objectBuilder, Constants.KEY_MAX, 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/Validator.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Validator.java
new file mode 100644
index 0000000..546fa1c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Validator.java
@@ -0,0 +1,341 @@
+/*
+ * 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.lang.reflect.Array;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Validate values
+ */
+public class Validator {
+
+ public interface ValidationResult {
+
+ boolean isValid();
+
+ List<String> getErrors();
+ }
+
+ /**
+ * Validate the value against the property definition
+ * @return {@code null} if the value is valid, a human readable validation error message otherwise
+ */
+ public static ValidationResult validate(final Property prop, final Object value) {
+ final List<String> messages = new ArrayList<>();
+ if ( value == null ) {
+ messages.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, messages);
+ }
+
+ if ( values != null ) {
+ // array or collection
+ if ( prop.getCardinality() > 0 && values.size() > prop.getCardinality() ) {
+ messages.add("Array/collection contains too many elements, only " + prop.getCardinality() + " allowed");
+ }
+ for(final Object val : values) {
+ validateValue(prop, val, messages);
+ }
+ 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");
+ }
+ }
+ }
+ }
+ }
+ return new ValidationResult(){
+
+ public boolean isValid() {
+ return messages.isEmpty();
+ }
+
+ public List<String> getErrors() {
+ return messages;
+ }
+ };
+ }
+
+ static void validateValue(final Property 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 CHAR : validateChar(prop, value, messages);
+ break;
+ case DOUBLE : validateDouble(prop, value, messages);
+ break;
+ case FLOAT : validateFloat(prop, value, messages);
+ break;
+ case INT : validateInt(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;
+ default : messages.add("Unable to validate value - unknown property type : " + prop.getType());
+ }
+ if ( prop.getRegex() != null ) {
+ final Pattern p = Pattern.compile(prop.getRegex());
+ if ( !p.matcher(value.toString()).matches() ) {
+ messages.add("Value " + value + " does not match regex " + prop.getRegex());
+ }
+ }
+ 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");
+ }
+ }
+ } else {
+ messages.add("Null value provided for validation");
+ }
+ }
+
+ static void validateBoolean(final Property 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);
+ }
+ }
+ }
+
+ static void validateByte(final Property 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 {
+ messages.add("Byte value must either be of type Byte or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Byte)value, messages);
+ }
+ }
+
+ static void validateShort(final Property 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 {
+ messages.add("Short value must either be of type Short or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Short)value, messages);
+ }
+ }
+
+ static void validateInt(final Property 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 {
+ messages.add("Integer value must either be of type Integer or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Integer)value, messages);
+ }
+ }
+
+ static void validateLong(final Property 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 {
+ messages.add("Long value must either be of type Long or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Long)value, messages);
+ }
+ }
+
+ static void validateFloat(final Property 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 {
+ messages.add("Float value must either be of type Float or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Float)value, messages);
+ }
+ }
+
+ static void validateDouble(final Property 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 {
+ messages.add("Double value must either be of type Double or String : " + value);
+ }
+ } else {
+ validateRange(prop, (Double)value, messages);
+ }
+ }
+
+ static void validateChar(final Property 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);
+ }
+ }
+ }
+
+ static void validateURL(final Property 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);
+ }
+ }
+
+ static void validateEmail(final Property 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);
+ }
+ }
+
+ static void validatePassword(final Property prop, final Object value, final List<String> messages) {
+ if ( prop.getVariable() == null ) {
+ messages.add("Value for a password must use a variable");
+ }
+ }
+
+ static void validateRange(final Property prop, final Number value, final List<String> messages) {
+ if ( prop.getRange() != null ) {
+ try {
+ if ( prop.getRange().getMin() != null ) {
+ final long min = Long.valueOf(prop.getRange().getMin());
+ if ( value.longValue() < min ) {
+ messages.add("Value " + value + " is too low; should not be lower than " + prop.getRange().getMin());
+ }
+ }
+ if ( prop.getRange().getMax() != null ) {
+ final long max = Long.valueOf(prop.getRange().getMax());
+ if ( value.longValue() > max ) {
+ messages.add("Value " + value + " is too high; should not be higher than " + prop.getRange().getMax());
+ }
+ }
+ } catch ( final NumberFormatException nfe) {
+ messages.add("Invalid range specified in " + prop.getRange());
+ }
+ }
+ }
+
+}
\ No newline at end of file