Package io.fabric8.fab.osgi.internal

Source Code of io.fabric8.fab.osgi.internal.BndUtils

/**
*  Copyright 2005-2014 Red Hat, Inc.
*
*  Red Hat 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 io.fabric8.fab.osgi.internal;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import aQute.bnd.header.Attrs;
import aQute.bnd.header.Parameters;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Descriptors;
import aQute.bnd.osgi.FileResource;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Processor;
import aQute.bnd.osgi.Resource;
import aQute.bnd.osgi.URLResource;
import aQute.lib.spring.SpringComponent;
import aQute.lib.spring.SpringXMLType;
import org.apache.felix.bundleplugin.BlueprintPlugin;
import io.fabric8.fab.osgi.ServiceConstants;
import io.fabric8.fab.osgi.bnd.ActiveMQNamespaceHandlerPlugin;
import io.fabric8.fab.osgi.bnd.CXFNamespaceHandlerPlugin;
import io.fabric8.fab.osgi.bnd.ClassPathImportsHandlerPlugin;
import io.fabric8.utils.Files;
import io.fabric8.utils.Strings;
import org.ops4j.lang.NullArgumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.fabric8.utils.Strings.emptyIfNull;
import static io.fabric8.utils.Strings.notEmpty;

/**
* Wrapper over PeterK's bnd lib.
*
* @since 0.1.0, January 14, 2008
*/
public class BndUtils
{

    private static final Logger LOG = LoggerFactory.getLogger(BndUtils.class);

    /**
     * Regex pattern for matching instructions when specified in url.
     */
    private static final Pattern INSTRUCTIONS_PATTERN =
        Pattern.compile( "([a-zA-Z_0-9-]+)=([-!\"'()*+,.0-9A-Z_a-z%;:=/]+)" );

    private static final String ALLOWED_PACKAGE_CLAUSES = Strings.join(Arrays.asList(Constants.directives), ",") + ",version";

    /**
     * The list of analyzer plugin names we are using for analyzing Blueprint and Spring XML files
     */
    private static final String ANALYZER_PLUGIN_NAMES =
            BlueprintPlugin.class.getName() + "," + SpringXMLType.class.getName() + "," +
            ActiveMQNamespaceHandlerPlugin.class.getName() + "," + ClassPathImportsHandlerPlugin.class.getName() + "," +
            CXFNamespaceHandlerPlugin.class.getName();

    /**
     * Utility class. Meant to be used using static methods
     */
    private BndUtils()
    {
        // utility class
    }

    /**
     * Processes the input jar and generates the necessary OSGi headers using specified instructions.
     *
     * @param jarInputStream input stream for the jar to be processed. Cannot be null.
     * @param instructions   bnd specific processing instructions. Cannot be null.
     * @param jarInfo        information about the jar to be processed. Usually the jar url. Cannot be null or empty.
     *
     * @return an input stream for the generated bundle
     *
     * @throws NullArgumentException if any of the parameters is null
     * @throws IOException           re-thron during jar processing
     */
    public static InputStream createBundle( final InputStream jarInputStream,
                                            final Properties instructions,
                                            final String jarInfo )
        throws Exception
    {
        return createBundle( jarInputStream, instructions, jarInfo, OverwriteMode.KEEP, Collections.EMPTY_MAP,  Collections.EMPTY_MAP, new HashSet<String>(), null );
    }

    /**
     * Processes the input jar and generates the necessary OSGi headers using specified instructions.
     *
     *
     * @param jarInputStream input stream for the jar to be processed. Cannot be null.
     * @param instructions   bnd specific processing instructions. Cannot be null.
     * @param jarInfo        information about the jar to be processed. Usually the jar url. Cannot be null or empty.
     * @param overwriteMode  manifets overwrite mode
     *
     * @param actualImports
     * @return an input stream for the generated bundle
     *
     * @throws NullArgumentException if any of the parameters is null
     * @throws IOException           re-thron during jar processing
     */
    public static InputStream createBundle(final InputStream jarInputStream,
                                           final Properties instructions,
                                           final String jarInfo,
                                           final OverwriteMode overwriteMode,
                                           final Map<String, Object> embeddedResources,
                                           final Map<String, Map<String, String>> extraImportPackages,
                                           final Set<String> actualImports,
                                           final VersionResolver versionResolver)
            throws Exception
    {
        Jar jar = createJar(jarInputStream,  instructions, jarInfo,
                            overwriteMode, embeddedResources, extraImportPackages,
                            actualImports, versionResolver);
        return createInputStream( jar );
    }

