org.wisdom.maven.osgi.BundlePackager.java Source code

Java tutorial

Introduction

Here is the source code for org.wisdom.maven.osgi.BundlePackager.java

Source

/*
 * #%L
 * Wisdom-Framework
 * %%
 * Copyright (C) 2013 - 2014 Wisdom Framework
 * %%
 * Licensed 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.
 * #L%
 */
package org.wisdom.maven.osgi;

import aQute.bnd.osgi.*;
import aQute.libg.reporter.ReporterAdapter;
import com.google.common.base.Joiner;
import org.apache.commons.io.IOUtils;
import org.apache.felix.ipojo.manipulator.Pojoization;
import org.apache.felix.ipojo.manipulator.util.Classpath;
import org.wisdom.bnd.plugins.ImportedPackageRangeFixer;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;

/**
 * Packages the bundle using BND.
 */
public final class BundlePackager implements org.wisdom.maven.Constants {

    private BundlePackager() {
        //Hide default constructor
    }

    /**
     * Creates the bundle.
     *
     * @param basedir the project's base directory
     * @param output  the output file
     * @throws IOException occurs when the bundle cannot be built correctly.
     */
    public static void bundle(File basedir, File output, Reporter reporter) throws IOException {
        ProjectScanner scanner = new ProjectScanner(basedir);

        // Loads the properties inherited from Maven.
        Properties instructions = readMavenProperties(basedir);
        // Loads the properties from the BND file.
        Properties fromBnd = readInstructionsFromBndFile(basedir);
        if (fromBnd == null) {
            // No bnd files, use default instructions
            instructions = populatePropertiesWithDefaults(basedir, instructions, scanner);
        } else {
            // We have a BND file.
            // Do we have to merge ?
            String noDefaultValue = fromBnd.getProperty("-no-default");
            if (!"true".equalsIgnoreCase(noDefaultValue)) {
                // So we need to merge the default with the bnd files
                // 1) merge the instructions from the bnd files with the default
                // 2) merge the resulting set of instruction onto the maven properties
                // (and override the default)
                instructions = Instructions.mergeAndOverrideExisting(instructions,
                        populatePropertiesWithDefaults(basedir, fromBnd, scanner));
            } else {
                instructions = Instructions.mergeAndOverrideExisting(instructions, fromBnd);
            }
        }

        // Manage Embedded dependencies
        DependencyEmbedder ed = new DependencyEmbedder(instructions, reporter);
        instructions = ed.generate(instructions, org.wisdom.maven.osgi.Classpath.load(basedir));

        // Integrate custom headers added by other plugins.
        instructions = mergeExtraHeaders(basedir, instructions);

        // For debugging purpose, dump the instructions to target/osgi/instructions.properties
        FileOutputStream fos = null;
        try {
            File out = new File(basedir, "target/osgi/instructions.properties");
            fos = new FileOutputStream(out);
            instructions.store(fos, "Wisdom BND Instructions");
        } catch (IOException e) { // NOSONAR
            // Ignore it.
        } finally {
            IOUtils.closeQuietly(fos);
        }

        // Instructions loaded, start the build sequence.
        final Jar[] jars = org.wisdom.maven.osgi.Classpath.computeClassPath(basedir);

        File bnd;
        File ipojo;
        Builder builder = null;
        try {
            builder = getOSGiBuilder(basedir, instructions, jars);
            // The next sequence is weird
            // First build the bundle with the given instruction
            // Then analyze to apply the plugin and fix
            // finally, rebuild with the updated metadata
            // Without the first build, embedded dependencies and private packages from classpath are not analyzed.
            builder.build();
            builder.analyze();
            builder.build();

            reportErrors(builder.getWarnings(), builder.getErrors(), reporter);
            bnd = File.createTempFile("bnd-", ".jar");
            ipojo = File.createTempFile("ipojo-", ".jar");
            builder.getJar().write(bnd);
        } catch (Exception e) {
            throw new IOException("Cannot build the OSGi bundle", e);
        } finally {
            if (builder != null) {
                builder.close();
            }
        }

        final Set<String> elements = org.wisdom.maven.osgi.Classpath.computeClassPathElement(basedir);
        Classpath classpath = new Classpath(elements);
        Pojoization pojoization = new Pojoization();
        pojoization.pojoization(bnd, ipojo, new File(basedir, "src/main/resources"), classpath.createClassLoader());
        reportErrors(pojoization.getWarnings(), pojoization.getErrors(), reporter);

        Files.move(Paths.get(ipojo.getPath()), Paths.get(output.getPath()), StandardCopyOption.REPLACE_EXISTING);
    }

    /**
     * If a bundle has added extra headers, they are added to the bundle manifest.
     *
     * @param baseDir    the project directory
     * @param properties the current set of properties in which the read metadata are written
     * @return the merged set of properties
     */
    private static Properties mergeExtraHeaders(File baseDir, Properties properties) throws IOException {
        File extra = new File(baseDir, EXTRA_HEADERS_FILE);
        return Instructions.merge(properties, extra);
    }

