Package aQute.lib.osgi

Source Code of aQute.lib.osgi.Analyzer

/* Copyright 2006 aQute SARL
* Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0 */
package aQute.lib.osgi;

/**
* This class can calculate the required headers for a (potential) JAR file. It
* analyzes a directory or JAR for the packages that are contained and that are
* referred to by the bytecodes. The user can the use regular expressions to
* define the attributes and directives. The matching is not fully regex for
* convenience. A * and ? get a . prefixed and dots are escaped.
*
* <pre>
*                                                    *;auto=true        any   
*                                                    org.acme.*;auto=true    org.acme.xyz
*                                                    org.[abc]*;auto=true    org.acme.xyz
* </pre>
*
* Additional, the package instruction can start with a '=' or a '!'. The '!'
* indicates negation. Any matching package is removed. The '=' is literal, the
* expression will be copied verbatim and no matching will take place.
*
* Any headers in the given properties are used in the output properties.
*/
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.jar.*;
import java.util.jar.Attributes.*;
import java.util.regex.*;

public class Analyzer extends Processor {
  public final static String  BUNDLE_CLASSPATH          = "Bundle-ClassPath";
  public final static String  BUNDLE_COPYRIGHT          = "Bundle-Copyright";
  public final static String  BUNDLE_DESCRIPTION          = "Bundle-Description";
  public final static String  BUNDLE_NAME              = "Bundle-Name";
  public final static String  BUNDLE_NATIVECODE          = "Bundle-NativeCode";
  public final static String  EXPORT_PACKAGE            = "Export-Package";
  public final static String  EXPORT_SERVICE            = "Export-Service";
  public final static String  IMPORT_PACKAGE            = "Import-Package";
  public final static String  DYNAMICIMPORT_PACKAGE        = "DynamicImport-Package";
  public final static String  IMPORT_SERVICE            = "Import-Service";
  public final static String  BUNDLE_VENDOR            = "Bundle-Vendor";
  public final static String  BUNDLE_VERSION            = "Bundle-Version";
  public final static String  BUNDLE_DOCURL            = "Bundle-DocURL";
  public final static String  BUNDLE_CONTACTADDRESS        = "Bundle-ContactAddress";
  public final static String  BUNDLE_ACTIVATOR          = "Bundle-Activator";
  public final static String  BUNDLE_REQUIREDEXECUTIONENVIRONMENT  = "Bundle-RequiredExecutionEnvironment";
  public final static String  BUNDLE_SYMBOLICNAME          = "Bundle-SymbolicName";
  public final static String  BUNDLE_LOCALIZATION          = "Bundle-Localization";
  public final static String  REQUIRE_BUNDLE            = "Require-Bundle";
  public final static String  FRAGMENT_HOST            = "Fragment-Host";
  public final static String  BUNDLE_MANIFESTVERSION        = "Bundle-ManifestVersion";
  public final static String  SERVICE_COMPONENT          = "Service-Component";
  public final static String  BUNDLE_LICENSE            = "Bundle-License";
  public static final String  PRIVATE_PACKAGE            = "Private-Package";
  public static final String  IGNORE_PACKAGE            = "Ignore-Package";
  public static final String  INCLUDE_RESOURCE          = "Include-Resource";

  public final static String  headers[]              = {
      BUNDLE_ACTIVATOR, BUNDLE_CONTACTADDRESS, BUNDLE_COPYRIGHT,
      BUNDLE_DOCURL, BUNDLE_LOCALIZATION, BUNDLE_NATIVECODE,
      BUNDLE_VENDOR, BUNDLE_VERSION, BUNDLE_LICENSE, BUNDLE_CLASSPATH,
      SERVICE_COMPONENT, EXPORT_PACKAGE, IMPORT_PACKAGE,
      BUNDLE_LOCALIZATION, BUNDLE_MANIFESTVERSION, BUNDLE_NAME,
      BUNDLE_NATIVECODE, BUNDLE_REQUIREDEXECUTIONENVIRONMENT,
      BUNDLE_SYMBOLICNAME, BUNDLE_VERSION, FRAGMENT_HOST,
      PRIVATE_PACKAGE, IGNORE_PACKAGE, INCLUDE_RESOURCE, REQUIRE_BUNDLE,
      IMPORT_SERVICE, EXPORT_SERVICE              };

  static final Pattern    VALID_PROPERTY_TYPES        = Pattern
                                      .compile("(String|Long|Double|Float|Integer|Byte|Character|Boolean|Short)");

  static Pattern        doNotCopy              = Pattern
                                      .compile("CVS|.svn");
  static String        version;
  /**
   * For each import, find the exporter and see what you can learn from it.
   */
  static Pattern        versionPattern            = Pattern
                                      .compile("(\\d+\\.\\d+)\\.\\d+");
  Properties          properties              /* String->String */= new Properties();
  File            base                = new File(
                                      "")
                                      .getAbsoluteFile();
  Map              contained              /* String->Map */= new HashMap();                            // package
  Map              referred              /* String->Map */= new HashMap();                            // package
  Map              uses                /* String->Map */= new HashMap();                            // package
  Map              classspace;
  boolean            analyzed;
  Map              exports;
  Map              imports;
  Map              bundleClasspath;                                          // Bundle
  Map              ignored              /* String -> Map */= new HashMap();                          // Ignored
  // packages
  Jar              dot;                                                            // The
  Map              cpExports              = new HashMap();

