blob: 478b498ba052a85f365e208bb7186e273841d301 [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.feature.applicationbuilder.impl;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.sling.feature.Application;
import org.apache.sling.feature.Artifact;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.Extension;
import org.apache.sling.feature.ExtensionType;
import org.apache.sling.feature.Feature;
import org.apache.sling.feature.builder.ApplicationBuilder;
import org.apache.sling.feature.builder.BuilderContext;
import org.apache.sling.feature.builder.FeatureProvider;
import org.apache.sling.feature.io.ArtifactHandler;
import org.apache.sling.feature.io.ArtifactManager;
import org.apache.sling.feature.io.ArtifactManagerConfig;
import org.apache.sling.feature.io.IOUtils;
import org.apache.sling.feature.io.json.ApplicationJSONWriter;
import org.apache.sling.feature.io.json.FeatureJSONReader;
import org.apache.sling.feature.io.json.FeatureJSONReader.SubstituteVariables;
import org.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.stream.Stream;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonString;
import javax.json.JsonValue;
public class Main {
private static Logger LOGGER;
private static String output;
private static String filesInput;
private static String dirsInput;
private static String repoUrls;
private static String propsFile;
private static String frameworkVersion;
private static File cacheDir;
/**
* Parse the command line parameters and update a configuration object.
* @param args Command line parameters
* @return Configuration object.
*/
private static void parseArgs(final String[] args) throws Exception {
final Option repoOption = Option.builder("u").hasArg().argName("Set repository url")
.desc("repository url").build();
final Option filesOption = new Option("f", true, "Set feature files (comma separated)");
final Option dirsOption = new Option("d", true, "Set feature file dirs (comma separated)");
final Option propsOption = new Option("p", true, "sling.properties file");
final Option frameworkOption = new Option("fv", true, "Set felix framework version");
final Option outputOption = Option.builder("o").hasArg().argName("Set output file")
.desc("output file").build();
final Option cacheOption = new Option("c", true, "Set cache dir");
final Option debugOption = new Option("v", false, "Verbose");
debugOption.setArgs(0);
final Options options = new Options();
options.addOption(repoOption);
options.addOption(filesOption);
options.addOption(dirsOption);
options.addOption(outputOption);
options.addOption(propsOption);
options.addOption(frameworkOption);
options.addOption(cacheOption);
options.addOption(debugOption);
final CommandLineParser parser = new DefaultParser();
try {
final CommandLine cl = parser.parse(options, args);
if ( cl.hasOption(repoOption.getOpt()) ) {
repoUrls = cl.getOptionValue(repoOption.getOpt());
}
if ( cl.hasOption(filesOption.getOpt()) ) {
filesInput = cl.getOptionValue(filesOption.getOpt());
}
if ( cl.hasOption(dirsOption.getOpt()) ) {
dirsInput = cl.getOptionValue(dirsOption.getOpt());
}
if ( cl.hasOption(outputOption.getOpt()) ) {
output = cl.getOptionValue(outputOption.getOpt());
}
if ( cl.hasOption(propsOption.getOpt()) ) {
propsFile = cl.getOptionValue(propsOption.getOpt());
}
if (cl.hasOption(frameworkOption.getOpt())) {
frameworkVersion = cl.getOptionValue(frameworkOption.getOpt());
}
if (cl.hasOption(cacheOption.getOpt())) {
cacheDir = new File(cl.getOptionValue(cacheOption.getOpt()));
}
if ( cl.hasOption(debugOption.getOpt()) ) {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
}
} catch ( final ParseException pe) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("applicationbuilder", options);
throw new Exception("Unable to parse command line: {}", pe);
}
if ( filesInput == null && dirsInput == null) {
throw new Exception("Required argument missing: model files or directory");
}
}
private static ArtifactManager getArtifactManager() {
final ArtifactManagerConfig amConfig = new ArtifactManagerConfig();
if ( repoUrls != null ) {
amConfig.setRepositoryUrls(repoUrls.split(","));
}
if (cacheDir != null) {
amConfig.setCacheDirectory(cacheDir);
}
try {
return ArtifactManager.getArtifactManager(amConfig);
} catch ( IOException ioe) {
LOGGER.error("Unable to create artifact manager " + ioe.getMessage(), ioe);
System.exit(1);
}
// we never reach this, but have to keep the compiler happy
return null;
}
public static void main(final String[] args) {
// setup logging
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
System.setProperty("org.slf4j.simpleLogger.showThreadName", "false");
System.setProperty("org.slf4j.simpleLogger.levelInBrackets", "true");
System.setProperty("org.slf4j.simpleLogger.showLogName", "false");
Exception clException = null;
try {
parseArgs(args);
}
catch (Exception ex) {
clException = ex;
}
LOGGER = LoggerFactory.getLogger("applicationbuilder");
LOGGER.info("Apache Sling Feature Application Builder");
LOGGER.info("");
if (clException != null) {
LOGGER.error(clException.getMessage(), clException);
System.exit(1);
}
final ArtifactManager am = getArtifactManager();
final String[] files =
Stream.concat(
Stream.of(filesInput != null ? filesInput.split(",") : new String[0]),
Stream.of(dirsInput != null ? dirsInput.split(",") : new String[0])
.map(path -> new File(path))
.filter(File::isDirectory)
.flatMap(dir ->
Stream.of(dir.listFiles()))
.filter(file -> !file.getName().startsWith("."))
.map(File::getAbsolutePath))
.toArray(String[]::new);
if (files.length == 0) {
LOGGER.error("No feature files found.");
System.exit(1);
}
try {
writeApplication(buildApplication(assembleApplication(null, am, files)), output == null ? "application.json" : output);
} catch ( final IOException ioe) {
LOGGER.error("Unable to read feature/application files " + ioe.getMessage(), ioe);
System.exit(1);
} catch ( final Exception e) {
LOGGER.error("Problem generating application", e);
System.exit(1);
}
}
private static Application assembleApplication(
Application app,
final ArtifactManager artifactManager,
final String... featureFiles)
throws IOException {
if ( featureFiles == null || featureFiles.length == 0 ) {
throw new IOException("No features found.");
}
Map<String, String> bundleFeatureMap = new HashMap<>();
Map<String, Set<String>> regionPackageMap = new HashMap<>();
Map<String, Set<String>> featureRegionMap = new HashMap<>();
List<Feature> features = new ArrayList<>();
for (final String initFile : featureFiles)
{
try
{
final Feature f = IOUtils.getFeature(initFile, artifactManager, FeatureJSONReader.SubstituteVariables.RESOLVE);
collectFeatureAndWhitelistMappings(f, artifactManager, bundleFeatureMap,
featureRegionMap, regionPackageMap);
features.add(f);
}
catch (Exception ex)
{
throw new IOException("Error reading feature: " + initFile, ex);
}
}
Collections.sort(features);
app = ApplicationBuilder.assemble(app, new BuilderContext(new FeatureProvider() {
@Override
public Feature provide(final ArtifactId id) {
try {
Feature f = IOUtils.getFeature(id.toMvnUrl(), artifactManager, SubstituteVariables.RESOLVE);
collectFeatureAndWhitelistMappings(f, artifactManager, bundleFeatureMap,
featureRegionMap, regionPackageMap);
return f;
} catch (final IOException e) {
// ignore
}
return null;
}
}), features.toArray(new Feature[0]));
Extension bundleFeatureExtension = createBundleFeatureExtension(bundleFeatureMap);
app.getExtensions().add(bundleFeatureExtension);
Extension featureRegionExtension = createFeatureRegionExtension(regionPackageMap, featureRegionMap);
app.getExtensions().add(featureRegionExtension);
// check framework
if ( app.getFramework() == null ) {
// use hard coded Apache Felix
app.setFramework(IOUtils.getFelixFrameworkId(null));
}
return app;
}
private static Extension createBundleFeatureExtension(Map<String, String> bundleFeatureMap) {
Extension bundleFeatureExtension = new Extension(ExtensionType.JSON, "bundle-feature-mapping", true);
JsonArrayBuilder ab = Json.createArrayBuilder();
JsonObjectBuilder ob = Json.createObjectBuilder();
for (Map.Entry<String, String> entry : bundleFeatureMap.entrySet()) {
ob.add(entry.getKey(), entry.getValue());
}
ab.add(ob);
bundleFeatureExtension.setJSON(ab.build().toString());
return bundleFeatureExtension;
}
private static Extension createFeatureRegionExtension(Map<String, Set<String>> regionPackageMap,
Map<String, Set<String>> featureRegionMap) {
Extension featureRegionExtension = new Extension(ExtensionType.JSON, "feature-region-mapping", true);
JsonArrayBuilder ab = Json.createArrayBuilder();
JsonObjectBuilder pob = Json.createObjectBuilder();
for (Map.Entry<String, Set<String>> entry : regionPackageMap.entrySet()) {
JsonArrayBuilder pab = Json.createArrayBuilder();
entry.getValue().stream().forEach(pab::add);
pob.add(entry.getKey(), pab);
}
JsonObjectBuilder ob = Json.createObjectBuilder();
ob.add("packages", pob);
JsonObjectBuilder fob = Json.createObjectBuilder();
for (Map.Entry<String, Set<String>> entry : featureRegionMap.entrySet()) {
JsonArrayBuilder rab = Json.createArrayBuilder();
entry.getValue().stream().forEach(rab::add);
fob.add(entry.getKey(), rab);
}
ob.add("regions", fob);
ab.add(ob);
featureRegionExtension.setJSON(ab.build().toString());
return featureRegionExtension;
}
private static void collectFeatureAndWhitelistMappings(Feature f,
ArtifactManager artifactManager,
Map<String, String> bundleFeatureMap,
Map<String, Set<String>> featureRegionMap,
Map<String, Set<String>> regionPackageMap)
throws IOException {
String featureID = f.getId().toMvnId();
for (Artifact bundle : f.getBundles()) {
ArtifactHandler handler = artifactManager.getArtifactHandler(bundle.getId().toMvnUrl());
String bsn = getBsn(handler.getFile());
bundleFeatureMap.put(bsn, featureID);
}
for (Iterator<Extension> it = f.getExtensions().iterator(); it.hasNext(); ) {
Extension e = it.next();
if ("api-regions".equals(e.getName())) {
it.remove(); // api-regions is not needed in the application.json
Set<String> regions = featureRegionMap.get(featureID);
if (regions == null) {
regions = new HashSet<>();
featureRegionMap.put(featureID, regions);
}
JsonArray ja = Json.createReader(new StringReader(e.getJSON())).readArray();
for (JsonValue jv : ja) {
if (jv instanceof JsonString) {
regions.add(((JsonString) jv).getString());
} else if (jv instanceof JsonObject) {
JsonObject jo = (JsonObject) jv;
String name = jo.getString("name");
regions.add(name);
JsonArray exports = jo.getJsonArray("exports");
Set<String> packages = regionPackageMap.get(name);
if (packages == null) {
packages = new HashSet<>();
regionPackageMap.put(name, packages);
}
for (JsonValue p : exports) {
if (p instanceof JsonString) {
packages.add(((JsonString) p).getString());
}
}
}
}
}
}
}
private static String getBsn(File artifactFile) throws IOException {
try (JarFile jf = new JarFile(artifactFile)) {
Attributes attrs = jf.getManifest().getMainAttributes();
String bsn = attrs.getValue(Constants.BUNDLE_SYMBOLICNAME);
String version = attrs.getValue(Constants.BUNDLE_VERSION);
if (version == null)
version = "0.0.0";
return bsn + ":" + version;
}
}
private static Application buildApplication(final Application app) {
final org.apache.sling.feature.Artifact a = new org.apache.sling.feature.Artifact(ArtifactId.parse("org.apache.sling/org.apache.sling.launchpad.api/1.2.0"));
a.getMetadata().put(org.apache.sling.feature.Artifact.KEY_START_ORDER, "1");
app.getBundles().add(a);
// sling.properties (TODO)
if ( propsFile == null ) {
app.getFrameworkProperties().put("org.osgi.framework.bootdelegation", "sun.*,com.sun.*");
} else {
}
// felix framework hard coded for now
app.setFramework(IOUtils.getFelixFrameworkId(frameworkVersion));
return app;
}
private static void writeApplication(final Application app, final String out) {
LOGGER.info("Writing application: " + out);
final File file = new File(out);
try ( final FileWriter writer = new FileWriter(file)) {
ApplicationJSONWriter.write(writer, app);
} catch ( final IOException ioe) {
LOGGER.error("Unable to write application to {} : {}", out, ioe.getMessage(), ioe);
System.exit(1);
}
}
}