org.fluidity.foundation.impl.BundleJarManifest.java Source code

Java tutorial

Introduction

Here is the source code for org.fluidity.foundation.impl.BundleJarManifest.java

Source

/*
 * Copyright (c) 2006-2016 Tibor Adam Varga (tibor.adam.varga on gmail)
 *
 * 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.
 */

package org.fluidity.foundation.impl;

import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.jar.Attributes;

import org.fluidity.composition.ComponentContainer;
import org.fluidity.composition.ContainerBoundary;
import org.fluidity.deployment.maven.Logger;
import org.fluidity.deployment.osgi.impl.BundleBootstrap;
import org.fluidity.deployment.plugin.spi.JarManifest;
import org.fluidity.deployment.plugin.spi.SecurityPolicy;
import org.fluidity.foundation.Archives;
import org.fluidity.foundation.ClassLoaders;
import org.fluidity.foundation.Exceptions;
import org.fluidity.foundation.Methods;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
import org.codehaus.plexus.interpolation.PrefixedPropertiesValueSource;
import org.codehaus.plexus.interpolation.PropertiesBasedValueSource;
import org.codehaus.plexus.interpolation.RecursionInterceptor;
import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
import org.osgi.framework.Version;

import static org.osgi.framework.Constants.BUNDLE_ACTIVATOR;
import static org.osgi.framework.Constants.BUNDLE_CLASSPATH;
import static org.osgi.framework.Constants.BUNDLE_COPYRIGHT;
import static org.osgi.framework.Constants.BUNDLE_DESCRIPTION;
import static org.osgi.framework.Constants.BUNDLE_DOCURL;
import static org.osgi.framework.Constants.BUNDLE_MANIFESTVERSION;
import static org.osgi.framework.Constants.BUNDLE_NAME;
import static org.osgi.framework.Constants.BUNDLE_NATIVECODE;
import static org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME;
import static org.osgi.framework.Constants.BUNDLE_VENDOR;
import static org.osgi.framework.Constants.BUNDLE_VERSION;
import static org.osgi.framework.Constants.DYNAMICIMPORT_PACKAGE;
import static org.osgi.framework.Constants.EXPORT_PACKAGE;
import static org.osgi.framework.Constants.FRAGMENT_HOST;
import static org.osgi.framework.Constants.IMPORT_PACKAGE;
import static org.osgi.framework.Constants.REQUIRE_BUNDLE;
import static org.osgi.framework.Constants.REQUIRE_CAPABILITY;

/**
 * Modifies the JAR manifest of the host project's artifact to make the artifact a self-containing OSGi bundle. That is, the project's dependencies will be
 * embedded in the produced artifact and the proper OSGi manifest headers will be generated for the OSGi framework to treat those dependencies as part of the
 * bundle.
 *
 * @author Tibor Varga
 */
@SuppressWarnings("WeakerAccess")
final class BundleJarManifest implements JarManifest {

    public static final String DEFAULT_BUNDLE_VERSION = Version.emptyVersion.toString();
    public static final String VERSION_PREFIX = "version:";

