All Downloads are FREE. Search and download functionalities are using the official Maven repository.

aQute.bnd.osgi.Analyzer Maven / Gradle / Ivy

The newest version!
package aQute.bnd.osgi;

import static aQute.bnd.exceptions.FunctionWithException.asFunctionOrElse;
import static aQute.bnd.exceptions.PredicateWithException.asPredicate;
import static aQute.libg.generics.Create.list;
import static aQute.libg.generics.Create.map;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static org.osgi.framework.Constants.RESOLUTION_MANDATORY;
import static org.osgi.framework.Constants.RESOLUTION_OPTIONAL;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URL;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.osgi.framework.namespace.ExecutionEnvironmentNamespace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aQute.bnd.annotation.Export;
import aQute.bnd.apiguardian.api.API;
import aQute.bnd.classindex.ClassIndexerAnalyzer;
import aQute.bnd.exceptions.ConsumerWithException;
import aQute.bnd.exceptions.Exceptions;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.OSGiHeader;
import aQute.bnd.header.Parameters;
import aQute.bnd.http.HttpClient;
import aQute.bnd.osgi.Clazz.JAVA;
import aQute.bnd.osgi.Clazz.QUERY;
import aQute.bnd.osgi.Descriptors.Descriptor;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.service.AnalyzerPlugin;
import aQute.bnd.service.ManifestPlugin;
import aQute.bnd.service.OrderedPlugin;
import aQute.bnd.service.classparser.ClassParser;
import aQute.bnd.signatures.ClassSignature;
import aQute.bnd.signatures.FieldSignature;
import aQute.bnd.signatures.MethodSignature;
import aQute.bnd.stream.MapStream;
import aQute.bnd.version.Version;
import aQute.bnd.version.VersionRange;
import aQute.lib.base64.Base64;
import aQute.lib.collections.Iterables;
import aQute.lib.collections.MultiMap;
import aQute.lib.collections.SortedList;
import aQute.lib.date.Dates;
import aQute.lib.filter.Filter;
import aQute.lib.hex.Hex;
import aQute.lib.io.IO;
import aQute.lib.strings.Strings;
import aQute.lib.utf8properties.UTF8Properties;
import aQute.libg.cryptography.Digester;
import aQute.libg.cryptography.MD5;
import aQute.libg.cryptography.SHA1;
import aQute.libg.generics.Create;
import aQute.libg.glob.Glob;
import aQute.libg.reporter.ReporterMessages;
import aQute.libg.tuple.Pair;

