| /* |
| * 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.maven.slingstart; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.io.Writer; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.maven.MavenExecutionException; |
| import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; |
| import org.apache.maven.artifact.resolver.ArtifactResolver; |
| import org.apache.maven.artifact.versioning.DefaultArtifactVersion; |
| import org.apache.maven.execution.MavenSession; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.sling.feature.ArtifactId; |
| import org.apache.sling.feature.Feature; |
| import org.apache.sling.feature.builder.FeatureProvider; |
| import org.apache.sling.feature.io.json.FeatureJSONReader; |
| import org.apache.sling.feature.io.json.FeatureJSONWriter; |
| import org.apache.sling.feature.modelconverter.FeatureToProvisioning; |
| import org.apache.sling.maven.slingstart.ModelPreprocessor.Environment; |
| import org.apache.sling.maven.slingstart.ModelPreprocessor.ProjectInfo; |
| |
| import aQute.bnd.version.MavenVersion; |
| |
| public class FeatureModelConverter { |
| static final String BUILD_DIR = "provisioning/converted"; |
| |
| static final String PROVISIONING_MODEL_NAME_VARIABLE = "provisioning.model.name"; |
| static final String PROVISIONING_RUNMODES = "provisioning.runmodes"; |
| |
| public static Feature getFeature(ArtifactId id, MavenSession session, MavenProject project, ArtifactHandlerManager manager, ArtifactResolver resolver) { |
| try { |
| File file = ModelUtils.getArtifact(project, session, manager, resolver, id.getGroupId(), id.getArtifactId(), id.getVersion(), id.getType(), id.getClassifier()).getFile(); |
| try (Reader reader = new InputStreamReader(new FileInputStream(file), "UTF-8")) { |
| return FeatureJSONReader.read(reader, file.toURI().toURL().toString()); |
| } |
| } catch (Exception ex) { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| public static void convert(Environment env) throws MavenExecutionException { |
| for (ProjectInfo pi : env.modelProjects.values()) { |
| convert(env, pi, pi.defaultProvisioningModelName); |
| } |
| } |
| |
| public static class FeatureFileEntry { |
| public File file; |
| public String runModes; |
| public String model; |
| } |
| |
| public static void convertDirectories(String featuresDirectory, MavenProject project, String defaultProvName, |
| FeatureProvider fp) throws MavenExecutionException { |
| final List<FeatureFileEntry> featureFiles = FeatureModelConverter.getFeatureFiles(project.getBasedir(), |
| featuresDirectory); |
| if (!featureFiles.isEmpty()) { |
| convert(featureFiles, project, defaultProvName, fp); |
| } |
| } |
| |
| static List<FeatureFileEntry> getFeatureFiles(final File baseDir, final String config) { |
| final List<FeatureFileEntry> files = new ArrayList<>(); |
| for (final ParsedHeaderClause cfg : parseStandardHeader(config)) { |
| final String directory = cfg.m_paths.get(0).trim().replace('/', File.separatorChar); |
| |
| String runmodes = (String) cfg.m_attrs.get(PROVISIONING_RUNMODES); |
| String model = (String) cfg.m_attrs.get(PROVISIONING_MODEL_NAME_VARIABLE); |
| |
| final File featuresDir = new File(baseDir, directory); |
| final File[] children = featuresDir.listFiles(); |
| if (children != null) { |
| for (final File f : children) { |
| if (f.isFile() && f.getName().endsWith(".json")) { |
| final FeatureFileEntry ff = new FeatureFileEntry(); |
| ff.file = f; |
| ff.runModes = runmodes; |
| ff.model = model; |
| files.add(ff); |
| } |
| } |
| } |
| } |
| |
| return files; |
| } |
| |
| public static void convert(Environment env, ProjectInfo info, String defaultProvName) |
| throws MavenExecutionException { |
| final String config = ModelPreprocessor.nodeValue(info.plugin, "featuresDirectory", "src/main/features"); |
| final List<FeatureFileEntry> files = getFeatureFiles(info.project.getBasedir(), config); |
| if (!files.isEmpty()) { |
| try { |
| convert(files, info.project, defaultProvName, |
| id -> getFeature(id, env.session, info.project, env.artifactHandlerManager, env.resolver)); |
| } catch (RuntimeException ex) { |
| throw new MavenExecutionException(ex.getMessage(), ex); |
| } |
| } |
| } |
| |
| static void convert(List<FeatureFileEntry> files, MavenProject project, String defaultProvName, FeatureProvider fp) |
| throws MavenExecutionException { |
| File processedFeaturesDir = new File(project.getBuild().getDirectory(), "features/processed"); |
| processedFeaturesDir.mkdirs(); |
| |
| List<File> substedFiles = new ArrayList<>(); |
| for (FeatureFileEntry featureFile : files) { |
| final File f = featureFile.file; |
| File outFile = new File(processedFeaturesDir, f.getName()); |
| if (!outFile.exists() || outFile.lastModified() <= f.lastModified()) { |
| try { |
| final String suggestedClassifier; |
| if (!f.getName().equals("feature.json")) { |
| final int lastDot = f.getName().lastIndexOf('.'); |
| suggestedClassifier = f.getName().substring(0, lastDot); |
| } else { |
| suggestedClassifier = null; |
| } |
| String json = readFeatureFile(project, f, suggestedClassifier); |
| |
| // handle extensions |
| try (final Reader reader = new StringReader(json)) { |
| final Feature feature = FeatureJSONReader.read(reader, f.getAbsolutePath()); |
| JSONFeatures.handleExtensions(feature, f); |
| try (final Writer writer = new StringWriter()) { |
| FeatureJSONWriter.write(writer, feature); |
| writer.flush(); |
| json = writer.toString(); |
| } |
| } |
| |
| // check for prov model name |
| if (defaultProvName != null || featureFile.runModes != null || featureFile.model != null) { |
| try (final Reader reader = new StringReader(json)) { |
| final Feature feature = FeatureJSONReader.read(reader, f.getAbsolutePath()); |
| boolean update = false; |
| if (featureFile.runModes != null) { |
| String oldValue = feature.getVariables().get(PROVISIONING_RUNMODES); |
| String newValue; |
| if (oldValue == null) { |
| newValue = featureFile.runModes; |
| } else { |
| newValue = oldValue.concat(",").concat(featureFile.runModes); |
| } |
| feature.getVariables().put(PROVISIONING_RUNMODES, newValue); |
| update = true; |
| } |
| |
| if (feature.getVariables().get(PROVISIONING_MODEL_NAME_VARIABLE) == null) { |
| boolean updateInner = true; |
| if (featureFile.model != null) { |
| feature.getVariables().put(PROVISIONING_MODEL_NAME_VARIABLE, featureFile.model); |
| } |
| else if (defaultProvName != null) { |
| feature.getVariables().put(PROVISIONING_MODEL_NAME_VARIABLE, defaultProvName); |
| } |
| else { |
| updateInner = update; |
| } |
| update = updateInner; |
| } |
| |
| if (update) { |
| try (final Writer writer = new StringWriter()) { |
| FeatureJSONWriter.write(writer, feature); |
| writer.flush(); |
| json = writer.toString(); |
| } |
| } |
| } |
| } |
| try (final Writer fileWriter = new FileWriter(outFile)) { |
| fileWriter.write(json); |
| } |
| } catch (IOException e) { |
| throw new MavenExecutionException("Problem processing feature file " + f.getAbsolutePath(), e); |
| } |
| } |
| substedFiles.add(outFile); |
| } |
| |
| File targetDir = new File(project.getBuild().getDirectory(), BUILD_DIR); |
| targetDir.mkdirs(); |
| |
| try { |
| for (File f : substedFiles) { |
| File genFile = new File(targetDir, f.getName() + ".txt"); |
| FeatureToProvisioning.convert(f, genFile, fp, substedFiles.toArray(new File[] {})); |
| } |
| } catch (Exception e) { |
| throw new MavenExecutionException("Cannot convert feature files to provisioning model", e); |
| } |
| } |
| |
| /** |
| * Read the json file, minify it, add id if missing and replace variables |
| * |
| * @param file The json file |
| * @return The read and minified JSON |
| */ |
| public static String readFeatureFile(final MavenProject project, final File file, |
| final String suggestedClassifier) { |
| final ArtifactId fileId = new ArtifactId(project.getGroupId(), |
| project.getArtifactId(), |
| project.getVersion(), |
| suggestedClassifier, |
| "slingosgifeature"); |
| try ( final Reader reader = new FileReader(file) ) { |
| return Substitution.replaceMavenVars(project, JSONFeatures.read(reader, fileId, file.getAbsolutePath())); |
| } catch (final IOException e) { |
| throw new RuntimeException("Unable to read feature file " + file.getAbsolutePath() + " : " + e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Remove leading zeros for a version part |
| */ |
| private static String cleanVersionString(final String version) { |
| final StringBuilder sb = new StringBuilder(); |
| boolean afterDot = false; |
| for(int i=0;i<version.length(); i++) { |
| final char c = version.charAt(i); |
| if ( c == '.' ) { |
| if (afterDot == true ) { |
| sb.append('0'); |
| } |
| afterDot = true; |
| sb.append(c); |
| } else if ( afterDot && c == '0' ) { |
| // skip |
| } else if ( afterDot && c == '-' ) { |
| sb.append('0'); |
| sb.append(c); |
| afterDot = false; |
| } else { |
| afterDot = false; |
| sb.append(c); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| public static String getOSGiVersion(final String version) { |
| final DefaultArtifactVersion dav = new DefaultArtifactVersion(cleanVersionString(version)); |
| final StringBuilder sb = new StringBuilder(); |
| sb.append(dav.getMajorVersion()); |
| sb.append('.'); |
| sb.append(dav.getMinorVersion()); |
| sb.append('.'); |
| sb.append(dav.getIncrementalVersion()); |
| if ( dav.getQualifier() != null ) { |
| sb.append('.'); |
| sb.append(dav.getQualifier()); |
| } |
| final MavenVersion mavenVersion = new MavenVersion(sb.toString()); |
| return mavenVersion.getOSGiVersion().toString(); |
| } |
| |
| private static class ParsedHeaderClause |
| { |
| public final List<String> m_paths; |
| public final Map<String, String> m_dirs; |
| public final Map<String, Object> m_attrs; |
| public final Map<String, String> m_types; |
| |
| public ParsedHeaderClause( |
| List<String> paths, Map<String, String> dirs, Map<String, Object> attrs, |
| Map<String, String> types) |
| { |
| m_paths = paths; |
| m_dirs = dirs; |
| m_attrs = attrs; |
| m_types = types; |
| } |
| } |
| |
| private static final char EOF = (char) -1; |
| |
| private static char charAt(int pos, String headers, int length) |
| { |
| if (pos >= length) |
| { |
| return EOF; |
| } |
| return headers.charAt(pos); |
| } |
| |
| private static final int CLAUSE_START = 0; |
| private static final int PARAMETER_START = 1; |
| private static final int KEY = 2; |
| private static final int DIRECTIVE_OR_TYPEDATTRIBUTE = 4; |
| private static final int ARGUMENT = 8; |
| private static final int VALUE = 16; |
| |
| @SuppressWarnings({ "unchecked", "rawtypes" }) |
| private static List<ParsedHeaderClause> parseStandardHeader(String header) |
| { |
| List<ParsedHeaderClause> clauses = new ArrayList<ParsedHeaderClause>(); |
| if (header == null) |
| { |
| return clauses; |
| } |
| ParsedHeaderClause clause = null; |
| String key = null; |
| Map targetMap = null; |
| int state = CLAUSE_START; |
| int currentPosition = 0; |
| int startPosition = 0; |
| int length = header.length(); |
| boolean quoted = false; |
| boolean escaped = false; |
| |
| char currentChar = EOF; |
| do |
| { |
| currentChar = charAt(currentPosition, header, length); |
| switch (state) |
| { |
| case CLAUSE_START: |
| clause = new ParsedHeaderClause( |
| new ArrayList<String>(), |
| new HashMap<String, String>(), |
| new HashMap<String, Object>(), |
| new HashMap<String, String>()); |
| clauses.add(clause); |
| state = PARAMETER_START; |
| case PARAMETER_START: |
| startPosition = currentPosition; |
| state = KEY; |
| case KEY: |
| switch (currentChar) |
| { |
| case ':': |
| case '=': |
| key = header.substring(startPosition, currentPosition).trim(); |
| startPosition = currentPosition + 1; |
| targetMap = clause.m_attrs; |
| state = currentChar == ':' ? DIRECTIVE_OR_TYPEDATTRIBUTE : ARGUMENT; |
| break; |
| case EOF: |
| case ',': |
| case ';': |
| clause.m_paths.add(header.substring(startPosition, currentPosition).trim()); |
| state = currentChar == ',' ? CLAUSE_START : PARAMETER_START; |
| break; |
| default: |
| break; |
| } |
| currentPosition++; |
| break; |
| case DIRECTIVE_OR_TYPEDATTRIBUTE: |
| switch(currentChar) |
| { |
| case '=': |
| if (startPosition != currentPosition) |
| { |
| clause.m_types.put(key, header.substring(startPosition, currentPosition).trim()); |
| } |
| else |
| { |
| targetMap = clause.m_dirs; |
| } |
| state = ARGUMENT; |
| startPosition = currentPosition + 1; |
| break; |
| default: |
| break; |
| } |
| currentPosition++; |
| break; |
| case ARGUMENT: |
| if (currentChar == '\"') |
| { |
| quoted = true; |
| currentPosition++; |
| } |
| else |
| { |
| quoted = false; |
| } |
| if (!Character.isWhitespace(currentChar)) { |
| state = VALUE; |
| } |
| else { |
| currentPosition++; |
| } |
| break; |
| case VALUE: |
| if (escaped) |
| { |
| escaped = false; |
| } |
| else |
| { |
| if (currentChar == '\\' ) |
| { |
| escaped = true; |
| } |
| else if (quoted && currentChar == '\"') |
| { |
| quoted = false; |
| } |
| else if (!quoted) |
| { |
| String value = null; |
| switch(currentChar) |
| { |
| case EOF: |
| case ';': |
| case ',': |
| value = header.substring(startPosition, currentPosition).trim(); |
| if (value.startsWith("\"") && value.endsWith("\"")) |
| { |
| value = value.substring(1, value.length() - 1); |
| } |
| if (targetMap.put(key, value) != null) |
| { |
| throw new IllegalArgumentException( |
| "Duplicate '" + key + "' in: " + header); |
| } |
| state = currentChar == ';' ? PARAMETER_START : CLAUSE_START; |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| currentPosition++; |
| break; |
| default: |
| break; |
| } |
| } while ( currentChar != EOF); |
| |
| if (state > PARAMETER_START) |
| { |
| throw new IllegalArgumentException("Unable to parse header: " + header); |
| } |
| return clauses; |
| } |
| } |