    public SecurityPolicy processManifest(final MavenProject project, final Attributes attributes,
            final SecurityPolicy policy, final Logger log, final Dependencies dependencies)
            throws MojoExecutionException {
        dependencies.attribute(BUNDLE_CLASSPATH, ",");

        addEntry(attributes, BUNDLE_MANIFESTVERSION, "2");
        addEntry(attributes, BUNDLE_NAME, project::getName);
        addEntry(attributes, BUNDLE_SYMBOLICNAME, project::getArtifactId);
        addEntry(attributes, BUNDLE_VERSION, project::getVersion);
        addEntry(attributes, BUNDLE_DESCRIPTION, project::getDescription);
        addEntry(attributes, BUNDLE_DOCURL, project::getUrl);
        addEntry(attributes, BUNDLE_VENDOR, () -> {
            return project.getOrganization().getName();
        });
        addEntry(attributes, BUNDLE_COPYRIGHT, () -> {
            final String year = project.getInceptionYear();
            return year == null ? null
                    : String.format("Copyright %s (c) %s. All rights reserved.",
                            project.getOrganization().getName(), year);
        });
        addEntry(attributes, REQUIRE_CAPABILITY, () -> {

            // https://www.oracle.com/technetwork/java/javase/versioning-naming-139433.html
            final String[] version = System.getProperty("java.version").split("\\.");
            return String.format("osgi.ee;filter:=(&(osgi.ee=JavaSE)(version>=%s.%s))", version[0], version[1]);
        });

        final String version = attributes.getValue(BUNDLE_VERSION);

        if (version != null) {
            attributes.putValue(BUNDLE_VERSION, verify(version));
        } else {
            addEntry(attributes, BUNDLE_VERSION, DEFAULT_BUNDLE_VERSION);
        }

        final RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
        final List<String> prefixes = Collections.singletonList("project.");

        interpolator.addValueSource(new PropertiesBasedValueSource(System.getProperties()));
        interpolator.addValueSource(new PrefixedPropertiesValueSource(prefixes, project.getProperties(), true));
        interpolator.addValueSource(new PrefixedObjectValueSource(prefixes, project.getModel(), true));

        final RecursionInterceptor interceptor = new PrefixAwareRecursionInterceptor(prefixes, true);

        interpolator.setCacheAnswers(true);
        interpolator.setReusePatterns(true);

        interpolator.addPostProcessor((ignored, value) -> verify((String) value));

        try {
            interpolateHeaders(attributes, interpolator, interceptor, EXPORT_PACKAGE, IMPORT_PACKAGE,
                    DYNAMICIMPORT_PACKAGE, FRAGMENT_HOST, REQUIRE_BUNDLE, BUNDLE_NATIVECODE);
        } catch (final InterpolationException e) {
            throw new IllegalStateException(e);
        }

        final Collection<Artifact> artifacts = dependencies.compiler();

        // if embedding project dependencies, see if Fluid Tools composition is included and if so, add a bundle activator to bootstrap the system.
        if (!artifacts.isEmpty()) {
            try {

                // create a class loader that sees the project's compile-time dependencies
                final Set<URL> urls = new HashSet<>();

                final String skippedId = project.getArtifact().getId();
                for (final Artifact dependency : artifacts) {

                    // we don't need the project artifact and don't want to prevent the caller from overwriting it by locking the file...
                    if (!skippedId.equals(dependency.getId())) {
                        urls.add(dependency.getFile().toURI().toURL());
                    }
                }

                final Method method = Methods.get(BootstrapDiscovery.class, BootstrapDiscovery::activator)[0];

                urls.add((Archives.containing(BootstrapDiscovery.class)));
                urls.add((Archives.containing(BootstrapDiscoveryImpl.class)));
                urls.add((Archives.containing(ComponentContainer.class)));

                final String activator = isolate(null, urls, BootstrapDiscoveryImpl.class, method);

                if (activator != null) {
                    addEntry(attributes, BUNDLE_ACTIVATOR, activator);
                }

                if (log.active()) {
                    final String value = attributes.getValue(BUNDLE_ACTIVATOR);
                    log.detail("Bundle activator: %s", value == null ? "none"
                            : value.equals(BundleBootstrap.class.getName()) ? "built in" : value);
                }
            } catch (final ClassNotFoundException e) {
                // that's OK
            } catch (final Exception e) {
                throw new IllegalStateException(e);
            }
        }

        return new OsgiLocalPermissions(policy, artifacts, attributes.getValue(DYNAMICIMPORT_PACKAGE),
                attributes.getValue(IMPORT_PACKAGE), attributes.getValue(EXPORT_PACKAGE));
    }

    private String verify(final String version) {
        try {
            Version.parseVersion(version);
            return version;
        } catch (final IllegalArgumentException ignored) {
            return correct(version);
        }
    }