    /**
     * This method is used by plugin willing to add custom header to the bundle manifest.
     *
     * @param baseDir the project directory
     * @param header  the header to add
     * @param value   the value to write
     * @throws IOException if the header cannot be added
     */
    public static void addExtraHeaderToBundleManifest(File baseDir, String header, String value)
            throws IOException {
        Properties props = new Properties();
        File extra = new File(baseDir, EXTRA_HEADERS_FILE);
        extra.getParentFile().mkdirs();
        // If the file exist it loads it, if not nothing happens.
        props = Instructions.merge(props, extra);
        if (value != null) {
            props.setProperty(header, value);
        } else {
            props.remove(header);
        }
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(extra);
            props.store(fos, "");
        } finally {
            IOUtils.closeQuietly(fos);
        }
    }

    /**
     * We should have generated a {@code target/osgi/osgi.properties} file with the metadata we inherit from Maven.
     *
     * @param baseDir the project directory
     * @return the computed set of properties
     */
    public static Properties readMavenProperties(File baseDir) throws IOException {
        return Instructions.load(new File(baseDir, org.wisdom.maven.Constants.OSGI_PROPERTIES));
    }

    /**
     * Populates the given properties object with our BND default instructions (computed for the current project).
     * Entries are not added if the given properties file already contains these values.
     *
     * @param basedir      the project's base directory
     * @param instructions the current set of properties in which the read metadata are written
     * @param scanner      the project scanner to retrieve information about the sources and resources contained in the
     *                     project
     * @return the final set of instructions (containing the default instructions merged into the
     * given set of properties)
     * @throws IOException if something wrong happens
     */
    protected static Properties populatePropertiesWithDefaults(File basedir, Properties instructions,
            ProjectScanner scanner) throws IOException {
        Properties defaultInstructions = new Properties();

        List<String> privates = new ArrayList<>();
        List<String> exports = new ArrayList<>();

        // Do local resources
        String resources = getLocalResources(basedir, false, scanner);
        if (!resources.isEmpty()) {
            defaultInstructions.put(Analyzer.INCLUDE_RESOURCE, resources);
        }

        defaultInstructions.put(Constants.IMPORT_PACKAGE, "*");

        for (String s : scanner.getPackagesFromMainSources()) {
            if (Packages.shouldBeExported(s)) {
                exports.add(s);
            } else {
                if (!s.isEmpty() && !s.equals(".")) {
                    privates.add(s + ";-split-package:=merge-first");
                }
            }
        }

        defaultInstructions.put(Constants.PRIVATE_PACKAGE, Packages.toClause(privates));
        defaultInstructions.put(Constants.EXPORT_PACKAGE, Packages.toClause(exports));

        return Instructions.mergeAndSkipExisting(instructions, defaultInstructions);
    }

    /**
     * Gets local resources.
     *
     * @param basedir the project's base directory
     * @param test    whether or not we compute the test resources
     * @param scanner the project scanner to retrieve information about the sources and resources contained in the
     *                project
     * @return the resource clause
     */
    public static String getLocalResources(File basedir, boolean test, ProjectScanner scanner) {
        final String basePath = basedir.getAbsolutePath();
        String target = "target/classes";
        if (test) {
            target = "target/test-classes";
        }
        Set<String> files = scanner.getLocalResources(test);
        Set<String> pathSet = new LinkedHashSet<>();

        for (String name : files) {
            String path = target + '/' + name;

            // make relative to project
            if (path.startsWith(basePath)) {
                if (path.length() == basePath.length()) {
                    path = ".";
                } else {
                    path = path.substring(basePath.length() + 1);
                }
            }

            // replace windows backslash with a slash
            // this is a workaround for a problem with bnd 0.0.189
            if (File.separatorChar != '/') {
                name = name.replace(File.separatorChar, '/');
                path = path.replace(File.separatorChar, '/');
            }

            // copy to correct place
            path = name + '=' + path;
            pathSet.add(path);
        }

        return Joiner.on(", ").join(pathSet);
    }

    private static Builder getOSGiBuilder(File basedir, Properties properties, Jar[] classpath) {
        Builder builder = new Builder();
        synchronized (BundlePackager.class) {
            builder.setBase(basedir);
        }
        // Add the range fixer plugin
        final ImportedPackageRangeFixer plugin = new ImportedPackageRangeFixer();
        plugin.setReporter(builder);
        plugin.setProperties(Collections.<String, String>emptyMap());
        builder.addBasicPlugin(plugin);

        builder.setProperties(Instructions.sanitize(properties));
        if (classpath != null) {
            builder.setClasspath(classpath);
        }
        return builder;
    }

    private static Properties readInstructionsFromBndFile(File basedir) throws IOException {
        File instructionFile = new File(basedir, INSTRUCTIONS_FILE);
        if (!instructionFile.isFile()) {
            return null;
        }
        return Instructions.load(instructionFile);
    }

    private static boolean reportErrors(List<String> warnings, List<String> errors, Reporter reporter) {
        for (String msg : warnings) {
            reporter.warn(msg);
        }

        boolean hasErrors = false;
        String fileNotFound = "Input file does not exist: ";
        for (String msg : errors) {
            if (msg.startsWith(fileNotFound) && msg.endsWith("~")) {
                // treat as warning; this error happens when you have duplicate entries in Include-Resource
                String duplicate = Processor.removeDuplicateMarker(msg.substring(fileNotFound.length()));
                reporter.warn("Duplicate path '" + duplicate + "' in Include-Resource");
            } else {
                reporter.error(msg);
                hasErrors = true;
            }
        }
        return hasErrors;
    }

}