| /* |
| * 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.Commentable; |
| 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.Section; |
| |
| /** |
| * This class offers a method to read a model using a {@code Reader} instance. |
| */ |
| public class ModelReader { |
| |
| private enum CATEGORY { |
| NONE(null, null), |
| FEATURE("feature", new String[] {"name", "type", "version"}), |
| VARIABLES("variables", null), |
| ARTIFACTS("artifacts", new String[] {"runModes", "startLevel"}), |
| SETTINGS("settings", new String[] {"runModes"}), |
| CONFIGURATIONS("configurations", new String[] {"runModes"}), |
| CONFIG(null, null), |
| ADDITIONAL(null, null); |
| |
| public final String name; |
| |
| public final String[] parameters; |
| |
| private CATEGORY(final String n, final String[] p) { |
| this.name = n; |
| this.parameters = p; |
| } |
| } |
| |
| /** |
| * Reads the model file |
| * The reader is not closed. It is up to the caller to close the reader. |
| * |
| * @param reader The reader providing the model |
| * @param location Optional location string identifying the source of the model. |
| * @throws IOException If an error occurs |
| */ |
| public static Model read(final Reader reader, final String location) |
| throws IOException { |
| final ModelReader mr = new ModelReader(location); |
| return mr.readModel(reader); |
| } |
| |
| private CATEGORY mode = CATEGORY.NONE; |
| |
| private final Model model = new Model(); |
| |
| private Feature feature; |
| private RunMode runMode; |
| private ArtifactGroup artifactGroup; |
| private Configuration config; |
| |
| private Section additionalSection; |
| |
| private String comment; |
| |
| private StringBuilder configBuilder; |
| |
| 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 ) { |
| // trim the line |
| line = line.trim(); |
| |
| // ignore empty line |
| if ( line.isEmpty() ) { |
| if ( this.mode == CATEGORY.ADDITIONAL ) { |
| if ( this.additionalSection.getContents() == null ) { |
| this.additionalSection.setContents(line); |
| } else { |
| this.additionalSection.setContents(this.additionalSection.getContents() + '\n' + line); |
| } |
| continue; |
| } |
| checkConfig(); |
| continue; |
| } |
| |
| // comment? |
| if ( line.startsWith("#") ) { |
| if ( config != null ) { |
| configBuilder.append(line); |
| configBuilder.append('\n'); |
| |
| continue; |
| } |
| if ( this.mode == CATEGORY.ADDITIONAL ) { |
| if ( this.additionalSection.getContents() == null ) { |
| this.additionalSection.setContents(line); |
| } else { |
| this.additionalSection.setContents(this.additionalSection.getContents() + '\n' + line); |
| } |
| continue; |
| } |
| final String c = line.substring(1).trim(); |
| if ( comment == null ) { |
| comment = c; |
| } else { |
| comment = comment + "\n" + c; |
| } |
| continue; |
| } |
| |
| if ( global ) { |
| global = false; |
| if ( !line.startsWith("[feature ") ) { |
| throw new IOException(exceptionPrefix + " Model file must start with a feature category."); |
| } |
| } |
| |
| if ( line.startsWith("[") ) { |
| additionalSection = null; |
| 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 ) { |
| // additional section |
| if ( !category.startsWith(":") ) { |
| throw new IOException(exceptionPrefix + "Unknown category in line " + this.lineNumberReader.getLineNumber() + ": " + category); |
| } |
| found = CATEGORY.ADDITIONAL; |
| } |
| 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, this.mode.parameters); |
| } |
| |
| switch ( this.mode ) { |
| case NONE : break; // this can never happen |
| case CONFIG : break; // this can never happen |
| case FEATURE : final String name = parameters.get("name"); |
| if ( name == null ) { |
| throw new IOException(exceptionPrefix + "Feature name missing in line " + this.lineNumberReader.getLineNumber() + ": " + line); |
| } |
| if ( model.getFeature(name) != null ) { |
| throw new IOException(exceptionPrefix + "Duplicate feature in line " + this.lineNumberReader.getLineNumber() + ": " + line); |
| } |
| this.feature = model.getOrCreateFeature(name); |
| this.feature.setType(parameters.get("type")); |
| this.feature.setVersion(parameters.get("version")); |
| this.init(this.feature); |
| this.runMode = null; |
| this.artifactGroup = null; |
| break; |
| case VARIABLES : checkFeature(); |
| this.init(this.feature.getVariables()); |
| break; |
| case SETTINGS: checkFeature(); |
| checkRunMode(parameters); |
| this.init(this.runMode.getSettings()); |
| break; |
| case ARTIFACTS: checkFeature(); |
| checkRunMode(parameters); |
| int startLevel = 0; |
| String level = parameters.get("startLevel"); |
| 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.getArtifactGroup(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(parameters); |
| this.init(this.runMode.getConfigurations()); |
| break; |
| case ADDITIONAL: checkFeature(); |
| this.runMode = null; |
| this.artifactGroup = null; |
| this.additionalSection = new Section(category.substring(1)); |
| this.init(this.additionalSection); |
| this.feature.getAdditionalSections().add(this.additionalSection); |
| this.additionalSection.getAttributes().putAll(parameters); |
| } |
| } 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: this.runMode = this.feature.getOrCreateRunMode(null); |
| this.artifactGroup = this.runMode.getOrCreateArtifactGroup(0); |
| // no break, we continue with ARTIFACT |
| case ARTIFACTS : 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(), null); |
| } |
| } |
| try { |
| final Artifact artifact = Artifact.fromMvnUrl("mvn:" + artifactUrl); |
| this.init(artifact); |
| this.artifactGroup.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(), new String[] {"format", "mode"}); |
| } |
| } |
| String format = cfgPars.get("format"); |
| 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; |
| } |
| String cfgMode= cfgPars.get("mode"); |
| if ( cfgMode != null ) { |
| if ( !ModelConstants.CFG_MODE_OVERWRITE.equals(cfgMode) |
| && !ModelConstants.CFG_MODE_MERGE.equals(cfgMode) ) { |
| throw new IOException(exceptionPrefix + "Unknown mode configuration parameter in line " + this.lineNumberReader.getLineNumber() + ": " + line); |
| } |
| } else { |
| cfgMode = ModelConstants.CFG_MODE_OVERWRITE; |
| } |
| final String pid; |
| final String factoryPid; |
| final int factoryPos = configId.indexOf('-'); |
| if ( factoryPos == -1 ) { |
| pid = configId; |
| factoryPid = null; |
| } else { |
| pid = configId.substring(factoryPos + 1); |
| factoryPid = configId.substring(0, factoryPos); |
| } |
| if ( runMode.getConfiguration(pid, factoryPid) != null ) { |
| throw new IOException(exceptionPrefix + "Duplicate configuration in line " + this.lineNumberReader.getLineNumber()); |
| } |
| config = runMode.getOrCreateConfiguration(pid, factoryPid); |
| this.init(config); |
| config.getProperties().put(ModelConstants.CFG_UNPROCESSED_FORMAT, format); |
| config.getProperties().put(ModelConstants.CFG_UNPROCESSED_MODE, cfgMode); |
| configBuilder = new StringBuilder(); |
| mode = CATEGORY.CONFIG; |
| break; |
| case CONFIG : configBuilder.append(line); |
| configBuilder.append('\n'); |
| break; |
| case ADDITIONAL : if ( this.additionalSection.getContents() == null ) { |
| this.additionalSection.setContents(line); |
| } else { |
| this.additionalSection.setContents(this.additionalSection.getContents() + '\n' + 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 ) { |
| throw new IOException(exceptionPrefix + "No preceding feature definition in line " + this.lineNumberReader.getLineNumber()); |
| } |
| } |
| |
| /** |
| * Check for a run mode object |
| */ |
| private void checkRunMode(final Map<String, String> parameters) throws IOException { |
| String[] runModes = null; |
| final String rmDef = parameters.get("runModes"); |
| if ( rmDef != null ) { |
| runModes = rmDef.split(","); |
| for(int i=0; i<runModes.length; i++) { |
| runModes[i] = runModes[i].trim(); |
| } |
| } |
| runMode = this.feature.getOrCreateRunMode(runModes); |
| } |
| |
| private void init(final Commentable 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, final String[] allowedParameters) 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); |
| } |
| final String key = kv.substring(0, sep).trim(); |
| parameters.put(key, kv.substring(sep + 1).trim()); |
| |
| if ( allowedParameters != null ) { |
| boolean found = false; |
| for(final String allowed : allowedParameters) { |
| if ( key.equals(allowed) ) { |
| found = true; |
| break; |
| } |
| } |
| if ( !found ) { |
| throw new IOException(exceptionPrefix + "Invalid parameter " + key + " in line " + this.lineNumberReader.getLineNumber()); |
| } |
| } |
| } |
| } |
| return parameters; |
| } |
| } |
| |
| |