Package org.knopflerfish.ant.taskdefs.bundle

Source Code of org.knopflerfish.ant.taskdefs.bundle.BundleArchives

/*
* Copyright (c) 2010-2011, KNOPFLERFISH project
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
*   this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
*   this list of conditions and the following disclaimer in the documentation
*   and/or other materials provided with the distribution.
*
* - Neither the name of the KNOPFLERFISH project nor the names of its
*   contributors may be used to endorse or promote products derived
*   from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package org.knopflerfish.ant.taskdefs.bundle;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileResource;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;

/**
* A class that analyzes all bundle jar files given by a list of resource
* collections (file sets).
*
*/
public class BundleArchives {

  // This constant is missing from org.osgi.framework.Constants
  public static final String BUNDLE_BLUEPRINT = "Bundle-Blueprint";
  public static final String BUNDLE_LICENSE = "Bundle-License";
  public static final String SERVICE_COMPONENT = "Service-Component";

  /** Path prefix for source files included into the bundle archive. */
  public static final String SRC_PREFIX = "OSGI-OPT/src";

  /**
   * The ant task that is using this helper class. Used for logging etc.
   */
  private final Task task;

  /**
   * Map from bundle name (the bundles file without version and ".jar" suffixes)
   * to a sorted set of a bundle archive object with details about the bundle .
   * The set will contain more than one element when more than one version of
   * the bundle has been found.
   */
  final Map bnToBundleArchives = new TreeMap();

  /**
   * Map from bundle symbolic name to a sorted set of a bundle archive object
   * with details about the bundle. The set will contain more than one element
   * when more than one version of the bundle has been found.
   */
  final Map bsnToBundleArchives = new TreeMap();

  /**
   * Set with all bundle archives specified by the given resource collections.
   */
  final SortedSet allBundleArchives = new TreeSet();

  /**
   * Set with all packages exported from some bundle in this collection of
   * bundle archives. The key in the map is the package name, the value is a map
   * from package version to a set of bundle archives that exports that version
   * of the package.
   *
   * <p>
   * A call to {@link #doProviders()} must be made to compute this mapping.
   * </p>
   */
  final SortedMap allExports = new TreeMap();

  /**
   * Traverse the given list of resource collections and create
   * <code>BundleArchive</code>-objects for all bundle jars found.
   *
   * @param task
   *          The task that uses this class, used for logging and project
   *          access.
   * @param resourceCollections
   *          The collection of resource collections selecting the bundle
   *          archives to load.
   */
  public BundleArchives(final Task task, final List resourceCollections) {
    this(task, resourceCollections, false);
  }

  /**
   * Traverse the given list of resource collections and create
   * <code>BundleArchive</code>-objects for all bundle jars found.
   *
   * @param task
   *          The task that uses this class, used for logging and project
   *          access.
   * @param resourceCollections
   *          The collection of resource collections selecting the bundle
   *          archives to load.
   * @param parseExportImport
   *          If <code>true</code> then the created bundle archive objects will
   *          parse the import / export package / service headers.
   */
  public BundleArchives(final Task task, final List resourceCollections,
                        final boolean parseExportImport) {
    this.task = task;

    if (resourceCollections.size() == 0) {
      task.log("BundleArchives called without any bundle archives to analyse",
               Project.MSG_ERR);
      throw new BuildException("No resource collections specified");
    }

    try {
      for (Iterator it = resourceCollections.iterator(); it.hasNext();) {
        final ResourceCollection rc = (ResourceCollection) it.next();

        // Ignore file sets with a non existing root dir.
        if (rc instanceof FileSet) {
          final FileSet fs = (FileSet) rc;
          final File fsRootDir = fs.getDir(task.getProject());
          if (!fsRootDir.exists()) {
            task.log("Skipping nested file set rooted at '" + fsRootDir
                + "' since that directory does not exist.", Project.MSG_WARN);
            continue;
          }
          try {
            if (fs.size()<1) {
              task.log("Skipping nested file set rooted at '" + fsRootDir
                  + "' since that file set is empty.", Project.MSG_VERBOSE);
              continue;
             
            }
          } catch (Exception e) {
            task.log("Skipping nested file set rooted at '" + fsRootDir
                + "' since size computation throws exception.", e, Project.MSG_VERBOSE);
            continue;
          }
        }

        for (Iterator rcIt = rc.iterator(); rcIt.hasNext();) {
          final Resource res = (Resource) rcIt.next();
          if (res.getName().endsWith(".jar")) {
            task.log("Adding bundle: " + res, Project.MSG_VERBOSE);
            final BundleArchive ba = new BundleArchive(task,
                                                       (FileResource) res, parseExportImport);
            allBundleArchives.add(ba);
            addToMap(bnToBundleArchives, ba.bundleName, ba);
            addToMap(bsnToBundleArchives, ba.bsn, ba);
          }
        }
      }// Scan done

    } catch (BuildException be) {
      throw be;
    } catch (Exception e) {
      final String msg = "Failed to analyze bundle archives: " + e;
      task.log(msg);
      throw new BuildException(msg, e);
    }
  }

