You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:57:12 UTC
[sling-org-apache-sling-provisioning-model] 16/34: Implement
provisioning model
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to annotated tag org.apache.sling.provisioning.model-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-provisioning-model.git
commit 456ebb16d5e6812308b3a0926e18d44707ce9cb8
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Tue Sep 30 13:27:21 2014 +0000
Implement provisioning model
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/tooling/support/slingstart-model@1628438 13f79535-47bb-0310-9956-ffa450edef68
---
.../apache/sling/provisioning/model/Artifact.java | 232 ++++++++++++
.../sling/provisioning/model/ArtifactGroup.java | 80 +++++
.../sling/provisioning/model/Configuration.java | 83 +++++
.../apache/sling/provisioning/model/Feature.java | 118 ++++++
.../org/apache/sling/provisioning/model/Model.java | 76 ++++
.../sling/provisioning/model/ModelConstants.java | 50 +++
.../sling/provisioning/model/ModelUtility.java | 329 +++++++++++++++++
.../apache/sling/provisioning/model/RunMode.java | 216 +++++++++++
.../apache/sling/provisioning/model/Traceable.java | 71 ++++
.../sling/provisioning/model/io/ModelReader.java | 397 +++++++++++++++++++++
.../sling/provisioning/model/io/ModelWriter.java | 233 ++++++++++++
.../sling/provisioning/model/io/package-info.java | 24 ++
.../sling/provisioning/model/package-info.java | 24 ++
13 files changed, 1933 insertions(+)
diff --git a/src/main/java/org/apache/sling/provisioning/model/Artifact.java b/src/main/java/org/apache/sling/provisioning/model/Artifact.java
new file mode 100644
index 0000000..bb5d844
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Artifact.java
@@ -0,0 +1,232 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Description of an artifact.
+ * An artifact is described by it's Apache Maven coordinates consisting of group id, artifact id, and version.
+ * In addition, the classifier and type can be specified as well.
+ * An artifact can have any metadata.
+ */
+public class Artifact extends Traceable {
+
+ private final String groupId;
+ private final String artifactId;
+ private final String version;
+ private final String classifier;
+ private final String type;
+
+ private final Map<String, String> metadata = new HashMap<String, String>();
+
+ /**
+ * Create a new artifact object
+ * @param gId The group id (required)
+ * @param aId The artifact id (required)
+ * @param version The version (required)
+ * @param classifier The classifier (optional)
+ * @param type The type/extension (optional, defaults to jar)
+ */
+ public Artifact(final String gId,
+ final String aId,
+ final String version,
+ final String classifier,
+ final String type) {
+ this.groupId = (gId != null ? gId.trim() : null);
+ this.artifactId = (aId != null ? aId.trim() : null);
+ this.version = (version != null ? version.trim() : null);
+ final String trimmedType = (type != null ? type.trim() : null);
+ if ( "bundle".equals(trimmedType) || trimmedType == null || trimmedType.isEmpty() ) {
+ this.type = "jar";
+ } else {
+ this.type = trimmedType;
+ }
+ final String trimmedClassifier = (classifier != null ? classifier.trim() : null);
+ if ( trimmedClassifier != null && trimmedClassifier.isEmpty() ) {
+ this.classifier = null;
+ } else {
+ this.classifier = trimmedClassifier;
+ }
+ }
+
+ /**
+ * Create a new artifact from a maven url,
+ * 'mvn:' [ repository-url '!' ] group-id '/' artifact-id [ '/' [version] [ '/' [type] [ '/' classifier ] ] ] ]
+ * @param url The url
+ * @return A new artifact
+ * @throws IllegalArgumentException If the url is not valid
+ */
+ public static Artifact fromMvnUrl(final String url) {
+ if ( url == null || !url.startsWith("mvn:") ) {
+ throw new IllegalArgumentException("Invalid mvn url: " + url);
+ }
+ final String content = url.substring(4);
+ // ignore repository url
+ int pos = content.indexOf('!');
+ if ( pos != -1 ) {
+ throw new IllegalArgumentException("Repository url is not supported for Maven artifacts at the moment.");
+ }
+ final String coordinates = (pos == -1 ? content : content.substring(pos + 1));
+ String gId = null;
+ String aId = null;
+ String version = null;
+ String type = null;
+ String classifier = null;
+ int part = 0;
+ String value = coordinates;
+ while ( value != null ) {
+ pos = value.indexOf('/');
+ final String current;
+ if ( pos == -1 ) {
+ current = value;
+ value = null;
+ } else {
+ if ( pos == 0 ) {
+ current = null;
+ } else {
+ current = value.substring(0, pos);
+ }
+ value = value.substring(pos + 1);
+ }
+ if ( current != null ) {
+ if ( part == 0 ) {
+ gId = current;
+ } else if ( part == 1 ) {
+ aId = current;
+ } else if ( part == 2 ) {
+ version = current;
+ } else if ( part == 3 ) {
+ type = current;
+ } else if ( part == 4 ) {
+ classifier = current;
+ }
+ }
+ part++;
+ }
+ if ( version == null ) {
+ version = "LATEST";
+ }
+ return new Artifact(gId, aId, version, classifier, type);
+ }
+
+ /**
+ * Return a mvn url
+ * @return A mvn url
+ * @see #fromMvnUrl(String)
+ */
+ public String toMvnUrl() {
+ final StringBuilder sb = new StringBuilder("mvn:");
+ sb.append(this.groupId);
+ sb.append('/');
+ sb.append(this.artifactId);
+ sb.append('/');
+ sb.append(this.version);
+ if ( this.classifier != null || !"jar".equals(this.type)) {
+ sb.append('/');
+ sb.append(this.type);
+ if ( this.classifier != null ) {
+ sb.append('/');
+ sb.append(this.classifier);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Return the group id.
+ * @return The group id.
+ */
+ public String getGroupId() {
+ return groupId;
+ }
+
+ /**
+ * Return the artifact id.
+ * @return The artifact id.
+ */
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ /**
+ * Return the version.
+ * @return The version.
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * Return the optional classifier.
+ * @return The classifier or null.
+ */
+ public String getClassifier() {
+ return classifier;
+ }
+
+ /**
+ * Return the type.
+ * @return The type.
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Get the metadata of the artifact.
+ * @return The metadata.
+ */
+ public Map<String, String> getMetadata() {
+ return this.metadata;
+ }
+
+ /**
+ * Create a Maven like relative repository path.
+ */
+ public String getRepositoryPath() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(groupId.replace('.', '/'));
+ sb.append('/');
+ sb.append(artifactId);
+ sb.append('/');
+ sb.append(version);
+ sb.append('/');
+ sb.append(artifactId);
+ sb.append('-');
+ sb.append(version);
+ if ( classifier != null ) {
+ sb.append('-');
+ sb.append(classifier);
+ }
+ sb.append('.');
+ sb.append(type);
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ return "Artifact [groupId=" + groupId
+ + ", artifactId=" + artifactId
+ + ", version=" + version
+ + ", classifier=" + classifier
+ + ", type=" + type
+ + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+ + "]";
+ }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/ArtifactGroup.java b/src/main/java/org/apache/sling/provisioning/model/ArtifactGroup.java
new file mode 100644
index 0000000..0d0ccf9
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/ArtifactGroup.java
@@ -0,0 +1,80 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A artifact group holds a set of artifacts.
+ * A valid start level is positive, start level 0 means the default OSGi start level.
+ */
+public class ArtifactGroup extends Traceable
+ implements Comparable<ArtifactGroup> {
+
+ private final int level;
+
+ private final List<Artifact> artifacts = new ArrayList<Artifact>();
+
+ public ArtifactGroup(final int level) {
+ this.level = level;
+ }
+
+ public int getLevel() {
+ return this.level;
+ }
+
+ public List<Artifact> getArtifacts() {
+ return this.artifacts;
+ }
+
+ /**
+ * Search an artifact with the same groupId, artifactId, version, type and classifier.
+ * Version is not considered.
+ */
+ public Artifact search(final Artifact template) {
+ Artifact found = null;
+ for(final Artifact current : this.artifacts) {
+ if ( current.getGroupId().equals(template.getGroupId())
+ && current.getArtifactId().equals(template.getArtifactId())
+ && current.getClassifier().equals(template.getClassifier())
+ && current.getType().equals(template.getType()) ) {
+ found = current;
+ break;
+ }
+ }
+ return found;
+ }
+
+ @Override
+ public int compareTo(final ArtifactGroup o) {
+ if ( this.level < o.level ) {
+ return -1;
+ } else if ( this.level > o.level ) {
+ return 1;
+ }
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "ArtifactGroup [level=" + level
+ + ", artifacts=" + artifacts
+ + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+ + "]";
+ }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/Configuration.java b/src/main/java/org/apache/sling/provisioning/model/Configuration.java
new file mode 100644
index 0000000..67535ce
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Configuration.java
@@ -0,0 +1,83 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+
+/**
+ * Configuration
+ */
+public class Configuration extends Traceable {
+
+ private final String pid;
+
+ private final String factoryPid;
+
+ private final Dictionary<String, Object> properties = new Hashtable<String, Object>();
+
+ public Configuration(final String pid, final String factoryPid) {
+ this.pid = (pid != null ? pid.trim() : null);
+ this.factoryPid = (factoryPid != null ? factoryPid.trim() : null);
+ }
+
+ /**
+ * Get the pid.
+ * If this is a factory configuration, it returns the alias for the configuration
+ * @return The pid.
+ */
+ public String getPid() {
+ return this.pid;
+ }
+
+ /**
+ * Return the factory pid
+ * @return The factory pid or null.
+ */
+ public String getFactoryPid() {
+ return this.factoryPid;
+ }
+
+ /**
+ * Is this a special configuration?
+ * @return Special config
+ */
+ public boolean isSpecial() {
+ if ( pid != null && pid.startsWith(":") ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get all properties of the configuration.
+ * @return The properties
+ */
+ public Dictionary<String, Object> getProperties() {
+ return this.properties;
+ }
+
+ @Override
+ public String toString() {
+ return "Configuration [pid=" + pid
+ + ", factoryPid=" + factoryPid
+ + ", properties=" + properties
+ + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+ + "]";
+ }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/Feature.java b/src/main/java/org/apache/sling/provisioning/model/Feature.java
new file mode 100644
index 0000000..7ca9da6
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Feature.java
@@ -0,0 +1,118 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * A feature is a collection of
+ * - variables
+ * - run modes
+ */
+public class Feature
+ extends Traceable
+ implements Comparable<Feature> {
+
+ /** All run modes. */
+ private final List<RunMode> runModes = new ArrayList<RunMode>();
+
+ /** Variables. */
+ private final Map<String, String> variables = new HashMap<String, String>();
+
+ private final String name;
+
+ /**
+ * Construct a new feature.
+ * @param name The feature name
+ */
+ public Feature(final String name) {
+ this.name = name;
+ }
+
+ /**
+ * Get the name of the feature.
+ * @return The name or {@code null} for an anonymous feature.
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Get all variables
+ * @return The set of variables
+ */
+ public Map<String, String> getVariables() {
+ return this.variables;
+ }
+
+ public List<RunMode> getRunModes() {
+ return this.runModes;
+ }
+
+ /**
+ * Find the run mode if available
+ * @param runModes
+ * @return The feature or null.
+ */
+ public RunMode findRunMode(final String[] runModes) {
+ final String[] sortedRunModes = RunMode.getSortedRunModesArray(runModes);
+ RunMode result = null;
+ for(final RunMode current : this.runModes) {
+ if ( Arrays.equals(sortedRunModes, current.getRunModes()) ) {
+ result = current;
+ break;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Get or create the run mode.
+ * @param runModes The run modes.
+ * @return The feature for the given run modes.
+ */
+ public RunMode getOrCreateFeature(final String[] runModes) {
+ RunMode result = findRunMode(runModes);
+ if ( result == null ) {
+ result = new RunMode(runModes);
+ this.runModes.add(result);
+ Collections.sort(this.runModes);
+ }
+ return result;
+ }
+
+ @Override
+ public int compareTo(final Feature o) {
+ if ( this.name == null ) {
+ if ( o.name == null ) {
+ return 0;
+ }
+ return -1;
+ }
+ if ( o.name == null ) {
+ return 1;
+ }
+ return this.name.compareTo(o.name);
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/Model.java b/src/main/java/org/apache/sling/provisioning/model/Model.java
new file mode 100644
index 0000000..5ce2a55
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Model.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.provisioning.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A model is the central object.
+ * It consists of features.
+ */
+public class Model extends Traceable {
+
+ /** All features. */
+ private final List<Feature> features = new ArrayList<Feature>();
+
+ /**
+ * Find the feature if available
+ * @param name The feature name
+ * @return The feature or {@code null}.
+ */
+ public Feature findFeature(final String name) {
+ for(final Feature f : this.features) {
+ if ( name.equals(f.getName()) ) {
+ return f;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get or create the feature.
+ * @param runModes The run modes.
+ * @return The feature for the given run modes.
+ */
+ public Feature getOrCreateFeature(final String name) {
+ Feature result = findFeature(name);
+ if ( result == null ) {
+ result = new Feature(name);
+ this.features.add(result);
+ Collections.sort(this.features);
+ }
+ return result;
+ }
+
+ /**
+ * Return all features.
+ * The returned list is modifiable and directly modifies the model.
+ * @return The list of features.
+ */
+ public List<Feature> getFeatures() {
+ return this.features;
+ }
+
+ @Override
+ public String toString() {
+ return "Model [features=" + features
+ + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+ + "]";
+ }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/ModelConstants.java b/src/main/java/org/apache/sling/provisioning/model/ModelConstants.java
new file mode 100644
index 0000000..14bb646
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/ModelConstants.java
@@ -0,0 +1,50 @@
+/*
+ * 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.provisioning.model;
+
+
+public abstract class ModelConstants {
+
+ /** Name of the configuration containing the web.xml. */
+ public static final String CFG_WEB_XML = ":web.xml";
+
+ /** Name of the configuration for the bootstrap contents. */
+ public static final String CFG_BOOTSTRAP = ":bootstrap";
+
+ /** Unprocessed configuration values. */
+ public static final String CFG_UNPROCESSED = ":rawconfig";
+
+ /** Format of the unprocessed configuration values. */
+ public static final String CFG_UNPROCESSED_FORMAT = ":rawconfig.format";
+
+ public static final String CFG_FORMAT_FELIX_CA = "felixca";
+
+ public static final String CFG_FORMAT_PROPERTIES = "properties";
+
+ /** Name of the base run mode for the Sling launchpad. */
+ public static final String RUN_MODE_BASE = ":base";
+
+ /** Name of the boot run mode. */
+ public static final String RUN_MODE_BOOT = ":boot";
+
+ /** Name of the webapp run mode. */
+ public static final String RUN_MODE_WEBAPP = ":webapp";
+
+ /** Name of the standalone run mode. */
+ public static final String RUN_MODE_STANDALONE = ":standalone";
+
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java b/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java
new file mode 100644
index 0000000..d479810
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java
@@ -0,0 +1,329 @@
+/*
+ * 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.provisioning.model;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.felix.cm.file.ConfigurationHandler;
+
+
+/**
+ * Merge two models
+ */
+public abstract class ModelUtility {
+
+ /**
+ * Merge the additional model into the base model.
+ * @param base The base model.
+ * @param additional The additional model.
+ */
+ public static void merge(final Model base, final Model additional) {
+ // features
+ for(final Feature feature : additional.getFeatures()) {
+ final Feature baseFeature = base.getOrCreateFeature(feature.getName());
+
+ // variables
+ baseFeature.getVariables().putAll(feature.getVariables());
+
+ // run modes
+ for(final RunMode runMode : feature.getRunModes()) {
+ final RunMode baseRunMode = baseFeature.getOrCreateFeature(runMode.getRunModes());
+
+ // artifact groups
+ for(final ArtifactGroup group : runMode.getArtifactGroups()) {
+ final ArtifactGroup baseGroup = baseRunMode.getOrCreateArtifactGroup(group.getLevel());
+
+ for(final Artifact artifact : group.getArtifacts()) {
+ final Artifact found = baseGroup.search(artifact);
+ if ( found != null ) {
+ baseGroup.getArtifacts().remove(found);
+ }
+ baseGroup.getArtifacts().add(artifact);
+ }
+ }
+
+ // configurations
+ for(final Configuration config : runMode.getConfigurations()) {
+ final Configuration found = baseRunMode.getOrCreateConfiguration(config.getPid(), config.getFactoryPid());
+ final Enumeration<String> e = config.getProperties().keys();
+ while ( e.hasMoreElements() ) {
+ final String key = e.nextElement();
+ found.getProperties().put(key, config.getProperties().get(key));
+ }
+ }
+
+ // settings
+ for(final Map.Entry<String, String> entry : runMode.getSettings().entrySet() ) {
+ baseRunMode.getSettings().put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ }
+ }
+
+ /**
+ * Optional variable resolver
+ */
+ public interface VariableResolver {
+
+ /**
+ * Resolve the variable.
+ * An implementation might get the value of a variable from the system properties,
+ * or the environment etc.
+ * As a fallback, the resolver should check the variables of the model.
+ * @param model The model
+ * @param name The variable name
+ * @return The variable value or null.
+ */
+ String resolve(final Feature model, final String name);
+ }
+
+ /**
+ * Replace all variables in the model and return a new model with the replaced values.
+ * @param model The base model.
+ * @param resolver Optional variable resolver.
+ * @return The model with replaced variables.
+ * @throws IllegalArgumentException If a variable can't be replaced or configuration properties can't be parsed
+ */
+ public static Model getEffectiveModel(final Model model, final VariableResolver resolver) {
+ final Model result = new Model();
+ result.setComment(model.getComment());
+ result.setLocation(model.getLocation());
+
+ for(final Feature feature : model.getFeatures()) {
+ final Feature newFeature = result.getOrCreateFeature(feature.getName());
+ newFeature.setComment(feature.getComment());
+ newFeature.setLocation(feature.getLocation());
+
+ newFeature.getVariables().putAll(feature.getVariables());
+
+ for(final RunMode runMode : feature.getRunModes()) {
+ final RunMode newRunMode = newFeature.getOrCreateFeature(runMode.getRunModes());
+ newRunMode.setComment(runMode.getComment());
+ newRunMode.setLocation(runMode.getLocation());
+
+ for(final ArtifactGroup group : runMode.getArtifactGroups()) {
+ final ArtifactGroup newGroup = newRunMode.getOrCreateArtifactGroup(group.getLevel());
+ newGroup.setComment(group.getComment());
+ newGroup.setLocation(group.getLocation());
+
+ for(final Artifact artifact : group.getArtifacts()) {
+ final Artifact newArtifact = new Artifact(replace(feature, artifact.getGroupId(), resolver),
+ replace(feature, artifact.getArtifactId(), resolver),
+ replace(feature, artifact.getVersion(), resolver),
+ replace(feature, artifact.getClassifier(), resolver),
+ replace(feature, artifact.getType(), resolver));
+ newArtifact.setComment(artifact.getComment());
+ newArtifact.setLocation(artifact.getLocation());
+
+ newGroup.getArtifacts().add(newArtifact);
+ }
+ }
+
+ for(final Configuration config : runMode.getConfigurations()) {
+ final Configuration newConfig = new Configuration(config.getPid(), config.getFactoryPid());
+ newConfig.setComment(config.getComment());
+ newConfig.setLocation(config.getLocation());
+
+ // check for raw configuration
+ final String rawConfig = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED);
+ if ( rawConfig != null ) {
+ final String format = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED_FORMAT);
+
+ if ( ModelConstants.CFG_FORMAT_PROPERTIES.equals(format) ) {
+ // properties
+ final Properties props = new Properties();
+ try {
+ props.load(new StringReader(rawConfig));
+ } catch ( final IOException ioe) {
+ throw new IllegalArgumentException("Unable to read configuration properties.", ioe);
+ }
+ final Enumeration<Object> i = props.keys();
+ while ( i.hasMoreElements() ) {
+ final String key = (String)i.nextElement();
+ newConfig.getProperties().put(key, props.get(key));
+ }
+ } else {
+ // Apache Felix CA format
+ ByteArrayInputStream bais = null;
+ try {
+ bais = new ByteArrayInputStream(rawConfig.getBytes("UTF-8"));
+ @SuppressWarnings("unchecked")
+ final Dictionary<String, Object> props = ConfigurationHandler.read(bais);
+ final Enumeration<String> i = props.keys();
+ while ( i.hasMoreElements() ) {
+ final String key = i.nextElement();
+ newConfig.getProperties().put(key, props.get(key));
+ }
+ } catch ( final IOException ioe) {
+ throw new IllegalArgumentException("Unable to read configuration properties.", ioe);
+ } finally {
+ if ( bais != null ) {
+ try {
+ bais.close();
+ } catch ( final IOException ignore ) {
+ // ignore
+ }
+ }
+ }
+ }
+ } else {
+ // simply copy
+ final Enumeration<String> i = config.getProperties().keys();
+ while ( i.hasMoreElements() ) {
+ final String key = i.nextElement();
+ newConfig.getProperties().put(key, config.getProperties().get(key));
+ }
+ }
+
+ newRunMode.getConfigurations().add(newConfig);
+ }
+
+ for(final Map.Entry<String, String> entry : runMode.getSettings().entrySet() ) {
+ newRunMode.getSettings().put(entry.getKey(), replace(feature, entry.getValue(), resolver));
+ }
+ }
+
+ }
+ return result;
+ }
+
+ /**
+ * Replace properties in the string.
+ *
+ * @param model The model
+ * @param v The variable name
+ * @param resolver Optional resolver
+ * @result The value of the variable
+ * @throws IllegalArgumentException
+ */
+ private static String replace(final Feature model, final String v, final VariableResolver resolver) {
+ if ( v == null ) {
+ return null;
+ }
+ String msg = v;
+ // check for variables
+ int pos = -1;
+ int start = 0;
+ while ( ( pos = msg.indexOf('$', start) ) != -1 ) {
+ if ( msg.length() > pos && msg.charAt(pos + 1) == '{' && (pos == 0 || msg.charAt(pos - 1) != '$') ) {
+ final int endPos = msg.indexOf('}', pos);
+ if ( endPos == -1 ) {
+ start = pos + 1;
+ } else {
+ final String name = msg.substring(pos + 2, endPos);
+ final String value;
+ if ( resolver != null ) {
+ value = resolver.resolve(model, name);
+ } else {
+ value = model.getVariables().get(name);
+ }
+ if ( value == null ) {
+ throw new IllegalArgumentException("Unknown variable: " + name);
+ }
+ msg = msg.substring(0, pos) + value + msg.substring(endPos + 1);
+ }
+ } else {
+ start = pos + 1;
+ }
+ }
+ return msg;
+ }
+
+ /**
+ * Validates the model.
+ * @param model
+ * @return A map with errors or {@code null}.
+ */
+ public static Map<Traceable, String> validate(final Model model) {
+ final Map<Traceable, String> errors = new HashMap<Traceable, String>();
+
+ for(final Feature feature : model.getFeatures() ) {
+ // validate feature
+ if ( feature.getName() == null || feature.getName().isEmpty() ) {
+ errors.put(feature, "Name is required for a feature.");
+ }
+ for(final RunMode runMode : feature.getRunModes()) {
+ final String[] rm = runMode.getRunModes();
+ if ( rm != null ) {
+ boolean hasSpecial = false;
+ for(final String m : rm) {
+ if ( m.startsWith(":") ) {
+ if ( hasSpecial ) {
+ errors.put(runMode, "Invalid modes " + Arrays.toString(rm));
+ break;
+ }
+ hasSpecial = true;
+ }
+ }
+ }
+
+ for(final ArtifactGroup sl : runMode.getArtifactGroups()) {
+ if ( sl.getLevel() < 0 ) {
+ errors.put(sl, "Invalid start level " + sl.getLevel());
+ }
+ for(final Artifact a : sl.getArtifacts()) {
+ String error = null;
+ if ( a.getGroupId() == null || a.getGroupId().isEmpty() ) {
+ error = "groupId missing";
+ }
+ if ( a.getArtifactId() == null || a.getArtifactId().isEmpty() ) {
+ error = (error != null ? error + ", " : "") + "artifactId missing";
+ }
+ if ( a.getVersion() == null || a.getVersion().isEmpty() ) {
+ error = (error != null ? error + ", " : "") + "version missing";
+ }
+ if ( a.getType() == null || a.getType().isEmpty() ) {
+ error = (error != null ? error + ", " : "") + "type missing";
+ }
+ if (error != null) {
+ errors.put(a, error);
+ }
+ }
+ }
+
+ for(final Configuration c : runMode.getConfigurations()) {
+ String error = null;
+ if ( c.getPid() == null || c.getPid().isEmpty() ) {
+ error = "pid missing";
+ }
+ if ( c.isSpecial() && c.getFactoryPid() != null ) {
+ error = (error != null ? error + ", " : "") + "factory pid not allowed for special configuration";
+ }
+ if ( c.getProperties().isEmpty() ) {
+ error = (error != null ? error + ", " : "") + "configuration properties missing";
+ }
+ if (error != null) {
+ errors.put(c, error);
+ }
+ }
+ }
+ }
+ if ( errors.size() == 0 ) {
+ return null;
+ }
+ return errors;
+ }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/RunMode.java b/src/main/java/org/apache/sling/provisioning/model/RunMode.java
new file mode 100644
index 0000000..677abaf
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/RunMode.java
@@ -0,0 +1,216 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A feature is a collection of
+ * - artifacts (through start levels)
+ * - configurations
+ * - settings
+ *
+ * A feature might be tied to run modes. Only if all run modes are active,
+ * this feature is active.
+ * In addition to custom, user defined run modes, special run modes exists.
+ * A special run mode name starts with a colon.
+ */
+public class RunMode
+ extends Traceable
+ implements Comparable<RunMode> {
+
+ private final String[] runModes;
+
+ private final List<ArtifactGroup> groups = new ArrayList<ArtifactGroup>();
+
+ private final List<Configuration> configurations = new ArrayList<Configuration>();
+
+ private final Map<String, String> settings = new HashMap<String, String>();
+
+ public RunMode(final String[] runModes) {
+ this.runModes = getSortedRunModesArray(runModes);
+ }
+
+ public static String[] getSortedRunModesArray(final String[] runModes) {
+ // sort run modes
+ if ( runModes != null ) {
+ final List<String> list = new ArrayList<String>();
+ for(final String m : runModes) {
+ if ( m != null ) {
+ if ( !m.trim().isEmpty() ) {
+ list.add(m.trim());
+ }
+ }
+ }
+ if ( list.size() > 0 ) {
+ Collections.sort(list);
+ return list.toArray(new String[list.size()]);
+ }
+ }
+ return null;
+ }
+
+ public String[] getRunModes() {
+ return this.runModes;
+ }
+
+ /**
+ * Check if this feature is active wrt the given set of active run modes.
+ */
+ public boolean isActive(final Set<String> activeRunModes) {
+ boolean active = true;
+ if ( runModes != null ) {
+ for(final String mode : runModes) {
+ if ( !activeRunModes.contains(mode) ) {
+ active = false;
+ break;
+ }
+ }
+ }
+ return active;
+ }
+
+ /**
+ * Check whether this feature is a special one
+ */
+ public boolean isSpecial() {
+ if ( runModes != null && runModes.length == 1 && runModes[0].startsWith(":") ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check if this feature is tied to a single specific run mode.
+ */
+ public boolean isRunMode(final String mode) {
+ if ( mode == null && this.runModes == null ) {
+ return true;
+ }
+ if ( mode != null
+ && this.runModes != null
+ && this.runModes.length == 1
+ && this.runModes[0].equals(mode) ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Find the artifact group.
+ */
+ public ArtifactGroup findArtifactGroup(final int startLevel) {
+ for(final ArtifactGroup g : this.groups) {
+ if ( g.getLevel() == startLevel ) {
+ return g;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get or create an artifact group
+ */
+ public ArtifactGroup getOrCreateArtifactGroup(final int startLevel) {
+ ArtifactGroup result = this.findArtifactGroup(startLevel);
+ if ( result == null ) {
+ result = new ArtifactGroup(startLevel);
+ this.groups.add(result);
+ Collections.sort(this.groups);
+ }
+ return result;
+ }
+
+ /**
+ * Search a configuration with a pid
+ */
+ public Configuration getConfiguration(final String pid) {
+ for(final Configuration c : this.configurations) {
+ if ( pid.equals(c.getPid()) ) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ public Configuration getOrCreateConfiguration(final String pid, final String factoryPid) {
+ Configuration found = null;
+ for(final Configuration current : this.configurations) {
+ if ( factoryPid == null ) {
+ if ( current.getFactoryPid() == null && current.getPid().equals(pid) ) {
+ found = current;
+ break;
+ }
+ } else {
+ if ( factoryPid.equals(current.getFactoryPid()) && current.getPid().equals(pid) ) {
+ found = current;
+ break;
+ }
+ }
+ }
+ if ( found == null ) {
+ found = new Configuration(pid, factoryPid);
+ this.configurations.add(found);
+ }
+ return found;
+ }
+
+ public List<ArtifactGroup> getArtifactGroups() {
+ return this.groups;
+ }
+
+ public List<Configuration> getConfigurations() {
+ return this.configurations;
+ }
+
+ public Map<String, String> getSettings() {
+ return this.settings;
+ }
+
+ /**
+ * @see java.lang.Comparable#compareTo(java.lang.Object)
+ */
+ @Override
+ public int compareTo( RunMode o2) {
+ if ( this.runModes == null ) {
+ if ( o2.runModes == null ) {
+ return 0;
+ }
+ return -1;
+ }
+ if ( o2.runModes == null ) {
+ return 1;
+ }
+ return Arrays.toString(this.runModes).compareTo(Arrays.toString(o2.runModes));
+ }
+
+ @Override
+ public String toString() {
+ return "RunMode [runModes=" + Arrays.toString(runModes) + ", groups="
+ + groups + ", configurations=" + configurations + ", settings="
+ + settings
+ + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+ + "]";
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/Traceable.java b/src/main/java/org/apache/sling/provisioning/model/Traceable.java
new file mode 100644
index 0000000..4345ab5
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Traceable.java
@@ -0,0 +1,71 @@
+/*
+ * 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.provisioning.model;
+
+/**
+ * A traceable has a comment and a location.
+ * Both are optional.
+ */
+public abstract class Traceable {
+
+ /** The location. */
+ private String location;
+
+ /** The comment. */
+ private String comment;
+
+ /**
+ * Get the location.
+ * The location might be the location of the model file or any other
+ * means identifying where the object is defined.
+ * @return The location or {@code null}.
+ */
+ public String getLocation() {
+ return this.location;
+ }
+
+ /**
+ * Set the location.
+ * @param value The new location.
+ */
+ public void setLocation(final String value) {
+ this.location = value;
+ }
+
+ /**
+ * Get the comment.
+ * @return The comment or {@code null}.
+ */
+ public String getComment() {
+ return this.comment;
+ }
+
+ /**
+ * Set the comment.
+ * @param value The new comment.
+ */
+ public void setComment(final String value) {
+ this.comment = value;
+ }
+
+ @Override
+ public String toString() {
+ return "SSMTraceable [location=" + location + ", comment=" + comment
+ + "]";
+ }
+}
+
diff --git a/src/main/java/org/apache/sling/provisioning/model/io/ModelReader.java b/src/main/java/org/apache/sling/provisioning/model/io/ModelReader.java
new file mode 100644
index 0000000..17a5b34
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/io/ModelReader.java
@@ -0,0 +1,397 @@
+/*
+ * 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.provisioning.model.io;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.provisioning.model.Artifact;
+import org.apache.sling.provisioning.model.ArtifactGroup;
+import org.apache.sling.provisioning.model.Configuration;
+import org.apache.sling.provisioning.model.Feature;
+import org.apache.sling.provisioning.model.Model;
+import org.apache.sling.provisioning.model.ModelConstants;
+import org.apache.sling.provisioning.model.RunMode;
+import org.apache.sling.provisioning.model.Traceable;
+
+
+public class ModelReader {
+
+ private enum CATEGORY {
+ NONE(null),
+ FEATURE("feature"),
+ VARIABLES("variables"),
+ GLOBAL("global"),
+ RUN_MODE("runMode"),
+ ARTIFACTS("artifacts"),
+ SETTINGS("settings"),
+ CONFIGURATIONS("configurations"),
+ CONFIG(null);
+
+ public final String name;
+
+ private CATEGORY(final String n) {
+ this.name = n;
+ }
+ }
+
+ /**
+ * Reads the model file
+ * The reader is not closed.
+ * @throws IOException
+ */
+ public static Model read(final Reader reader, final String location)
+ throws IOException {
+ final ModelReader mr = new ModelReader(location);
+ return mr.readModel(reader);
+ }
+
+ /** Is this a single feature model? */
+ private boolean isSingleFeature = false;
+
+ private CATEGORY mode = CATEGORY.NONE;
+
+ private final Model model = new Model();
+
+ private Feature feature = null;
+ private RunMode runMode = null;
+ private ArtifactGroup artifactGroup = null;
+ private Configuration config = null;
+
+ private String comment = null;
+
+ private StringBuilder configBuilder = null;
+
+ private LineNumberReader lineNumberReader;
+
+ private final String exceptionPrefix;
+
+ private ModelReader(final String location) {
+ this.model.setLocation(location);
+ if ( location == null ) {
+ exceptionPrefix = "";
+ } else {
+ exceptionPrefix = location + " : ";
+ }
+ }
+
+ private Model readModel(final Reader reader)
+ throws IOException {
+
+ boolean global = true;
+
+ lineNumberReader = new LineNumberReader(reader);
+ String line;
+ while ( (line = lineNumberReader.readLine()) != null ) {
+ line = line.trim();
+ // ignore empty line
+ if ( line.isEmpty() ) {
+ checkConfig();
+ continue;
+ }
+ // comment?
+ if ( line.startsWith("#") ) {
+ checkConfig();
+ mode = CATEGORY.NONE;
+ final String c = line.substring(1).trim();
+ if ( comment == null ) {
+ comment = c;
+ } else {
+ comment = comment + "\n" + c;
+ }
+ continue;
+ }
+
+ if ( global ) {
+ global = false;
+ model.setComment(comment);
+ comment = null;
+ }
+
+ if ( line.startsWith("[") ) {
+ if ( !line.endsWith("]") ) {
+ throw new IOException(exceptionPrefix + "Illegal category definition in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ int pos = 1;
+ while ( line.charAt(pos) != ']' && !Character.isWhitespace(line.charAt(pos))) {
+ pos++;
+ }
+ final String category = line.substring(1, pos);
+ CATEGORY found = null;
+ for (CATEGORY c : CATEGORY.values()) {
+ if ( category.equals(c.name)) {
+ found = c;
+ break;
+ }
+ }
+ if ( found == null ) {
+ throw new IOException(exceptionPrefix + "Unknown category in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ this.mode = found;
+ Map<String, String> parameters = Collections.emptyMap();
+ if (line.charAt(pos) != ']') {
+ final String parameterLine = line.substring(pos + 1, line.length() - 1).trim();
+ parameters = parseParameters(parameterLine);
+ }
+
+ switch ( this.mode ) {
+ case NONE : break; // this can never happen
+ case CONFIG : break; // this can never happen
+ case FEATURE : if ( this.isSingleFeature ) {
+ throw new IOException(exceptionPrefix + "Single feature model allows only one feature.");
+ }
+ final String name = parameters.get("name");
+ if ( name == null ) {
+ throw new IOException(exceptionPrefix + "Feature name missing in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ if ( parameters.size() > 1 ) {
+ throw new IOException(exceptionPrefix + "Unknown feature parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ if ( model.findFeature(name) != null ) {
+ throw new IOException(exceptionPrefix + "Duplicate feature in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ this.feature = model.getOrCreateFeature(name);
+ this.init(this.feature);
+ this.runMode = null;
+ this.artifactGroup = null;
+ break;
+ case VARIABLES : checkFeature();
+ break;
+ case RUN_MODE : checkFeature();
+ final String names = parameters.get("names");
+ if ( names == null ) {
+ throw new IOException(exceptionPrefix + "Run mode names missing in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ if ( parameters.size() > 1 ) {
+ throw new IOException(exceptionPrefix + "Unknown run mode parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ final String[] rm = names.split(",");
+ if ( this.feature.findRunMode(rm) != null ) {
+ throw new IOException(exceptionPrefix + "Duplicate run mode in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ this.runMode = this.feature.getOrCreateFeature(rm);
+ this.init(this.runMode);
+ this.artifactGroup = null;
+ break;
+ case GLOBAL : checkFeature();
+ if ( !parameters.isEmpty() ) {
+ throw new IOException(exceptionPrefix + "Unknown global parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ if ( this.feature.findRunMode(null) != null ) {
+ throw new IOException(exceptionPrefix + "Duplicate global run mode in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ this.runMode = this.feature.getOrCreateFeature(null);
+ this.init(this.runMode);
+ this.artifactGroup = null;
+ break;
+ case SETTINGS: checkFeature();
+ checkRunMode();
+ break;
+ case ARTIFACTS: checkFeature();
+ checkRunMode();
+ String level = parameters.get("startLevel");
+ if ( (level == null && !parameters.isEmpty())
+ || (level != null && parameters.size() > 1 ) ) {
+ throw new IOException(exceptionPrefix + "Unknown artifacts parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ int startLevel = 0;
+ if ( level != null ) {
+ try {
+ startLevel = Integer.valueOf(level);
+ } catch ( final NumberFormatException nfe) {
+ throw new IOException(exceptionPrefix + "Invalid start level in line " + this.lineNumberReader.getLineNumber() + ": " + line + ":" + level);
+ }
+ }
+ if ( this.runMode.findArtifactGroup(startLevel) != null ) {
+ throw new IOException(exceptionPrefix + "Duplicate artifact group in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ this.artifactGroup = this.runMode.getOrCreateArtifactGroup(startLevel);
+ this.init(this.artifactGroup);
+ break;
+ case CONFIGURATIONS: checkFeature();
+ checkRunMode();
+ break;
+ }
+ } else {
+ switch ( this.mode ) {
+ case NONE : break;
+ case VARIABLES : final String[] vars = parseProperty(line);
+ feature.getVariables().put(vars[0], vars[1]);
+ break;
+ case SETTINGS : final String[] settings = parseProperty(line);
+ runMode.getSettings().put(settings[0], settings[1]);
+ break;
+ case FEATURE:
+ case RUN_MODE:
+ case GLOBAL:
+ case ARTIFACTS : this.checkFeature();
+ this.checkRunMode();
+ if ( this.artifactGroup == null ) {
+ this.artifactGroup = this.runMode.getOrCreateArtifactGroup(0);
+ }
+ String artifactUrl = line;
+ Map<String, String> parameters = Collections.emptyMap();
+ if ( line.endsWith("]") ) {
+ final int startPos = line.indexOf("[");
+ if ( startPos != -1 ) {
+ artifactUrl = line.substring(0, startPos).trim();
+ parameters = parseParameters(line.substring(startPos + 1, line.length() - 1).trim());
+ }
+ }
+ try {
+ final Artifact artifact = Artifact.fromMvnUrl("mvn:" + artifactUrl);
+ this.init(artifact);
+ this.artifactGroup.getArtifacts().add(artifact);
+ artifact.getMetadata().putAll(parameters);
+ } catch ( final IllegalArgumentException iae) {
+ throw new IOException(exceptionPrefix + iae.getMessage() + " in line " + this.lineNumberReader.getLineNumber(), iae);
+ }
+ break;
+ case CONFIGURATIONS : String configId = line;
+ Map<String, String> cfgPars = Collections.emptyMap();
+ if ( line.endsWith("]") ) {
+ final int startPos = line.indexOf("[");
+ if ( startPos != -1 ) {
+ configId = line.substring(0, startPos).trim();
+ cfgPars = parseParameters(line.substring(startPos + 1, line.length() - 1).trim());
+ }
+ }
+ String format = cfgPars.get("format");
+ if ( (format == null && !cfgPars.isEmpty())
+ || (format != null && cfgPars.size() > 1 ) ) {
+ throw new IOException(exceptionPrefix + "Unknown configuration parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ if ( format != null ) {
+ if ( !ModelConstants.CFG_FORMAT_FELIX_CA.equals(format)
+ && !ModelConstants.CFG_FORMAT_PROPERTIES.equals(format) ) {
+ throw new IOException(exceptionPrefix + "Unknown format configuration parameter in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ } else {
+ format = ModelConstants.CFG_FORMAT_FELIX_CA;
+ }
+ final int factoryPos = configId.indexOf('-');
+ if ( factoryPos == -1 ) {
+ config = new Configuration(configId, null);
+ } else {
+ config = new Configuration(configId.substring(factoryPos + 1), configId.substring(0, factoryPos));
+ }
+ this.init(config);
+ config.getProperties().put(ModelConstants.CFG_UNPROCESSED_FORMAT, format);
+ runMode.getConfigurations().add(config);
+ configBuilder = new StringBuilder();
+ mode = CATEGORY.CONFIG;
+ break;
+ case CONFIG : configBuilder.append(line);
+ break;
+ }
+ }
+
+ }
+ checkConfig();
+ if ( comment != null ) {
+ throw new IOException(exceptionPrefix + "Comment not allowed at the end of file");
+ }
+
+ return model;
+ }
+
+ /**
+ * Check for a feature object
+ */
+ private void checkFeature() throws IOException {
+ if ( feature == null ) {
+ if ( model.getLocation() == null ) {
+ throw new IOException(exceptionPrefix + "No preceding feature definition in line " + this.lineNumberReader.getLineNumber());
+ }
+ final int beginPos = model.getLocation().replace('\\', '/').lastIndexOf("/");
+ String newName = model.getLocation().substring(beginPos + 1);
+ final int endPos = newName.lastIndexOf('.');
+ if ( endPos != -1 ) {
+ newName = newName.substring(0, endPos);
+ }
+ this.isSingleFeature = true;
+ feature = model.getOrCreateFeature(newName);
+ }
+ }
+
+ /**
+ * Check for a run mode object
+ */
+ private void checkRunMode() throws IOException {
+ if ( runMode == null ) {
+ runMode = this.feature.getOrCreateFeature(null);
+ }
+ }
+
+ private void init(final Traceable traceable) {
+ traceable.setComment(this.comment);
+ this.comment = null;
+ final String number = String.valueOf(this.lineNumberReader.getLineNumber());
+ if ( model.getLocation() != null ) {
+ traceable.setLocation(model.getLocation() + ":" + number);
+ } else {
+ traceable.setLocation(number);
+ }
+ }
+
+ private void checkConfig() {
+ if ( config != null ) {
+ config.getProperties().put(ModelConstants.CFG_UNPROCESSED, configBuilder.toString());
+ this.mode = CATEGORY.CONFIGURATIONS;
+ }
+ config = null;
+ configBuilder = null;
+ }
+
+ /**
+ * Parse a single property line
+ * @param line The line
+ * @return The key and the value
+ * @throws IOException If something goes wrong
+ */
+ private String[] parseProperty(final String line) throws IOException {
+ final int equalsPos = line.indexOf('=');
+ final String key = line.substring(0, equalsPos).trim();
+ final String value = line.substring(equalsPos + 1).trim();
+ if (key.isEmpty() || value.isEmpty() ) {
+ throw new IOException(exceptionPrefix + "Invalid property; " + line + " in line " + this.lineNumberReader.getLineNumber());
+ }
+ return new String[] {key, value};
+ }
+
+ private Map<String, String> parseParameters(final String line) throws IOException {
+ final Map<String, String>parameters = new HashMap<String, String>();
+ final String[] keyValuePairs = line.split(" ");
+ for(String kv : keyValuePairs) {
+ kv = kv.trim();
+ if ( !kv.isEmpty() ) {
+ final int sep = kv.indexOf('=');
+ if ( sep == -1 ) {
+ throw new IOException(exceptionPrefix + "Invalid parameter definition in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+ }
+ parameters.put(kv.substring(0, sep).trim(), kv.substring(sep + 1).trim());
+ }
+ }
+ return parameters;
+ }
+}
+
+
diff --git a/src/main/java/org/apache/sling/provisioning/model/io/ModelWriter.java b/src/main/java/org/apache/sling/provisioning/model/io/ModelWriter.java
new file mode 100644
index 0000000..4b023f0
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/io/ModelWriter.java
@@ -0,0 +1,233 @@
+/*
+ * 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.provisioning.model.io;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.felix.cm.file.ConfigurationHandler;
+import org.apache.sling.provisioning.model.Artifact;
+import org.apache.sling.provisioning.model.ArtifactGroup;
+import org.apache.sling.provisioning.model.Configuration;
+import org.apache.sling.provisioning.model.Feature;
+import org.apache.sling.provisioning.model.Model;
+import org.apache.sling.provisioning.model.ModelConstants;
+import org.apache.sling.provisioning.model.RunMode;
+import org.apache.sling.provisioning.model.Traceable;
+
+/**
+ * Simple writer for the a model
+ */
+public class ModelWriter {
+
+ private static void writeComment(final PrintWriter pw, final Traceable traceable)
+ throws IOException {
+ if ( traceable.getComment() != null ) {
+ final LineNumberReader lnr = new LineNumberReader(new StringReader(traceable.getComment()));
+ try {
+ String line = null;
+ while ( (line = lnr.readLine()) != null ) {
+ pw.print("# ");
+ pw.println(line);
+ }
+ } finally {
+ lnr.close();
+ }
+ }
+ }
+
+ /**
+ * Writes the model to the writer.
+ * The writer is not closed.
+ * @param writer
+ * @param subystem
+ * @throws IOException
+ */
+ public static void write(final Writer writer, final Model model)
+ throws IOException {
+ final PrintWriter pw = new PrintWriter(writer);
+
+ writeComment(pw, model);
+
+ // features
+ for(final Feature feature : model.getFeatures()) {
+ writeComment(pw, feature);
+ pw.print("[feature name=");
+ pw.print(feature.getName());
+ pw.println("]");
+ pw.println();
+
+ // variables
+ if ( !feature.getVariables().isEmpty() ) {
+ pw.println("[variables]");
+ for(final Map.Entry<String, String> entry : feature.getVariables().entrySet()) {
+ pw.print(" ");
+ pw.print(entry.getKey());
+ pw.print("=");
+ pw.println(entry.getValue());
+ }
+ pw.println();
+ }
+
+ // run modes
+ for(final RunMode runMode : feature.getRunModes()) {
+ // skip empty run mode
+ if ( runMode.getConfigurations().isEmpty() && runMode.getSettings().isEmpty() ) {
+ boolean hasArtifacts = false;
+ for(final ArtifactGroup sl : runMode.getArtifactGroups()) {
+ if ( !sl.getArtifacts().isEmpty() ) {
+ hasArtifacts = true;
+ break;
+ }
+ }
+ if ( !hasArtifacts ) {
+ continue;
+ }
+ }
+ writeComment(pw, runMode);
+ final String[] runModes = runMode.getRunModes();
+ if ( runModes == null || runModes.length == 0 ) {
+ pw.println("[global]");
+ } else {
+ pw.print("[runMode names=");
+ boolean first = true;
+ for(final String mode : runModes) {
+ if ( first ) {
+ first = false;
+ } else {
+ pw.print(",");
+ }
+ pw.print(mode);
+ }
+ pw.println("]");
+ }
+ pw.println();
+
+ // settings
+ if ( !runMode.getSettings().isEmpty() ) {
+ pw.println("[settings]");
+
+ for(final Map.Entry<String, String> entry : runMode.getSettings().entrySet()) {
+ pw.print(" ");
+ pw.print(entry.getKey());
+ pw.print("=");
+ pw.println(entry.getValue());
+ }
+ pw.println();
+ }
+
+ // artifact groups
+ for(final ArtifactGroup group : runMode.getArtifactGroups()) {
+ // skip empty groups
+ if ( group.getArtifacts().isEmpty() ) {
+ continue;
+ }
+ writeComment(pw, group);
+ pw.print("[artifacts");
+ if ( group.getLevel() > 0 ) {
+ pw.print(" startLevel=");
+ pw.print(String.valueOf(group.getLevel()));
+ }
+ pw.println("]");
+ pw.println();
+
+ // artifacts
+ for(final Artifact ad : group.getArtifacts()) {
+ writeComment(pw, ad);
+ pw.print(" ");
+ pw.print(ad.toMvnUrl().substring(4));
+ if ( !ad.getMetadata().isEmpty() ) {
+ boolean first = true;
+ for(final Map.Entry<String, String> entry : ad.getMetadata().entrySet()) {
+ if ( first ) {
+ first = false;
+ pw.print("{ ");
+ } else {
+ pw.print(", ");
+ }
+ pw.print(entry.getKey());
+ pw.print("=");
+ pw.println(entry.getValue());
+ }
+ pw.print("}");
+ }
+ pw.println();
+ }
+ if ( !group.getArtifacts().isEmpty() ) {
+ pw.println();
+ }
+ }
+
+ // configurations
+ if ( !runMode.getConfigurations().isEmpty() ) {
+ pw.println("[configurations]");
+ for(final Configuration config : runMode.getConfigurations()) {
+ writeComment(pw, config);
+ final String raw = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED);
+ String format = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED_FORMAT);
+ if ( format == null ) {
+ format = ModelConstants.CFG_FORMAT_FELIX_CA;
+ }
+ pw.print(" ");
+ if ( config.getFactoryPid() != null ) {
+ pw.print(config.getFactoryPid());
+ pw.print("-");
+ }
+ pw.print(config.getPid());
+ if ( !ModelConstants.CFG_FORMAT_FELIX_CA.equals(format) ) {
+ pw.print(" { format=}");
+ pw.print(format);
+ pw.print(" }");
+ }
+ pw.println();
+
+ final String configString;
+ if ( raw != null ) {
+ configString = raw;
+ } else if ( config.isSpecial() ) {
+ configString = config.getProperties().get(config.getPid()).toString();
+ } else {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ try {
+ ConfigurationHandler.write(os , config.getProperties());
+ } finally {
+ os.close();
+ }
+ configString = new String(os.toByteArray(), "UTF-8");
+ }
+ // we have to read the configuration line by line to properly indent
+ final LineNumberReader lnr = new LineNumberReader(new StringReader(configString));
+ String line = null;
+ while ((line = lnr.readLine()) != null ) {
+ if ( line.trim().isEmpty() ) {
+ continue;
+ }
+ pw.print(" ");
+ pw.println(line.trim());
+ }
+ pw.println();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/io/package-info.java b/src/main/java/org/apache/sling/provisioning/model/io/package-info.java
new file mode 100644
index 0000000..31f116d
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/io/package-info.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.
+ */
+
+@Version("1.0")
+package org.apache.sling.provisioning.model.io;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/provisioning/model/package-info.java b/src/main/java/org/apache/sling/provisioning/model/package-info.java
new file mode 100644
index 0000000..5e50b7f
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/package-info.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.
+ */
+
+@Version("1.0")
+package org.apache.sling.provisioning.model;
+
+import aQute.bnd.annotation.Version;
+
--
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.