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

Java tutorial

Introduction

Here is the source code for org.wisdom.maven.utils.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.utils;

import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Processor;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.io.IOUtils;
import org.apache.felix.ipojo.manipulator.Pojoization;
import org.apache.felix.ipojo.manipulator.util.Classpath;

import java.io.*;
import java.lang.reflect.Array;
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) throws IOException {
        Properties properties = new Properties();
        // Loads the properties inherited from Maven.
        readMavenProperties(basedir, properties);
        // Loads the properties from the BND file.
        boolean provided = readInstructionsFromBndFiles(properties, basedir);
        if (!provided) {
            // No bnd files, set default valued
            populatePropertiesWithDefaults(basedir, properties);
        }

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

        // Instruction loaded, start the build sequence.
        final Jar[] jars = computeClassPath(basedir);
        final Set<String> elements = computeClassPathElement(basedir);

        File bnd = null;
        File ipojo = null;
        try {
            Builder builder = getOSGiBuilder(basedir, properties, jars);
            builder.build();

            reportErrors("BND ~> ", builder.getWarnings(), builder.getErrors());
            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);
        }

        Classpath classpath = new Classpath(elements);
        Pojoization pojoization = new Pojoization();
        pojoization.pojoization(bnd, ipojo, new File(basedir, "src/main/resources"), classpath.createClassLoader());
        reportErrors("iPOJO ~> ", pojoization.getWarnings(), pojoization.getErrors());

        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
     */
    private static void mergeExtraHeaders(File baseDir, Properties properties) throws IOException {
        File extra = new File(baseDir, EXTRA_HEADERS_FILE);
        merge(properties, extra);
    }

    private static void merge(Properties properties, File extra) throws IOException {
        if (extra.isFile()) {
            FileInputStream fis = null;
            try {
                Properties headers = new Properties();
                fis = new FileInputStream(extra);
                headers.load(fis);
                properties.putAll(headers);
            } finally {
                IOUtils.closeQuietly(fis);
            }
        }
    }

    /**
     * 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.
        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 target/osgi/osgi.properties file will all the metadata we inherit from Maven.
     *
     * @param baseDir    the project directory
     * @param properties the current set of properties in which the read metadata are written
     */
    private static void readMavenProperties(File baseDir, Properties properties) throws IOException {
        File osgi = new File(baseDir, org.wisdom.maven.Constants.OSGI_PROPERTIES);
        merge(properties, osgi);
    }

    private static void populatePropertiesWithDefaults(File basedir, Properties properties) throws IOException {
        List<String> privates = new ArrayList<>();
        List<String> exports = new ArrayList<>();

        File classes = new File(basedir, "target/classes");

        Set<String> packages = new LinkedHashSet<>();
        if (classes.isDirectory()) {
            Jar jar = new Jar("", classes);
            packages.addAll(jar.getPackages());
            jar.close();
        }

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

        properties.put(Constants.PRIVATE_PACKAGE, toClause(privates));
        if (!exports.isEmpty()) {
            properties.put(Constants.EXPORT_PACKAGE, toClause(exports));
        }
    }

    /**
     * Checks whether the given package must be exported. The decision is made from heuristics.
     *
     * @param packageName the package name
     * @return {@literal true} if the package has to be exported, {@literal false} otherwise.
     */
    public static boolean shouldBeExported(String packageName) {
        boolean service = packageName.endsWith(".service");
        service = service || packageName.contains(".service.") || packageName.endsWith(".services")
                || packageName.contains(".services.");

        boolean api = packageName.endsWith(".api");
        api = api || packageName.contains(".api.") || packageName.endsWith(".apis")
                || packageName.contains(".apis.");

        boolean model = packageName.endsWith(".model");
        model = model || packageName.contains(".model.") || packageName.endsWith(".models")
                || packageName.contains(".models.");

        boolean entity = packageName.endsWith(".entity");
        entity = entity || packageName.contains(".entity.") || packageName.endsWith(".entities")
                || packageName.contains(".entities.");

        return !packageName.isEmpty() && (service || api || model || entity);
    }

    private static String toClause(List<String> packages) {
        StringBuilder builder = new StringBuilder();
        for (String p : packages) {
            if (builder.length() != 0) {
                builder.append(", ");
            }
            builder.append(p);
        }
        return builder.toString();
    }

    private static Jar[] computeClassPath(File basedir) throws IOException {
        List<Jar> list = new ArrayList<>();
        File classes = new File(basedir, "target/classes");

        if (classes.isDirectory()) {
            list.add(new Jar("", classes));
        }

        ObjectMapper mapper = new ObjectMapper();
        ArrayNode array = mapper.readValue(new File(basedir, DEPENDENCIES_FILE), ArrayNode.class);
        Iterator<JsonNode> items = array.elements();
        while (items.hasNext()) {
            ObjectNode node = (ObjectNode) items.next();
            String scope = node.get("scope").asText();
            if (!"test".equalsIgnoreCase(scope)) {
                File file = new File(node.get("file").asText());
                if (file.getName().endsWith(".jar")) {
                    Jar jar = new Jar(node.get("artifactId").asText(), file);
                    list.add(jar);
                }
                // If it's not a jar file - ignore it.
            }
        }
        Jar[] cp = new Jar[list.size()];
        list.toArray(cp);

        return cp;
    }

    private static Set<String> computeClassPathElement(File basedir) throws IOException {
        Set<String> list = new LinkedHashSet<>();
        File classes = new File(basedir, "target/classes");

        if (classes.isDirectory()) {
            list.add(classes.getAbsolutePath());
        }

        ObjectMapper mapper = new ObjectMapper();
        ArrayNode array = mapper.readValue(new File(basedir, DEPENDENCIES_FILE), ArrayNode.class);
        Iterator<JsonNode> items = array.elements();
        while (items.hasNext()) {
            ObjectNode node = (ObjectNode) items.next();
            String scope = node.get("scope").asText();
            if (!"test".equalsIgnoreCase(scope)) {
                File file = new File(node.get("file").asText());
                if (file.getName().endsWith(".jar")) {
                    list.add(file.getAbsolutePath());
                }
                // If it's not a jar file - ignore it.
            }
        }
        return list;
    }

    private static Builder getOSGiBuilder(File basedir, Properties properties, Jar[] classpath) {
        Builder builder = new Builder();
        synchronized (BundlePackager.class) {
            builder.setBase(basedir);
        }
        builder.setProperties(sanitize(properties));
        if (classpath != null) {
            builder.setClasspath(classpath);
        }
        return builder;
    }

    private static boolean readInstructionsFromBndFiles(Properties properties, File basedir) throws IOException {
        Properties props = new Properties();
        File instructionFile = new File(basedir, INSTRUCTIONS_FILE);
        if (instructionFile.isFile()) {
            InputStream is = null;
            try {
                is = new FileInputStream(instructionFile);
                props.load(is);
            } finally {
                IOUtils.closeQuietly(is);
            }
        } else {
            return false;
        }

        // Insert in the given properties to the list of properties.
        @SuppressWarnings("unchecked")
        Enumeration<String> names = (Enumeration<String>) props.propertyNames();
        while (names.hasMoreElements()) {
            String key = names.nextElement();
            properties.put(key, props.getProperty(key));
        }

        return true;
    }

    private static Properties sanitize(Properties properties) {
        // convert any non-String keys/values to Strings
        Properties sanitizedEntries = new Properties();
        for (Iterator<?> itr = properties.entrySet().iterator(); itr.hasNext();) {
            Map.Entry entry = (Map.Entry) itr.next();
            if (!(entry.getKey() instanceof String)) {
                String key = sanitize(entry.getKey());
                if (!properties.containsKey(key)) {
                    sanitizedEntries.setProperty(key, sanitize(entry.getValue()));
                }
                itr.remove();
            } else if (!(entry.getValue() instanceof String)) {
                entry.setValue(sanitize(entry.getValue()));
            }
        }
        properties.putAll(sanitizedEntries);
        return properties;
    }

    private static String sanitize(Object value) {
        if (value instanceof String) {
            return (String) value;
        } else if (value instanceof Iterable) {
            String delim = "";
            StringBuilder buf = new StringBuilder();
            for (Object i : (Iterable<?>) value) {
                buf.append(delim).append(i);
                delim = ", ";
            }
            return buf.toString();
        } else if (value.getClass().isArray()) {
            String delim = "";
            StringBuilder buf = new StringBuilder();
            for (int i = 0, len = Array.getLength(value); i < len; i++) {
                buf.append(delim).append(Array.get(value, i));
                delim = ", ";
            }
            return buf.toString();
        } else {
            return String.valueOf(value);
        }
    }

    private static boolean reportErrors(String prefix, List<String> warnings, List<String> errors) {
        for (String msg : warnings) {
            System.err.println(prefix + " : " + 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()));
                System.err.println(prefix + " Duplicate path '" + duplicate + "' in Include-Resource");
            } else {
                System.err.println(prefix + " : " + msg);
                hasErrors = true;
            }
        }
        return hasErrors;
    }
}