  SortedSet getKnownNames() {
    final TreeSet knownNames = new TreeSet(bnToBundleArchives.keySet());
    knownNames.addAll(bsnToBundleArchives.keySet());
    return knownNames;
  }

  /**
   * Computes the global <code>allExports</code> mapping and the
   * <code>pkgProvidersMap</code> for all bundle archives.
   */
  void doProviders() {
    // First build the allExports structure.
    allExports.clear();
    for (Iterator itBa = allBundleArchives.iterator(); itBa.hasNext();) {
      final BundleArchive ba = (BundleArchive) itBa.next();
      // Clear bundle archive maps holding results from this analysis
      ba.pkgProvidersMap.clear();
      ba.pkgUnprovidedMap.clear();
      ba.pkgProvidedMap.clear();

      final Iterator itEe = ba.pkgExportMap.entrySet().iterator();
      while (itEe.hasNext()) {
        final Map.Entry eE = (Map.Entry) itEe.next();
        final String pkgName = (String) eE.getKey();
        final Version pkgVersion = (Version) eE.getValue();

        SortedMap versions = (SortedMap) allExports.get(pkgName);
        if (null == versions) {
          versions = new TreeMap();
          allExports.put(pkgName, versions);
        }

        SortedSet exporters = (SortedSet) versions.get(pkgVersion);
        if (null == exporters) {
          exporters = new TreeSet();
          versions.put(pkgVersion, exporters);
        }
        exporters.add(ba);
      }
    }

    // For each bundle build the pkgProvidersMap
    for (Iterator itBa = allBundleArchives.iterator(); itBa.hasNext();) {
      final BundleArchive ba = (BundleArchive) itBa.next();

      final Iterator itIe = ba.pkgImportMap.entrySet().iterator();
      while (itIe.hasNext()) {
        final Map.Entry iE = (Map.Entry) itIe.next();
        final String pkgName = (String) iE.getKey();
        final VersionRange range = (VersionRange) iE.getValue();

        SortedMap versions = (SortedMap) allExports.get(pkgName);
        if (null != versions) {
          final Iterator itV = versions.entrySet().iterator();
          while (itV.hasNext()) {
            final Map.Entry vE = (Map.Entry) itV.next();
            final Version pkgVersion = (Version) vE.getKey();

            if (range.contains(pkgVersion)) {
              final SortedSet providers = (SortedSet) vE.getValue();

              final Iterator itP = providers.iterator();
              while (itP.hasNext()) {
                final BundleArchive provider = (BundleArchive) itP.next();

                // The package pkgName may be imported by ba from provider,
                // update ba's providers map
                SortedSet pkgNames = (SortedSet) ba.pkgProvidersMap
                  .get(provider);
                if (null == pkgNames) {
                  pkgNames = new TreeSet();
                  ba.pkgProvidersMap.put(provider, pkgNames);
                }
                pkgNames.add(pkgName);

                // Non self exported package, add to pkgCtProvidersMap
                if (!ba.pkgExportMap.containsKey(pkgName)) {
                  SortedSet pkgNamesCt = (SortedSet) ba.pkgCtProvidersMap
                    .get(provider);
                  if (null == pkgNamesCt) {
                    pkgNamesCt = new TreeSet();
                    ba.pkgCtProvidersMap.put(provider, pkgNamesCt);
                  }
                  pkgNamesCt.add(pkgName);
                }

                // The package pkgName is provided (exported) by
                // provider to ba, update provider.pkgProvidedMap to
                // reflect this.
                SortedSet pkgNamesProvidedToBa = (SortedSet) provider.pkgProvidedMap
                  .get(ba);
                if (null == pkgNamesProvidedToBa) {
                  pkgNamesProvidedToBa = new TreeSet();
                  provider.pkgProvidedMap.put(ba, pkgNamesProvidedToBa);
                }
                pkgNamesProvidedToBa.add(pkgName);
              }
            }
          }
        } else {
          ba.pkgUnprovidedMap.put(pkgName, range);
          task.log(ba + " importing no provider for package " + pkgName + " "
                   + range, Project.MSG_DEBUG);
        }
      }
    }
  }

