/* * 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; } }