    /**
    * Processes the input jar and generates the necessary OSGi headers using specified instructions.
    *
    *
    * @param jarInputStream input stream for the jar to be processed. Cannot be null.
    * @param instructions   bnd specific processing instructions. Cannot be null.
    * @param jarInfo        information about the jar to be processed. Usually the jar url. Cannot be null or empty.
    * @param overwriteMode  manifets overwrite mode
    *
    * @param actualImports
    * @return the bnd jar
    *
    * @throws NullArgumentException if any of the parameters is null
    * @throws IOException           re-thron during jar processing
    */
    public static Jar createJar(final InputStream jarInputStream,
                                final Properties instructions,
                                final String jarInfo,
                                final OverwriteMode overwriteMode,
                                final Map<String, Object> embeddedResources,
                                final Map<String, Map<String, String>> extraImportPackages,
                                final Set<String> actualImports,
                                final VersionResolver versionResolver)
        throws Exception
    {
        NullArgumentException.validateNotNull( jarInputStream, "Jar URL" );
        NullArgumentException.validateNotNull( instructions, "Instructions" );
        NullArgumentException.validateNotEmpty( jarInfo, "Jar info" );

        LOG.debug( "Creating bundle for [" + jarInfo + "]" );
        LOG.debug( "Overwrite mode: " + overwriteMode );
        LOG.trace( "Using instructions " + instructions );

        final Jar jar = new Jar( "dot", jarInputStream );

        final Manifest manifest = jar.getManifest();

        // Make the jar a bundle if it is not already a bundle
        if( manifest == null
            || OverwriteMode.KEEP != overwriteMode
            || ( manifest.getMainAttributes().getValue( Analyzer.EXPORT_PACKAGE ) == null
                 && manifest.getMainAttributes().getValue( Analyzer.IMPORT_PACKAGE ) == null )
            )
        {
            // Do not use instructions as default for properties because it looks like BND uses the props
            // via some other means then getProperty() and so the instructions will not be used at all
            // So, just copy instructions to properties
            final Properties properties = new Properties();
            properties.putAll(instructions);

            properties.put("Generated-By-FAB-From", jarInfo);
            properties.put(Analyzer.PLUGIN, ANALYZER_PLUGIN_NAMES);

            final Analyzer analyzer = new Analyzer();
            analyzer.setJar(jar);
            analyzer.setProperties(properties);

            // now lets add all the new embedded jars
            for (Map.Entry<String, Object> entry : embeddedResources.entrySet()) {
                String path = entry.getKey();
                Object value = entry.getValue();
                Resource resource = toResource(value);
                if (resource != null) {
                    jar.putResource(path, resource);
                    try {
                        File file = toFile(value);
                        analyzer.addClasspath(file);
                    } catch (IOException e) {
                        LOG.warn("Failed to get File for " + value + ". " + e, e);
                    }
                }
            }


            if (manifest != null && OverwriteMode.MERGE == overwriteMode) {
                analyzer.mergeManifest(manifest);
            }
            checkMandatoryProperties(analyzer, jar, jarInfo);

            Manifest newManifest = analyzer.calcManifest();
            jar.setManifest( newManifest );

            Attributes main = jar.getManifest().getMainAttributes();

            // Hack to add back META-INF imports
            String importPackage = main.getValue(Analyzer.IMPORT_PACKAGE);
            for (Descriptors.PackageRef key : analyzer.getReferred().keySet()) {
                if (key.getFQN().startsWith("META-INF.")) {
                    if (importPackage == null) {
                        importPackage = key.getFQN();
                    } else {
                        importPackage += "," + key.getFQN();
                    }
                }
            }
            if (importPackage != null) {
                main.putValue(Analyzer.IMPORT_PACKAGE, importPackage);
            }

            String importPackages = emptyIfNull(main.getValue(Analyzer.IMPORT_PACKAGE));
            Parameters values = new Analyzer().parseHeader(importPackages);

            // add any missing version clauses
            if (versionResolver != null) {
                for (Map.Entry<String, Attrs> entry : values.entrySet()) {
                    String packageName = entry.getKey();
                    Map<String, String> packageValues = entry.getValue();
                    if (!packageValues.containsKey("version")) {
                        String version = versionResolver.resolvePackageVersion(packageName);
                        if (version != null) {
                            packageValues.put("version", version);
                        }
                    }
                }
            }

            // Merge in the extra imports - lets not add versions to extra imports as they come from exports which might not have versions.
            for (Map.Entry<String, Map<String, String>> entry : extraImportPackages.entrySet()) {
                Map<String, String> original = values.get(entry.getKey());
                if (original == null) {
                    original = entry.getValue();
                } else {
                    original.putAll(entry.getValue());
                }
                Attrs newAttrs = new Attrs();
                newAttrs.putAll(original);
                values.put(entry.getKey(), newAttrs);
            }

            // lets remove any excluded import packages
            String excludedPackagesText = main.getValue(ServiceConstants.INSTR_FAB_EXCLUDE_IMPORTS_PACKAGE);
            if (notEmpty(excludedPackagesText)) {
                StringTokenizer e = new StringTokenizer(excludedPackagesText);
                while (e.hasMoreTokens()) {
                    String expression = e.nextToken();
                    String ignore = expression;
                    if (ignore.endsWith("*")) {
                        do {
                            ignore = ignore.substring(0, ignore.length() - 1);
                        } while (ignore.endsWith("*"));

                        if (ignore.length() == 0) {
                            LOG.debug("Ignoring all imports due to %s value of %s", ServiceConstants.INSTR_FAB_EXCLUDE_IMPORTS_PACKAGE, expression);
                            values.clear();
                        } else {
                            List<String> packageNames = new ArrayList<String>(values.keySet());
                            for (String packageName : packageNames) {
                                if (packageName.equals(ignore) || packageName.startsWith(ignore)) {
                                    if (LOG.isDebugEnabled()) {
                                        LOG.debug("Ignoring package " + packageName + " due to " + ServiceConstants.INSTR_FAB_EXCLUDE_IMPORTS_PACKAGE + " value of " + expression);
                                    }
                                    values.remove(packageName);
                                }
                            }
                        }
                    } else {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Ignoring package " + ignore + " due to " + ServiceConstants.INSTR_FAB_EXCLUDE_IMPORTS_PACKAGE + " header");
                        }
                        values.remove(ignore);
                    }
                }
            }

            // lets remove optional dependency if they are exported from a non-optional dependency...
            for (Map.Entry<String, Attrs> entry : values.entrySet()) {
                String packageName = entry.getKey();
                Map<String, String> map = entry.getValue();
                String res = map.get("resolution:");
                if ("optional".equals(res)) {
                    if (!versionResolver.isPackageOptional(packageName)) {
                        map.remove("resolution:");
                        res = null;
                    }
                }
                if (!"optional".equals(res)) {
                    // add all the non-optional deps..
                    actualImports.add(packageName);
                }
            }

            // TODO do we really need to filter out any of the attribute values?
            // we were filtering out everything bar resolution:
            //importPackages  = Processor.printClauses(values, "resolution:");
            importPackages = Processor.printClauses(values /*, ALLOWED_PACKAGE_CLAUSES */);
            if (notEmpty(importPackages)) {
                main.putValue(Analyzer.IMPORT_PACKAGE, importPackages);
            }


            String exportPackages = emptyIfNull(main.getValue(Analyzer.EXPORT_PACKAGE));
            Parameters exports = new Analyzer().parseHeader(exportPackages);
            for (Map.Entry<String, Attrs> entry : exports.entrySet()) {
                String packageName = entry.getKey();
                Map<String, String> map = entry.getValue();
                String version = map.get("version");
                if (version == null) {
                    version = versionResolver.resolveExportPackageVersion(packageName);
                    if (version != null) {
                        map.put("version", version);
                    }
                }
            }
            exportPackages = Processor.printClauses(exports /*, ALLOWED_PACKAGE_CLAUSES */);
            if (notEmpty(exportPackages)) {
                main.putValue(Analyzer.EXPORT_PACKAGE, exportPackages);
            }

        }
        return jar;
    }

    private static File toFile(Object value) throws IOException {
        if (value instanceof File) {
            return (File) value;
        } else if (value instanceof URL) {
            return Files.urlToFile((URL) value, "fabric-analyser-jar-", ".jar");
        } else {
            throw new IllegalArgumentException("Cannot convert value " + value + " into a Resource. Expected File or URL");
        }
    }

    protected static Resource toResource(Object value) {
        if (value instanceof File) {
            return new FileResource((File) value);
        } else if (value instanceof URL) {
            return new URLResource((URL) value);
        } else {
            throw new IllegalArgumentException("Cannot convert value " + value + " into a Resource. Expected File or URL");
        }
    }

    /**
     * Creates an piped input stream for the wrapped jar.
     * This is done in a thread so we can return quickly.
     *
     * @param jar the wrapped jar
     *
     * @return an input stream for the wrapped jar
     *
     * @throws java.io.IOException re-thrown
     */
    public static PipedInputStream createInputStream( final Jar jar )
        throws IOException
    {
        final PipedInputStream pin = new PipedInputStream();
        final PipedOutputStream pout = new PipedOutputStream( pin );

        new Thread()
        {
            public void run()
            {
                try
                {
                    jar.write( pout );
                }
                catch( Exception e )
                {
                    LOG.warn( "Bundle cannot be generated" );
                }
                finally
                {
                    try
                    {
                        jar.close();
                        pout.close();
                    }
                    catch( IOException ignore )
                    {
                        // if we get here something is very wrong
                        LOG.error( "Bundle cannot be generated", ignore );
                    }
                }
            }
        }.start();

        return pin;
    }

    /**
     * Check if manadatory properties are present, otherwise generate default.
     *
     * @param analyzer     bnd analyzer
     * @param jar          bnd jar
     * @param symbolicName bundle symbolic name
     */
    private static void checkMandatoryProperties( final Analyzer analyzer,
                                                  final Jar jar,
                                                  final String symbolicName )
    {
        final String importPackage = analyzer.getProperty( Analyzer.IMPORT_PACKAGE );
        if( importPackage == null || importPackage.trim().length() == 0 )
        {
            analyzer.setProperty( Analyzer.IMPORT_PACKAGE, "*;resolution:=optional" );
        }
        final String exportPackage = analyzer.getProperty( Analyzer.EXPORT_PACKAGE );
        if( exportPackage == null || exportPackage.trim().length() == 0 )
        {
            analyzer.setProperty( Analyzer.EXPORT_PACKAGE, analyzer.calculateExportsFromContents( jar ) );
        }
        final String localSymbolicName = analyzer.getProperty( Analyzer.BUNDLE_SYMBOLICNAME, symbolicName );
        analyzer.setProperty( Analyzer.BUNDLE_SYMBOLICNAME, generateSymbolicName( localSymbolicName ) );
    }

    /**
     * Processes symbolic name and replaces osgi spec invalid characters with "_".
     *
     * @param symbolicName bundle symbolic name
     *
     * @return a valid symbolic name
     */
    private static String generateSymbolicName( final String symbolicName )
    {
        return symbolicName.replaceAll( "[^a-zA-Z_0-9.-]", "_" );
    }

    /**
     * Parses bnd instructions out of an url query string.
     *
     * @param query query part of an url.
     *
     * @return parsed instructions as properties
     *
     * @throws java.net.MalformedURLException if provided path does not comply to syntax.
     */
    public static Properties parseInstructions( final String query )
        throws MalformedURLException
    {
        final Properties instructions = new Properties();
        if( query != null )
        {
            try
            {
                // just ignore for the moment and try out if we have valid properties separated by "&"
                final String segments[] = query.split( "&" );
                for( String segment : segments )
                {
                    // do not parse empty strings
                    if( segment.trim().length() > 0 )
                    {
                        final Matcher matcher = INSTRUCTIONS_PATTERN.matcher( segment );
                        if( matcher.matches() )
                        {
                            instructions.setProperty(
                                matcher.group( 1 ),
                                URLDecoder.decode( matcher.group( 2 ), "UTF-8" )
                            );
                        }
                        else
                        {
                            throw new MalformedURLException( "Invalid syntax for instruction [" + segment
                                                             + "]. Take a look at http://www.aqute.biz/Code/Bnd."
                            );
                        }
                    }
                }
            }
            catch( UnsupportedEncodingException e )
            {
                // thrown by URLDecoder but it should never happen
                throwAsMalformedURLException( "Could not retrieve the instructions from [" + query + "]", e );
            }
        }
        return instructions;
    }

    /**
     * Creates an MalformedURLException with a message and a cause.
     *
     * @param message exception message
     * @param cause   exception cause
     *
     * @throws MalformedURLException the created MalformedURLException
     */
    private static void throwAsMalformedURLException( final String message, final Exception cause )
        throws MalformedURLException
    {
        final MalformedURLException exception = new MalformedURLException( message );
        exception.initCause( cause );
        throw exception;
    }

}
TOP

Related Classes of io.fabric8.fab.osgi.internal.BndUtils

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.