  /**
   * Format the OSGi version as Maven 2 does in versioned file names.
   */
  static private String toMavenVersion(final Version version) {
    final StringBuffer sb = new StringBuffer(40);

    sb.append(String.valueOf(version.getMajor())).append(".");
    sb.append(String.valueOf(version.getMinor())).append(".");
    sb.append(String.valueOf(version.getMicro()));

    final String qualifier = version.getQualifier();
    if (0 < qualifier.length()) {
      sb.append("-").append(qualifier);
    }

    return sb.toString();
  }

  /**
   * Add a bundle archive to a map using the given key. The values of the map
   * are sorted sets of bundle archives.
   */
  private void addToMap(final Map map, final String key, final BundleArchive ba) {
    if (null == key)
      return;

    SortedSet bas = (SortedSet) map.get(key);
    if (null == bas) {
      bas = new TreeSet();
      map.put(key, bas);
      task.log("Found bundle '" + key + "'.", Project.MSG_DEBUG);
    }
    if (bas.add(ba)) {
      task.log("Found bundle '" + key + "' '" + ba.version + "'.",
               Project.MSG_DEBUG);
    }
  }

  static String encodeBundleName(final String bundleName) {
    String name = bundleName;
    if (null != name) {
      name = name.replace(':', '.');
      name = name.replace(' ', '_');
    }
    return name;
  }

  /**
   * A BundleArchive-object describes one bundle with data derived from its file
   * name and manifest.
   */
  static class BundleArchive implements Comparable {
    /** Task that uses this class, for logging. */
    final Task task;

    /** File object referencing the bundle jar. */
    final File file;

    /** The relative path from the root of the file set holding the bundle. */
    final String relPath;

    /** The name of the bundle file without version and ".jar" suffix. */
    final String bundleName;

    /** The name of the project that the bundle belongs to. */
    final String projectName;

    // The manifest attributes and some parsed values
    final Attributes mainAttributes;
    final String manifestVersion;
    final String bsn;
    final Version version;
    final String name;

    /** Mapping from exported package name to its version. */
    final Map pkgExportMap;

    /** Mapping from imported package name to its version range constraint. */
    final Map pkgImportMap;

    /** Set with names of imported packages that are optional. */
    final Set pkgImportOptional = new TreeSet();

    /**
     * Collection of bundles that provides packages that this bundle are
     * importing. The mapping key is a bundle archive and the value is the set
     * of package names that the bundle archive may provide to this bundle.
     *
     * <p>
     * Initially empty, to fill in this map call {@link #doProviders()}.
     * </p>
     */
    final Map pkgProvidersMap = new TreeMap();