  String            activator;

  List            classpath              = new ArrayList();

  Macro            replacer              = new Macro(
                                      this);

  /**
   * Specifically for Maven
   *
   * @param properties
   *            the properties
   */

  public static Properties getManifest(File dirOrJar) throws IOException {
    Analyzer analyzer = new Analyzer();
    analyzer.setJar(dirOrJar);
    Properties properties = new Properties();
    properties.put(IMPORT_PACKAGE, "*");
    properties.put(EXPORT_PACKAGE, "*");
    analyzer.setProperties(properties);
    Manifest m = analyzer.calcManifest();
    Properties result = new Properties();
    for (Iterator i = m.getMainAttributes().keySet().iterator(); i
        .hasNext();) {
      Attributes.Name name = (Attributes.Name) i.next();
      result.put(name.toString(), m.getMainAttributes().getValue(name));
    }
    return result;
  }

  /**
   * Calcualtes the data structures for generating a manifest.
   *
   * @throws IOException
   */
  public void analyze() throws IOException {
    if (!analyzed) {
      analyzed = true;
      cpExports = new HashMap();
      activator = getProperty(BUNDLE_ACTIVATOR);
      bundleClasspath = parseHeader(getProperty(BUNDLE_CLASSPATH));

      analyzeClasspath();

      classspace = analyzeBundleClasspath(dot, bundleClasspath,
          contained, referred, uses);

      referred.keySet().removeAll(contained.keySet());

      Map exportInstructions = parseHeader(getProperty(EXPORT_PACKAGE));
      Map importInstructions = parseHeader(getProperty(IMPORT_PACKAGE));
      Map dynamicImports = parseHeader(getProperty(DYNAMICIMPORT_PACKAGE));

      if (dynamicImports != null) {
        // Remove any dynamic imports from the referred set.
        referred.keySet().removeAll(dynamicImports.keySet());
      }

      Set superfluous = new TreeSet();
      // Tricky!
      for (Iterator i = exportInstructions.keySet().iterator(); i
          .hasNext();) {
        String instr = (String) i.next();
        if (!instr.startsWith("!"))
          superfluous.add(instr);
      }

      exports = merge("export-package", exportInstructions, contained,
          superfluous);
      if (!superfluous.isEmpty()) {
        warnings.add("Superfluous export-package instructions: "
            + superfluous);
      }

      // Add all exports that do not have an -noimport: directive
      // to the imports.
      Map referredAndExported = new HashMap(referred);
      referredAndExported.putAll(addExportsToImports(exports));

      // match the imports to the referred and exported packages,
      // merge the info for matching packages
      Set extra = new TreeSet(importInstructions.keySet());
      imports = merge("import-package", importInstructions,
          referredAndExported, extra);

      // Instructions that have not been used could be superfluous
      // or if they do not contain wildcards, should be added
      // as extra imports, the user knows best.
      for (Iterator i = extra.iterator(); i.hasNext();) {
        String p = (String) i.next();
        if (p.startsWith("!") || p.indexOf('*') > 0
            || p.indexOf('?') > 0 || p.indexOf('[') > 0) {
          warning("Did not find matching referal for " + p);
        } else {
          Map map = (Map) importInstructions.get(p);
          imports.put(p, map);
        }
      }

      // See what information we can find to augment the
      // imports. I.e. look on the classpath
      augmentImports();

      // Add the uses clause to the exports
      doUses(exports, uses);
    }
  }

