/*
 * Copyright (c) 2005-2013, 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.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.jar.JarInputStream;
import java.util.zip.ZipEntry;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.FileScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Jar;
import org.apache.tools.ant.taskdefs.Manifest;
import org.apache.tools.ant.taskdefs.ManifestException;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ZipFileSet;

import org.osgi.framework.Version;

/**
 * <p>
 * An extension of the <a href="http://ant.apache.org/manual/CoreTasks/jar.html"
 * target="_top">Jar</a> task that builds an OSGi bundle. It can generate the
 * Bundle-Activator, Bundle-ClassPath and Import-Package manifest headers based
 * on the content specified in the task.
 * </p>
 *
 * <h3>Parameters</h3>
 * <table border="1px">
 * <tr>
 * <th align="left">Attribute</th>
 * <th align="left">Description</th>
 * <th align="left">Required</th>
 * </tr>
 * <tr>
 * <td valign="top">file</td>
 * <td valign="top">The bundle file to create.</td>
 * <td valign="top">Yes</td>
 * </tr>
 * <tr>
 * <td valign="top">activator</td>
 * <td valign="top">
 * The bundle activator class name. If set to "none" no Bundle-Activator
 * manifest header will be generated. If set to "auto" the bundle task will try
 * to find an activator in the included class files.</td>
 * <td valign="top">No, default is "auto"</td>
 * </tr>
 * <tr>
 * <td valign="top">packageanalysis</td>
 * <td valign="top">
 * Analyzes the class files of the bundle and the contents of the
 * <tt>exportpackage</tt> and <tt>importpackage</tt> nested elements.
 * <ul>
 * <li>none &ndash; no analysis is performed.</li>
 * <li>warn &ndash; a warning will be displayed for each referenced package not
 * found in the bundle or the <tt>importpackage</tt> nested elements.</li>
 * <li>auto &ndash; each referenced package not found in the bundle or the
 * <tt>importpackage</tt> nested elements will be added to the Import-Package
 * manifest header. Packages exported by the bundle will be added to the
 * Import-Package manifest header with a version range following the OSGi
 * versioning recommendation (if a package is incompatible with previous version
 * then the major number of the version must be incremented). E.g., if the
 * bundles exports version 1.0.2 the version range in the import will be
 * "[1.0.2,2)".</li>
 * </ul>
 * </td>
 * <td valign="top">No, default is "warn"</td>
 * </tr>
 * </table>
 *
 * <h3>Nested elements</h3>
 *
 * <h4>classes</h4>
 * <p>
 * The nested <tt>classes</tt> element specifies a <a
 * href="http://ant.apache.org/manual/CoreTypes/zipfileset.html"
 * target="_top">ZipFileSet</a>. The <tt>prefix</tt> attribute will be added to
 * the Bundle-ClassPath manifest header. The classes specified by the file set
 * will be included in the class analysis.
 * </p>
 *
 * <h4>lib</h4>
 * <p>
 * The nested <tt>lib</tt> element specifies a <a
 * href="http://ant.apache.org/manual/CoreTypes/zipfileset.html"
 * target="_top">ZipFileSet</a>. The locations of all files in the file set will
 * be added to the Bundle-ClassPath manifest header. All files of this file set
 * must be either zip or jar files. The classes available in the zip or jar
 * files will be included in the class analysis.
 * </p>
 *
 * <h4>exportpackage</h4>
 * <p>
 * The nested <tt>exportpackage</tt> element specifies the name and version of a
 * package to add to the Export-Package manifest header. If package analysis is
 * not turned off, a warning will be issued if the specified package cannot be
 * found in the bundle. When package analysis is turned on the version from the
 * <tt>packageinfo</tt>-file in the directory of the package is read and used as
 * the version of the exported package. In this case, if a version is specified
 * in the <tt>exportpackage</tt> element that version is compared with the one
 * from the <tt>packageinfo</tt>-file and if there is a mismatch a build error
 * will be issued.
 * </p>
 *
 * <h4>importpackage</h4>
 * <p>
 * The nested <tt>importpackage</tt> element specifies the name and version of a
 * package to add to the Import-Package manifest header.
 * </p>
 *
 * <h4>standardpackage</h4>
 * <p>
 * The nested <tt>standardpackage</tt> element specifies the name or prefix of a
 * package that should be excluded from the package analysis. It can be used to
 * avoid importing packages that are available in the underlying runtime
 * environment (i.e., via boot delegation).
 * </p>
 *
 * <h3>Implicit file set</h3>
 * <p>
 * The implicit fileset is specified by the <tt>baseDir</tt> attribute of the
 * bundle task and the nested <tt>include</tt> and <tt>exclude</tt> elements.
 * </p>
 * <p>
 * The implicit fileset of the <tt>bundle</tt> task will be included in the
 * class analysis and in the Bundle-ClassPath manifest header if needed.
 * </p>
 *
 * <h3>Examples</h3>
 *
 * <pre>
 * &lt;bundle activator="auto"
 *         packageanalysis="auto"
 *         file="out/${ant.project.name}.jar"&gt;
 *
 *   &lt;standardpackage name="javax.imageio"/&gt;
 *
 *   &lt;exportpackage name="se.weilenmann.bundle.test" version="1.0"/&gt;
 *
 *   &lt;manifest&gt;
 *     &lt;attribute name="Bundle-Name" value="testbundle"/&gt;
 *     &lt;attribute name="Bundle-Version" value="1.0"/&gt;
 *     &lt;attribute name="Bundle-Vendor" value="Kaspar Weilenmann"/&gt;
 *   &lt;/manifest&gt;
 *
 *   &lt;classes dir="out/classes"&gt;
 *     &lt;include name="se/weilenmann/bundle/test/**"/&gt;
 *   &lt;/classes&gt;
 *   &lt;classes dir="out/classes" prefix="util"&gt;
 *     &lt;include name="se/weilenmann/util/**"/&gt;
 *   &lt;/classes&gt;
 *   &lt;classes src="osgi/jars/log/log_api.jar" prefix="log_api"&gt;
 *     &lt;include name="*&#42;/*.class"/&gt;
 *   &lt;/classes&gt;
 *
 *   &lt;lib dir="osgi/jars/cm" includes="cm_api.jar" prefix="osgi"/&gt;
 *   &lt;lib dir="lib/commons" includes="commons-logging.jar" prefix="commons"/&gt;
 *
 * &lt;/bundle&gt;
 * </pre>
 * <p>
 * Creates a bundle with the following manifest:
 * <p>
 *
 * <pre>
 * Manifest-Version: 1.0
 * Ant-Version: Apache Ant 1.6.2
 * Created-By: 1.4.2_02-b03 (Sun Microsystems Inc.)
 * Bundle-Name: testbundle
 * Bundle-Version: 1.0
 * Bundle-Vendor: Kaspar Weilenmann
 * Bundle-Activator: se.weilenmann.bundle.test.Activator
 * Bundle-ClassPath: .,util,log_api,osgi/cm_api.jar,commons/commons-loggi
 * ng.jar
 * Import-Package: se.weilenmann.bundle.test;specification-version=1.0,or
 * g.osgi.framework
 * Export-Package: se.weilenmann.bundle.test;specification-version=1.0
 * </pre>
 *
 * @author <a href="mailto:kaspar@weilenmann.se">Kaspar Weilenmann</a>
 */