    /**
     * Collection of bundles that provides packages that this bundle needs
     * access to at compile time. I.e., packages that the bundle is importing
     * but not exporting. The mapping key is a bundle archive and the value is
     * the set of package names that the bundle archive may provide to this
     * bundle.
     *
     * <p>
     * Initially empty, to fill in this map call {@link #doProviders()}.
     * </p>
     */
    final Map pkgCtProvidersMap = new TreeMap();

    /**
     * Collection of bundles that this bundle provides packages to, i.e.,
     * bundles importing the exports of this bundle. The mapping key is a bundle
     * archive and the value is the set of package names that the bundle archive
     * may import from this bundle.
     *
     * <p>
     * Initially empty, to fill in this map call {@link #doProviders()}.
     * </p>
     */
    final Map pkgProvidedMap = new TreeMap();

    /**
     * Sub set of the entries in the imported packages map for which there are
     * no matching exporter. Mapping from package name to its version range
     * constraint.
     *
     * <p>
     * Initially empty, to fill in this map call {@link #doProviders()}.
     * </p>
     */
    final Map pkgUnprovidedMap = new TreeMap();

    /** Mapping from exported service name to its version. */
    final Map serviceExportMap;

    /** Mapping from imported service name to its version range constraint. */
    final Map serviceImportMap;

    /** Number of source files inside the bundle archive. */
    final int srcCount;