  /**
   * One of the main workhorses of this class. This will analyze the current
   * setp and calculate a new manifest according to this setup. This method
   * will also set the manifest on the main jar dot
   *
   * @return
   * @throws IOException
   */
  public Manifest calcManifest() throws IOException {
    analyze();
    Manifest manifest = new Manifest();
    Attributes main = manifest.getMainAttributes();

    main.putValue(BUNDLE_MANIFESTVERSION, "2");
    main.putValue("Created-By", "Bnd-" + getVersion());

    String exportHeader = printClauses(exports,
        "uses:|include:|exclude:|mandatory:");

    if (exportHeader.length() > 0)
      main.putValue(EXPORT_PACKAGE, exportHeader);
    else
      main.remove(EXPORT_PACKAGE);

    Map temp = removeKeys(imports, "java.");
    if (!temp.isEmpty()) {
      main.putValue(IMPORT_PACKAGE, printClauses(temp, "resolution:"));
    } else {
      main.remove(IMPORT_PACKAGE);
    }

    temp = new TreeMap(contained);
    temp.keySet().removeAll(exports.keySet());

    if (!temp.isEmpty())
      main.putValue(PRIVATE_PACKAGE, printClauses(temp, ""));
    else
      main.remove(PRIVATE_PACKAGE);

    if (!ignored.isEmpty()) {
      main.putValue(IGNORE_PACKAGE, printClauses(ignored, ""));
    } else {
      main.remove(IGNORE_PACKAGE);
    }

    if (bundleClasspath != null && !bundleClasspath.isEmpty())
      main.putValue(BUNDLE_CLASSPATH, printClauses(bundleClasspath, ""));
    else
      main.remove(BUNDLE_CLASSPATH);

    Map l = doServiceComponent(getProperty(SERVICE_COMPONENT));
    if (!l.isEmpty())
      main.putValue(SERVICE_COMPONENT, printClauses(l, ""));
    else
      main.remove(SERVICE_COMPONENT);

    for (Iterator h = properties.keySet().iterator(); h.hasNext();) {
      String header = (String) h.next();
      if (!Character.isUpperCase(header.charAt(0)))
        continue;

      if (header.equals(BUNDLE_CLASSPATH)
          || header.equals(EXPORT_PACKAGE)
          || header.equals(IMPORT_PACKAGE))
        continue;

      String value = getProperty(header);
      if (value != null && main.getValue(header) == null)
        main.putValue(header, value);
    }

    main.put(Attributes.Name.MANIFEST_VERSION, "1");

    // Copy old values into new manifest, when they
    // exist in the old one, but not in the new one
    merge(manifest, dot.getManifest());

    // Check for some defaults
    String p = getProperty("project");
    if (p != null) {
      if (main.getValue(BUNDLE_SYMBOLICNAME) == null) {
        main.putValue(BUNDLE_SYMBOLICNAME, p);
      }
      if (main.getValue(BUNDLE_NAME) == null) {
        main.putValue(BUNDLE_NAME, p);
      }
    }
    if (main.getValue(BUNDLE_VERSION) == null)
      main.putValue(BUNDLE_VERSION, "0");

    dot.setManifest(manifest);
    return manifest;
  }

  /**
   * Calculate an export header solely based on the contents of a JAR file
   *
   * @param bundle
   *            The jar file to analyze
   * @return
   */
  public String calculateExportsFromContents(Jar bundle) {
    String ddel = ",";
    StringBuffer sb = new StringBuffer();
    Map map = bundle.getDirectories();
    for (Iterator i = map.keySet().iterator(); i.hasNext();) {
      String directory = (String) i.next();
      if (directory.startsWith("META-INF/"))
        continue;
      if (directory.equals("/"))
        continue;

      if (directory.endsWith("/"))
        directory = directory.substring(0, directory.length() - 1);

      directory = directory.replace('/', '.');
      sb.append(ddel);
      sb.append(directory);
      ddel = ",";
    }
    return sb.toString();
  }

  /**
   * Check if a service component header is actually referring to a class. If
   * so, replace the reference with an XML file reference. This makes it
   * easier to create and use components.
   *
   * @throws UnsupportedEncodingException
   *
   */
  public Map doServiceComponent(String serviceComponent)
      throws UnsupportedEncodingException {
    Map list = new LinkedHashMap();
    Map sc = parseHeader(serviceComponent);
    if (!sc.isEmpty()) {
      for (Iterator i = sc.entrySet().iterator(); i.hasNext();) {
        Map.Entry entry = (Map.Entry) i.next();
        String name = (String) entry.getKey();
        Map info = (Map) entry.getValue();
        if (name == null) {
          error("No name in Service-Component header: " + info);
          continue;
        }
        if (dot.exists(name)) {
          // Normal service component
          list.put(name, info);
        } else {
          if (!checkClass(name))
            error("Not found Service-Component header: " + name);
          else {
            // We have a definition, so make an XML resources
            Resource resource = createComponentResource(name, info);
            dot.putResource("OSGI-INF/" + name + ".xml", resource);
            list.put("OSGI-INF/" + name + ".xml", new HashMap());
          }
        }
      }
    }
    return list;
  }

  public Map getBundleClasspath() {
    return bundleClasspath;
  }

  public Map getContained() {
    return contained;
  }

  public Map getExports() {
    return exports;
  }

  public Map getImports() {
    return imports;
  }

  public Jar getJar() {
    return dot;
  }

  public Properties getProperties() {
    return properties;
  }

  public String getProperty(String headerName) {
    String value = properties.getProperty(headerName);
    if (value != null)
      return replacer.process(value);
    else
      return null;
  }

  public Map getReferred() {
    return referred;
  }

  /**
   * Return the set of unreachable code depending on exports and the bundle
   * activator.
   *
   * @return
   */
  public Set getUnreachable() {
    Set unreachable = new HashSet(uses.keySet()); // all
    for (Iterator r = exports.keySet().iterator(); r.hasNext();) {
      String packageName = (String) r.next();
      removeTransitive(packageName, unreachable);
    }
    if (activator != null) {
      String pack = activator.substring(0, activator.lastIndexOf('.'));
      removeTransitive(pack, unreachable);
    }
    return unreachable;
  }

  public Map getUses() {
    return uses;
  }

  /**
   * Get the version from the manifest, a lot of work!
   *
   * @return version or unknown.
   */
  public String getVersion() {
    if (version == null) {
      version = "unknown";
      try {
        Enumeration e = getClass().getClassLoader().getResources(
            "META-INF/MANIFEST.MF");
        while (e.hasMoreElements()) {
          URL url = (URL) e.nextElement();
          InputStream in = url.openStream();

          Manifest manifest = new Manifest(in);
          in.close();
          String bsn = manifest.getMainAttributes().getValue(
              BUNDLE_SYMBOLICNAME);
          if (bsn != null && bsn.indexOf("biz.aQute.bnd") >= 0) {
            version = manifest.getMainAttributes().getValue(
                BUNDLE_VERSION);
            return version;
          }
        }
      } catch (IOException e) {
        // Well, too bad
        warning("bnd jar file is corrupted, can not find manifest " + e);
      }
    }
    return version;
  }

