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 <cziege...@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" <commits@sling.apache.org>.