    /**
     * Create a bundle archive object for the given bundle jar file.
     *
     * @param task
     *          The task that uses this class, used for logging and project
     *          access.
     * @param resource
     *          The bundle jar file to create a bundle archive object for.
     * @param parseExportImport
     *          If <code>true</code> then populate the pkgExportMap,
     *          pkgImportMap, pkgImportOptional set with data parsed from the
     *          import / export package manifest attributes.
     */
    public BundleArchive(final Task task, final FileResource resource,
                         final boolean parseExportImport) throws IOException {
      this.task = task;
      this.file = resource.getFile();
      this.relPath = resource.getName();

      // The version derived from the file name, to be checked against
      // the version from the manifest.
      Version nameVersion = null;

      // Derive bundle name from the file name
      final String fileName = file.getName();
      String bn = null;
      // The file name format is "<bundleName>-<version>.jar"
      final int ix = fileName.lastIndexOf('-');
      if (0 < ix) {
        bn = fileName.substring(0, ix);
        final String versionS = fileName.substring(ix + 1,
                                                   fileName.length() - 4);
        try {
          nameVersion = new Version(versionS);
        } catch (NumberFormatException nfe) {
          bn = null; // Not valid due to missing version.
          task.log("Invalid version in bundle file name '" + versionS + "': "
                   + nfe, Project.MSG_VERBOSE);
        }
      } else {
        // No version in file name, just remove the ".jar"-suffix
        bn = fileName.substring(0, fileName.length() - 4);
      }

      final JarFile bundle = new JarFile(file);
      try {
        final Manifest manifest = bundle.getManifest();
        this.mainAttributes = manifest.getMainAttributes();
        this.manifestVersion = deriveBundleManifestVersion();
        this.bsn = deriveBundleSymbolicName();
        this.version = deriveBundleVersion();
        this.name = mainAttributes.getValue(Constants.BUNDLE_NAME);

        int count = 0;
        for (Enumeration e = bundle.entries(); e.hasMoreElements();) {
          ZipEntry entry = (ZipEntry) e.nextElement();

          if (entry.getName().startsWith(SRC_PREFIX)) {
            count++;
          }
        }
        srcCount = count;

      } finally {
        if (null != bundle) {
          try {
            bundle.close();
          } catch (IOException _ioe) {
          }
        }
      }

      // Compare version from the file name with the one from the manifest
      if (null != nameVersion && 0 != nameVersion.compareTo(version)) {
        if (nameVersion.getMajor() == version.getMajor()
            && nameVersion.getMinor() == version.getMinor()
            && nameVersion.getMicro() == version.getMicro()
            && "".equals(nameVersion.getQualifier())) {
          task.log("Found version '" + nameVersion + "' in the file name '"
                   + fileName + "', but the version in the bundle's manifest "
                   + "has qualifier '" + version + "'.", Project.MSG_DEBUG);
        } else {
          task.log("Found version '" + nameVersion + "' in the file name '"
                   + fileName + "', but the version in the bundle's manifest is '"
                   + version + "'.", Project.MSG_INFO);
        }
      }

      if (0 < version.getQualifier().length()) {
        // Maven uses '-' and not '.' as separator for the qualifier
        // in its bundle names. Check if bundleName needs to be
        // updated.
        final String mavenSuffix = "-" + toMavenVersion(version) + ".jar";
        if (fileName.endsWith(mavenSuffix)) {
          bn = fileName.substring(0, fileName.length() - mavenSuffix.length());
        }
      }

      // The bundle name is now ready!
      this.bundleName = bn;

      // The project name is the name of the parent directory of the
      // bundle in its relative path, if empty use the bundle name
      // without "_all" / "_impl" suffix.
      final File parentDir = new File(relPath).getParentFile();
      final String parentDirName = null == parentDir ? null : parentDir
        .getName();
      if (null != parentDirName && 0 < parentDirName.length()) {
        this.projectName = parentDirName;
      } else if (bn.endsWith("_api") || bn.endsWith("_all")) {
        this.projectName = bn.substring(0, bn.length() - 4);
      } else {
        this.projectName = bn;
      }

      if ("2".equals(manifestVersion) && null == bsn) {
        final String msg = "Found bundle with Bundle-MainfestVersion >= 2 "
          + "without Bundle-SymbolicName: " + fileName;
        throw new BuildException(msg);
      }

      if (parseExportImport) {
        pkgExportMap = parseNames("Export-Package", false, null);
        pkgImportMap = parseNames("Import-Package", true, pkgImportOptional);
        serviceExportMap = parseNames("Export-Service", false, null);
        serviceImportMap = parseNames("Import-Service", true, null);
      } else {
        pkgExportMap = null;
        pkgImportMap = null;
        serviceExportMap = null;
        serviceImportMap = null;
      }
    }

    /**
     * Sort on bundle symbolic name then on the bundle version. Compares bundle
     * symbolic name and then bundle version.
     *
     * @param o
     *          the object to compare this bundle archive to.
     * @return a negative integer, zero, or a positive integer as this object is
     *         less than, equal to, or greater than the specified object.
     */
    public int compareTo(Object o) {
      BundleArchive other = (BundleArchive) o;
      // The bsn may be null for pre OSGi R4 bundles!
      String objName = this.bsn != null ? this.bsn : this.name;
      String otherName = other.bsn != null ? other.bsn : other.name;

      int res = objName.compareTo(otherName);
      return res != 0 ? res : this.version.compareTo(other.version);
    }

    public String toString() {
      return file.toString();
    }

    /**
     * Get the bundle manifest version from the manifest.
     */
    private String deriveBundleManifestVersion() throws NumberFormatException {
      String manifestVersion = mainAttributes
        .getValue(Constants.BUNDLE_MANIFESTVERSION);
      if (null == manifestVersion) {
        // Pre OSGi R4 bundle with non-standard version format; use
        // version "1".
        manifestVersion = "1";
      }
      return manifestVersion.trim();
    }