  /**
   * Merge the existing manifest with the instructions.
   *
   * @param manifest
   *            The manifest to merge with
   * @throws IOException
   */
  public void mergeManifest(Manifest manifest) throws IOException {
    if (manifest != null) {
      Attributes attributes = manifest.getMainAttributes();
      for (Iterator i = attributes.keySet().iterator(); i.hasNext();) {
        Name name = (Name) i.next();
        String key = name.toString();
        // Dont want instructions
        if (key.startsWith("-"))
          continue;

        if (getProperty(key) == null)
          setProperty(key, (String) attributes.get(name));
      }
    }
  }

  // public Signer getSigner() {
  // String sign = getProperty("-sign");
  // if (sign == null) return null;
  //
  // Map parsed = parseHeader(sign);
  // Signer signer = new Signer();
  // String password = (String) parsed.get("password");
  // if (password != null) {
  // signer.setPassword(password);
  // }
  //
  // String keystore = (String) parsed.get("keystore");
  // if (keystore != null) {
  // File f = new File(keystore);
  // if (!f.isAbsolute()) f = new File(base, keystore);
  // signer.setKeystore(f);
  // } else {
  // error("Signing requires a keystore");
  // return null;
  // }
  //
  // String alias = (String) parsed.get("alias");
  // if (alias != null) {
  // signer.setAlias(alias);
  // } else {
  // error("Signing requires an alias for the key");
  // return null;
  // }
  // return signer;
  // }

  public void setBase(File file) {
    base = file;
    properties.put("project.dir", base.getAbsolutePath());
  }

  /**
   * Set the classpath for this analyzer by file.
   *
   * @param classpath
   * @throws IOException
   */
  public void setClasspath(File[] classpath) throws IOException {
    List list = new ArrayList();
    for (int i = 0; i < classpath.length; i++) {
      if (classpath[i].exists()) {
        Jar current = new Jar(classpath[i]);
        list.add(current);
      } else {
        errors.add("Missing file on classpath: " + classpath[i]);
      }
    }
    this.classpath.addAll(list);
  }

  public void setClasspath(Jar[] classpath) {
    this.classpath.addAll(Arrays.asList(classpath));
  }

  public void setClasspath(String[] classpath) {
    List list = new ArrayList();
    for (int i = 0; i < classpath.length; i++) {
      Jar jar = getJarFromName(classpath[i], " setting classpath");
      if (jar != null)
        list.add(jar);
    }
    this.classpath.addAll(list);
  }

  /**
   * Set the JAR file we are going to work in. This will read the JAR in
   * memory.
   *
   * @param jar
   * @return
   * @throws IOException
   */
  public Jar setJar(File jar) throws IOException {
    return setJar(new Jar(jar));
  }

  /**
   * Set the JAR directly we are going to work on.
   *
   * @param jar
   * @return
   */
  public Jar setJar(Jar jar) {
    this.dot = jar;
    return jar;
  }

  /**
   * Set the properties by file. Setting the properties this way will also set
   * the base for this analyzer. After reading the properties, this will call
   * setProperties(Properties) which will handle the includes.
   *
   * @param propertiesFile
   * @throws FileNotFoundException
   * @throws IOException
   */
  public void setProperties(File propertiesFile)
      throws FileNotFoundException, IOException {

    setBase(propertiesFile.getAbsoluteFile().getParentFile());

    Properties local = loadProperties(propertiesFile);

    local.put("project.file", propertiesFile.getAbsolutePath());
    local.put("project.name", propertiesFile.getName());
    local.put("project", stem(propertiesFile.getName()));

    setProperties(local);

    if (getProperty(BUNDLE_SYMBOLICNAME) == null) {
      // Calculate a default symbolic name
      // from the file name.
      String name = propertiesFile.getName();
      int n = name.lastIndexOf('.');
      if (n > 0)
        name = name.substring(0, n);
      local.setProperty(BUNDLE_SYMBOLICNAME, name);
    }
  }

  public void setProperties(Properties properties) {
    this.properties = properties;
    doPropertyIncludes(getBaseURL(), properties, new HashSet());
    replacer = new Macro(properties, this);

    String doNotCopy = getProperty("-donotcopy");
    if (doNotCopy != null)
      Analyzer.doNotCopy = Pattern.compile(doNotCopy);

    String cp = properties.getProperty("-classpath");
    if (cp != null)
      doClasspath(cp);

    if (getProperty(IMPORT_PACKAGE) == null)
      setProperty(IMPORT_PACKAGE, "*");
    verifyManifestHeadersCase(properties);
  }

  private URL getBaseURL() {
    try {
      return base.toURL();
    } catch (Exception e) {
      // who cares, can not happen
    }
    return null;
  }