public class Bundle
  extends Jar
{

  // private fields

  private static final String BUNDLE_CLASS_PATH_KEY = "Bundle-ClassPath";
  private static final String BUNDLE_ACTIVATOR_KEY = "Bundle-Activator";
  private static final String IMPORT_PACKAGE_KEY = "Import-Package";
  private static final String EXPORT_PACKAGE_KEY = "Export-Package";

  private static final String ACTIVATOR_NONE = "none";
  private static final String ACTIVATOR_AUTO = "auto";

  private static final String PACKAGE_ANALYSIS_NONE = "none";
  private static final String PACKAGE_ANALYSIS_WARN = "warn";
  private static final String PACKAGE_ANALYSIS_AUTO = "auto";

  private String activator = ACTIVATOR_AUTO;
  private String packageAnalysis = PACKAGE_ANALYSIS_WARN;
  private final Map<String, String> importPackage =
    new HashMap<String, String>();
  private final Map<String, String> exportPackage =
    new HashMap<String, String>();
  private final List<ZipFileSet> libs = new ArrayList<ZipFileSet>();
  private final List<ZipFileSet> classes = new ArrayList<ZipFileSet>();

  private File baseDir = null;
  private final List<FileSet> zipgroups = new ArrayList<FileSet>();
  private final List<FileSet> srcFilesets = new ArrayList<FileSet>();

  private final Manifest generatedManifest = new Manifest();

  private final Set<String> standardPackagePrefixes = new HashSet<String>();
  {
    standardPackagePrefixes.add("java.");
  }

  private final BundlePackagesInfo bpInfo = new BundlePackagesInfo(this);
  private final ClassAnalyserASM asmAnalyser = new ClassAnalyserASM(bpInfo,
                                                                    this);

  private void analyze()
  {
    if (activator == ACTIVATOR_AUTO || packageAnalysis != PACKAGE_ANALYSIS_NONE) {
      addZipGroups();
      addImplicitFileset();

      for (final FileSet fileset : srcFilesets) {
        for (@SuppressWarnings("unchecked")
        final Iterator<Resource> fsIt = fileset.iterator(); fsIt.hasNext();) {
          final Resource res = fsIt.next();
          analyze(res);
        }
      }
      // Scan done
      bpInfo.toJavaNames();

      final Set<String> publicPackages = exportPackage.keySet();

      if (packageAnalysis != PACKAGE_ANALYSIS_NONE) {
        for (final String packageName : publicPackages) {
          if (!bpInfo.providesPackage(packageName)) {
            log("Exported package not provided by bundle: " + packageName,
                Project.MSG_WARN);
          }
          // The Version from the packageinfo-file or null
          final Version piVersion =
            bpInfo.getProvidedPackageVersion(packageName);
          if (null != piVersion) {
            final String epVersionS = exportPackage.get(packageName);
            if (null == epVersionS) {
              // Use the version form the packageinfo-file
              exportPackage.put(packageName, piVersion.toString());
            } else {
              // Check that the versions match, if not trigger a build error
              try {
                final Version epVersion = Version.parseVersion(epVersionS);
                if (0 != epVersion.compareTo(piVersion)) {
                  final String msg =
                    "Multiple versions found for export of " + "the package '"
                        + packageName + "'. The packageinfo file ("
                        + bpInfo.getProvidedPackageVersionSource(packageName)
                        + ") states '" + piVersion.toString()
                        + "' but the <exportpackage> element says '"
                        + epVersion.toString() + "'.";
                  log(msg, Project.MSG_ERR);
                  throw new BuildException(msg);
                }
              } catch (final IllegalArgumentException iae) {
                final String msg =
                  "Invalid version '" + epVersionS
                      + "' in <exportpackage name=\"" + packageName
                      + "\" ...>: " + iae.getMessage();
                log(msg, Project.MSG_ERR);
                throw new BuildException(msg, iae);
              }
            }
          }
        }
      }

      final SortedSet<String> privatePackages = bpInfo.getProvidedPackages();
      privatePackages.removeAll(publicPackages);

      final SortedSet<String> referencedPackages =
        bpInfo.getReferencedPackages();
      referencedPackages.removeAll(privatePackages);
      for (final Object element : referencedPackages) {
        final String packageName = (String) element;
        if (!isStandardPackage(packageName)
            && !importPackage.containsKey(packageName)) {
          if (packageAnalysis == PACKAGE_ANALYSIS_AUTO) {
            final String version = exportPackage.get(packageName);
            try {
              importPackage.put(packageName, toImportRange(version));
            } catch (final IllegalArgumentException iae) {
              final String msg =
                "Invalid version value, '" + version
                    + "' for exported package \"" + packageName
                    + "\" can not derive version range for auto-import. "
                    + iae.getMessage();
              log(msg, Project.MSG_ERR);
              throw new BuildException(msg, iae);
            }
          } else if (packageAnalysis == PACKAGE_ANALYSIS_WARN) {
            log("Referenced package not found in bundle or imports: "
                + packageName, Project.MSG_WARN);
          }
        }
      }
    }
  }

  /**
   * Analyze a resource by checking its suffix and delegate to
   * {@link #analyzeClass(Resource)}, {@link #analyzeJar(Resource)}, etc.
   *
   * @param res
   *          The resource to be analyzed.
   */
  protected void analyze(Resource res)
      throws BuildException
  {
    if (res.getName().endsWith(".class")) {
      analyzeClass(res);
    } else if (res.getName().endsWith(".jar")) {
      analyzeJar(res);
    } else if (res.getName().endsWith("/packageinfo")) {
      analyzePackageinfo(res);
    } else {
      // Just ignore all other files
    }
  }

  protected void analyzeJar(Resource res)
      throws BuildException
  {
    log("Analyze jar file " + res, Project.MSG_VERBOSE);

    try {
      final JarInputStream jarStream = new JarInputStream(res.getInputStream());

      ZipEntry ze = jarStream.getNextEntry();
      while (null != ze) {
        final String fileName = ze.getName();
        if (fileName.endsWith(".class")) {
          log("Analyze jar class file " + fileName, Project.MSG_VERBOSE);
          asmAnalyser.analyseClass(jarStream, fileName);
        }
        ze = jarStream.getNextEntry();
      }
    } catch (final Exception e) {
      e.printStackTrace();
      throw new BuildException("Failed to analyze class-file " + res
                               + ", exception=" + e, getLocation());
    }
  }

  protected void analyzeClass(Resource res)
      throws BuildException
  {
    log("Analyze class file " + res, Project.MSG_VERBOSE);

    try {
      asmAnalyser.analyseClass(res.getInputStream(), res.toString());
    } catch (final BuildException be) {
      throw be;
    } catch (final Exception e) {
      e.printStackTrace();
      throw new BuildException("Failed to analyze class-file " + res
                               + ", exception=" + e, getLocation());
    }
  }

  protected void analyzePackageinfo(Resource res)
      throws BuildException
  {
    log("Analyze packageinfo file " + res, Project.MSG_VERBOSE);

    bpInfo.setPackageVersion(res);
  }

  private void addZipGroups()
  {
    for (int i = 0; i < zipgroups.size(); i++) {
      final FileSet fileset = zipgroups.get(i);
      final FileScanner fs = fileset.getDirectoryScanner(getProject());
      final String[] files = fs.getIncludedFiles();
      final File basedir = fs.getBasedir();
      for (final String file : files) {
        final ZipFileSet zipfileset = new ZipFileSet();
        zipfileset.setSrc(new File(basedir, file));
        srcFilesets.add(zipfileset);
      }
    }
  }

  private void addImplicitFileset()
  {
    if (baseDir != null) {
      final FileSet fileset = (FileSet) getImplicitFileSet().clone();
      fileset.setDir(baseDir);
      srcFilesets.add(fileset);
    }
  }

  private boolean isStandardPackage(String packageName)
  {
    for (final String prefix : standardPackagePrefixes) {
      if (packageName.startsWith(prefix)) {
        return true;
      }
    }
    return false;
  }

  private void handleActivator()
      throws ManifestException
  {
    if (activator == ACTIVATOR_NONE) {
      log("No BundleActivator set", Project.MSG_DEBUG);
    } else if (activator == ACTIVATOR_AUTO) {
      switch (bpInfo.countProvidedActivatorClasses()) {
      case 0: {
        log("No class implementing BundleActivator found", Project.MSG_INFO);
        break;
      }
      case 1: {
        activator = bpInfo.getActivatorClass();
        break;
      }
      default: {
        log("More than one class implementing BundleActivator found: "
                + bpInfo.providedActivatorClassesAsString(), Project.MSG_WARN);
        break;
      }
      }
    }
    if (activator != ACTIVATOR_NONE && activator != ACTIVATOR_AUTO) {
      log("Bundle-Activator: " + activator, Project.MSG_INFO);
      generatedManifest
          .addConfiguredAttribute(createAttribute(BUNDLE_ACTIVATOR_KEY,
                                                  activator));
    }
  }

  private void handleClassPath()
      throws ManifestException
  {
    final StringBuffer value = new StringBuffer();

    boolean rootIncluded = false;
    if (baseDir != null || classes.size() == 0) {
      value.append(".,");
      rootIncluded = true;
    }

    Iterator<ZipFileSet> i = classes.iterator();
    while (i.hasNext()) {
      final ZipFileSet zipFileSet = i.next();
      final String prefix = zipFileSet.getPrefix(getProject());
      if (prefix.length() > 0) {
        value.append(prefix);
        value.append(',');
      } else if (!rootIncluded) {
        value.append(".,");
        rootIncluded = true;
      }
    }

    i = libs.iterator();
    while (i.hasNext()) {
      final ZipFileSet fileset = i.next();
      if (fileset.getSrc(getProject()) == null) {
        final DirectoryScanner ds = fileset.getDirectoryScanner(getProject());
        final String[] files = ds.getIncludedFiles();
        if (files.length != 0) {
          zipgroups.add(fileset);
          final String prefix = fixPrefix(fileset.getPrefix(getProject()));
          for (final String file : files) {
            value.append(prefix.replace('\\', '/'));
            value.append(file.replace('\\', '/'));
            value.append(',');
          }
        }
      }
    }

    if (value.length() > 2) {
      generatedManifest
          .addConfiguredAttribute(createAttribute(BUNDLE_CLASS_PATH_KEY, value
              .substring(0, value.length() - 1)));
    }
  }

  private static String fixPrefix(String prefix)
  {
    if (prefix.length() > 0) {
      final char c = prefix.charAt(prefix.length() - 1);
      if (c != '/' && c != '\\') {
        prefix = prefix + "/";
      }
    }
    return prefix;
  }

  private void addPackageHeader(String headerName,
                                Map<String, String> packageMap)
      throws ManifestException
  {
    final Iterator<Entry<String,String>> i = packageMap.entrySet().iterator();
    if (i.hasNext()) {
      final StringBuffer valueBuffer = new StringBuffer();
      while (i.hasNext()) {
        final Entry<String, String> entry = i.next();
        final String name = entry.getKey();
        String version = entry.getValue();
        valueBuffer.append(name);
        if (version != null) {
          version = version.trim();
          if (0 < version.length()) {
            valueBuffer.append(";version=");
            final boolean quotingNeeded =
              -1 != version.indexOf(',') && '"' != version.charAt(0);
            if (quotingNeeded) {
              valueBuffer.append('"');
            }
            valueBuffer.append(version);
            if (quotingNeeded) {
              valueBuffer.append('"');
            }
          }
        }
        valueBuffer.append(',');
      }
      valueBuffer.setLength(valueBuffer.length() - 1);
      final String value = valueBuffer.toString();
      generatedManifest.addConfiguredAttribute(createAttribute(headerName,
                                                               value));
      log(headerName + ": " + value, Project.MSG_INFO);
    }
  }

  private static Manifest.Attribute createAttribute(String name, String value)
  {
    final Manifest.Attribute attribute = new Manifest.Attribute();
    attribute.setName(name);
    attribute.setValue(value);
    return attribute;
  }

  /**
   * Given a precise version create a version range suitable for an import
   * package specification. Currently an input version of M.N.U.Q will result in
   * an output range "[M.N.U.Q, M+1)" following the version usage recommended by
   * OSGi (a package that is not backwards compatible must increment the major
   * number in its version number).
   *
   * @param version
   *          an OSGi compatibel version on string form.
   * @return a quoted version range starting with the given version (inclusive)
   *         ending with the next major version (exclusive). If the specified
   *         version is <code>null</code> or an empty string a <code>null</code>
   *         is returned.
   */
  private static String toImportRange(final String version)
      throws IllegalArgumentException
  {
    if (null == version || 0 == version.length()) {
      return null;
    }

    final Version vStart = Version.parseVersion(version);
    final Version vEnd = new Version(vStart.getMajor() + 1, 0, 0, null);
    return "\"[" + vStart.toString() + "," + vEnd.toString() + ")\"";
  }

  // public methods

  public void setActivator(String activator)
  {
    if (ACTIVATOR_NONE.equalsIgnoreCase(activator)) {
      this.activator = ACTIVATOR_NONE;
    } else if (ACTIVATOR_AUTO.equalsIgnoreCase(activator)) {
      this.activator = ACTIVATOR_AUTO;
    } else {
      this.activator = activator;
    }
  }

  public void setPackageAnalysis(String packageAnalysis)
  {
    packageAnalysis = packageAnalysis.trim().toLowerCase();
    if (PACKAGE_ANALYSIS_NONE.equals(packageAnalysis)) {
      this.packageAnalysis = PACKAGE_ANALYSIS_NONE;
    } else if (PACKAGE_ANALYSIS_WARN.equals(packageAnalysis)) {
      this.packageAnalysis = PACKAGE_ANALYSIS_WARN;
    } else if (PACKAGE_ANALYSIS_AUTO.equals(packageAnalysis)) {
      this.packageAnalysis = PACKAGE_ANALYSIS_AUTO;
    } else {
      throw new BuildException("Illegal value: " + packageAnalysis);
    }
  }

  public void addConfiguredStandardPackage(OSGiPackage osgiPackage)
  {
    final String name = osgiPackage.getName();
    final String prefix = osgiPackage.getPrefix();
    if (name != null && prefix == null) {
      bpInfo.addProvidedPackage(name);
    } else if (prefix != null && name == null) {
      standardPackagePrefixes.add(prefix);
    } else {
      throw new BuildException(
                               "StandardPackage must have exactly one of the name and prefix attributes defined");
    }
  }

  public void addConfiguredImportPackage(OSGiPackage osgiPackage)
  {
    final String name = osgiPackage.getName();
    if (name == null) {
      throw new BuildException("ImportPackage must have a name");
    } else if (osgiPackage.getPrefix() != null) {
      throw new BuildException("ImportPackage must not have a prefix attribute");
    } else {
      importPackage.put(name, osgiPackage.getVersion());
    }
  }

  public void addConfiguredExportPackage(OSGiPackage osgiPackage)
  {
    final String name = osgiPackage.getName();
    if (name == null) {
      throw new BuildException("ExportPackage must have a name");
    } else if (osgiPackage.getPrefix() != null) {
      throw new BuildException("ExportPackage must not have a prefix attribute");
    } else {
      exportPackage.put(name, osgiPackage.getVersion());
    }
  }

  public void addConfiguredLib(ZipFileSet fileset)
  {
    // TODO: handle refid
    if (fileset.getSrc(getProject()) == null) {
      addFileset(fileset);
      libs.add(fileset);
    } else {
      addClasses(fileset);
    }
  }

  public void addClasses(ZipFileSet fileset)
  {
    super.addZipfileset(fileset);
    srcFilesets.add(fileset);
    classes.add(fileset);
  }

  // extends Jar

  @Override
  public void execute()
  {
    try {
      handleClassPath();

      analyze();

      handleActivator();

      addPackageHeader(IMPORT_PACKAGE_KEY, importPackage);
      addPackageHeader(EXPORT_PACKAGE_KEY, exportPackage);

      // TODO: better merge may be needed, currently overwrites
      // pre-existing headers
      addConfiguredManifest(generatedManifest);
    } catch (final ManifestException me) {
      throw new BuildException("Error merging manifest headers", me);
    }
    super.execute();
  }

  @Override
  public void setBasedir(File baseDir)
  {
    super.setBasedir(baseDir);
    this.baseDir = baseDir;
  }

  @Override
  public void addZipGroupFileset(FileSet fileset)
  {
    super.addZipGroupFileset(fileset);
    zipgroups.add(fileset);
  }

  @Override
  public void addZipfileset(ZipFileSet fileset)
  {
    super.addZipfileset(fileset);
  }

} // Bundle