    /**
     * Get the bundle symbolic name from the manifest. If the
     * <tt>Bundle-SymbolicName</tt> attribute is missing use the
     * <tt>Bundle-Name</tt> but replace all ':' with '.' and all '&nbsp;' with
     * '_' in the returned value.
     *
     */
    private String deriveBundleSymbolicName() {
      String name = mainAttributes.getValue(Constants.BUNDLE_SYMBOLICNAME);
      if (null == name) {
        if ("1".equals(manifestVersion)) {
          name = mainAttributes.getValue(Constants.BUNDLE_NAME);
        }
      } else {
        // Remove any directive from the name
        final int semiPos = name.indexOf(";");
        if (-1 < semiPos) {
          name = name.substring(0, semiPos);
        }
        name = name.trim();
      }

      return BundleArchives.encodeBundleName(name);
    }

    /**
     * Get the bundle version from the manifest.
     */
    private Version deriveBundleVersion() throws NumberFormatException {
      final String versionS = mainAttributes.getValue(Constants.BUNDLE_VERSION);
      if (null == versionS) {
        return Version.emptyVersion;
      }

      try {
        return new Version(versionS);
      } catch (NumberFormatException nfe) {
        if ("1".equals(manifestVersion)) {
          // Pre OSGi R4 bundle with non-standard version format; use
          // the default version.
          return Version.emptyVersion;
        }
        final String msg = "Invalid bundle version '" + versionS
          + "' found in " + file + ": " + nfe;
        task.log(msg, Project.MSG_ERR);
        throw new BuildException(msg, nfe);
      }
    }

    /**
     * Parse import/export package/service headers.
     *
     * @param s
     *          The name the header to parse.
     * @param range
     *          if versions shall be parsed as ranges or not.
     * @param optionals
     *          Optional Set to add packages that are marked with the directive
     *          resolution:=optional to.
     *
     * @return Mapping from package/service name to version/version range.
     */
    private Map parseNames(String s, boolean range, Set optionals) {
      final Map res = new TreeMap();

      final String v = mainAttributes.getValue(s);
      final Iterator pathIter = Util.parseEntries(s, v, false, true, false);

      while (pathIter.hasNext()) {
        final Map pathMap = (Map) pathIter.next();
        String versionS = (String) pathMap.get("version");
        // Fall back to "specification-version" for pre OSGi R4 bundles.
        if (null == versionS) {
          versionS = (String) pathMap.get("specification-version");
        }
        if (null == versionS) {
          versionS = "0"; // The default version
        }
        final Object version = range ? (Object) new VersionRange(versionS)
          : (Object) new Version(versionS);

        final Iterator nameIter = ((List) pathMap.get("$keys")).iterator();
        while (nameIter.hasNext()) {
          final String pkgName = (String) nameIter.next();
          res.put(pkgName, version);
          // Is this package/service optional
          if (null != optionals) {
            Set directiveNames = (Set) pathMap.get("$directives");
            if (directiveNames.contains("resolution")
                && "optional".equals(pathMap.get("resolution"))) {
              optionals.add(pkgName);
            }
          }
        }
      }
      return res;
    }

    public String getBundleDescription() {
      final String res = mainAttributes.getValue(Constants.BUNDLE_DESCRIPTION);
      return null == res ? res : res.trim();
    }

    public Iterator getBundleLicense() {
      String value = mainAttributes.getValue(BundleArchives.BUNDLE_LICENSE);
      if (null != value) {
        value = value.trim();
        if (value.startsWith("http://")) {
          // Unquoted URI, try to add quotes in a couple of simple cases
          int pos = value.indexOf(';');
          if (-1 < pos) {
            // param present, stop quote before it starts
            value = "\"" + value.substring(0, pos) + "\""
              + value.substring(pos);
          } else if (-1 < (pos = value.indexOf(','))) {
            // Multiple licenses present, quote the first one...
            value = "\"" + value.substring(0, pos) + "\""
              + value.substring(pos);
          } else {
            value = "\"" + value + "\"";
          }
        }
      }

      try {
        return Util.parseEntries(BundleArchives.BUNDLE_LICENSE, value, true,
                                 true, false);
      } catch (Exception e) {
        final String msg = "Failed to parse " + BundleArchives.BUNDLE_LICENSE
          + " manifest header '" + value + "' in " + file + ": " + e;
        throw new BuildException(msg, e);
      }
    }

