/* * 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.karaf.tooling; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.karaf.profile.assembly.Builder; import org.apache.karaf.tooling.utils.IoUtils; import org.apache.karaf.tooling.utils.MavenUtil; import org.apache.karaf.tooling.utils.MojoSupport; import org.apache.karaf.tools.utils.model.KarafPropertyEdits; import org.apache.karaf.tools.utils.model.io.stax.KarafPropertyInstructionsModelStaxReader; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; /** * Creates a customized Karaf distribution by installing features and setting up * configuration files. The plugin gets features from feature.xml files and KAR * archives declared as dependencies or as files configured with the * featureRespositories parameter. It picks up other files, such as config files, * from ${project.build.directory}/classes. Thus, a file in src/main/resources/etc * will be copied by the resource plugin to ${project.build.directory}/classes/etc, * and then added to the assembly by this goal. */ @Mojo(name = "assembly", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true) public class AssemblyMojo extends MojoSupport { /** * Base directory used to overwrite resources in generated assembly after the build (resource directory). */ @Parameter(defaultValue = "${project.basedir}/src/main/resources/assembly") protected File sourceDirectory; /** * Base directory used to copy the resources during the build (working directory). */ @Parameter(defaultValue = "${project.build.directory}/assembly") protected File workDirectory; /** * Features configuration file (etc/org.apache.karaf.features.cfg). */ @Parameter(defaultValue = "${project.build.directory}/assembly/etc/org.apache.karaf.features.cfg") protected File featuresCfgFile; /** * startup.properties file. */ @Parameter(defaultValue = "${project.build.directory}/assembly/etc/startup.properties") protected File startupPropertiesFile; /** * Directory used during build to construction the Karaf system repository. */ @Parameter(defaultValue="${project.build.directory}/assembly/system") protected File systemDirectory; /** * default start level for bundles in features that don't specify it. */ @Parameter protected int defaultStartLevel = 30; @Parameter private List<String> startupRepositories; @Parameter private List<String> bootRepositories; @Parameter private List<String> installedRepositories; @Parameter private List<String> blacklistedRepositories; /** * List of features from runtime-scope features xml and kars to be installed into system and listed in startup.properties. */ @Parameter private List<String> startupFeatures; /** * List of features from runtime-scope features xml and kars to be installed into system repo and listed in features service boot features. */ @Parameter private List<String> bootFeatures; /** * List of features from runtime-scope features xml and kars to be installed into system repo and not mentioned elsewhere. */ @Parameter private List<String> installedFeatures; @Parameter private List<String> blacklistedFeatures; @Parameter private List<String> startupBundles; @Parameter private List<String> bootBundles; @Parameter private List<String> installedBundles; @Parameter private List<String> blacklistedBundles; @Parameter private String profilesUri; @Parameter private List<String> bootProfiles; @Parameter private List<String> startupProfiles; @Parameter private List<String> installedProfiles; @Parameter private List<String> blacklistedProfiles; @Parameter private Builder.BlacklistPolicy blacklistPolicy = Builder.BlacklistPolicy.Discard; /** * Ignore the dependency attribute (dependency="[true|false]") on bundle */ @Parameter(defaultValue = "false") protected boolean ignoreDependencyFlag; /** * Additional feature repositories */ @Parameter protected List<String> featureRepositories; @Parameter protected List<String> libraries; /** * Use reference: style urls in startup.properties */ @Parameter(defaultValue = "false") protected boolean useReferenceUrls; /** * Include project build output directory in the assembly */ @Parameter(defaultValue = "true") protected boolean includeBuildOutputDirectory; @Parameter protected boolean installAllFeaturesByDefault = true; @Parameter protected Builder.KarafVersion karafVersion = Builder.KarafVersion.v4x; /** * Specify the version of Java SE to be assumed for osgi.ee. */ @Parameter(defaultValue = "1.8") protected String javase; /** * Specify which framework to use * (one of framework, framework-logback, static-framework, static-framework-logback). */ @Parameter protected String framework; /** * Specify an XML file that instructs this goal to apply edits to * one or more standard Karaf property files. * The contents of this file are documented in detail on * <a href="karaf-property-instructions-model.html">this page</a>. * This allows you to * customize these files without making copies in your resources * directories. Here's a simple example: * <pre> * {@literal <property-edits xmlns="http://karaf.apache.org/tools/property-edits/1.0.0"> <edits> <edit> <file>config.properties</file> <operation>put</operation> <key>karaf.framework</key> <value>equinox</value> </edit> <edit> <file>config.properties</file> <operation>extend</operation> <key>org.osgi.framework.system.capabilities</key> <value>my-magic-capability</value> </edit> <edit> <file>config.properties</file> <operation prepend='true'>extend</operation> <key>some-other-list</key> <value>my-value-goes-first</value> </edit> </edits> </property-edits> } </pre> */ @Parameter(defaultValue = "${project.basedir}/src/main/karaf/assembly-property-edits.xml") protected String propertyFileEdits; /** * Glob specifying which configuration pids in the selected boot features * should be extracted to the etc directory. */ @Parameter protected List<String> pidsToExtract = Collections.singletonList("*"); /** * Specify a set of translated urls to use instead of downloading the artifacts * from their original locations. The given set will be extended with already * built artifacts from the maven project. */ @Parameter protected Map<String, String> translatedUrls; @Parameter protected Map<String, String> config; @Parameter protected Map<String, String> system; @Override public void execute() throws MojoExecutionException, MojoFailureException { try { doExecute(); } catch (MojoExecutionException | MojoFailureException e) { throw e; } catch (Exception e) { throw new MojoExecutionException("Unable to build assembly", e); } } protected void doExecute() throws Exception { startupRepositories = nonNullList(startupRepositories); bootRepositories = nonNullList(bootRepositories); installedRepositories = nonNullList(installedRepositories); startupBundles = nonNullList(startupBundles); bootBundles = nonNullList(bootBundles); installedBundles = nonNullList(installedBundles); blacklistedBundles = nonNullList(blacklistedBundles); startupFeatures = nonNullList(startupFeatures); bootFeatures = nonNullList(bootFeatures); installedFeatures = nonNullList(installedFeatures); blacklistedFeatures = nonNullList(blacklistedFeatures); startupProfiles = nonNullList(startupProfiles); bootProfiles = nonNullList(bootProfiles); installedProfiles = nonNullList(installedProfiles); blacklistedProfiles = nonNullList(blacklistedProfiles); blacklistedRepositories = nonNullList(blacklistedRepositories); if (!startupProfiles.isEmpty() || !bootProfiles.isEmpty() || !installedProfiles.isEmpty()) { if (profilesUri == null) { throw new IllegalArgumentException("profilesDirectory must be specified"); } } if (featureRepositories != null && !featureRepositories.isEmpty()) { getLog().warn("Use of featureRepositories is deprecated, use startupRepositories, bootRepositories or installedRepositories instead"); startupRepositories.addAll(featureRepositories); bootRepositories.addAll(featureRepositories); installedRepositories.addAll(featureRepositories); } StringBuilder remote = new StringBuilder(); for (Object obj : project.getRemoteProjectRepositories()) { if (remote.length() > 0) { remote.append(","); } remote.append(invoke(obj, "getUrl")); remote.append("@id=").append(invoke(obj, "getId")); if (!((Boolean) invoke(getPolicy(obj, false), "isEnabled"))) { remote.append("@noreleases"); } if ((Boolean) invoke(getPolicy(obj, true), "isEnabled")) { remote.append("@snapshots"); } } getLog().info("Using repositories: " + remote.toString()); Builder builder = Builder.newInstance(); builder.offline(mavenSession.isOffline()); builder.localRepository(localRepo.getBasedir()); builder.mavenRepositories(remote.toString()); builder.javase(javase); // Set up config and system props if (config != null) { config.forEach(builder::config); } if (system != null) { system.forEach(builder::system); } // Set up blacklisted items builder.blacklistBundles(blacklistedBundles); builder.blacklistFeatures(blacklistedFeatures); builder.blacklistProfiles(blacklistedProfiles); builder.blacklistRepositories(blacklistedRepositories); builder.blacklistPolicy(blacklistPolicy); if (propertyFileEdits != null) { File file = new File(propertyFileEdits); if (file.exists()) { KarafPropertyEdits edits; try (InputStream editsStream = new FileInputStream(propertyFileEdits)) { KarafPropertyInstructionsModelStaxReader kipmsr = new KarafPropertyInstructionsModelStaxReader(); edits = kipmsr.read(editsStream, true); } builder.propertyEdits(edits); } } builder.pidsToExtract(pidsToExtract); Map<String, String> urls = new HashMap<>(); List<Artifact> artifacts = new ArrayList<>(project.getAttachedArtifacts()); artifacts.add(project.getArtifact()); for (Artifact artifact : artifacts) { if (artifact.getFile() != null && artifact.getFile().exists()) { String mvnUrl = "mvn:" + artifact.getGroupId() + "/" + artifact.getArtifactId() + "/" + artifact.getVersion(); String type = artifact.getType(); if ("bundle".equals(type)) { type = "jar"; } if (!"jar".equals(type) || artifact.getClassifier() != null) { mvnUrl += "/" + type; if (artifact.getClassifier() != null) { mvnUrl += "/" + artifact.getClassifier(); } } urls.put(mvnUrl, artifact.getFile().toURI().toString()); } } if (translatedUrls != null) { urls.putAll(translatedUrls); } builder.translatedUrls(urls); // creating system directory getLog().info("Creating work directory"); builder.homeDirectory(workDirectory.toPath()); IoUtils.deleteRecursive(workDirectory); workDirectory.mkdirs(); List<String> startupKars = new ArrayList<>(); List<String> bootKars = new ArrayList<>(); List<String> installedKars = new ArrayList<>(); // Loading kars and features repositories getLog().info("Loading kar and features repositories dependencies"); for (Artifact artifact : project.getDependencyArtifacts()) { Builder.Stage stage; switch (artifact.getScope()) { case "compile": stage = Builder.Stage.Startup; break; case "runtime": stage = Builder.Stage.Boot; break; case "provided": stage = Builder.Stage.Installed; break; default: continue; } if ("kar".equals(artifact.getType())) { String uri = artifactToMvn(artifact); switch (stage) { case Startup: startupKars.add(uri); break; case Boot: bootKars.add(uri); break; case Installed: installedKars.add(uri); break; } } else if ("features".equals(artifact.getClassifier()) || "karaf".equals(artifact.getClassifier())) { String uri = artifactToMvn(artifact); switch (stage) { case Startup: startupRepositories.add(uri); break; case Boot: bootRepositories.add(uri); break; case Installed: installedRepositories.add(uri); break; } } else if ("jar".equals(artifact.getType()) || "bundle".equals(artifact.getType())) { String uri = artifactToMvn(artifact); switch (stage) { case Startup: startupBundles.add(uri); break; case Boot: bootBundles.add(uri); break; case Installed: installedBundles.add(uri); break; } } } builder.karafVersion(karafVersion) .useReferenceUrls(useReferenceUrls) .defaultAddAll(installAllFeaturesByDefault) .ignoreDependencyFlag(ignoreDependencyFlag); if (profilesUri != null) { builder.profilesUris(profilesUri); } if (libraries != null) { builder.libraries(libraries.toArray(new String[libraries.size()])); } // Startup boolean hasFrameworkKar = false; for (String kar : startupKars) { if (kar.startsWith("mvn:org.apache.karaf.features/framework/") || kar.startsWith("mvn:org.apache.karaf.features/static/")) { hasFrameworkKar = true; startupKars.remove(kar); if (framework == null) { framework = kar.startsWith("mvn:org.apache.karaf.features/framework/") ? "framework" : "static-framework"; } builder.kars(Builder.Stage.Startup, false, kar); break; } } if (!hasFrameworkKar) { Properties versions = new Properties(); try (InputStream is = getClass().getResourceAsStream("versions.properties")) { versions.load(is); } catch (IOException e) { throw new IllegalStateException(e); } String realKarafVersion = versions.getProperty("karaf-version"); String kar; switch (framework) { case "framework": kar = "mvn:org.apache.karaf.features/framework/" + realKarafVersion + "/xml/features"; break; case "framework-logback": kar = "mvn:org.apache.karaf.features/framework/" + realKarafVersion + "/xml/features"; break; case "static-framework": kar = "mvn:org.apache.karaf.features/static/" + realKarafVersion + "/xml/features"; break; case "static-framework-logback": kar = "mvn:org.apache.karaf.features/static/" + realKarafVersion + "/xml/features"; break; default: throw new IllegalArgumentException("Unsupported framework: " + framework); } builder.kars(Builder.Stage.Startup, false, kar); } if (!startupFeatures.contains(framework)) { builder.features(Builder.Stage.Startup, framework); } builder.defaultStage(Builder.Stage.Startup) .kars(toArray(startupKars)) .repositories(startupFeatures.isEmpty() && startupProfiles.isEmpty() && installAllFeaturesByDefault, toArray(startupRepositories)) .features(toArray(startupFeatures)) .bundles(toArray(startupBundles)) .profiles(toArray(startupProfiles)); // Boot builder.defaultStage(Builder.Stage.Boot) .kars(toArray(bootKars)) .repositories(bootFeatures.isEmpty() && bootProfiles.isEmpty() && installAllFeaturesByDefault, toArray(bootRepositories)) .features(toArray(bootFeatures)) .bundles(toArray(bootBundles)) .profiles(toArray(bootProfiles)); // Installed builder.defaultStage(Builder.Stage.Installed) .kars(toArray(installedKars)) .repositories(installedFeatures.isEmpty() && installedProfiles.isEmpty() && installAllFeaturesByDefault, toArray(installedRepositories)) .features(toArray(installedFeatures)) .bundles(toArray(installedBundles)) .profiles(toArray(installedProfiles)); // Generate the assembly builder.generateAssembly(); // Include project classes content if (includeBuildOutputDirectory) IoUtils.copyDirectory(new File(project.getBuild().getOutputDirectory()), workDirectory); // Overwrite assembly dir contents if (sourceDirectory.exists()) IoUtils.copyDirectory(sourceDirectory, workDirectory); // Chmod the bin/* scripts File[] files = new File(workDirectory, "bin").listFiles(); if( files!=null ) { for (File file : files) { if( !file.getName().endsWith(".bat") ) { try { Files.setPosixFilePermissions(file.toPath(), PosixFilePermissions.fromString("rwxr-xr-x")); } catch (Throwable ignore) { // we tried our best, perhaps the OS does not support posix file perms. } } } } } private Object invoke(Object object, String getter) throws MojoExecutionException { try { return object.getClass().getMethod(getter).invoke(object); } catch (Exception e) { throw new MojoExecutionException("Unable to build remote repository from " + object.toString(), e); } } private Object getPolicy(Object object, boolean snapshots) throws MojoExecutionException { return invoke(object, "getPolicy", new Class[] { Boolean.TYPE }, new Object[] { snapshots }); } private Object invoke(Object object, String getter, Class[] types, Object[] params) throws MojoExecutionException { try { return object.getClass().getMethod(getter, types).invoke(object, params); } catch (Exception e) { throw new MojoExecutionException("Unable to build remote repository from " + object.toString(), e); } } private String artifactToMvn(Artifact artifact) throws MojoExecutionException { String uri; String groupId = artifact.getGroupId(); String artifactId = artifact.getArtifactId(); String version = artifact.getBaseVersion(); String type = artifact.getArtifactHandler().getExtension(); String classifier = artifact.getClassifier(); if (MavenUtil.isEmpty(classifier)) { if ("jar".equals(type)) { uri = String.format("mvn:%s/%s/%s", groupId, artifactId, version); } else { uri = String.format("mvn:%s/%s/%s/%s", groupId, artifactId, version, type); } } else { uri = String.format("mvn:%s/%s/%s/%s/%s", groupId, artifactId, version, type, classifier); } return uri; } private String[] toArray(List<String> strings) { return strings.toArray(new String[strings.size()]); } private List<String> nonNullList(List<String> list) { return list == null ? new ArrayList<String>() : list; } }