  /**
   * Add or override a new property.
   *
   * @param key
   * @param value
   */
  public void setProperty(String key, String value) {
    checkheader: for (int i = 0; i < headers.length; i++) {
      if (headers[i].equalsIgnoreCase(value)) {
        value = headers[i];
        break checkheader;
      }
    }
    properties.put(key, value);
  }

  /**
   * Check if the given class or interface name is contained in the jar.
   *
   * @param interfaceName
   * @return
   */
  boolean checkClass(String interfaceName) {
    String path = interfaceName.replace('.', '/') + ".class";
    if (classspace.containsKey(path))
      return true;

    String pack = interfaceName;
    int n = pack.lastIndexOf('.');
    if (n > 0)
      pack = pack.substring(0, n);
    else
      pack = ".";

    return imports.containsKey(pack);
  }

  /**
   * Create the resource for a DS component.
   *
   * @param list
   * @param name
   * @param info
   * @throws UnsupportedEncodingException
   */
  Resource createComponentResource(String name, Map info)
      throws UnsupportedEncodingException {

    ByteArrayOutputStream out = new ByteArrayOutputStream();
    PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
    pw.println("<?xml version='1.0' encoding='utf-8'?>");
    pw.print("<component name='" + name + "'");

    String factory = (String) info.get("factory:");
    if (factory != null)
      pw.print(" factory='" + factory + "'");

    String immediate = (String) info.get("immediate:");
    if (immediate != null)
      pw.print(" immediate='" + immediate + "'");

    String enabled = (String) info.get("enabled:");
    if (enabled != null)
      pw.print(" enabled='" + enabled + "'");

    pw.println(">");
    pw.println("  <implementation class='" + name + "'/>");
    String provides = (String) info.get("provide:");
    boolean servicefactory = Boolean.parseBoolean(info.get("servicefactory:")
        + "");
    provides(pw, provides, servicefactory);
    properties(pw, info);
    reference(info, pw);
    pw.println("</component>");
    pw.close();
    return new EmbeddedResource(out.toByteArray());
  }

  /**
   * Parse the -classpath header. This is a comma separated list of urls or
   * file names.
   *
   * @param cp
   */
  void doClasspath(String cp) {
    for (Iterator i = getClauses(cp).iterator(); i.hasNext();) {
      Jar jar = getJarFromName((String) i.next(), "getting classpath");
      if (jar != null)
        classpath.add(jar);
    }
  }

  /**
   * Try to get a Jar from a file name/path or a url, or in last resort from
   * the classpath name part of their files.
   *
   * @param name
   *            URL or filename relative to the base
   * @param from
   *            Message identifying the caller for errors
   * @return null or a Jar with the contents for the name
   */
  Jar getJarFromName(String name, String from) {
    File file = new File(name);
    if ( !file.isAbsolute() )
      file = new File(base, name);
   
    if (file.exists())
      try {
        Jar jar = new Jar(file.getName(), file);
        return jar;
      } catch (Exception e) {
        error("Exception in parsing jar file for " + from + ": " + name
            + " " + e);
      }
    // It is not a file ...
    try {
      // Lets try a URL
      URL url = new URL(name);
      Jar jar = new Jar(fileName(url.getPath()));
      EmbeddedResource.build(jar, url.openStream());
      return jar;
    } catch (IOException ee) {
      // Check if we have files on the classpath
      // that have the right name, allows us to specify those
      // names instead of the full path.
      for (Iterator cp = classpath.iterator(); cp.hasNext();) {
        Jar entry = (Jar) cp.next();
        if (entry.source != null && entry.source.getName().equals(name)) {
          return entry;
        }
      }
      error("Can not find jar file for " + from + ": " + name);
    }
    return null;
  }

  private String fileName(String path) {
    int n = path.lastIndexOf('/');
    if (n > 0)
      return path.substring(n + 1);
    return path;
  }

  /**
   * Read a manifest but return a properties object.
   *
   * @param in
   * @return
   * @throws IOException
   */
  Properties getManifestAsProperties(InputStream in) throws IOException {
    Properties p = new Properties();
    Manifest manifest = new Manifest(in);
    for (Iterator it = manifest.getMainAttributes().keySet().iterator(); it
        .hasNext();) {
      Attributes.Name key = (Attributes.Name) it.next();
      String value = manifest.getMainAttributes().getValue(key);
      p.put(key.toString(), value);
    }
    return p;
  }

  /**
   * Helper routine to create a set of a comma separated string.
   *
   * @param list
   * @return
   */
  Set getClauses(String list) {
    if (list == null)
      return new HashSet();
    String[] parts = list.split("\\s*,\\s*");
    return new HashSet(Arrays.asList(parts));
  }

  /**
   *
   * @param manifest
   * @throws Exception
   */
  void merge(Manifest result, Manifest old) throws IOException {
    if (old != null) {
      for (Iterator e = old.getMainAttributes().entrySet().iterator(); e
          .hasNext();) {
        Map.Entry entry = (Map.Entry) e.next();
        Attributes.Name name = (Attributes.Name) entry.getKey();
        String value = (String) entry.getValue();
        if (name.toString().equalsIgnoreCase("Created-By"))
          name = new Attributes.Name("Originally-Created-By");
        if (!result.getMainAttributes().containsKey(name))
          result.getMainAttributes().put(name, value);
      }

      // do not overwrite existing entries
      Map oldEntries = old.getEntries();
      Map newEntries = result.getEntries();
      for (Iterator e = oldEntries.entrySet().iterator(); e.hasNext();) {
        Map.Entry entry = (Map.Entry) e.next();
        if (!newEntries.containsKey(entry.getKey())) {
          newEntries.put(entry.getKey(), entry.getValue());
        }
      }
    }
  }