/**
 * 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.
 *
 * 
 * *;auto=true             any
 * org.acme.*;auto=true    org.acme.xyz
 * org.[abc]*;auto=true    org.acme.xyz
 * 
* * 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. */ public class Analyzer extends Processor { private final static Logger logger = LoggerFactory.getLogger(Analyzer.class); private final static Version frameworkR7 = new Version("1.9"); private final SortedSet ees = new TreeSet<>(); // Bundle parameters private Jar dot; private final Packages contained = new Packages(); private final Packages referred = new Packages(); private Packages exports; private Packages imports; private TypeRef activator; // Global parameters private final MultiMap uses = new MultiMap<>(PackageRef.class, PackageRef.class, true); private final MultiMap apiUses = new MultiMap<>(PackageRef.class, PackageRef.class, true); private final Contracts contracts = new Contracts(this); private final Packages classpathExports = new Packages(); private final Descriptors descriptors = new Descriptors(); private final List classpath = list(); private final Map classspace = map(); private final Map lookAsideClasses = map(); private final Map importedClassesCache = map(); private boolean analyzed = false; private boolean diagnostics = false; private boolean inited = false; final protected AnalyzerMessages msgs = ReporterMessages.base(this, AnalyzerMessages.class); private AnnotationHeaders annotationHeaders; private final Set packagesVisited = new HashSet<>(); private Set nonClassReferences = new HashSet<>(); private Set checks; private final Map bcpTypes = map(); final TypeRef providerType = getTypeRef( "org/osgi/annotation/versioning/ProviderType"); public enum Check { ALL, IMPORTS, EXPORTS; } public Analyzer() {} public Analyzer(Jar jar) throws Exception { this.dot = Objects.requireNonNull(jar, "the jar must not be null"); Manifest manifest = dot.getManifest(); if (manifest != null) copyFrom(Domain.domain(manifest)); } public Analyzer(Processor parent) { super(parent); } @Override protected void setTypeSpecificPlugins(PluginsContainer pluginsContainer) { super.setTypeSpecificPlugins(pluginsContainer); pluginsContainer.add(new ClassIndexerAnalyzer()); } /** * Specifically for Maven */ public static Properties getManifest(File dirOrJar) throws Exception { try (Analyzer analyzer = new Analyzer()) { analyzer.setJar(dirOrJar); Properties properties = new UTF8Properties(); properties.put(IMPORT_PACKAGE, "*"); properties.put(EXPORT_PACKAGE, "*"); analyzer.setProperties(properties); Manifest m = analyzer.calcManifest(); Properties result = new UTF8Properties(); for (Object key : m.getMainAttributes() .keySet()) { Attributes.Name name = (Attributes.Name) key; result.put(name.toString(), m.getMainAttributes() .getValue(name)); } return result; } } /** * Calculates the data structures for generating a manifest. * * @throws IOException */ public void analyze() throws Exception { bracketed(this::analyze0); } private void analyze0() throws Exception { if (!analyzed) { analyzed = true; analyzeContent(); Instructions instructions = new Instructions( OSGiHeader.parseHeader(getProperty(Constants.BUNDLEANNOTATIONS, "*"))); annotationHeaders = new AnnotationHeaders(this, instructions); doPlugins(); // // calculate class versions in use // classspace.values() .stream() .filter(c -> !c.isModule()) .map(Clazz::getFormat) .forEach(ees::add); try (ClassDataCollectors cds = new ClassDataCollectors(this)) { List parsers = getPlugins(ClassParser.class); for (ClassParser cp : parsers) { cds.add(cp.getClassDataCollector(this)); } // // built ins // cds.add(annotationHeaders); for (Clazz c : classspace.values()) { cds.parse(c); } } referred.keySet() .removeAll(contained.keySet()); // // EXPORTS // { Set unused = Create.set(); Instructions filter = new Instructions(getExportPackage()); filter.appendIfAbsent(getExportContents()); // // get the packages exported by the annotation. However, // we remove any explicitly explicitly named private package so // that // you can always override an annotation // Parameters exportedByAnnotation = getExportedByAnnotation(); exportedByAnnotation.keySet() .removeAll(getPrivatePackage().keySet()); filter.appendIfAbsent(exportedByAnnotation); exports = filter(filter, contained, unused); if (!unused.isEmpty()) { warning("Unused " + Constants.EXPORT_PACKAGE + " instructions: %s ", unused) .header(Constants.EXPORT_PACKAGE) .context(unused.iterator() .next() .getInput()); } // See what information we can find to augment the // exports. I.e. look on the classpath augmentExports(exports); } // // IMPORTS // Imports MUST come after exports because we use information from // the exports // { // Add all exports that do not have an -noimport: directive // to the imports. Packages referredAndExported = new Packages(referred); referredAndExported.putAll(doExportsToImports(exports)); removeDynamicImports(referredAndExported); getHostPackages().ifPresent(hostPackages -> referredAndExported.keySet() .removeAll(hostPackages)); getRequireBundlePackages().ifPresent(hostPackages -> referredAndExported.keySet() .removeAll(hostPackages)); String h = getProperty(IMPORT_PACKAGE); if (h == null) // If not set use a default h = "*"; if (isPedantic() && h.trim() .length() == 0) warning("Empty " + Constants.IMPORT_PACKAGE + " header"); Instructions filter = new Instructions(h); Set unused = Create.set(); imports = filter(filter, referredAndExported, unused); if (!unused.isEmpty()) { // We ignore the end wildcard catch if (!(unused.size() == 1 && unused.iterator() .next() .isAny())) warning("Unused " + Constants.IMPORT_PACKAGE + " instructions: %s ", unused) .header(Constants.IMPORT_PACKAGE) .context(unused.iterator() .next() .getInput()); } augmentImports(imports, exports); boolean frameworkSupportsJavaImports = getExportVersion(getPackageRef("org.osgi.framework")) .map(actual -> actual.compareTo(frameworkR7) >= 0) .orElse(true); boolean elideImports = is(NOIMPORTJAVA) || !frameworkSupportsJavaImports; if (elideImports) { imports.keySet() .removeIf(PackageRef::isJava); } } // // USES // // Add the uses clause to the exports boolean api = true; // brave, // lets see doUses(exports, api ? apiUses : uses, imports); // // Verify that no exported package has a reference to a private // package // This can cause a lot of harm. // TODO restrict the check to public API only, but even then // exported packages // should preferably not refer to private packages. // Set privatePackages = getPrivates(); // References to java are not imported so they would show up as // private // packages, lets kill them as well. privatePackages.removeIf(PackageRef::isJava); for (PackageRef exported : exports.keySet()) { List used = uses.get(exported); if (used != null) { List apiUsed = apiUses.get(exported); if (apiUsed != null) { Set privateReferences = new TreeSet<>(apiUsed); privateReferences.retainAll(privatePackages); if (!privateReferences.isEmpty()) msgs.Export_Has_PrivateReferences_(exported, privateReferences.size(), privateReferences); } } } // // Checks // if (referred.containsKey(Descriptors.DEFAULT_PACKAGE)) { error( "The default package '.' is not permitted by the " + Constants.IMPORT_PACKAGE + " syntax.%n" + " This can be caused by compile errors in Eclipse because Eclipse creates%n" + "valid class files regardless of compile errors.%n" + "The following package(s) import from the default package %s", uses.transpose() .get(Descriptors.DEFAULT_PACKAGE)); } // Check for use of the deprecated bnd @Export annotation TypeRef bndAnnotation = descriptors.getTypeRefFromFQN(aQute.bnd.annotation.Export.class.getName()); contained.keySet() .stream() .map(this::getPackageInfo) .filter(Objects::nonNull) .distinct() .filter(clz -> clz.annotations() .contains(bndAnnotation)) .map(Clazz::getClassName) .map(TypeRef::getPackageRef) .map(PackageRef::getFQN) .forEach(fqn -> warning( "The annotation aQute.bnd.annotation.Export applied to package %s is deprecated and will be removed in a future release. The org.osgi.annotation.bundle.Export should be used instead", fqn)); } } /** * Get the export version of a package * * @param packageRef the package reference * @return the version or empty */ private Optional getExportVersion(PackageRef packageRef) { Attrs attrs = classpathExports.get(packageRef); String version; if (attrs == null || (version = attrs.getVersion()) == null || !Verifier.isVersion(version)) return Optional.empty(); return Optional.of(Version.parseVersion(version)); } public void reset() { contained.clear(); classspace.clear(); referred.clear(); uses.clear(); apiUses.clear(); classpathExports.clear(); contracts.clear(); packagesVisited.clear(); nonClassReferences.clear(); bcpTypes.clear(); } private void analyzeContent() throws Exception { // Parse all the classes in the // the jar according to the OSGi Bundle-ClassPath analyzeBundleClasspath(); // // Get exported packages from the // entries on the classpath and // collect any contracts // for (Jar current : getClasspath()) { getManifestInfoFromClasspath(current, classpathExports, contracts); Manifest m = current.getManifest(); if (m == null) { for (String dir : current.getDirectories() .keySet()) { learnPackage(current, "", getPackageRef(dir), classpathExports); } } } addDefinedContracts(); // Handle the bundle activator String s = getProperty(BUNDLE_ACTIVATOR); if (s != null && !s.isEmpty()) { activator = getTypeRefFromFQN(s); referTo(activator); logger.debug("activator {} {}", s, activator); } // Conditional packages doConditionalPackages(); } /** * Get the packages from the host if this is a fragment bundle * * @return the host packages or an empty set if not a fragment */ public Optional> getHostPackages() { Entry host = getFragmentHost(); if (host != null) { Jar jar = toJar(host); if (jar != null) { return Optional.of(jar.getDirectories() .keySet() .stream() .map(this::getPackageRef) .filter(this::isNormalPackage) .collect(toSet())); } } return Optional.empty(); } /** * Find the packages belonging to the required bundles * * @return the packages from the required bundles, with no Require-Bundle * return an empty Optional */ public Optional> getRequireBundlePackages() { Parameters required = getRequireBundle(); if (required.isEmpty()) return Optional.empty(); Set refs = required.entrySet() .stream() .map(this::toJar) .filter(Objects::nonNull) .map(asFunctionOrElse(Jar::getManifest, null)) .filter(Objects::nonNull) .flatMap(manifest -> { Domain domain = Domain.domain(manifest); Parameters exportPackages = domain.getExportPackage(); return exportPackages.keySet() .stream(); }) .map(this::getPackageRef) .filter(this::isNormalPackage) .collect(toSet()); return Optional.of(refs); } private boolean isNormalPackage(PackageRef pRef) { return !pRef.isJava() && !pRef.isMetaData(); } private Jar toJar(Map.Entry host) { String bsn = host.getKey(); String v = host.getValue() .get(Constants.BUNDLE_VERSION_ATTRIBUTE); Jar jar = findClasspathEntry(bsn, v); if ((jar == null) && !"system.bundle".equals(bsn)) { warning("Host %s for this fragment/require bundle cannot be found on the classpath", host); } return jar; } private final static String STATUS_PROPERTY = "status"; private Parameters getExportedByAnnotation() { TypeRef exportAnnotation = descriptors.getTypeRef("org/osgi/annotation/bundle/Export"); Parameters exportedByAnnotation = getContained().keySet() .stream() .map(this::getPackageInfo) .filter(Objects::nonNull) .distinct() .filter(c -> c.annotations() .contains(exportAnnotation)) .map(Clazz::getClassName) .map(TypeRef::getPackageRef) .map(PackageRef::getFQN) .collect(Parameters.toParameters()); // Opt-in is required for processing API Guardian annotations Parameters headerAPIGuardian = OSGiHeader.parseHeader(getProperty(Constants.EXPORT_APIGUARDIAN)); if (headerAPIGuardian.isEmpty()) { return exportedByAnnotation; } Instructions instructions = new Instructions(headerAPIGuardian); TypeRef apiAnnotation = descriptors.getTypeRef("org/apiguardian/api/API"); Parameters exportedByAPIGuardian = new Parameters(false); MapStream.of(getClassspace().values() .stream() .filter(c -> !c.isModule() && !c.isInnerClass() && (!c.isSynthetic() || c.isPackageInfo()) && c.annotations() .contains(apiAnnotation)) .flatMap(c -> instructions.matchesStream(c.getFQN()) .mapKey(instruction -> c) .entries())) .forEachOrdered((c, a) -> c.annotations(apiAnnotation.getBinary()) .map(ann -> API.Status.valueOf(ann.get(STATUS_PROPERTY))) .max(API.Status::compareTo) .ifPresent(status -> exportedByAPIGuardian.computeIfAbsent(c.getClassName() .getPackageRef() .getFQN(), k -> new Attrs(a)) .compute(STATUS_PROPERTY, (k, v) -> (v == null) ? status.name() : (API.Status.valueOf(v) .compareTo(status) > 0 ? v : status.name())))); exportedByAPIGuardian.values() .stream() .filter(attrs -> API.Status.valueOf(attrs.get(STATUS_PROPERTY)) == API.Status.INTERNAL) .forEach(attrs -> { attrs.put(Constants.MANDATORY_DIRECTIVE, STATUS_PROPERTY); attrs.put(Constants.NO_IMPORT_DIRECTIVE, "true"); }); exportedByAnnotation.mergeWith(exportedByAPIGuardian, false); return exportedByAnnotation; } // Handle org.osgi.annotation.bundle.Referenced annotation private Set referencesByAnnotation(Clazz clazz) { TypeRef referencedAnnotation = descriptors.getTypeRef("org/osgi/annotation/bundle/Referenced"); if (clazz.annotations() .contains(referencedAnnotation)) { Set referenced = clazz.annotations(referencedAnnotation.getBinary()) .flatMap(ann -> ann.stream("value", TypeRef.class)) .map(TypeRef::getPackageRef) .collect(toSet()); return referenced; } return Collections.emptySet(); } public Clazz getPackageInfo(PackageRef packageRef) { TypeRef tr = descriptors.getPackageInfo(packageRef); try { return findClass(tr); } catch (Exception e) { return null; } } private void doConditionalPackages() throws Exception { // // We need to find out the contained packages // again ... so we need to clear any visited // packages otherwise new packages are not // added to contained // packagesVisited.clear(); for (Jar extra; (extra = getExtra()) != null;) { dot.addAll(extra); analyzeJar(extra, "", true, null, false); } } /* * Learn the package details from the Jar. This can either be the manifest * (in that case the attrs are already set on the package), a * package-info.java file, or a packageinfo file (in the given priority). * This method will only learn a package once, so this method an be called * for every class. It will also ignore any metadata or java packages. */ private void learnPackage(Jar jar, String prefix, PackageRef packageRef, Packages map) throws Exception { if (packageRef.isMetaData() || packageRef.isJava() || packageRef.isPrimitivePackage()) return; // // We should ignore empty directories/packages. Empty packages should // not take the package slot. See #708 // String packagePath = appendPath(prefix, packageRef.getBinary()); Map dir = jar.getDirectory(packagePath); if (dir == null || dir.isEmpty()) return; // // Make sure we only do this once for each package // in cp order (which is the calling order) // if (packagesVisited.contains(packageRef)) return; packagesVisited.add(packageRef); // // The manifest has priority over the packageinfo or package-info.java // so if any user defined attributes are set, skip this package info // Attrs attrs = map.get(packageRef); if (attrs != null && !attrs.select(AttributeClasses.MANIFEST) .isEmpty()) return; String jarname = map.getOrDefault(packageRef, Attrs.EMPTY_ATTRS) .getOrDefault(INTERNAL_SOURCE_DIRECTIVE, getName(jar)); Resource resource = jar.getResource(packagePath.concat("/package-info.class")); if (resource != null) { Attrs info = parsePackageInfoClass(resource); if (info != null && info.containsKey(VERSION_ATTRIBUTE)) { info.put(FROM_DIRECTIVE, resource.toString()); info.put(INTERNAL_SOURCE_DIRECTIVE, jarname); map.put(packageRef, info); return; } } resource = jar.getResource(packagePath.concat("/packageinfo")); if (resource != null) { Attrs info = parsePackageinfo(packageRef, resource); if (info != null) { info.put(FROM_DIRECTIVE, resource.toString()); info.putIfAbsent(INTERNAL_SOURCE_DIRECTIVE, getName(jar)); info.put(INTERNAL_SOURCE_DIRECTIVE, jarname); fixupOldStyleVersions(info); map.put(packageRef, info); return; } } // // We need to set an attribute because this is how the set // of available packages is remembered // map.put(packageRef) .put(INTERNAL_SOURCE_DIRECTIVE, jarname); // trace("%s from %s has no package info (either manifest, packageinfo // or package-info.class", // packageRef, jar); } protected String getName(Jar jar) throws Exception { String name = jar.getBsn(); if (name == null) { name = jar.getName(); if (name.equals("dot") && jar.getSource() != null) name = jar.getSource() .getName(); } String version = jar.getVersion(); if (version == null) version = "0.0.0"; return name + "-" + version; } /* * Helper method to set the package info resource */ final static Pattern OLD_PACKAGEINFO_SYNTAX_P = Pattern .compile("class\\s+(.+)\\s+version\\s+(" + Verifier.VERSION_S + ")"); Attrs parsePackageinfo(PackageRef packageRef, Resource r) throws Exception { Properties p = new UTF8Properties(); try { try (InputStream in = r.openInputStream()) { p.load(in); } Attrs attrs = new Attrs(); for (String key : Iterables.iterable(p.propertyNames(), String.class::cast)) { String propvalue = p.getProperty(key); if (key.equalsIgnoreCase("include")) { // Ouch, could be really old syntax // We ignore the include part upto we // ignored it long before. Matcher m = OLD_PACKAGEINFO_SYNTAX_P.matcher(propvalue); if (m.matches()) { key = Constants.VERSION_ATTRIBUTE; propvalue = m.group(2); if (isPedantic()) warning("found old syntax in package info in package %s, from resource %s", packageRef, r); } } String value = propvalue; // Messy, to allow directives we need to // allow the value to start with a ':' upto we cannot // encode this in a property name if (value.startsWith(":")) { key = key + ":"; value = value.substring(1); } attrs.put(key, value); } return attrs; } catch (Exception e) { msgs.NoSuchFile_(r); } return null; } /* * Parse the package-info.java class */ final static Pattern OBJECT_REFERENCE = Pattern.compile("([^.]+\\.)*([^.]+)"); private Attrs parsePackageInfoClass(Resource r) throws Exception { final Attrs info = new Attrs(); final Clazz clazz = new Clazz(this, "", r); clazz.parseClassFileWithCollector(new ClassDataCollector() { @Override public void annotation(Annotation a) { String name = a.getName() .getFQN(); switch (name) { case "org.osgi.annotation.versioning.Version" : // Check version String version = a.get("value"); if (!info.containsKey(Constants.VERSION_ATTRIBUTE)) { if (version != null) { String processedVersion = getReplacer().process(version); if (!Objects.equals(version, processedVersion)) { warning("In class %s," + " Version(\"%s\") does not have a literal version value and uses Bnd macros." + " This is highly unrecommended as it can produce results that vary depending upon the context in which it is evaluated.", clazz, version); version = processedVersion; } if (Verifier.VERSION.matcher(version) .matches()) info.put(VERSION_ATTRIBUTE, version); else error("Version annotation in %s has invalid version info: %s", clazz, version); } } else { // Verify this matches with packageinfo String presentVersion = info.get(VERSION_ATTRIBUTE); try { Version av = new Version(presentVersion).getWithoutQualifier(); Version bv = new Version(version).getWithoutQualifier(); if (!av.equals(bv)) { error("Version from annotation for %s differs with packageinfo or Manifest", clazz.getClassName() .getFQN()); } } catch (Exception e) { // Ignore } } break; case "org.osgi.annotation.versioning.ProviderType" : if (!info.containsKey(PROVIDE_DIRECTIVE)) { // let Export.substitution override ProviderType info.put(PROVIDE_DIRECTIVE, "true"); } break; case "org.osgi.annotation.bundle.Export" : Object[] usesClauses = a.get("uses"); if (usesClauses != null) { StringJoiner sj = new StringJoiner(","); String old = info.get(USES_DIRECTIVE); if (old != null) sj.add(old); for (Object usesClause : usesClauses) { sj.add(usesClause.toString()); } info.put(USES_DIRECTIVE, sj.toString()); } Object substitution = a.get("substitution"); if (substitution != null) { switch (substitution.toString() .toUpperCase(Locale.ROOT)) { case "CONSUMER" : info.put(PROVIDE_DIRECTIVE, "false"); break; case "PROVIDER" : info.put(PROVIDE_DIRECTIVE, "true"); break; case "NOIMPORT" : info.put(NO_IMPORT_DIRECTIVE, "true"); break; case "CALCULATED" : // nothing to do; this is the normal case break; default : error("Export annotation in %s has invalid substitution value: %s", clazz, substitution); break; } } Object[] attributes = a.get("attribute"); if (attributes != null) { for (Object attribute : attributes) { info.mergeWith(OSGiHeader.parseProperties(attribute.toString(), Analyzer.this), false); } } break; case "aQute.bnd.annotation.Export" : // Check mandatory attributes Attrs attrs = doAttrbutes(a.get(Export.MANDATORY), clazz, getReplacer()); if (!attrs.isEmpty()) { info.putAll(attrs); info.put(MANDATORY_DIRECTIVE, Processor.join(attrs.keySet())); } // Check optional attributes attrs = doAttrbutes(a.get(Export.OPTIONAL), clazz, getReplacer()); if (!attrs.isEmpty()) { info.putAll(attrs); } // Check Included classes Object[] included = a.get(Export.INCLUDE); if (included != null && included.length > 0) { StringBuilder sb = new StringBuilder(); String del = ""; for (Object i : included) { Matcher m = OBJECT_REFERENCE.matcher(((TypeRef) i).getFQN()); if (m.matches()) { sb.append(del); sb.append(m.group(2)); del = ","; } } info.put(INCLUDE_DIRECTIVE, sb.toString()); } // Check Excluded classes Object[] excluded = a.get(Export.EXCLUDE); if (excluded != null && excluded.length > 0) { StringBuilder sb = new StringBuilder(); String del = ""; for (Object i : excluded) { Matcher m = OBJECT_REFERENCE.matcher(((TypeRef) i).getFQN()); if (m.matches()) { sb.append(del); sb.append(m.group(2)); del = ","; } } info.put(EXCLUDE_DIRECTIVE, sb.toString()); } // Check Uses Object[] uses = a.get(Export.USES); if (uses != null && uses.length > 0) { String old = info.get(USES_DIRECTIVE); if (old == null) old = ""; StringBuilder sb = new StringBuilder(old); String del = sb.length() == 0 ? "" : ","; for (Object use : uses) { sb.append(del); sb.append(use); del = ","; } info.put(USES_DIRECTIVE, sb.toString()); } break; } } }); return info; } /** * Discussed with BJ and decided to kill the . * * @param referredAndExported */ void removeDynamicImports(Packages referredAndExported) { // // Remove any matching a dynamic import package instruction // Instructions dynamicImports = new // Instructions(getDynamicImportPackage()); // Collection dynamic = dynamicImports.select( // referredAndExported.keySet(), false); // referredAndExported.keySet().removeAll(dynamic); } protected Jar getExtra() throws Exception { return null; } /* * Call AnalyzerPlugins to analyze the content. */ private void doPlugins() { doPlugins(AnalyzerPlugin.class, (plugin) -> { boolean reanalyze; Processor previous = beginHandleErrors(plugin.toString()); try { reanalyze = plugin.analyzeJar(this); } finally { endHandleErrors(previous); } if (reanalyze) { // Builder adds -internal-source information Map sourceInformation = contained.stream() .mapValue(attrs -> attrs.get(INTERNAL_SOURCE_DIRECTIVE)) .filterValue(Objects::nonNull) .collect(MapStream.toMap()); reset(); analyzeContent(); // Restore -internal-source information // if the package still exists sourceInformation.forEach((pkgRef, source) -> { Attrs attrs = contained.get(pkgRef); if (attrs != null) { attrs.put(INTERNAL_SOURCE_DIRECTIVE, source); } }); } }); } /* * Call plugins to analyze the content. */ private void doPlugins(Class type, ConsumerWithException cb) { List plugins = getPlugins(type); plugins.sort(Comparator.comparingInt(OrderedPlugin::ordering)); for (T plugin : plugins) { try { cb.accept(plugin); } catch (Exception e) { exception(e, "Analyzer Plugin %s failed %s for %s", plugin, e, type.getSimpleName()); } } } /** * @return {@code true} if the {@code -resourceonly} instruction is set, * {@code false} otherwise */ boolean isResourceOnly() { return isTrue(getProperty(RESOURCEONLY)); } /** * One of the main workhorses of this class. This will analyze the current * setup and calculate a new manifest according to this setup. * * @throws IOException */ public Manifest calcManifest() throws Exception { return bracketed(this::calcManifest0); } private Manifest calcManifest0() throws Exception { try { analyze(); Manifest manifest = new Manifest(); Attributes main = manifest.getMainAttributes(); main.put(Attributes.Name.MANIFEST_VERSION, "1.0"); main.putValue(BUNDLE_MANIFESTVERSION, "2"); boolean noExtraHeaders = "true".equalsIgnoreCase(getProperty(NOEXTRAHEADERS)); if (!noExtraHeaders) { main.putValue(CREATED_BY, System.getProperty("java.version") + " (" + System.getProperty("java.vendor") + ")"); main.putValue(TOOL, "Bnd-" + getBndVersion()); if (!dot.isReproducible()) { main.putValue(BND_LASTMODIFIED, Long.toString(System.currentTimeMillis())); } } String exportHeader = printClauses(exports, true); if (exportHeader.length() > 0) main.putValue(EXPORT_PACKAGE, exportHeader); else main.remove(new Name(EXPORT_PACKAGE)); // Divide imports with resolution:=dynamic to DynamicImport-Package // and add them to the existing DynamicImport-Package instruction Pair regularAndDynamicImports = divideRegularAndDynamicImports(); Packages regularImports = regularAndDynamicImports.getFirst(); if (!regularImports.isEmpty()) { main.putValue(IMPORT_PACKAGE, printClauses(regularImports)); } else { main.remove(new Name(IMPORT_PACKAGE)); } Parameters dynamicImports = regularAndDynamicImports.getSecond(); if (!dynamicImports.isEmpty()) { main.putValue(DYNAMICIMPORT_PACKAGE, printClauses(dynamicImports)); } else { main.remove(new Name(DYNAMICIMPORT_PACKAGE)); } Packages temp = new Packages(contained); temp.keySet() .removeAll(exports.keySet()); // // This actually can contain file names if the look // like packages. So remove anything that maps // to a resource in the JAR // temp.keySet() .removeIf(packageRef -> dot.getResource(packageRef.getBinary()) != null); if (!temp.isEmpty()) main.putValue(PRIVATE_PACKAGE, printClauses(temp)); else main.remove(new Name(PRIVATE_PACKAGE)); Parameters bcp = getBundleClasspath(); if (bcp.isEmpty() || (bcp.containsKey(".") && bcp.size() == 1)) main.remove(new Name(BUNDLE_CLASSPATH)); else main.putValue(BUNDLE_CLASSPATH, printClauses(bcp)); // ----- Require/Capabilities section Parameters requirements = new Parameters(annotationHeaders.getHeader(REQUIRE_CAPABILITY), this, true); Parameters capabilities = new Parameters(annotationHeaders.getHeader(PROVIDE_CAPABILITY), this, true); // // Do any contracts contracts // contracts.addToRequirements(requirements); // // We want to add the minimum EE as a requirement // based on the class version // if (!isTrue(getProperty(NOEE)) // && !ees.isEmpty() // no use otherwise && since(About._2_3) // we want people to not have to // automatically add it && !requirements.containsKey(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE) // and // it // should // not // be // there // already ) { JAVA highest = ees.last(); Attrs attrs = new Attrs(); String filter = doEEProfiles(highest); attrs.put(Constants.FILTER_DIRECTIVE, filter); // // Java 1.8 introduced profiles. // If -eeprofile= auto | (="...")+ is set then // we add a requirements.add(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE, attrs); } if (!requirements.isEmpty()) main.putValue(REQUIRE_CAPABILITY, requirements.toString()); if (!capabilities.isEmpty()) main.putValue(PROVIDE_CAPABILITY, capabilities.toString()); for (String header : Iterables.iterable(getProperties().propertyNames(), String.class::cast)) { if (header.trim() .length() == 0) { warning("Empty property set with value: %s", getProperties().getProperty(header)); continue; } if (isMissingPlugin(header.trim())) { error("Missing plugin for command %s", header); } if (!Character.isUpperCase(header.charAt(0))) { if (header.charAt(0) == '@') doNameSection(manifest, header); continue; } if (header.equals(BUNDLE_CLASSPATH) || header.equals(EXPORT_PACKAGE) || header.equals(IMPORT_PACKAGE) || header.equals(DYNAMICIMPORT_PACKAGE) || header.equals(REQUIRE_CAPABILITY) || header.equals(PROVIDE_CAPABILITY)) continue; if (header.equalsIgnoreCase("Name")) { error( "Your bnd file contains a header called 'Name'. This interferes with the manifest name section."); continue; } if (Verifier.HEADER_PATTERN.matcher(header) .matches()) { doHeader(main, header); } else { // TODO should we report? } } // Copy old values into new manifest, when they // exist in the old one, but not in the new one merge(manifest, dot.getManifest()); // // Calculate the bundle symbolic name if it is // not set. // 1. set // 2. name of properties file (must be != bnd.bnd) // 3. name of directory, which is usualy project name // String bsn = getBsn(); if (main.getValue(BUNDLE_SYMBOLICNAME) == null) { main.putValue(BUNDLE_SYMBOLICNAME, bsn); } // // Use the same name for the bundle name as BSN when // the bundle name is not set // if (main.getValue(BUNDLE_NAME) == null) { main.putValue(BUNDLE_NAME, bsn); } if (main.getValue(BUNDLE_VERSION) == null) main.putValue(BUNDLE_VERSION, "0"); // Remove all the headers mentioned in -removeheaders Instructions instructions = new Instructions(mergeProperties(REMOVEHEADERS)); Collection result = instructions.select(main.keySet(), false); main.keySet() .removeAll(result); // from here we do NOT change the main attributes anymore doPlugins(ManifestPlugin.class, (mp) -> { mp.mainSet(this, manifest); }); // start name section doNamesection(dot, manifest); doPlugins(ManifestPlugin.class, (mp) -> { mp.nameSet(this, manifest); }); return manifest; } catch (Exception e) { // This should not really happen. The code should never throw // exceptions in normal situations. So if it happens we need more // information. So to help diagnostics. We do a full property dump throw new IllegalStateException("Calc manifest failed, state=\n" + getFlattenedProperties(), e); } } /** * Find a class path entry based on bsn and versionrange * * @param bsn The bundle symbolic name * @param r The version range specified like in OSGi (version => * [version,infinite)) * @return first JAR that matches bsn &r or null if not found */ public Jar findClasspathEntry(String bsn, String r) { VersionRange range = VersionRange.likeOSGi(r); if (range == null) { error("Fragment-Host %s specifies invalid version range %s", bsn, r); range = VersionRange.likeOSGi("0"); } for (Jar jar : getClasspath()) try { if (bsn.equals(jar.getBsn())) { String version = jar.getVersion(); if (version == null) { version = "0"; } if (Version.isVersion(version)) { if (!range.includes(new Version(version))) { continue; } } return jar; } } catch (Exception e) { warning("Classpath entry with invalid bsn or version %s, throws %s", jar, e.getMessage()); } return null; } /** * Added for 1.8 profiles. A 1.8 profile is a set of packages so the VM can * be delivered in smaller versions. This method will look at the * {@link Constants#EEPROFILE} option. If it is set, it can be "auto" or it * can contain a list of profiles specified as name="a,b,c" values. If we * find a package outside the profiles, no profile is set. Otherwise the * highest found profile is added. This only works for java packages. */ private String doEEProfiles(JAVA highest) throws IOException { String ee = getProperty(EEPROFILE); if (ee == null) return highest.getFilter(); ee = ee.trim(); Map> profiles; if (ee.equals(EEPROFILE_AUTO_ATTRIBUTE)) { profiles = highest.getProfiles(); if (profiles == null) return highest.getFilter(); } else { profiles = OSGiHeader.parseProperties(ee) .stream() .mapValue(v -> Strings.splitAsStream(v) .collect(Collectors.collectingAndThen(toList(), SortedList::new))) .collect(MapStream.toMap()); } SortedSet found = new TreeSet<>(); nextPackage: for (PackageRef p : referred.keySet()) { if (p.isJava()) { String fqn = p.getFQN(); for (Entry> entry : profiles.entrySet()) { if (entry.getValue() .contains(fqn)) { found.add(entry.getKey()); // // Check if we found all the possible profiles // that means we're finished // if (found.size() == profiles.size()) break nextPackage; // // Profiles should be exclusive // so we can break if we found one // continue nextPackage; } } // // Ouch, outside any profile // return highest.getFilter(); } } String filter = highest.getFilter(); if (!found.isEmpty()) filter = filter.replaceAll("JavaSE", "JavaSE/" + found.last()); // TODO a more elegant way to build the filter, we now assume JavaSE return filter; } private void doHeader(Attributes main, String header) { String value = annotationHeaders.getHeader(header); if (value == null) return; Name name = new Name(header); if (main.getValue(name) != null) return; String trimmed = value.trim(); if (trimmed.isEmpty()) main.remove(name); else if (EMPTY_HEADER.equals(trimmed)) main.put(name, ""); else main.put(name, value); } /** * Parse the namesection as instructions and then match them against the * current set of resources For example: * *
	 *  -namesection: *;baz=true,
	 * abc/def/bar/X.class=3
	 * 
* * The raw value of {@link Constants#NAMESECTION} is used but the values of * the attributes are replaced where @ is set to the resource name. This * allows macro to operate on the resource */ private void doNamesection(Jar dot, Manifest manifest) { Parameters namesection = parseHeader(getProperties().getProperty(NAMESECTION)); Instructions instructions = new Instructions(namesection); Set resources = new HashSet<>(dot.getResources() .keySet()); // Support package attributes. See // https://docs.oracle.com/javase/8/docs/technotes/guides/versioning/spec/versioning2.html#wp89936 MapStream.of(dot.getDirectories()) .filterValue(mdir -> Objects.nonNull(mdir) && !mdir.isEmpty()) .keys() .map(d -> d.concat("/")) .forEach(resources::add); // // For each instruction, iterator over the resources and filter // them. If a resource matches, it must be removed even if the // instruction is negative. If positive, add a name section // to the manifest for the given resource name. Then add all // attributes from the instruction to that name section. // for (Map.Entry instr : instructions.entrySet()) { boolean matched = false; // For each instruction for (Iterator i = resources.iterator(); i.hasNext();) { String path = i.next(); // For each resource Instruction instruction = instr.getKey(); if (path.endsWith("/") && !instruction.toString() .endsWith("/")) { // continue } else if (instruction.matches(path)) { // Instruction matches the resource matched = true; if (!instr.getKey() .isNegated()) { // Positive match, add the attributes Attributes attrs = manifest.getAttributes(path); if (attrs == null) { attrs = new Attributes(); manifest.getEntries() .put(path, attrs); } // // Add all the properties from the instruction to the // name section // for (Map.Entry property : instr.getValue() .entrySet()) { setProperty("@", path); try { String processed = getReplacer().process(property.getValue()); attrs.putValue(property.getKey(), processed); } finally { unsetProperty("@"); } } } i.remove(); } } if (!matched && resources.size() > 0) warning("The instruction %s in %s did not match any resources", instr.getKey(), NAMESECTION); } } /** * This method is called when the header starts with a @, signifying a name * section header. The name part is defined by replacing all the @ signs to * a /, removing the first and the last, and using the last part as header * name: * *
	 *  @org@osgi@service@event@Implementation-Title
	 * 
* * This will be the header Implementation-Title in the * org/osgi/service/event named section. * * @param manifest * @param header */ void doNameSection(Manifest manifest, String header) { String path = header.replace('@', '/'); int n = path.lastIndexOf('/'); // Must succeed because we start with @ String name = path.substring(n + 1); // Skip first / path = path.substring(1, n == 0 ? 1 : n); if (name.length() != 0 && path.length() != 0) { Attributes attrs = manifest.getAttributes(path); if (attrs == null) { attrs = new Attributes(); manifest.getEntries() .put(path, attrs); } attrs.putValue(name, getProperty(header)); } else { warning("Invalid header (starts with @ but does not seem to be for the Name section): %s", header); } } /** * Clear the key part of a header. I.e. remove everything from the first ';' */ public String getBsn() { String value = getProperty(BUNDLE_SYMBOLICNAME); if (value == null) { if (getPropertiesFile() != null) value = getPropertiesFile().getName(); String projectName = getBase().getName(); if (value == null || value.equals("bnd.bnd")) { value = projectName; } else if (value.endsWith(".bnd")) { value = value.substring(0, value.length() - 4); // // This basically unknown feature allowed you to // define a sub-bundle that specified a name that used the // project name as a prefix, the project prefix would then // be skipped. This caused several problems in practice // and we actually did not take this into account in other // places. // // if (!value.startsWith(getBase().getName())) value = projectName + "." + value; } } if (value == null) return "untitled"; int n = value.indexOf(';'); if (n > 0) value = value.substring(0, n); return value.trim(); } public String _bsn(@SuppressWarnings("unused") String args[]) { return getBsn(); } /** * Calculate an export header solely based on the contents of a JAR file * * @param bundle The jar file to analyze */ public String calculateExportsFromContents(Jar bundle) { String ddel = ""; StringBuilder sb = new StringBuilder(); Map> map = bundle.getDirectories(); for (String directory : map.keySet()) { if (directory.equals("META-INF") || directory.startsWith("META-INF/")) continue; if (directory.equals("OSGI-OPT") || directory.startsWith("OSGI-OPT/")) continue; if (directory.equals("/")) continue; Map resources = map.get(directory); if (resources == null || resources.isEmpty()) continue; if (directory.endsWith("/")) directory = directory.substring(0, directory.length() - 1); directory = directory.replace('/', '.'); sb.append(ddel); sb.append(directory); ddel = ","; } return sb.toString(); } public Packages getContained() { return contained; } public Packages getExports() { return exports; } public Packages getImports() { return imports; } public Set getPrivates() { HashSet privates = new HashSet<>(contained.keySet()); privates.removeAll(exports.keySet()); privates.removeAll(imports.keySet()); return privates; } public Jar getJar() { return dot; } public Packages getReferred() { return referred; } /** * Return the set of unreachable code depending on exports and the bundle * activator. */ public Set getUnreachable() { Set unreachable = new HashSet<>(uses.keySet()); // all for (PackageRef packageRef : exports.keySet()) { removeTransitive(packageRef, unreachable); } if (activator != null) { removeTransitive(activator.getPackageRef(), unreachable); } return unreachable; } public Map> getUses() { return uses; } public Map> getAPIUses() { return apiUses; } public Packages getClasspathExports() { return classpathExports; } /** * Get the version for this bnd * * @return version or unknown. */ public String getBndVersion() { return About.getBndVersion(); } public long getBndLastModified() { String time = About.getBndInfo("lastmodified", () -> "0"); if (time.equals("0")) { return 0L; } ZonedDateTime zdt = Dates.parse(time); if (zdt != null) { return zdt.toInstant() .toEpochMilli(); } return 0L; } public String getBndInfo(String key, String defaultValue) { return About.getBndInfo(key, () -> defaultValue); } /** * Merge the existing manifest with the instructions but do not override * existing properties. * * @param manifest The manifest to merge with * @throws IOException */ public void mergeManifest(Manifest manifest) throws IOException { if (manifest != null) { Attributes attributes = manifest.getMainAttributes(); for (Object k : attributes.keySet()) { Name name = (Name) k; String key = name.toString(); // Dont want instructions if (key.startsWith("-")) continue; if (getProperty(key) == null) setProperty(key, attributes.getValue(name)); } } } @Override public void setBase(File file) { super.setBase(file); getProperties().put("project.dir", IO.absolutePath(getBase())); } /** * Set the classpath for this analyzer by file. * * @param classpath * @throws IOException */ public void setClasspath(Collection classpath) throws IOException { for (Object cpe : classpath) { if (cpe instanceof Jar jar) { addClasspath(jar); } else if (cpe instanceof File f) { if (!f.exists()) { error("Missing file on classpath: %s", IO.absolutePath(f)); continue; } addClasspath(f); } else if (cpe instanceof String string) { Jar j = getJarFromName(string, " setting classpath"); if (j == null) { continue; } addClasspath(j); } else { error("Cannot convert to JAR to add to classpath %s. Not a File, Jar, or String", cpe); } } } public void setClasspath(File[] classpath) throws IOException { List list = new ArrayList<>(); for (File element : classpath) { if (element.exists()) { Jar current = new Jar(element); list.add(current); } else { error("Missing file on classpath: %s", IO.absolutePath(element)); } } for (Jar jar : list) { addClasspath(jar); } } public void setClasspath(Jar[] classpath) { for (Jar element : classpath) { addClasspath(element); } } public void setClasspath(String[] classpath) { for (String element : classpath) { Jar jar = getJarFromName(element, " setting classpath"); if (jar != null) addClasspath(jar); } } /** * Set the JAR file we are going to work in. This will read the JAR in * memory. * * @param file * @throws IOException */ public Jar setJar(File file) throws IOException { Jar jar = new Jar(file); setJar(jar); return jar; } /** * Set the JAR directly we are going to work on. * * @param jar */ public Jar setJar(Jar jar) { if (dot != null) removeClose(dot); this.dot = jar; if (dot != null) addClose(dot); return jar; } @Override protected void begin() { if (inited == false) { inited = true; super.begin(); updateModified(getBndLastModified(), "bnd last modified"); About.fixup(this); verifyManifestHeadersCase(getProperties()); } } /** * 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 */ @Override public Jar getJarFromName(String name, String from) { return jarsFromName(name, from).findFirst() .orElse(null); } public List getJarsFromName(String name, String from) { return jarsFromName(name, from).collect(toList()); } private Stream jarsFromName(String name, String from) { Jar j = super.getJarFromName(name, from); if (j != null) { return Stream.of(j); } // This may be a -classpath entry on Windows, // so we normalize to avoid regex failure with backslash. // Since we only match against a file name, the change // from backslash to slash won't matter. Glob g = new Glob(IO.normalizePath(name)); return getClasspath().stream() .filter(entry -> (entry.getSource() != null) && g.matches(entry.getSource() .getName())); } private void merge(Manifest result, Manifest old) { if (old != null) { for (Map.Entry entry : old.getMainAttributes() .entrySet()) { Attributes.Name name = (Attributes.Name) entry.getKey(); String value = (String) entry.getValue(); if (name.toString() .equalsIgnoreCase(Constants.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 (Map.Entry entry : oldEntries.entrySet()) { if (!newEntries.containsKey(entry.getKey())) { newEntries.put(entry.getKey(), entry.getValue()); } } } } /** * 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 (Object key : properties.keySet()) { String header = (String) key; for (String canonical : headers) { if (!canonical.equals(header) && canonical.equalsIgnoreCase(header)) { warning( "Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: %s and expecting: %s", header, canonical); 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. We also remove any version parameter so that augmentImports can * do the version policy. The following method is really tricky and evolved * over time. Coming from the original background of OSGi, it was a weird * idea for me to have a public package that should not be substitutable. I * was so much convinced that this was the right rule that I rücksichtlos * imported them all. Alas, the real world was more subtle than that. It * turns out that it is not a good idea to always import. First, there must * be a need to import, i.e. there must be a contained package that refers * to the exported package for it to make use importing that package. * Second, if an exported package refers to an internal package than it * should not be imported. Additionally, it is necessary to treat the * exports in groups. If an exported package refers to another exported * packages than it must be in the same group. A framework can only * substitute exports for imports for the whole of such a group. WHY????? * Not clear anymore ... */ Packages doExportsToImports(Packages exports) { // private packages = contained - exported. Set privatePackages = new HashSet<>(contained.keySet()); privatePackages.removeAll(exports.keySet()); // private references = ∀ p : contained packages | uses(p) Set containedReferences = contained.keySet() .stream() .map(uses::get) .filter(Objects::nonNull) .flatMap(List::stream) .collect(toSet()); // #2876 Add non-class references such as DS xml. containedReferences.addAll(nonClassReferences); // Assume we are going to import all exported packages Packages result = exports.keySet() .stream() // Remove packages that are not referenced locally .filter(containedReferences::contains) // Remove exported packages that are referring to // private packages. // Each exported package has a uses clause. We just use // the used packages for each exported package to find out // if it refers to an internal package. .filter(p -> { List used = uses.get(p); return (used == null) || used.isEmpty() || privatePackages.stream() .noneMatch(used::contains); }) // Remove packages with -noimport:=true .filter(p -> { Attrs parameters = exports.get(p); if (parameters == null) { return true; } String noimport = parameters.get(NO_IMPORT_DIRECTIVE); return !Boolean.parseBoolean(noimport); }) // Remove packages without a version range // because this would lead imports without a version. // and this can cause to surprising resolver problems // because the resolver has too many options in case multiple // provideers of that package .filter(p -> { Attrs parameters = exports.get(p); if (parameters == null) { return true; } // check only for presence of version (not validity, because // this done by #check()) // (see also test.VerifierTest.testStrict()) return parameters.getVersion() != null; }) // Clean up attributes and generate result map .collect(toMap(p -> p, p -> new Attrs(), (a1, a2) -> a1, Packages::new)); return result; } public boolean referred(PackageRef packageName) { boolean result = MapStream.of(uses) .filterKey(pkg -> !pkg.equals(packageName)) .values() .anyMatch(list -> list.contains(packageName)); return result; } private void getManifestInfoFromClasspath(Jar jar, Packages classpathExports, Contracts contracts) { logger.debug("get Manifest Info From Classpath for {}", jar); try { Manifest m = jar.getManifest(); if (m != null) { Domain domain = Domain.domain(m); Parameters exported = domain.getExportPackage(); String bsn = jar.getBsn(); String version = jar.getVersion(); String bsn_version = bsn + "-" + version; exported.stream() .mapKey(this::getPackageRef) .filterKey(ref -> !classpathExports.containsKey(ref)) .forEachOrdered((ref, attrs) -> { attrs.put(Constants.INTERNAL_EXPORTED_DIRECTIVE, bsn_version); if (bsn != null) { attrs.put(Constants.INTERNAL_BUNDLESYMBOLICNAME_DIRECTIVE, bsn); attrs.put(Constants.INTERNAL_BUNDLEVERSION_DIRECTIVE, (version != null) ? version : "0.0.0"); } // // Fixup any old style specification versions // fixupOldStyleVersions(attrs); classpathExports.put(ref, attrs); }); // // Collect any declared contracts // Parameters pcs = domain.getProvideCapability(); contracts.collectContracts(jar.getName(), pcs); } } catch (Exception e) { warning("Erroneous Manifest for %s %s", jar, e); } } private void fixupOldStyleVersions(Attrs attrs) { if (attrs.containsKey(SPECIFICATION_VERSION) && attrs.getVersion() == null) { attrs.put("version", attrs.get(SPECIFICATION_VERSION)); attrs.remove(SPECIFICATION_VERSION); } } /** * Find some more information about imports in manifest and other places. It * is assumed that the augmentsExports has already copied external attrs * from the classpathExports. * * @throws Exception */ void augmentImports(Packages imports, Packages exports) throws Exception { List noimports = Create.list(); Set provided = findProvidedPackages(); for (PackageRef packageRef : imports.keySet()) { String packageName = packageRef.getFQN(); setProperty(CURRENT_PACKAGE, packageName); try { Attrs importAttributes = imports.get(packageRef); // // Check if we have a java.* package. If so, we should remove // all attributes (except resolution:) to have a plain import. // if (packageRef.isJava()) { String resolution = importAttributes.get(Constants.RESOLUTION_DIRECTIVE); importAttributes.clear(); if (resolution != null) { importAttributes.put(Constants.RESOLUTION_DIRECTIVE, resolution); } continue; } Attrs defaultAttrs = new Attrs(); Attrs exportAttributes = exports.get(packageRef, classpathExports.get(packageRef, defaultAttrs)); String bundlesymbolicname = exportAttributes.get(INTERNAL_BUNDLESYMBOLICNAME_DIRECTIVE); if (bundlesymbolicname != null) { setProperty(CURRENT_BUNDLESYMBOLICNAME, bundlesymbolicname); String bundleversion = exportAttributes.get(INTERNAL_BUNDLEVERSION_DIRECTIVE); setProperty(CURRENT_BUNDLEVERSION, (bundleversion != null) ? bundleversion : "0.0.0"); } String exportVersion = exportAttributes.getVersion(); String importRange = importAttributes.getVersion(); if (check(Check.IMPORTS)) { if (exportAttributes == defaultAttrs) { warning( "Import package %s not found in any bundle on the -buildpath. List explicitly in Import-Package: p,* to get rid of this warning if false", packageRef); continue; } if (!exportAttributes.containsKey(INTERNAL_EXPORTED_DIRECTIVE) && !exports.containsKey(packageRef)) { warning("'%s' is a private package import from %s", packageRef, exportAttributes.get(INTERNAL_SOURCE_DIRECTIVE)); continue; } } // // Check if we have a package from a contract. // if (contracts.isContracted(packageRef)) { // yes, contract based, so remove // the version and don't do the // version policy stuff importAttributes.remove(VERSION_ATTRIBUTE); continue; } if (exportVersion == null) { // TODO Should check if the source is from a bundle. if (importRange != null) { importRange = cleanupVersion(importRange); importAttributes.put(VERSION_ATTRIBUTE, importRange); } } else { // // Version Policy - Import version substitution. We // calculate the export version and then allow the // import version attribute to use it in a substitution // by using a ${@} macro. The export version can // be defined externally or locally // boolean provider; if (importAttributes.containsKey(PROVIDE_DIRECTIVE)) { provider = isTrue(importAttributes.get(PROVIDE_DIRECTIVE)); } else if (exportAttributes.containsKey(PROVIDE_DIRECTIVE)) { provider = isTrue(exportAttributes.get(PROVIDE_DIRECTIVE)); } else { provider = provided.contains(packageRef); } exportVersion = cleanupVersion(exportVersion); importRange = applyVersionPolicy(exportVersion, importRange, provider); if (Strings.nonNullOrTrimmedEmpty(importRange)) { importAttributes.put(VERSION_ATTRIBUTE, importRange); } } // // Check if exporter has mandatory attributes // Strings.splitAsStream(exportAttributes.get(MANDATORY_DIRECTIVE)) .filter(key -> !importAttributes.containsKey(key)) .forEachOrdered(key -> importAttributes.put(key, exportAttributes.get(key))); if (exportAttributes.containsKey(IMPORT_DIRECTIVE)) importAttributes.put(IMPORT_DIRECTIVE, exportAttributes.get(IMPORT_DIRECTIVE)); fixupAttributes(packageRef, importAttributes); removeAttributes(importAttributes); String result = importAttributes.get(Constants.VERSION_ATTRIBUTE); if (result == null || !Verifier.isVersionRange(result)) noimports.add(packageRef); } finally { unsetProperty(CURRENT_PACKAGE); unsetProperty(CURRENT_BUNDLESYMBOLICNAME); unsetProperty(CURRENT_BUNDLEVERSION); } } if (isPedantic() && noimports.size() != 0) { warning("Imports that lack version ranges: %s", noimports); } } Pair divideRegularAndDynamicImports() { Packages regularImports = new Packages(imports); Parameters dynamicImports = getDynamicImportPackage(); Iterator> regularImportsIterator = regularImports.entrySet() .iterator(); while (regularImportsIterator.hasNext()) { Entry packageEntry = regularImportsIterator.next(); PackageRef packageRef = packageEntry.getKey(); Attrs attrs = packageEntry.getValue(); String resolution = attrs.get(Constants.RESOLUTION_DIRECTIVE); if (Constants.RESOLUTION_DYNAMIC.equals(resolution)) { attrs.remove(Constants.RESOLUTION_DIRECTIVE); dynamicImports.put(packageRef.fqn, attrs); regularImportsIterator.remove(); } } return new Pair<>(regularImports, dynamicImports); } String applyVersionPolicy(String exportVersion, String importRange, boolean provider) { try { setProperty(CURRENT_VERSION, exportVersion); if (importRange != null) { importRange = cleanupVersion(importRange); importRange = getReplacer().process(importRange); } else { importRange = getVersionPolicy(provider); } } finally { unsetProperty(CURRENT_VERSION); } return importRange; } /** * Find the packages we depend on, where we implement an interface that is a * Provider Type. These packages, when we import them, must use the provider * policy. * * @throws Exception */ Set findProvidedPackages() throws Exception { Set providers = classspace.values() .stream() .flatMap(c -> { // filter out interfaces in the same package as the class // implementing the // interface. PackageRef pkg = c.getClassName() .getPackageRef(); return c.interfaces() .stream() .filter(i -> !Objects.equals(pkg, i.getPackageRef())); }) .distinct() .filter(this::isProvider) .map(TypeRef::getPackageRef) .collect(toCollection(LinkedHashSet::new)); return providers; } boolean isProvider(TypeRef t) { if (t == null || t.isJava()) return false; try { if (isProvider(t.getPackageRef())) return true; Clazz c = findClass(t); return c.annotations() .contains(providerType) || isProvider(c.superClass); } catch (Exception e) { return false; } } boolean isProvider(PackageRef packageRef) throws Exception { if (packageRef == null) return false; TypeRef packageInfo = getTypeRef(packageRef.binaryName.concat("/package-info")); Clazz c = findClass(packageInfo); return c != null && c.annotations() .contains(providerType); } /** * Provide any macro substitutions and versions for exported packages. * * @throws IOException */ void augmentExports(Packages exports) throws IOException { for (PackageRef packageRef : exports.keySet()) { String packageName = packageRef.getFQN(); Attrs attributes = exports.get(packageRef); setProperty(CURRENT_PACKAGE, packageName); try { Attrs exporterAttributes = classpathExports.get(packageRef); if (exporterAttributes == null) { if (check(Check.EXPORTS)) { Map map = dot.getDirectory(packageRef.getBinary()); if ((map == null || map.isEmpty())) { error("Exporting an empty package '%s'", packageRef.getFQN()); } } } else { for (Map.Entry entry : exporterAttributes.entrySet()) { String key = entry.getKey(); // no directives if (key.endsWith(":")) { continue; } // don't overwrite if (!attributes.containsKey(key)) attributes.put(key, entry.getValue()); else { if (since(About._2_4)) { // we have the attribute from the classpath // and we have set it. if (key.equals(Constants.VERSION_ATTRIBUTE)) { try { Version fromExport = new Version( cleanupVersion(exporterAttributes.getVersion())); Version fromSet = new Version(cleanupVersion(attributes.getVersion())); if (!fromExport.equals(fromSet)) { SetLocation location = warning( "Version for package %s is set to different values in the source (%s) and in the manifest (%s). The version in the manifest is not " + "picked up by an other sibling bundles in this project or projects that directly depend on this project", packageName, attributes.get(key), exporterAttributes.get(key)); if (getPropertiesFile() != null) location.file(getPropertiesFile().getAbsolutePath()); location.header(EXPORT_PACKAGE); location.context(packageName); } } catch (Exception e) { // Ignored here, is picked up in // other places } } } } } } fixupAttributes(packageRef, attributes); removeAttributes(attributes); } finally { unsetProperty(CURRENT_PACKAGE); } } } /** * Fixup Attributes Execute any macros on an export and * * @throws IOException */ void fixupAttributes(PackageRef packageRef, Attrs attributes) throws IOException { // Convert any attribute values that have macros. for (String key : attributes.keySet()) { String value = attributes.get(key); if (value.indexOf('$') >= 0) { value = getReplacer().process(value); attributes.put(key, value); } if (!key.endsWith(":")) { String from = attributes.get(FROM_DIRECTIVE); verifyAttribute(from, "package info for " + packageRef, key, value); } } } /** * Remove the attributes mentioned in the REMOVE_ATTRIBUTE_DIRECTIVE. You * can add a remove-attribute: directive with a regular expression for * attributes that need to be removed. We also remove all attributes that * have a value of !. This allows you to use macros with ${if} to remove * values. */ void removeAttributes(Attrs attributes) { String remove = attributes.remove(REMOVE_ATTRIBUTE_DIRECTIVE); if (remove != null) { Instructions removeInstr = new Instructions(remove); attributes.keySet() .removeAll(removeInstr.select(attributes.keySet(), false)); } // Remove any ! valued attributes attributes.entrySet() .removeIf(entry -> entry.getValue() .equals("!")); } /** * Calculate a version from a version policy. * * @param version The actual exported version * @param impl true for implementations and false for clients */ String calculateVersionRange(String version, boolean impl) { setProperty(CURRENT_VERSION, version); try { return getVersionPolicy(impl); } finally { unsetProperty(CURRENT_VERSION); } } /** * Add the uses clauses. This method iterates over the exports and cal * * @param exports * @param uses * @throws MojoExecutionException */ void doUses(Packages exports, Map> uses, Packages imports) { if (isTrue(getProperty(NOUSES))) return; for (PackageRef packageRef : exports.keySet()) { String packageName = packageRef.getFQN(); setProperty(CURRENT_PACKAGE, packageName); try { doUses(packageRef, exports, uses, imports); } finally { unsetProperty(CURRENT_PACKAGE); } } } /** * @param packageRef * @param exports * @param uses * @param imports */ protected void doUses(PackageRef packageRef, Packages exports, Map> uses, Packages imports) { Attrs clause = exports.get(packageRef); // Check if someone already set the uses: directive String override = clause.get(USES_DIRECTIVE, USES_USES); // Get the used packages Collection usedPackages = uses.get(packageRef); if (usedPackages != null) { // Only do a uses on exported or imported packages // and uses should also not contain our own package // name Set sharedPackages = new TreeSet<>(); sharedPackages.addAll(imports.keySet()); sharedPackages.addAll(exports.keySet()); sharedPackages.retainAll(usedPackages); sharedPackages.remove(packageRef); StringBuilder sb = new StringBuilder(); String del = ""; for (PackageRef usedPackage : sharedPackages) { if (!usedPackage.isJava()) { sb.append(del); sb.append(usedPackage.getFQN()); del = ","; } } if (override.indexOf('$') >= 0) { setProperty(CURRENT_USES, sb.toString()); override = getReplacer().process(override); unsetProperty(CURRENT_USES); } else // This is for backward compatibility 0.0.287 // can be deprecated over time override = override.replaceAll(USES_USES, Matcher.quoteReplacement(sb.toString())) .trim(); if (override.endsWith(",")) override = override.substring(0, override.length() - 1); if (override.startsWith(",")) override = override.substring(1); if (override.isEmpty()) { clause.remove(USES_DIRECTIVE); } else { clause.put(USES_DIRECTIVE, override); } } else { if (override.equals(USES_USES)) { clause.remove(USES_DIRECTIVE); } } } /** * Transitively remove all elemens from unreachable through the uses link. * * @param name * @param unreachable */ void removeTransitive(PackageRef name, Set unreachable) { if (!unreachable.contains(name)) return; unreachable.remove(name); List ref = uses.get(name); if (ref != null) { for (PackageRef element : ref) { removeTransitive(element, unreachable); } } } private void verifyAttribute(String path, String where, String key, String value) throws IOException { SetLocation location; if (!Verifier.isExtended(key)) { location = error("%s attribute [%s='%s'], key must be an EXTENDED (CORE1.3.2 %s). From %s", where, key, value, Verifier.EXTENDED_S, path); } else if (value == null || value.trim() .length() == 0) { location = error( "%s attribute [%s='%s'], value is empty which is not allowed in ARGUMENT_S (CORE1.3.2 %s). From %s", where, key, value, Verifier.ARGUMENT_S, path); } else return; if (path != null) { File f = new File(path); if (f.isFile()) { FileLine fl = findHeader(f, key); if (fl != null) fl.set(location); } } } @Override public void close() throws IOException { if (diagnostics) { PrintStream out = System.err; out.printf("Current directory : %s%n", new File("").getAbsolutePath()); out.println("Classpath used"); for (Jar jar : getClasspath()) { out.printf("File : %s%n", jar.getSource()); out.printf("File abs path : %s%n", jar.getSource() .getAbsolutePath()); out.printf("Name : %s%n", jar.getName()); Map> dirs = jar.getDirectories(); for (Map.Entry> entry : dirs.entrySet()) { Map dir = entry.getValue(); String name = entry.getKey() .replace('/', '.'); if (dir != null) { out.printf(" %-30s %d%n", name, dir.size()); } else { out.printf(" %-30s <>%n", name); } } } } super.close(); } /** * Findpath looks through the contents of the JAR and finds paths that end * with the given regular expression ${findpath (; reg-expr (; replacement)? * )? } * * @param args */ public String _findpath(String args[]) { return findPath("findpath", args, true); } public String _findname(String args[]) { return findPath("findname", args, false); } String findPath(String name, String[] args, boolean fullPathName) { if (args.length > 3) { warning("Invalid nr of arguments to %s %s, syntax: ${%s (; reg-expr (; replacement)? )? }", name, Arrays.asList(args), name); return null; } String regexp = ".*"; String replace = null; switch (args.length) { case 3 : replace = args[2]; //$FALL-THROUGH$ case 2 : regexp = args[1]; } StringBuilder sb = new StringBuilder(); String del = ""; Pattern expr = Pattern.compile(regexp); for (String path : dot.getResources() .keySet()) { if (!fullPathName) { int n = path.lastIndexOf('/'); if (n >= 0) { path = path.substring(n + 1); } } Matcher m = expr.matcher(path); if (m.matches()) { if (replace != null) path = m.replaceAll(replace); sb.append(del); sb.append(path); del = ", "; } } return sb.toString(); } public void putAll(Map additional, boolean force) { MapStream stream = MapStream.of(additional); if (!force) { stream = stream.filterKey(key -> getProperties().get(key) == null); } stream.forEachOrdered(this::setProperty); } boolean firstUse = true; public List getClasspath() { if (firstUse) { firstUse = false; String cp = getProperty(CLASSPATH); if (cp != null) for (String s : split(cp)) { Jar jar = getJarFromName(s, "getting classpath"); if (jar != null) addClasspath(jar); else warning("Cannot find entry on -classpath: %s", s); } } return Collections.unmodifiableList(classpath); } public void addClasspath(Jar jar) { if (isPedantic() && jar.getResources() .isEmpty()) warning("There is an empty jar or directory on the classpath: %s", jar.getName()); addClose(jar); classpath.add(jar); updateModified(jar.lastModified(), jar.toString()); } public void addClasspath(Collection jars) throws IOException { for (Object item : jars) { if (item instanceof Jar jar) addClasspath(jar); else if (item instanceof File file) addClasspath(file); else if (item instanceof String string) addClasspath(getFile(string)); else error("Cannot convert to JAR to add to classpath %s. Not a File, Jar, or String", item); } } public void addClasspath(File cp) throws IOException { if (!cp.exists()) warning("File on classpath that does not exist: %s", cp); Jar jar = new Jar(cp); addClasspath(jar); } @Override public void clear() { // TODO remove classpath entries from to-be-closed set? classpath.clear(); } @Override public void forceRefresh() { super.forceRefresh(); checks = null; } public Jar getTarget() { return getJar(); } private void analyzeBundleClasspath() throws Exception { Parameters bcp = getBundleClasspath(); if (bcp.isEmpty()) { analyzeJar(dot, "", true, null, false); } else { // Cleanup entries bcp = bcp.stream() .mapKey(path -> (path.equals(".") || path.equals("/")) ? "." : appendPath(path)) .collect(MapStream.toMap((u, v) -> u, Parameters::new)); boolean okToIncludeDirs = bcp.keySet() .stream() .noneMatch(dot::hasDirectory); for (String path : bcp.keySet()) { if (path.equals(".")) { analyzeJar(dot, "", okToIncludeDirs, null, false); continue; } // // There are 3 cases: // - embedded JAR file // - directory // - error // Resource resource = dot.getResource(path); if (resource != null) { try { Jar jar = Jar.fromResource(path, resource); // Don't want to close Jar from JarResource if (!(resource instanceof JarResource)) { addClose(jar); } analyzeJar(jar, "", true, path, false); } catch (Exception e) { warning("Invalid bundle classpath entry: %s: %s", path, e); } } else { if (dot.hasDirectory(path)) { // if directories are used, we should not have dot as we // would have the classes in these directories on the // class path twice. if (bcp.containsKey(".")) warning(Constants.BUNDLE_CLASSPATH + " uses a directory '%s' as well as '.'. This means bnd does not know if a directory is a package.", path); analyzeJar(dot, path.concat("/"), true, path, false); } else { Attrs info = bcp.get(path); if (!"optional".equals(info.get(RESOLUTION_DIRECTIVE))) warning("No sub JAR or directory %s", path); } } } } } /** * We traverse through all the classes that we can find and calculate the * contained and referred set and uses. This method ignores the Bundle * classpath. */ private boolean analyzeJar(Jar jar, String prefix, boolean okToIncludeDirs, String bcpEntry, boolean allowOverride) throws Exception { Map mismatched = new HashMap<>(); Parameters importPackage = Optional.ofNullable(jar.getManifest()) .map(Domain::domain) .map(Domain::getImportPackage) .orElseGet(() -> new Parameters()); next: for (String path : jar.getResources() .keySet()) { if (path.startsWith(prefix)) { String relativePath = path.substring(prefix.length()); if (relativePath.startsWith("META-INF/")) continue; if (okToIncludeDirs) { int n = relativePath.lastIndexOf('/'); if (n < 0) n = relativePath.length(); String relativeDir = relativePath.substring(0, n); PackageRef packageRef = getPackageRef(relativeDir); learnPackage(jar, prefix, packageRef, contained); } // Check class resources, we need to analyze them if (path.endsWith(".class")) { Resource resource = jar.getResource(path); Clazz clazz; try { clazz = new Clazz(this, path, resource); clazz.parseClassFile(); } catch (Exception e) { exception(e, "Invalid class file %s (%s)", relativePath, e); continue next; } TypeRef className = clazz.getClassName(); String calculatedPath = className.getPath(); if (!calculatedPath.equals(relativePath)) { // If there is a mismatch we // warning if (okToIncludeDirs) { // assume already reported mismatched.put(clazz.getAbsolutePath(), clazz); } continue next; } if (allowOverride || !classspace.containsKey(className)) { classspace.put(className, clazz); PackageRef packageRef = className.getPackageRef(); learnPackage(jar, prefix, packageRef, contained); // Look at the referred packages // and copy them to our baseline Set refs = new LinkedHashSet<>(clazz.getReferred()); refs.addAll(referencesByAnnotation(clazz)); for (PackageRef p : refs) { referred.compute(p, (pRef, existing) -> mergeImportAttrs(existing, importPackage.get(pRef.getFQN()))); } refs.remove(packageRef); uses.addAll(packageRef, refs); // Collect the API apiUses.addAll(packageRef, clazz.getAPIUses()); if (bcpEntry != null) { bcpTypes.put(className, bcpEntry); } } } } } if (mismatched.size() > 0) { error("Classes found in the wrong directory: %s", mismatched); return false; } return true; } /** * Clean up version parameters. Other builders use more fuzzy definitions of * the version syntax. This method cleans up such a version to match an OSGi * version. */ final static Pattern fuzzyVersion = Pattern.compile("(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^\\p{Alnum}](.*))?", Pattern.DOTALL); final static Pattern fuzzyVersionRange = Pattern .compile("(\\(|\\[)\\s*([-.\\w]+)\\s*,\\s*([-.\\w]+)\\s*(\\]|\\))", Pattern.DOTALL); final static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)", Pattern.DOTALL); static public String cleanupVersion(String version) { if (version == null) return "0"; version = Strings.trim(version); Matcher m = Verifier.VERSIONRANGE.matcher(version); if (m.matches()) { try { VersionRange vr = new VersionRange(version); return version; } catch (Exception e) { // ignore } } m = fuzzyVersionRange.matcher(version); if (m.matches()) { String prefix = m.group(1); String first = m.group(2); String last = m.group(3); String suffix = m.group(4); return prefix + cleanupVersion(first) + "," + cleanupVersion(last) + suffix; } if (version.startsWith("@")) { version = cleanupVersion(version.substring(1)); String right = Macro.version(Version.valueOf(version), "+0"); return "[" + version + "," + right + ")"; } else if (version.endsWith("@")) { version = cleanupVersion(version.substring(0, version.length() - 1)); String right = Macro.version(Version.valueOf(version), "=+"); return "[" + version + "," + right + ")"; } else if (version.startsWith("=")) { version = cleanupVersion(version.substring(1)); return "[" + version + "," + version + "]"; } m = fuzzyVersion.matcher(version); if (m.matches()) { StringBuilder result = new StringBuilder(); String major = removeLeadingZeroes(m.group(1)); String minor = removeLeadingZeroes(m.group(3)); String micro = removeLeadingZeroes(m.group(5)); String qualifier = m.group(7); if (qualifier == null) { if (!isInteger(minor)) { qualifier = minor; minor = "0"; } else if (!isInteger(micro)) { qualifier = micro; micro = "0"; } } if (major != null) { result.append(major); if (minor != null) { result.append("."); result.append(minor); if (micro != null) { result.append("."); result.append(micro); if (qualifier != null) { result.append("."); cleanupModifier(result, qualifier); } } else if (qualifier != null) { result.append(".0."); cleanupModifier(result, qualifier); } } else if (qualifier != null) { result.append(".0.0."); cleanupModifier(result, qualifier); } return result.toString(); } } return version; } /** * TRhe cleanup version got confused when people used numeric dates like * 201209091230120 as qualifiers. These are too large for Integers. This * method checks if the all digit string fits in an integer. * *
	 *  maxint =
	 * 2,147,483,647 = 10 digits
	 * 
* * @return if this fits in an integer */ private static boolean isInteger(String minor) { return minor.length() < 10 || (minor.length() == 10 && minor.compareTo("2147483647") < 0); } private static String removeLeadingZeroes(String group) { if (group == null) return "0"; int n = 0; while (n < group.length() - 1 && group.charAt(n) == '0') n++; if (n == 0) return group; return group.substring(n); } static void cleanupModifier(StringBuilder result, String modifier) { Matcher m = fuzzyModifier.matcher(modifier); if (m.matches()) modifier = m.group(2); for (int i = 0; i < modifier.length(); i++) { char c = modifier.charAt(i); if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '-') result.append(c); } } final static String DEFAULT_PROVIDER_POLICY = "${range;[==,=+)}"; final static String DEFAULT_CONSUMER_POLICY = "${range;[==,+)}"; public String getVersionPolicy(boolean implemented) { if (implemented) { return getProperty(PROVIDER_POLICY, DEFAULT_PROVIDER_POLICY); } return getProperty(CONSUMER_POLICY, DEFAULT_CONSUMER_POLICY); } /** * The extends macro traverses all classes and returns a list of class names * that extend a base class. */ static final String _classesHelp = "${classes[;;]*}, Return a list of fully qualified class names of the contained classes matching the queries.\n" + "A query must be one of " + join(Clazz.QUERY.values()); public String _classes(String... args) throws Exception { Collection matched = getClasses(args); if (matched.isEmpty()) return ""; return join(matched); } public Collection getClasses(String... args) throws Exception { Set matched = new HashSet<>(classspace.values()); for (int i = 1; i < args.length; i++) { String typeName = args[i].toUpperCase(Locale.ROOT); QUERY type = switch (typeName) { case "EXTENDING" -> Clazz.QUERY.EXTENDS; case "IMPORTING" -> Clazz.QUERY.IMPORTS; case "ANNOTATION" -> Clazz.QUERY.ANNOTATED; case "IMPLEMENTING" -> Clazz.QUERY.IMPLEMENTS; default -> Clazz.QUERY.valueOf(typeName); }; Instruction instr; if (Clazz.HAS_ARGUMENT.contains(type)) { if (++i == args.length) { throw new IllegalArgumentException( "${classes} query " + type + " must have a pattern argument. " + _classesHelp); } String s = args[i]; instr = new Instruction(s); } else { instr = null; } matched.removeIf(asPredicate(clazz -> !clazz.is(type, instr, this))); } return new SortedList<>(matched, Clazz.NAME_COMPARATOR); } static final String _packagesHelp = "${packages[;;]}, Return a list of packages contained in the bundle matching the query.\n" + "A query must be one of " + join(Packages.QUERY.values()); public String _packages(String... args) throws Exception { Collection matched = getPackages(contained, args); return matched.isEmpty() ? "" : join(matched); } public Collection getPackages(Packages scope, String... args) throws Exception { List pkgs = new LinkedList<>(); Packages.QUERY queryType; Instruction instr; if (args.length == 1) { queryType = null; instr = null; } else if (args.length >= 2) { queryType = Packages.QUERY.valueOf(args[1].toUpperCase(Locale.ROOT)); if (args.length > 2) instr = new Instruction(args[2]); else instr = null; } else { throw new IllegalArgumentException("${packages} macro: invalid argument count"); } for (Entry entry : scope.entrySet()) { PackageRef pkg = entry.getKey(); TypeRef pkgInfoTypeRef = descriptors.getPackageInfo(pkg); Clazz pkgInfo = classspace.get(pkgInfoTypeRef); boolean accept = false; if (queryType != null) { switch (queryType) { case ANY : accept = true; break; case NAMED : if (instr == null) throw new IllegalArgumentException("Not enough arguments in ${packages} macro"); accept = instr.matches(pkg.getFQN()) ^ instr.isNegated(); break; case ANNOTATED : if (instr == null) throw new IllegalArgumentException("Not enough arguments in ${packages} macro"); accept = (pkgInfo == null && instr.isNegated()) || (pkgInfo != null && pkgInfo.is(Clazz.QUERY.ANNOTATED, instr, this)); break; case VERSIONED : accept = entry.getValue() .getVersion() != null; break; case CONDITIONAL : accept = entry.getValue() .get(INTERNAL_SOURCE_DIRECTIVE) .startsWith(CONDITIONALPACKAGE); break; default : break; } } else { accept = true; } if (accept) pkgs.add(pkg); } return pkgs; } /** * Get the exporter of a package ... */ public String _exporters(String args[]) throws Exception { Macro.verifyCommand(args, "${exporters;}, returns the list of jars that export the given package", null, 2, 2); String pack = Descriptors.fqnToBinary(args[1]); String result = getClasspath().stream() .filter(jar -> jar.hasDirectory(pack)) .map(Jar::getName) .collect(Strings.joining()); return result; } public Map getClassspace() { return classspace; } /** * Return an attribute of a package */ public String _packageattribute(String[] args) { Macro.verifyCommand(args, "${packageattribute;[;]}, Return an attribute of a package, default the version. Only available after analysis", null, 2, 3); String packageName = args[1]; String attrName = (args.length > 2) ? args[2] : "version"; Attrs attrs = contained.getByFQN(packageName); if (attrs == null) return "version".equals(attrName) ? "0" : ""; String value = attrs.get(attrName); if (value == null) return "version".equals(attrName) ? "0" : ""; else return value; } /** * Locate a resource on the class path. * * @param path Path of the reosurce * @return A resource or null */ public Resource findResource(String path) { Resource result = getClasspath().stream() .map(entry -> entry.getResource(path)) .filter(Objects::nonNull) .findFirst() .orElse(null); return result; } public Stream findResources(Predicate matches) { return getClasspath().stream() .flatMap(jar -> jar.getResources(matches)); } /** * Find a clazz on the class path. This class has been parsed. */ public Clazz findClass(TypeRef typeRef) throws Exception { Clazz c = classspace.get(typeRef); if (c != null) return c; c = importedClassesCache.get(typeRef); if (c != null) return c; c = lookAsideClasses.get(typeRef); if (c != null) return c; Resource r = findResource(typeRef.getPath()); if (r == null) { getClass().getClassLoader(); URL url = ClassLoader.getSystemResource(typeRef.getPath()); if (url != null) r = Resource.fromURL(url, getPlugin(HttpClient.class)); } if (r != null) { c = new Clazz(this, typeRef.getPath(), r); c.parseClassFile(); importedClassesCache.put(typeRef, c); } return c; } /** * Answer the bundle version. */ public String getVersion() { String version = getProperty(BUNDLE_VERSION); if (version == null) version = "0.0.0"; return version; } public boolean isNoBundle() { return isTrue(getProperty(RESOURCEONLY)) || isTrue(getProperty(NOMANIFEST)); } public void referTo(TypeRef ref) { PackageRef pack = ref.getPackageRef(); if (!referred.containsKey(pack)) referred.put(pack, new Attrs()); } public void nonClassReferTo(TypeRef ref) { referTo(ref); PackageRef pack = ref.getPackageRef(); nonClassReferences.add(pack); } public void referToByBinaryName(String binaryClassName) { TypeRef ref = descriptors.getTypeRef(binaryClassName); referTo(ref); } /** * Ensure that we are running on the correct bnd. */ protected void doRequireBnd() { Attrs require = OSGiHeader.parseProperties(getProperty(REQUIRE_BND)); if (require == null || require.isEmpty()) return; Map map = Collections.singletonMap(Constants.VERSION_FILTER, Version.valueOf(getBndVersion())); for (String filter : require.keySet()) { try { Filter f = new Filter(filter); if (f.matchMap(map)) continue; error("%s fails for filter %s values=%s", REQUIRE_BND, filter, map); } catch (Exception t) { exception(t, "%s with value %s throws exception", REQUIRE_BND, require); } } } /** * md5 macro */ private final static Pattern BASE64HEX_P = Pattern.compile("base64|hex"); final static String _md5Help = "${md5;path}"; public String _md5(String args[]) throws Exception { Macro.verifyCommand(args, _md5Help, new Pattern[] { null, null, BASE64HEX_P }, 2, 3); try (Digester digester = MD5.getDigester()) { Resource r = dot.getResource(args[1]); if (r == null) throw new FileNotFoundException("From " + digester + ", not found " + args[1]); IO.copy(r.openInputStream(), digester); boolean hex = args.length > 2 && args[2].equals("hex"); if (hex) return Hex.toHexString(digester.digest() .digest()); return Base64.encodeBase64(digester.digest() .digest()); } } /** * SHA1 macro */ final static String _sha1Help = "${sha1;path}"; public String _sha1(String args[]) throws Exception { Macro.verifyCommand(args, _sha1Help, new Pattern[] { null, null, BASE64HEX_P }, 2, 3); try (Digester digester = SHA1.getDigester()) { Resource r = dot.getResource(args[1]); if (r == null) throw new FileNotFoundException("From sha1, not found " + args[1]); IO.copy(r.openInputStream(), digester); return Base64.encodeBase64(digester.digest() .digest()); } } public Descriptor getDescriptor(String descriptor) { return descriptors.getDescriptor(descriptor); } public TypeRef getTypeRef(String binaryClassName) { return descriptors.getTypeRef(binaryClassName); } public PackageRef getPackageRef(String binaryName) { return descriptors.getPackageRef(binaryName); } public TypeRef getTypeRefFrom(Class clazz) { return descriptors.getTypeRefFromFQN(clazz.getName()); } public TypeRef getTypeRefFromFQN(String fqn) { return descriptors.getTypeRefFromFQN(fqn); } public TypeRef getTypeRefFromPath(String path) { return descriptors.getTypeRefFromPath(path); } public ClassSignature getClassSignature(String signature) { return descriptors.getClassSignature(signature); } public MethodSignature getMethodSignature(String signature) { return descriptors.getMethodSignature(signature); } public FieldSignature getFieldSignature(String signature) { return descriptors.getFieldSignature(signature); } public boolean isImported(PackageRef packageRef) { return imports.containsKey(packageRef); } /** * Merge the attributes of two maps, where the first map can contain * wildcarded names. The idea is that the first map contains instructions * (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.
* 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: *
    *
  • com.foo.bar
  • *
  • com.foo.*
  • *
  • com.foo.???
  • *
  • com.*.[^b][^a][^r]
  • *
  • !com.foo.* (throws away any match for com.foo.*)
  • *
* Enough rope to hang the average developer I would say. * * @param instructions the instructions with patterns. * @param source the actual found packages, contains no duplicates * @return Only the packages that were filtered by the given instructions */ Packages filter(Instructions instructions, Packages source, Set nomatch) { Packages result = new Packages(); List refs = source.keySet() .stream() .filter(packageRef -> !packageRef.isMetaData()) .sorted() .collect(toCollection(LinkedList::new)); if (nomatch == null) nomatch = Create.set(); for (Instruction instruction : instructions.keySet()) { boolean match = false; for (Iterator i = refs.iterator(); i.hasNext();) { PackageRef packageRef = i.next(); String packageName = packageRef.getFQN(); if (instruction.matches(packageName)) { match = true; if (!instruction.isNegated()) { result.merge(packageRef, instruction.isDuplicate(), source.get(packageRef), instructions.get(instruction)); } i.remove(); // Can never match again for another pattern } } if (!match && !instruction.isAny()) nomatch.add(instruction); } /* * Tricky. If we have umatched instructions they might indicate that we * want to have multiple decorators for the same package. So we check * the unmatched against the result list. If then then match and have * actually interesting properties then we merge them */ for (Iterator i = nomatch.iterator(); i.hasNext();) { Instruction instruction = i.next(); // We assume the user knows what he is // doing and inserted a literal. So // we ignore any not matched literals // #252, we should not be negated to make it a constant if (instruction.isLiteral() && !instruction.isNegated()) { result.merge(getPackageRef(instruction.getLiteral()), true, instructions.get(instruction)); i.remove(); continue; } // Not matching a negated instruction looks // like an error ... Though so, but // in the second phase of Export-Package // the !package will never match anymore. if (instruction.isNegated()) { i.remove(); continue; } // An optional instruction should not generate // an error if (instruction.isOptional()) { i.remove(); continue; } // boolean matched = false; // Set prefs = new HashSet(result.keySet()); // for (PackageRef ref : prefs) { // if (instruction.matches(ref.getFQN())) { // result.merge(ref, true, source.get(ref), // instructions.get(instruction)); // matched = true; // } // } // if (matched) // i.remove(); } return result; } public void setDiagnostics(boolean b) { diagnostics = b; } public Clazz.JAVA getLowestEE() { if (ees.isEmpty()) return Clazz.JAVA.Java_1_4; return ees.first(); } public Clazz.JAVA getHighestEE() { if (ees.isEmpty()) return Clazz.JAVA.Java_1_4; return ees.last(); } public String _ee(String args[]) { return getHighestEE().getEE(); } /** * Calculate the output file for the given target. The strategy is: * *
	 * parameter given if not null and not directory if directory, this will be
	 * the output directory based on bsn-version.jar name of the source file if
	 * exists Untitled-[n]
	 * 
* * @param output may be null, otherwise a file path relative to base */ public File getOutputFile(String output) { if (output == null) output = get(Constants.OUTPUT); File outputDir; if (output != null) { File outputFile = getFile(output); if (outputFile.isDirectory()) outputDir = outputFile; else return outputFile; } else outputDir = getBase(); Entry name = getBundleSymbolicName(); if (name != null) { String bsn = name.getKey(); String version = getBundleVersion(); Version v = Version.parseVersion(version); String outputName = bsn + "-" + v.toStringWithoutQualifier() + Constants.DEFAULT_JAR_EXTENSION; return new File(outputDir, outputName); } File source = getJar().getSource(); if (source != null) { String outputName = source.getName(); return new File(outputDir, outputName); } if (getPropertiesFile() != null) { String nm = getPropertiesFile().getName(); if (nm.endsWith(Constants.DEFAULT_BND_EXTENSION)) { nm = nm.substring(0, nm.length() - Constants.DEFAULT_BND_EXTENSION.length()) + Constants.DEFAULT_JAR_EXTENSION; logger.debug("name is {}", nm); return new File(outputDir, nm); } } error("Cannot establish an output name from %s, nor bsn, nor source file name, using Untitled", output); int n = 0; File f = getFile(outputDir, "Untitled"); while (f.isFile()) { f = getFile(outputDir, "Untitled-" + n++); } return f; } /** * Utility function to carefully save the file. Will create a backup if the * source file has the same path as the output. It will also only save if * the file was modified or the force flag is true * * @param output the output file, if null {@link #getOutputFile(String)} is * used. * @param force if it needs to be overwritten * @throws Exception */ public boolean save(File output, boolean force) throws Exception { if (output == null) output = getOutputFile(null); Jar jar = getJar(); File source = jar.getSource(); logger.debug("check for modified build={} file={}, diff={}", jar.lastModified(), output.lastModified(), jar.lastModified() - output.lastModified()); if (!output.exists() || output.lastModified() <= jar.lastModified() || force) { File op = output.getParentFile(); IO.mkdirs(op); if (source != null && output.getCanonicalPath() .equals(source.getCanonicalPath())) { File bak = new File(source.getParentFile(), source.getName() .concat(".bak")); try { IO.rename(source, bak); } catch (IOException e) { exception(e, "Could not create backup file %s", bak); } } try { logger.debug("Saving jar to {}", output); getJar().write(output); } catch (Exception e) { IO.delete(output); exception(e, "Cannot write JAR file to %s due to %s", output, e); } return true; } logger.debug("Not modified {}", output); return false; } /** * Set default import and export instructions if none are set */ public void setDefaults(String bsn, Version version) { if (getExportPackage() == null) setExportPackage("*"); if (getImportPackage() == null) setExportPackage("*"); if (bsn != null && getBundleSymbolicName() == null) setBundleSymbolicName(bsn); if (version != null && getBundleVersion() == null) setBundleVersion(version); } /** * Remove the own references and optional java references from the uses lib * * @param apiUses * @param removeJava */ public Map> cleanupUses(Map> apiUses, boolean removeJava) { MultiMap map = new MultiMap<>(apiUses); for (Entry> e : map.entrySet()) { e.getValue() .remove(e.getKey()); if (!removeJava) continue; e.getValue() .removeIf(PackageRef::isJava); } return map; } /** * Return the classes for a given source package. * * @param source the source package * @return a set of classes for the requested package. */ public Set getClassspace(PackageRef source) { Set result = getClassspace().values() .stream() .filter(c -> c.getClassName() .getPackageRef() == source) .collect(toSet()); return result; } /** * Create a cross reference from package source, to packages in dest * * @param source * @param dest * @param sourceModifiers * @throws Exception */ public Map> getXRef(final PackageRef source, final Collection dest, final int sourceModifiers) throws Exception { final MultiMap xref = new MultiMap<>(Clazz.Def.class, TypeRef.class, true); for (final Clazz clazz : getClassspace().values()) { if ((clazz.getAccess() & sourceModifiers) == 0) continue; if (source != null && source != clazz.getClassName() .getPackageRef()) continue; clazz.parseClassFileWithCollector(new ClassDataCollector() { Clazz.MemberDef member; @Override public void extendsClass(TypeRef zuper) throws Exception { if (dest.contains(zuper.getPackageRef())) xref.add(clazz.getExtends(zuper), zuper); } @Override public void implementsInterfaces(TypeRef[] interfaces) throws Exception { for (TypeRef i : interfaces) { if (dest.contains(i.getPackageRef())) xref.add(clazz.getImplements(i), i); } } @Override public void referTo(TypeRef to, int modifiers) { if (to.isJava()) return; if (!dest.contains(to.getPackageRef())) return; if (member != null && ((modifiers & sourceModifiers) != 0)) { xref.add(member, to); } } @Override public void method(Clazz.MethodDef defined) { member = defined; } @Override public void field(Clazz.FieldDef defined) { member = defined; } }); } return xref; } public String _exports(String[] args) { return join(filter(getExports().keySet(), args)); } public String _imports(String[] args) { return join(filter(getImports().keySet(), args)); } private Collection filter(Collection list, String[] args) { if (args == null || args.length <= 1) return list; if (args.length > 2) warning("Too many arguments for ${%s} macro", args[0]); Instructions instrs = new Instructions(args[1]); return instrs.select(list, false); } /** * Report the details of this analyzer */ @Override public void report(Map table) throws Exception { super.report(table); analyze(); table.put("Contained", getContained().entrySet()); table.put("Imported", getImports().entrySet()); table.put("Exported", getExports().entrySet()); table.put("Referred", getReferred().entrySet()); table.put("Bundle Symbolic Name", getBsn()); table.put("Execution Environments", ees); } /** * Return the EEs */ public SortedSet getEEs() { return ees; } /** * @param name */ public String validResourcePath(String name, String reportIfWrong) { boolean changed = false; StringBuilder sb = new StringBuilder(name); for (int i = 0; i < sb.length(); i++) { char c = sb.charAt(i); if (c == '-' || c == '.' || c == '_' || c == '$' || Character.isLetterOrDigit(c)) continue; sb.replace(i, i + 1, "-"); changed = true; } if (changed) { if (reportIfWrong != null) warning("%s: %s", reportIfWrong, name); return sb.toString(); } return name; } /** * Check if we have an a check option */ public boolean check(Check key) { if (checks == null) { Parameters p = new Parameters(getProperty("-check"), this); checks = EnumSet.noneOf(Check.class); for (String k : p.keyList()) { try { if (k.equalsIgnoreCase("all")) { checks = EnumSet.allOf(Check.class); break; } Check c = Check.valueOf(k.toUpperCase(Locale.ROOT) .replace('-', '_')); checks.add(c); } catch (Exception e) { error("Invalid -check constant, allowed values are %s", Arrays.toString(Check.values())); } } } return checks.contains(key) || checks.contains(Check.ALL); } /** * Find the source file for this type * * @param type * @throws Exception */ public String getSourceFileFor(TypeRef type) throws Exception { Set sp = Collections.singleton(getFile(getProperty(DEFAULT_PROP_SRC_DIR, "src"))); return getSourceFileFor(type, sp); } public String getSourceFileFor(TypeRef type, Collection sourcePath) throws Exception { Clazz clazz = findClass(type); if (clazz == null) { Attrs attrs = classpathExports.get(type.getPackageRef()); String from = attrs.get(Constants.FROM_DIRECTIVE); if (from != null) { return from; } return null; } String path = type.getPackageRef() .getBinary() + "/" + clazz.getSourceFile(); for (File srcDir : sourcePath) { if (!srcDir.isFile()) continue; File file = IO.getFile(srcDir, path); if (file.isFile()) { return IO.absolutePath(file); } } return ""; } /** * Set location information for a type. */ public void setTypeLocation(SetLocation location, TypeRef type) throws Exception { String sf = getSourceFileFor(type); if (sf != null) { File sff = IO.getFile(sf); if (sff != null) { String names[] = { type.getShorterName(), type.getFQN(), type.getShortName() .replace('$', '.') }; for (String name : names) { FileLine fl = Processor.findHeader(sff, Pattern.compile("(class|interface)\\s*" + name, Pattern.DOTALL)); if (fl != null) fl.set(location); } } location.file(sf); } } public boolean assignable(String annoService, String inferredService) { return assignable(annoService, inferredService, true); } public boolean assignable(String annoService, String inferredService, boolean unknownResult) { if ((annoService == null) || (inferredService == null) || annoService.isEmpty() || inferredService.isEmpty()) { return unknownResult; } if (inferredService.equals("java.lang.Object")) { return true; // shortcut } try { Clazz annoServiceClazz = findClass(getTypeRefFromFQN(annoService)); Clazz inferredServiceClazz = findClass(getTypeRefFromFQN(inferredService)); return assignable(annoServiceClazz, inferredServiceClazz, unknownResult); } catch (Exception e) { // we couldn't determine return unknownResult; } } public boolean assignable(Clazz annoServiceClazz, Clazz inferredServiceClazz) { return assignable(annoServiceClazz, inferredServiceClazz, true); } public boolean assignable(Clazz annoServiceClazz, Clazz inferredServiceClazz, boolean unknownResult) { try { Boolean result = assignable0(annoServiceClazz, inferredServiceClazz); return (result != null) ? result.booleanValue() : unknownResult; } catch (Exception e) { // we couldn't determine return unknownResult; } } public Map getBundleClassPathTypes() { return bcpTypes; } public Optional getBundleClassPathEntry(Clazz clazz) { return Optional.ofNullable(bcpTypes.get(clazz.getClassName())); } private Boolean assignable0(Clazz annoServiceClazz, Clazz inferredServiceClazz) throws Exception { if ((annoServiceClazz == null) || (inferredServiceClazz == null)) { return null; // unknown } if (annoServiceClazz.equals(inferredServiceClazz)) { return Boolean.TRUE; } if (inferredServiceClazz.isInterface()) { TypeRef[] intfs = annoServiceClazz.getInterfaces(); if (intfs != null) { for (TypeRef intf : intfs) { Boolean result = assignable0(findClass(intf), inferredServiceClazz); if (result != Boolean.FALSE) { return result; } } } } else if (annoServiceClazz.isInterface()) { return Boolean.FALSE; } TypeRef zuper = annoServiceClazz.getSuper(); if (zuper == null) { return Boolean.FALSE; } return assignable0(findClass(zuper), inferredServiceClazz); } private void addDefinedContracts() { Parameters definedContracts = getMergedParameters(Constants.DEFINE_CONTRACT); contracts.collectContracts(Constants.DEFINE_CONTRACT, definedContracts); } private Attrs mergeImportAttrs(Attrs existing, Attrs imported) { if (existing == null) { if ((imported != null) && imported.containsKey(RESOLUTION_DIRECTIVE)) { return Attrs.create(RESOLUTION_DIRECTIVE, imported.get(RESOLUTION_DIRECTIVE)); } else { return new Attrs(); } } if (imported != null) { String existingResolution = existing.get(RESOLUTION_DIRECTIVE); String importResolution = imported.get(RESOLUTION_DIRECTIVE); if (RESOLUTION_OPTIONAL.equals(existingResolution)) { if (null == importResolution) { existing.remove(RESOLUTION_DIRECTIVE); } else if (RESOLUTION_MANDATORY.equals(importResolution)) { existing.put(RESOLUTION_DIRECTIVE, RESOLUTION_MANDATORY); } } } return existing; } /** * Add the content of the JAR to the current JAR and reset the analyzed flag * so that a new manifest will be calculated with the files in the JAR * included. * * @param delta a set of resources added to our jar and then set to be * reanalyzed */ public void addDelta(Jar delta) { try { if (dot == null) dot = new Jar(delta.getName()); packagesVisited.clear(); analyzeJar(delta, "", true, null, true); analyzed = false; } catch (Exception e) { throw Exceptions.duck(e); } } /** * Useful to reuse the annotation processing. The Annotation can be created * from other sources than Java code. This is mostly useful for the * annotations that generate Manifest headers. */ public void addAnnotation(Annotation ann, TypeRef c) throws Exception { Clazz clazz = findClass(c); if (clazz == null) { error("analyzer processing annotation %s but the associated class is not found in the JAR", c); return; } annotationHeaders.classStart(clazz); annotationHeaders.annotation(ann); annotationHeaders.classEnd(); } /** * Get a class from our own class path * * @param type a local type on the bnd classpath */ public void addClasspathDefault(Class type) { assert type != null : "type must be given"; try { TypeRef ref = getTypeRefFrom(type); URL resource = type.getClassLoader() .getResource(ref.getPath()); if (resource == null) { error("analyzer.addclasspathdefault expected class %s to be on the classpath since we have a type", type); } else { Resource r = new URLResource(resource, null); Clazz c = new Clazz(this, ref.getFQN(), r); if (c != null) { c.parseClassFile(); // we don't want that class in our classspace lookAsideClasses.putIfAbsent(ref, c); } } } catch (Exception e) { error("analyzer.findclassorlocal Failed to read a class from the bnd classpath"); } } }