blob: bb0c564e169502e7e98590c53bbf6ef3233f1616 [file] [log] [blame]
/*
* 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 javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonValue;
import javax.json.stream.JsonGenerator;
import org.apache.felix.configurator.impl.json.JSMin;
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);
// 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 StringBuilder sb = new StringBuilder();
try (final Reader reader = new FileReader(file)) {
final char[] buf = new char[4096];
int l = 0;
while ((l = reader.read(buf)) > 0) {
sb.append(buf, 0, l);
}
} catch (final IOException io) {
throw new RuntimeException("Unable to read feature " + file.getAbsolutePath(), io);
}
final String readJson = sb.toString();
// minify JSON (remove comments)
String json;
try (final Writer out = new StringWriter(); final Reader in = new StringReader(readJson)) {
final JSMin min = new JSMin(in, out);
min.jsmin();
json = out.toString();
} catch (final IOException e) {
throw new RuntimeException("Unable to read feature file " + file.getAbsolutePath(), e);
}
// check if "id" is set
try (final JsonReader reader = Json.createReader(new StringReader(json))) {
final JsonObject obj = reader.readObject();
if (!obj.containsKey("id")) {
final StringBuilder isb = new StringBuilder();
isb.append(project.getGroupId());
isb.append(':');
isb.append(project.getArtifactId());
isb.append(':');
isb.append("slingosgifeature");
if (suggestedClassifier != null) {
isb.append(':');
isb.append(suggestedClassifier);
}
isb.append(':');
isb.append(project.getVersion());
final StringWriter writer = new StringWriter();
try (final JsonGenerator generator = Json.createGenerator(writer)) {
generator.writeStartObject();
generator.write("id", isb.toString());
for (final Map.Entry<String, JsonValue> entry : obj.entrySet()) {
generator.write(entry.getKey(), entry.getValue());
}
generator.writeEnd();
}
json = writer.toString();
}
}
// replace variables
return Substitution.replaceMavenVars(project, json);
}
/**
* 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;
}
}