    /**
     * @return <code>true</code> if this bundle includes Declarative Services
     *         Components, <code>false</code> otherwise.
     */
    public boolean isSCBundle() {
      return null != mainAttributes.getValue(BundleArchives.SERVICE_COMPONENT);
    }

    /**
     * @return <code>true</code> if this bundle includes blueprint components,
     *         <code>false</code> otherwise.
     */
    public boolean isBlueprintBundle() {
      // TODO: If header not present the default pattern,
      // "OSGI-INF/blueprint/*.xml" should be checked.

      return null != mainAttributes.getValue(BundleArchives.BUNDLE_BLUEPRINT);
    }

    /**
     * @return <code>true</code> if this bundle includes a bundle activator,
     *         <code>false</code> otherwise.
     */
    public boolean isActivatorBundle() {
      return null != mainAttributes.getValue(Constants.BUNDLE_ACTIVATOR);
    }

    /**
     * @return <code>true</code> if this bundle is an API only bundle. I.e. if
     *         it exports packages but does not have a bundle activator, service
     *         component or blueprint component. activator, <code>false</code>
     *         otherwise.
     */
    public boolean isAPIBundle() {
      return 0 < pkgExportMap.size() && !isActivatorBundle() && !isSCBundle()
        && !isBlueprintBundle();
    }

    /**
     * Extract source from inside the bundle to the given destination directory.
     * If bundle does not contain any source files try to copy source files from
     * the bundles src-directory (derived from the <code>Built-From</code>
     * manifest attribute.
     *
     * @param destDir
     *          The directory to extract the source files to.
     * @return List with one entry for each extracted file. The value is the
     *         path relative to <code>destDir</code>.
     * @throws IOException
     */
    public List extractSources(File destDir) throws IOException {
      final List res = new ArrayList();
      final JarFile jarFile = new JarFile(file);
      for (Enumeration e = jarFile.entries(); e.hasMoreElements();) {
        ZipEntry entry = (ZipEntry) e.nextElement();

        if (entry.getName().startsWith(SRC_PREFIX)) {
          if (0 == res.size()) {
            destDir.mkdirs();
          }

          if (entry.isDirectory()) {
            makeDir(destDir, entry, SRC_PREFIX);
          } else {
            copyEntry(destDir, jarFile, entry, SRC_PREFIX);
            res.add(Util.replace(entry.getName(), SRC_PREFIX + "/", ""));
          }
        }
      }

      if (0 == res.size()) {
        // Check if we can copy source from original pos
        String sourceDir = (String) mainAttributes.getValue("Built-From");
        if (sourceDir != null && !"".equals(sourceDir)) {
          File src = new File(sourceDir, "src");
          res.addAll(copyTree(src, destDir, src.toString() + File.separator, ".java"));
        }
      }
      return res;
    }

    private static void makeDir(final File destDir, final ZipEntry entry,
                                final String prefix) throws IOException {
      final File d = new File(destDir,
                              Util.replace(entry.getName(), prefix, ""));

      d.mkdirs();
    }

    private static void copyEntry(File destDir, ZipFile file, ZipEntry entry,
                                  String prefix) throws IOException {
      final File destFile = new File(destDir, Util.replace(entry.getName(),
                                                           prefix, ""));

      final File dir = destFile.getParentFile();

      if (!dir.exists()) {
        dir.mkdirs();
      }
      copyStream(new BufferedInputStream(file.getInputStream(entry)),
                 new BufferedOutputStream(new FileOutputStream(destFile)));
    }