  void properties(PrintWriter pw, Map info) {
    Set properties = getClauses((String) info.get("properties:"));
    for (Iterator p = properties.iterator(); p.hasNext();) {
      String clause = (String) p.next();
      int n = clause.indexOf('=');
      if (n <= 0) {
        error("Not a valid property in service component: " + clause);
      } else {
        String type = null;
        String name = clause.substring(0, n);
        if (name.contains("@")) {
          String parts[] = name.split("@");
          name = parts[1];
          type = parts[0];
        }
        String value = clause.substring(n + 1);
        // TODO verify validity of name and value
        pw.print("<property name='");
        pw.print(name);
        pw.print("'");

        if (type != null) {
          if (VALID_PROPERTY_TYPES.matcher(type).matches()) {
            pw.print(" type='");
            pw.print(type);
            pw.print("'");
          } else {
            warnings.add("Invalid property type '" + type
                + "' for property " + name);
          }
        }

        if (value.contains("\\n")) {
          pw.print("'>");
          pw.print(value.replaceAll("\\n", "\n"));
          pw.println("</property>");
        } else {
          pw.print(" value='");
          pw.print(value);
          pw.print("'/>");
        }
      }
    }
  }

  /**
   * @param pw
   * @param provides
   */
  void provides(PrintWriter pw, String provides, boolean servicefactory) {
    if (provides != null) {
      if (!servicefactory)
        pw.println("  <service>");
      else
        pw.println("  <service servicefactory='true'>");

      StringTokenizer st = new StringTokenizer(provides, ",");
      while (st.hasMoreTokens()) {
        String interfaceName = st.nextToken();
        pw.println("    <provide interface='" + interfaceName + "'/>");
        if (!checkClass(interfaceName))
          error("Component definition provides a class that is neither imported nor contained: "
              + interfaceName);
      }
      pw.println("  </service>");
    }
  }

  /**
   * @param info
   * @param pw
   */
  void reference(Map info, PrintWriter pw) {
    Set dynamic = getClauses((String) info.get("dynamic:"));
    Set optional = getClauses((String) info.get("optional:"));
    Set multiple = getClauses((String) info.get("multiple:"));

    for (Iterator r = info.entrySet().iterator(); r.hasNext();) {
      Map.Entry ref = (Map.Entry) r.next();
      String referenceName = (String) ref.getKey();
      String interfaceName = (String) ref.getValue();
      // TODO check if the interface is contained or imported

      if (referenceName.endsWith(":"))
        continue;

      if (!checkClass(interfaceName))
        error("Component definition refers to a class that is neither imported nor contained: "
            + interfaceName);

      pw.print("  <reference name='" + referenceName + "' interface='"
          + interfaceName + "'");

      String cardinality = optional.contains(referenceName) ? "0" : "1";
      cardinality += "..";
      cardinality += multiple.contains(referenceName) ? "n" : "1";
      if (!cardinality.equals("1..1"))
        pw.print(" cardinality='" + cardinality + "'");

      if (Character.isLowerCase(referenceName.charAt(0))) {
        String z = referenceName.substring(0, 1).toUpperCase()
            + referenceName.substring(1);
        pw.print(" bind='set" + z + "'");
        // TODO Verify that the methods exist

        // TODO ProSyst requires both a bind and unbind :-(
        // if ( dynamic.contains(referenceName) )
        pw.print(" unbind='unset" + z + "'");
        // TODO Verify that the methods exist
      }
      if (dynamic.contains(referenceName)) {
        pw.print(" policy='dynamic'");
      }
      pw.println("/>");
    }
  }

  String stem(String name) {
    int n = name.lastIndexOf('.');
    if (n > 0)
      return name.substring(0, n);
    else
      return name;
  }

  /**
   * Bnd is case sensitive for the instructions so we better check people are
   * not using an invalid case. We do allow this to set headers that should
   * not be processed by us but should be used by the framework.
   *
   * @param properties
   *            Properties to verify.
   */

  void verifyManifestHeadersCase(Properties properties) {
    for (Iterator i = properties.keySet().iterator(); i.hasNext();) {
      String header = (String) i.next();
      for (int j = 0; j < headers.length; j++) {
        if (!headers[j].equals(header)
            && headers[j].equalsIgnoreCase(header)) {
          warnings
              .add("Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: "
                  + header + " and expecting: " + headers[j]);
          break;
        }
      }
    }
  }