    private void interpolateHeaders(final Attributes attributes, final Interpolator interpolator,
            final RecursionInterceptor interceptor, final String... headers) throws InterpolationException {
        assert interpolator != null;

        for (final String header : headers) {
            final String value = attributes.getValue(header);

            if (value != null) {
                attributes.putValue(header, interpolator.interpolate(value, VERSION_PREFIX, interceptor));
            }
        }
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private String correct(final String version) {
        final StringBuilder bundleVersion = new StringBuilder();

        int partCount = 0;
        for (final String part : version.split("[\\.-]")) {
            if (partCount < 3) {
                try {
                    Integer.parseInt(part); // just checking if part is numeric

                    // part is indeed numeric
                    bundleVersion.append('.').append(part);
                } catch (final NumberFormatException e) {

                    // part is not numeric
                    partCount = numericVersion(bundleVersion, partCount);
                    bundleVersion.append('.').append(part.replaceAll("\\W", "_"));
                }
            } else {
                bundleVersion.append(partCount == 3 ? '.' : '-').append(part.replaceAll("\\W", "_"));
            }

            ++partCount;
        }

        numericVersion(bundleVersion, partCount);

        return bundleVersion.toString().substring(1);
    }

    /**
     * Creates an isolated URL class loader for the given list of URLs, loads using that class loader and instantiates the given <code>type</code>, and calls
     * the given <code>method</code> on it with the given <code>arguments</code> to return its return value.
     *
     * @param parent    the class loader to load the method parameters and the return values; this class loader must not see the given <code>type</code>.
     * @param urls      the list of URLs to use for the isolated class loader; make sure the list contains the JARs containing the type to load.
     * @param type      the command class to load and invoke the given method on; must have a public zero-argument constructor.
     * @param run       the public method to call; the parameter types and the return type must be loaded by the <code>parent</code> class loader and the declaring
     *                  class must be either visible to the <code>parent</code> class loader or listed in the given list of URLs.
     * @param arguments the arguments to pass to the command.
     * @param <T>       the return type of the given <code>method</code>.
     *
     * @return whatever the command returns.
     *
     * @throws ClassNotFoundException when the given class could not be found in the given URLs.
     * @throws InstantiationException when the given class could not be instantiated.
     * @throws org.fluidity.foundation.Exceptions.Wrapper when anything else goes wrong.
     */
    @SuppressWarnings("unchecked")
    private static <T> T isolate(final ClassLoader parent, final Collection<URL> urls, final Class<?> type,
            final Method run, final Object... arguments)
            throws Exceptions.Wrapper, ClassNotFoundException, InstantiationException {
        try {
            return Archives.Cache.access(() -> Exceptions.wrap(() -> {
                final ClassLoader isolated = ClassLoaders.create(urls, parent, null);

                try {

                    // find the command
                    final Object command = isolated.loadClass(type.getName()).newInstance();

                    // find the method to call in the other class loader
                    final Method method = isolated.loadClass(run.getDeclaringClass().getName())
                            .getDeclaredMethod(run.getName(), (Class[]) run.getParameterTypes());

                    method.setAccessible(true);
                    return (T) method.invoke(command, arguments);
                } catch (final NoSuchMethodException | IllegalAccessException e) {
                    throw new AssertionError(e);
                }
            }));
        } catch (final Exceptions.Wrapper wrapper) {
            throw wrapper.rethrow(ClassNotFoundException.class).rethrow(InstantiationException.class);
        }
    }

    /**
     * Returns the name of the bundle activator class if Fluid Tools is used in the host Maven project.
     */
    interface BootstrapDiscovery {

        /**
         * Returns the name of the bundle activator class, if any.
         *
         * @return the name of the bundle activator, or <code>null</code> if no Fluid Tools is used in the project.
         */
        String activator();
    }

    /**
     * This class is loaded from a bespoke class loader defined on the compile-time dependencies of the host Maven project. It checks if the {@link
     * ContainerBoundary} and {@link BundleBootstrap} classes can be found in the project's compile-time class path.
     */
    @SuppressWarnings("WeakerAccess") // can't be loaded by BundleJarManifest unless made public
    public static final class BootstrapDiscoveryImpl implements BootstrapDiscovery {

        @SuppressWarnings("ConstantConditions") // the condition in question is used to attempt to load a class
        public String activator() {
            try {

                // try to load the classes from the artifact's compile-time dependencies
                return ContainerBoundary.class != null ? BundleBootstrap.class.getName() : null;
            } catch (final NoClassDefFoundError e) {

                // class could not be found
                return null;
            }
        }
    }

    private void addEntry(final Attributes attributes, final String entry, final Supplier<String> metadata) {
        if (attributes.getValue(entry) == null) {
            String value;

            try {
                value = metadata.get();
            } catch (final NullPointerException | IndexOutOfBoundsException e) {
                value = null;
            }

            if (value != null) {
                attributes.putValue(entry, value);
            }
        }
    }

    private boolean addEntry(final Attributes attributes, final String entry, final String value) {
        if (attributes.getValue(entry) == null && value != null) {
            attributes.putValue(entry, value);
            return true;
        } else {
            return false;
        }
    }

    private int numericVersion(final StringBuilder bundleVersion, int partCount) {
        for (; partCount < 3; ++partCount) {
            bundleVersion.append(".0");
        }

        return partCount;
    }
}