    private static void copyStream(final InputStream is, final OutputStream os)
      throws IOException {
      final byte[] buf = new byte[1024];

      BufferedInputStream in = null;
      BufferedOutputStream out = null;

      try {
        in = new BufferedInputStream(is);
        out = new BufferedOutputStream(os);
        int n;
        while ((n = in.read(buf)) > 0) {
          out.write(buf, 0, n);
        }
      } finally {
        try {
          in.close();
        } catch (Exception ignored) {
        }
        try {
          out.close();
        } catch (Exception ignored) {
        }
      }
    }

    private static List copyTree(File dest, File src, String prefix,
                                 String suffix) throws IOException {
      final List res = new ArrayList();

      if (src.isDirectory()) {
        if (!dest.exists()) {
          dest.mkdirs();
        }

        final String[] files = src.list();
        for (int i = 0; i < files.length; i++) {
          res.addAll(copyTree(new File(dest, files[i]),
                              new File(src, files[i]), prefix, suffix));
        }
      } else if (src.isFile()) {
        if (src.getName().endsWith(suffix)) {
          copyStream(new FileInputStream(src), new FileOutputStream(dest));
          res.add(Util.replace(Util.replace(src.getAbsolutePath(), prefix, ""),
                               "\\", "/"));
        }
      }
      return res;
    }

    /**
     * Build a mapping holding one entry for each source file found in the
     * SVN-repository for the bundle. The repository URL of a source file is the
     * repoUrl followed by the part of the file path that remains when the
     * pathPrefix has been removed.
     *
     * @param pathPrefix
     *          The path of source files to include must start with this value.
     * @param repoURL
     *          The URL to the SVN repository.
     *
     * @return Mapping where the key is the name (path) of the source file and
     *         the value the repository URL to it.
     * @throws IOException
     */
    public Map getSrcRepositoryLinks(final String pathPrefix, final URL repoURL)
      throws IOException {
      final Map res = new TreeMap();

      // Check if we can locate a source tree based on the
      // "Built-From" header.
      String rootDir = (String) mainAttributes.getValue("Built-From");
      if (null != rootDir && 0 < rootDir.length()) {
        final File src = new File(rootDir, "src");
        if (src.isDirectory()) {
          res.putAll(srcRepositoryLinks(pathPrefix, repoURL, src,
                                        src.getAbsolutePath() + File.separator));
        } else {
          task.log(
                   "No src sub-directory in 'Built-From' location, "
                   + src.getAbsolutePath() + ", can not locate source files.",
                   Project.MSG_VERBOSE);
        }
      } else {
        task.log("No 'Built-From' manifest header in bundle, "
                 + "can not locate source files.", Project.MSG_VERBOSE);
      }
      return res;
    }

    private Map srcRepositoryLinks(final String pathPrefix, final URL repoURL,
                                   final File src, final String prefix) throws IOException {
      final Map res = new TreeMap();

      if (src.isDirectory()) {
        final String[] files = src.list();
        for (int i = 0; i < files.length; i++) {
          res.putAll(srcRepositoryLinks(pathPrefix, repoURL, new File(src,
                                                                      files[i]), prefix));
        }
      } else if (src.isFile()) {
        final String path = src.getAbsolutePath();
        if (src.getName().endsWith(".java")) {
          final String bundlePath = Util.replace(path, prefix, "");
          if (path.startsWith(pathPrefix)) {
            final String repoPath = Util.replace(path, pathPrefix, "").replace(
                                                                               File.separatorChar, '/');
            final String href = new URL(repoURL, repoPath).toString();

            res.put(bundlePath, href);

            task.log("Found Java source file in repository, " + path
                     + " with href '" + href + "'.", Project.MSG_VERBOSE);
          } else {
            task.log("Skipping non-repository Java source file, " + path + ".",
                     Project.MSG_DEBUG);
          }

        } else {
          task.log("Skipping non-Java source file, " + path + ".",
                   Project.MSG_DEBUG);
        }
      }
      return res;
    }

  } // class BundleArchive

}
TOP

Related Classes of org.knopflerfish.ant.taskdefs.bundle.BundleArchives

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.