  /**
   * We will add all exports to the imports unless there is a -noimport
   * directive specified on an export. This directive is skipped for the
   * manifest.
   *
   */
  Map addExportsToImports(Map exports) {
    Map importsFromExports = new HashMap();
    for (Iterator export = exports.entrySet().iterator(); export.hasNext();) {
      Map.Entry entry = (Map.Entry) export.next();
      String packageName = (String) entry.getKey();
      Map parameters = (Map) entry.getValue();
      String noimport = (String) parameters.get("-noimport:");
      if (noimport == null || !noimport.equalsIgnoreCase("true")) {
        Map importParameters = (Map) importsFromExports
            .get(packageName);
        if (importParameters == null)
          importsFromExports.put(packageName, parameters);
      }
    }
    return importsFromExports;
  }

  /**
   * Create the imports/exports by parsing
   *
   * @throws IOException
   */
  void analyzeClasspath() throws IOException {
    cpExports = new HashMap();
    for (Iterator c = classpath.iterator(); c.hasNext();) {
      Jar current = (Jar) c.next();
      checkManifest(current);
      for (Iterator j = current.getDirectories().keySet().iterator(); j
          .hasNext();) {
        String dir = (String) j.next();
        Resource resource = current.getResource(dir + "/packageinfo");
        if (resource != null) {
          String version = parsePackageInfo(resource
              .openInputStream());
          setPackageInfo(dir, "version", version);
        }
      }
    }
  }

  /**
   *
   * @param jar
   */
  void checkManifest(Jar jar) {
    try {
      Manifest m = jar.getManifest();
      if (m != null) {
        String exportHeader = m.getMainAttributes().getValue(
            EXPORT_PACKAGE);
        if (exportHeader != null) {
          Map exported = (Map) parseHeader(exportHeader);
          if (exported != null)
            cpExports.putAll(exported);
        }
      }
    } catch (Exception e) {
      warning("Erroneous Manifest for " + jar + " " + e);
    }
  }

  /**
   * Find some more information about imports in manifest and other places.
   */
  void augmentImports() {
    for (Iterator imp = imports.keySet().iterator(); imp.hasNext();) {
      String packageName = (String) imp.next();
      Map currentAttributes = (Map) imports.get(packageName);

      Map exporter = (Map) cpExports.get(packageName);
      if (exporter != null) {
        // See if we can borrow te version
        String version = (String) exporter.get("version");
        if (version == null)
          version = (String) exporter.get("specification-version");
        if (version != null) {
          // We remove the micro part of the version
          // to a bit more lenient
          Matcher m = versionPattern.matcher(version);
          if (m.matches())
            version = m.group(1);
          currentAttributes.put("version", version);
        }

        // If we use an import with mandatory
        // attributes we better all use them
        String mandatory = (String) exporter.get("mandatory:");
        if (mandatory != null) {
          String[] attrs = mandatory.split("\\w*,\\w*");
          for (int i = 0; i < attrs.length; i++) {
            currentAttributes.put(attrs[i], exporter.get(attrs[i]));
          }
        }
      }
    }
  }

  /**
   * Inspect the properties and if you find -includes parse the line included
   * manifest files or propertie files. The files are relative from the given
   * base, this is normally the base for the analyzer.
   *
   * @param ubase
   * @param p
   * @param done
   * @throws IOException
   */
  void doPropertyIncludes(URL ubase, Properties p, Set done) {
    String includes = p.getProperty("-include");
    if (includes != null) {
      includes = replacer.process(includes);
      Set clauses = getClauses(includes);
      outer: for (Iterator i = clauses.iterator(); i.hasNext();) {
        String value = (String) i.next();
        boolean noFileOk=false;
        if ( value.startsWith("-")) {
          noFileOk = true;
          value = value.substring(1).trim();
        }
        try {
          URL next = null;
          try {
            next = new URL(ubase, value );
          } catch (MalformedURLException e) {
           
            File f= new File(value);
            if ( !f.isAbsolute() )
              f = new File(base,value);
            if ( f.exists() )
              next = f.getAbsoluteFile().toURL();
            else {
              if ( noFileOk )
                error("Can not find include file: " + value);
              continue outer;
            }
          }
          if (done.contains(next))
            return;
          done.add(next);

          InputStream in = next.openStream();
          Properties sub;
          if (next.getFile().toLowerCase().endsWith(".mf")) {
            sub = getManifestAsProperties(in);
          } else
            sub = loadProperties(in);
          doPropertyIncludes(next, sub, done);
          p.putAll(sub);
          in.close();
        } catch( FileNotFoundException e ) {
          if ( noFileOk )
            error("Can not find included file: " + value );
        } catch (IOException e) {
          if ( noFileOk )
            error("Error in processing included file: " + value + "(" + e + ")");
        }
      }
    }
  }

  /**
   * Add the uses clauses
   *
   * @param exports
   * @param uses
   * @throws MojoExecutionException
   */
  void doUses(Map exports, Map uses) {
    for (Iterator i = exports.keySet().iterator(); i.hasNext();) {
      String packageName = (String) i.next();
      Map clause = (Map) exports.get(packageName);

      Set t = (Set) uses.get(packageName);
      if (t != null && !t.isEmpty()) {
        StringBuffer sb = new StringBuffer();
        String del = "";
        for (Iterator u = t.iterator(); u.hasNext();) {
          String usedPackage = (String) u.next();
          if (!usedPackage.equals(packageName)
              && !usedPackage.startsWith("java.")) {
            sb.append(del);
            sb.append(usedPackage);
            del = ",";
          }
        }
        String s = sb.toString();
        if (s.length() > 0)
          clause.put("uses:", sb.toString());
      }
    }
  }

  /**
   * Get a property with a proper default
   *
   * @param headerName
   * @param deflt
   * @return
   */
  String getProperty(String headerName, String deflt) {
    String v = getProperty(headerName);
    return v == null ? deflt : v;
  }

  /**
   * Helper to load a properties file from disk.
   *
   * @param file
   * @return
   * @throws IOException
   */
  Properties loadProperties(File file) throws IOException {
    InputStream in = new FileInputStream(file);
    return loadProperties(in);
  }

  Properties loadProperties(InputStream in) throws IOException {
    Properties p = new Properties();
    p.load(in);
    in.close();
    return p;
  }

  /**
   * Merge the attributes of two maps, where the first map can contain
   * wildcarded names. The idea is that the first map contains patterns (for
   * example *) with a set of attributes. These patterns are matched against
   * the found packages in actual. If they match, the result is set with the
   * merged set of attributes. It is expected that the instructions are
   * ordered so that the instructor can define which pattern matches first.
   * Attributes in the instructions override any attributes from the actual.<br/>
   *
   * A pattern is a modified regexp so it looks like globbing. The * becomes a .*
   * just like the ? becomes a .?. '.' are replaced with \\. Additionally, if
   * the pattern starts with an exclamation mark, it will remove that matches
   * for that pattern (- the !) from the working set. So the following
   * patterns should work:
   * <ul>
   * <li>com.foo.bar</li>
   * <li>com.foo.*</li>
   * <li>com.foo.???</li>
   * <li>com.*.[^b][^a][^r]</li>
   * <li>!com.foo.* (throws away any match for com.foo.*)</li>
   * </ul>
   * Enough rope to hang the average developer I would say.
   *
   *
   * @param instructions
   *            the instructions with patterns. A
   * @param actual
   *            the actual found packages
   */

  Map merge(String type, Map instructions, Map actual, Set superfluous) {
    actual = new HashMap(actual); // we do not want to ruin our original
    Map result = new HashMap();
    for (Iterator i = instructions.keySet().iterator(); i.hasNext();) {
      String instruction = (String) i.next();
      String originalInstruction = instruction;

      Map instructedAttributes = (Map) instructions.get(instruction);

      if (instruction.startsWith("=")) {
        result.put(instruction.substring(1), instructedAttributes);
        superfluous.remove(originalInstruction);
        continue;
      }

      Instruction instr = Instruction.getPattern(instruction);

      for (Iterator p = actual.keySet().iterator(); p.hasNext();) {
        String packageName = (String) p.next();

        if (instr.matches(packageName)) {
          superfluous.remove(originalInstruction);
          if (!instr.isNegated()) {
            Map newAttributes = new HashMap();
            newAttributes.putAll((Map) actual.get(packageName));
            newAttributes.putAll(instructedAttributes);
            result.put(packageName, newAttributes);
          } else {
            ignored.put(packageName, new HashMap());
          }
          p.remove(); // Can never match again for another pattern
        }
      }

    }
    return result;
  }

  /**
   * Print a standard Map based OSGi header.
   *
   * @param exports
   *            map { name => Map { attribute|directive => value } }
   * @return the clauses
   */
  String printClauses(Map exports, String allowedDirectives) {
    StringBuffer sb = new StringBuffer();
    String del = "";
    for (Iterator i = exports.keySet().iterator(); i.hasNext();) {
      String name = (String) i.next();
      Map map = (Map) exports.get(name);
      sb.append(del);
      sb.append(name);

      for (Iterator j = map.keySet().iterator(); j.hasNext();) {
        String key = (String) j.next();

        // Skip directives we do not recognize
        if (key.endsWith(":") && allowedDirectives.indexOf(key) < 0)
          continue;

        String value = (String) map.get(key);
        sb.append(";");
        sb.append(key);
        sb.append("=");
        boolean dirty = value.indexOf(',') >= 0
            || value.indexOf(';') >= 0;
        if (dirty)
          sb.append("\"");
        sb.append(value);
        if (dirty)
          sb.append("\"");
      }
      del = ",";
    }
    return sb.toString();
  }

  /**
   * Transitively remove all elemens from unreachable through the uses link.
   *
   * @param name
   * @param unreachable
   */
  void removeTransitive(String name, Set unreachable) {
    if (!unreachable.contains(name))
      return;

    unreachable.remove(name);

    Set ref = (Set) uses.get(name);
    if (ref != null) {
      for (Iterator r = ref.iterator(); r.hasNext();) {
        String element = (String) r.next();
        removeTransitive(element, unreachable);
      }
    }
  }

  /**
   * Helper method to set the package info
   *
   * @param dir
   * @param key
   * @param value
   */
  void setPackageInfo(String dir, String key, String value) {
    if (value != null) {
      String pack = dir.replace('/', '.');
      Map map = (Map) cpExports.get(pack);
      if (map == null) {
        map = new HashMap();
        cpExports.put(pack, map);
      }
      map.put(key, value);
    }
  }

}
TOP

Related Classes of aQute.lib.osgi.Analyzer

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.