diff --git a/framework/src/main/java/org/apache/felix/framework/capabilityset/SimpleFilter.java b/framework/src/main/java/org/apache/felix/framework/capabilityset/SimpleFilter.java index c112d8ef17..e560cb0733 100644 --- a/framework/src/main/java/org/apache/felix/framework/capabilityset/SimpleFilter.java +++ b/framework/src/main/java/org/apache/felix/framework/capabilityset/SimpleFilter.java @@ -21,6 +21,7 @@ import org.osgi.framework.VersionRange; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -569,8 +570,12 @@ private static int skipWhitespace(String s, int startIdx) * over the map's entry set. If ordering of attributes is important (e.g., * for hitting attribute indices), then the map's entry set should iterate * in the desired order. Equality testing is assumed for all attribute types - * other than version ranges, which are handled appropriated. If the attribute - * map is empty, then a filter that matches anything is returned. + * other than version ranges and collections, which are handled appropriated. + * A collection, such as a {@code List} typed attribute, becomes one equality + * test per element, so it matches a capability attribute that contains all + * of the elements; the OSGi specification does not define list matching for + * these headers. An empty collection only tests that the attribute is present. + * If the attribute map is empty, then a filter that matches anything is returned. * @param attrs Map of attributes to convert to a filter. * @return A filter corresponding to the attributes. */ @@ -629,26 +634,24 @@ public static SimpleFilter convert(Map attrs) } } } - else + else if (entry.getValue() instanceof Collection) { - List values = SimpleFilter.parseSubstring(entry.getValue().toString()); - if (values.size() > 1) + // A list attribute matches if it contains all of the values, + // an empty list still requires the attribute to be present. + Collection values = (Collection) entry.getValue(); + if (values.isEmpty()) { - filters.add( - new SimpleFilter( - entry.getKey(), - values, - SimpleFilter.SUBSTRING)); + filters.add(new SimpleFilter(entry.getKey(), "*", SimpleFilter.PRESENT)); } - else + for (Object value : values) { - filters.add( - new SimpleFilter( - entry.getKey(), - values.get(0), - SimpleFilter.EQ)); + filters.add(convertValue(entry.getKey(), value)); } } + else + { + filters.add(convertValue(entry.getKey(), entry.getValue())); + } } SimpleFilter sf = null; @@ -657,7 +660,7 @@ public static SimpleFilter convert(Map attrs) { sf = filters.get(0); } - else if (attrs.size() > 1) + else if (filters.size() > 1) { sf = new SimpleFilter(null, filters, SimpleFilter.AND); } @@ -668,4 +671,14 @@ else if (filters.isEmpty()) return sf; } + + private static SimpleFilter convertValue(String name, Object value) + { + List values = SimpleFilter.parseSubstring(value.toString()); + if (values.size() > 1) + { + return new SimpleFilter(name, values, SimpleFilter.SUBSTRING); + } + return new SimpleFilter(name, values.get(0), SimpleFilter.EQ); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ManifestParser.java b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ManifestParser.java index 670290e0eb..181cf3971f 100644 --- a/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ManifestParser.java +++ b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ManifestParser.java @@ -334,44 +334,18 @@ private static List normalizeImportClauses( Logger logger, List clauses, String mv) throws BundleException { + // Typed attributes are an R4 feature, R3 bundles keep their lenient handling. + if (mv.equals("2")) + { + convertTypedAttributes(clauses); + } + // Verify that the values are equals if the package specifies // both version and specification-version attributes. Set dupeSet = new HashSet<>(); for (ParsedHeaderClause clause : clauses) { - // Check for "version" and "specification-version" attributes - // and verify they are the same if both are specified. - Object v = clause.m_attrs.get(Constants.VERSION_ATTRIBUTE); - Object sv = clause.m_attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION); - if ((v != null) && (sv != null)) - { - // Verify they are equal. - if (!((String) v).trim().equals(((String) sv).trim())) - { - throw new IllegalArgumentException( - "Both version and specification-version are specified, but they are not equal."); - } - } - - // Ensure that only the "version" attribute is used and convert - // it to the VersionRange type. - if ((v != null) || (sv != null)) - { - clause.m_attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); - v = (v == null) ? sv : v; - clause.m_attrs.put( - Constants.VERSION_ATTRIBUTE, - new VersionRange(v.toString())); - } - - // If bundle version is specified, then convert its type to VersionRange. - v = clause.m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); - if (v != null) - { - clause.m_attrs.put( - Constants.BUNDLE_VERSION_ATTRIBUTE, - new VersionRange(v.toString())); - } + convertVersionRanges(clause); // Verify no duplicate imports. for (String pkgName : clause.m_paths) @@ -509,6 +483,12 @@ private static List normalizeDynamicImportClauses( Logger logger, List clauses, String mv) throws BundleException { + // Typed attributes are an R4 feature, R3 bundles keep their lenient handling. + if (mv.equals("2")) + { + convertTypedAttributes(clauses); + } + // Verify that the values are equals if the package specifies // both version and specification-version attributes. for (ParsedHeaderClause clause : clauses) @@ -527,39 +507,7 @@ private static List normalizeDynamicImportClauses( clause.m_dirs.put(Constants.RESOLUTION_DIRECTIVE, FelixConstants.RESOLUTION_DYNAMIC); - // Check for "version" and "specification-version" attributes - // and verify they are the same if both are specified. - Object v = clause.m_attrs.get(Constants.VERSION_ATTRIBUTE); - Object sv = clause.m_attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION); - if ((v != null) && (sv != null)) - { - // Verify they are equal. - if (!((String) v).trim().equals(((String) sv).trim())) - { - throw new IllegalArgumentException( - "Both version and specification-version are specified, but they are not equal."); - } - } - - // Ensure that only the "version" attribute is used and convert - // it to the VersionRange type. - if ((v != null) || (sv != null)) - { - clause.m_attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); - v = (v == null) ? sv : v; - clause.m_attrs.put( - Constants.VERSION_ATTRIBUTE, - new VersionRange(v.toString())); - } - - // If bundle version is specified, then convert its type to VersionRange. - v = clause.m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); - if (v != null) - { - clause.m_attrs.put( - Constants.BUNDLE_VERSION_ATTRIBUTE, - new VersionRange(v.toString())); - } + convertVersionRanges(clause); // Dynamic imports can have duplicates, verify that no partial package name wild carding is used for (String pkgName : clause.m_paths) @@ -736,99 +684,193 @@ private static List normalizeCapabilityClauses( // Should we error here if we are not an R4 bundle? } + return convertTypedAttributes(clauses); + } + + private static List convertTypedAttributes( + List clauses) + throws BundleException + { // Convert attributes into specified types. for (ParsedHeaderClause clause : clauses) { for (Entry entry : clause.m_types.entrySet()) { - String type = entry.getValue(); - if (!type.equals("String")) + Object value = clause.m_attrs.get(entry.getKey()); + try { - if (type.equals("Double")) - { - clause.m_attrs.put( - entry.getKey(), - Double.valueOf(clause.m_attrs.get(entry.getKey()).toString().trim())); - } - else if (type.equals("Version")) - { - clause.m_attrs.put( - entry.getKey(), - new Version(clause.m_attrs.get(entry.getKey()).toString().trim())); - } - else if (type.equals("Long")) - { - clause.m_attrs.put( - entry.getKey(), - Long.valueOf(clause.m_attrs.get(entry.getKey()).toString().trim())); - } - else if (type.startsWith("List")) - { - int startIdx = type.indexOf('<'); - int endIdx = type.indexOf('>'); - if (((startIdx > 0) && (endIdx <= startIdx)) - || ((startIdx < 0) && (endIdx > 0))) - { - throw new BundleException( - "Invalid Provide-Capability attribute list type for '" - + entry.getKey() - + "' : " - + type); - } + clause.m_attrs.put( + entry.getKey(), + convertTypedValue(entry.getKey(), entry.getValue(), value.toString())); + } + catch (IllegalArgumentException ex) + { + throw new BundleException( + "Invalid value for attribute '" + + entry.getKey() + + "' of type " + + entry.getValue() + + " : " + + value, + BundleException.MANIFEST_ERROR, + ex); + } + } + // The attributes have their types now, so converting the clause + // again must not split a list value a second time. + clause.m_types.clear(); + } - String listType = "String"; - if (endIdx > startIdx) - { - listType = type.substring(startIdx + 1, endIdx).trim(); - } + return clauses; + } - List tokens = parseDelimitedString( - clause.m_attrs.get(entry.getKey()).toString(), ",", false); - List values = new ArrayList<>(tokens.size()); - for (String token : tokens) - { - if (listType.equals("String")) - { - values.add(token); - } - else if (listType.equals("Double")) - { - values.add(Double.valueOf(token.trim())); - } - else if (listType.equals("Version")) - { - values.add(new Version(token.trim())); - } - else if (listType.equals("Long")) - { - values.add(Long.valueOf(token.trim())); - } - else - { - throw new BundleException( - "Unknown Provide-Capability attribute list type for '" - + entry.getKey() - + "' : " - + type); - } - } - clause.m_attrs.put( - entry.getKey(), - values); - } - else - { - throw new BundleException( - "Unknown Provide-Capability attribute type for '" - + entry.getKey() - + "' : " - + type); - } + private static Object convertTypedValue(String key, String type, String value) + throws BundleException + { + if (type.equals("String")) + { + return value; + } + else if (type.equals("Double")) + { + return Double.valueOf(value.trim()); + } + else if (type.equals("Version")) + { + return new Version(value.trim()); + } + else if (type.equals("Long")) + { + return Long.valueOf(value.trim()); + } + else if (type.startsWith("List")) + { + int startIdx = type.indexOf('<'); + int endIdx = type.indexOf('>'); + if (((startIdx > 0) && (endIdx <= startIdx)) + || ((startIdx < 0) && (endIdx > 0))) + { + throw new BundleException( + "Invalid attribute list type for '" + + key + + "' : " + + type, + BundleException.MANIFEST_ERROR); + } + + String listType = "String"; + if (endIdx > startIdx) + { + listType = type.substring(startIdx + 1, endIdx).trim(); + } + + List tokens = parseDelimitedString(value, ",", false); + List values = new ArrayList<>(tokens.size()); + for (String token : tokens) + { + if (listType.equals("String")) + { + values.add(token); + } + else if (listType.equals("Double")) + { + values.add(Double.valueOf(token.trim())); + } + else if (listType.equals("Version")) + { + values.add(new Version(token.trim())); + } + else if (listType.equals("Long")) + { + values.add(Long.valueOf(token.trim())); + } + else + { + throw new BundleException( + "Unknown attribute list type for '" + + key + + "' : " + + type, + BundleException.MANIFEST_ERROR); } } + return values; } + throw new BundleException( + "Unknown attribute type for '" + + key + + "' : " + + type, + BundleException.MANIFEST_ERROR); + } - return clauses; + // Converts the version and bundle-version attributes of an import + // clause to the VersionRange type. + private static void convertVersionRanges(ParsedHeaderClause clause) + throws BundleException + { + // Ensure that only the "version" attribute is used. + String v = getPackageVersion(clause, true); + if (v != null) + { + clause.m_attrs.put( + Constants.VERSION_ATTRIBUTE, + new VersionRange(v)); + } + + v = getVersionAttribute(clause, Constants.BUNDLE_VERSION_ATTRIBUTE); + if (v != null) + { + clause.m_attrs.put( + Constants.BUNDLE_VERSION_ATTRIBUTE, + new VersionRange(v)); + } + } + + // Merges the deprecated specification-version attribute into the version + // attribute and returns the version, or null if neither is specified. + private static String getPackageVersion(ParsedHeaderClause clause, boolean range) + throws BundleException + { + String v = getVersionAttribute(clause, Constants.VERSION_ATTRIBUTE); + String sv = getVersionAttribute(clause, Constants.PACKAGE_SPECIFICATION_VERSION); + clause.m_attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); + if ((v != null) && (sv != null)) + { + // Compare versions rather than strings, a typed version:Version=1.0 + // is 1.0.0 while specification-version="1.0" stays a string. + boolean equal = range + ? new VersionRange(v).equals(new VersionRange(sv)) + : Version.parseVersion(v).equals(Version.parseVersion(sv)); + if (!equal) + { + throw new BundleException( + "Both version and specification-version are specified, but they are not equal.", + BundleException.MANIFEST_ERROR); + } + } + return (v != null) ? v : sv; + } + + // Returns a version attribute as a string, or null if it is not specified. + private static String getVersionAttribute(ParsedHeaderClause clause, String name) + throws BundleException + { + Object value = clause.m_attrs.get(name); + if (value == null) + { + return null; + } + if (!(value instanceof String) && !(value instanceof Version)) + { + throw new BundleException( + "Attribute '" + + name + + "' must be of type String or Version : " + + value, + BundleException.MANIFEST_ERROR); + } + return value.toString().trim(); } private static List convertProvideCapabilities( @@ -874,6 +916,12 @@ private static List normalizeExportClauses( String mv, String bsn, Version bv, boolean connectModule) throws BundleException { + // Typed attributes are an R4 feature, R3 bundles keep their lenient handling. + if (mv.equals("2")) + { + convertTypedAttributes(clauses); + } + for (ParsedHeaderClause clause : clauses) { // Verify that the named package has not already been declared. @@ -902,37 +950,12 @@ else if (pkgName.length() == 0) } } - // Check for "version" and "specification-version" attributes - // and verify they are the same if both are specified. - Object v = clause.m_attrs.get(Constants.VERSION_ATTRIBUTE); - Object sv = clause.m_attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION); - if ((v != null) && (sv != null)) - { - // Verify they are equal. - if (!((String) v).trim().equals(((String) sv).trim())) - { - throw new IllegalArgumentException( - "Both version and specification-version are specified, but they are not equal."); - } - } - - // Always add the default version if not specified. - if ((v == null) && (sv == null)) - { - v = Version.emptyVersion; - } - // Ensure that only the "version" attribute is used and convert - // it to the appropriate type. - if ((v != null) || (sv != null)) - { - // Convert version attribute to type Version. - clause.m_attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); - v = (v == null) ? sv : v; - clause.m_attrs.put( - Constants.VERSION_ATTRIBUTE, - Version.parseVersion(v.toString())); - } + // it to type Version, always adding the default version if not specified. + String v = getPackageVersion(clause, false); + clause.m_attrs.put( + Constants.VERSION_ATTRIBUTE, + (v == null) ? Version.emptyVersion : Version.parseVersion(v)); // If this is an R4 bundle, then make sure it doesn't specify // bundle symbolic name or bundle version attributes. @@ -1549,8 +1572,8 @@ private static List parseFragmentHost( String mv = getManifestVersion(headerMap); if ((mv != null) && mv.equals("2")) { - List clauses = parseStandardHeader( - (String) headerMap.get(Constants.FRAGMENT_HOST)); + List clauses = convertTypedAttributes(parseStandardHeader( + (String) headerMap.get(Constants.FRAGMENT_HOST))); if (clauses.size() > 0) { // Make sure that only one fragment host symbolic name is specified. @@ -1569,14 +1592,10 @@ else if (clauses.get(0).m_paths.size() > 1) // If the bundle-version attribute is specified, then convert // it to the proper type. - Object value = clauses.get(0).m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); - value = (value == null) ? "0.0.0" : value; - if (value != null) - { - clauses.get(0).m_attrs.put( - Constants.BUNDLE_VERSION_ATTRIBUTE, - new VersionRange(value.toString())); - } + String value = getVersionAttribute(clauses.get(0), Constants.BUNDLE_VERSION_ATTRIBUTE); + clauses.get(0).m_attrs.put( + Constants.BUNDLE_VERSION_ATTRIBUTE, + new VersionRange((value == null) ? "0.0.0" : value)); // Note that we use a linked hash map here to ensure the // host symbolic name is first, which will make indexing @@ -1749,6 +1768,7 @@ private static String getBreeVersionClause(Version ver) private static List normalizeRequireClauses( Logger logger, List clauses, String mv) + throws BundleException { // R3 bundles cannot require other bundles. if (!mv.equals("2")) @@ -1757,15 +1777,17 @@ private static List normalizeRequireClauses( } else { + convertTypedAttributes(clauses); + // Convert bundle version attribute to VersionRange type. for (ParsedHeaderClause clause : clauses) { - Object value = clause.m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); + String value = getVersionAttribute(clause, Constants.BUNDLE_VERSION_ATTRIBUTE); if (value != null) { clause.m_attrs.put( Constants.BUNDLE_VERSION_ATTRIBUTE, - new VersionRange(value.toString())); + new VersionRange(value)); } } } diff --git a/framework/src/test/java/org/apache/felix/framework/capabilityset/SimpleFilterTest.java b/framework/src/test/java/org/apache/felix/framework/capabilityset/SimpleFilterTest.java index 7b1e2765c8..bb02e78454 100644 --- a/framework/src/test/java/org/apache/felix/framework/capabilityset/SimpleFilterTest.java +++ b/framework/src/test/java/org/apache/felix/framework/capabilityset/SimpleFilterTest.java @@ -21,11 +21,45 @@ import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; +import org.osgi.framework.VersionRange; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; class SimpleFilterTest { + @Test + void convertListAttribute() + { + SimpleFilter sf = SimpleFilter.convert( + Collections.singletonMap("colors", Arrays.asList("red", "blue"))); + assertThat(sf).hasToString("(&(colors=red)(colors=blue))"); + } + + @Test + void convertEmptyListAttribute() + { + SimpleFilter sf = SimpleFilter.convert( + Collections.singletonMap("colors", Collections.emptyList())); + assertThat(sf).hasToString("(colors=*)").isEqualTo(SimpleFilter.parse("(colors=*)")); + + Map attrs = new LinkedHashMap<>(); + attrs.put("osgi.wiring.package", "org.example"); + attrs.put("colors", Collections.emptyList()); + assertThat(SimpleFilter.convert(attrs)).hasToString("(&(osgi.wiring.package=org.example)(colors=*))"); + } + + @Test + void convertSingleBoundedVersionRange() + { + SimpleFilter sf = SimpleFilter.convert( + Collections.singletonMap("version", new VersionRange("[1.0,2.0)"))); + assertThat(sf).hasToString("(&(version>=1.0.0)(!(version>=2.0.0)))"); + } + @Test void substringMatching() { diff --git a/framework/src/test/java/org/apache/felix/framework/util/manifestparser/ManifestParserTest.java b/framework/src/test/java/org/apache/felix/framework/util/manifestparser/ManifestParserTest.java index e8dbdb10d8..be89adce27 100644 --- a/framework/src/test/java/org/apache/felix/framework/util/manifestparser/ManifestParserTest.java +++ b/framework/src/test/java/org/apache/felix/framework/util/manifestparser/ManifestParserTest.java @@ -19,6 +19,7 @@ package org.apache.felix.framework.util.manifestparser; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -30,6 +31,7 @@ import java.util.Map; import org.apache.felix.framework.BundleRevisionImpl; +import org.apache.felix.framework.Logger; import org.apache.felix.framework.cache.ConnectContentContent; import org.apache.felix.framework.cache.Content; import org.apache.felix.framework.util.FelixConstants; @@ -41,8 +43,11 @@ import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.Version; import org.osgi.framework.connect.ConnectContent; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.namespace.IdentityNamespace; import org.osgi.framework.namespace.NativeNamespace; +import org.osgi.framework.namespace.PackageNamespace; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleRequirement; @@ -165,6 +170,220 @@ void attributes() throws BundleException { assertThat(rList).hasSize(3); } + @Test + void exportPackageTypedAttributes() throws BundleException + { + Map headers = new HashMap<>(); + headers.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + headers.put(Constants.BUNDLE_SYMBOLICNAME, "com.example.exporter"); + headers.put(Constants.EXPORT_PACKAGE, + "com.example;version=\"1.0\";x-directive:=directiveValue;attr=value;" + + "longAttr:Long=42;stringAttr:String=stringValue;doubleAttr:Double=4.2;" + + "versionAttr:Version=4.2;longList:List=\"2,3,4\";" + + "stringList:List=\"one,two,three\";doubleList:List=\"2.3,3.4,4.5\";" + + "versionList:List=\"2.3,3.4,4.5\"," + + "com.example.typed;version:Version=2.0"); + + ManifestParser mp = new ManifestParser(null, null, null, headers); + + BundleCapability pc = findPackageCapability(mp.getCapabilities(), "com.example"); + assertThat(pc.getAttributes()) + .containsEntry("attr", "value") + .containsEntry("longAttr", Long.valueOf(42)) + .containsEntry("stringAttr", "stringValue") + .containsEntry("doubleAttr", Double.valueOf(4.2)) + .containsEntry("versionAttr", new Version("4.2")) + .containsEntry("longList", Arrays.asList(Long.valueOf(2), Long.valueOf(3), Long.valueOf(4))) + .containsEntry("stringList", Arrays.asList("one", "two", "three")) + .containsEntry("doubleList", Arrays.asList(Double.valueOf(2.3), Double.valueOf(3.4), Double.valueOf(4.5))) + .containsEntry("versionList", Arrays.asList(new Version("2.3"), new Version("3.4"), new Version("4.5"))) + .containsEntry(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version("1.0")) + .doesNotContainKey("x-directive"); + assertThat(pc.getDirectives()).containsEntry("x-directive", "directiveValue"); + + BundleCapability typed = findPackageCapability(mp.getCapabilities(), "com.example.typed"); + assertThat(typed.getAttributes()) + .containsEntry(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version("2.0")); + } + + @Test + void typedVersionWithSpecificationVersion() throws BundleException + { + Map headers = new HashMap<>(); + headers.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + headers.put(Constants.BUNDLE_SYMBOLICNAME, "com.example.versions"); + headers.put(Constants.EXPORT_PACKAGE, "com.example;version:Version=1.0;specification-version=\"1.0\""); + headers.put(Constants.IMPORT_PACKAGE, "com.example;version:Version=1.0;specification-version=\"1.0\""); + + ManifestParser mp = new ManifestParser(null, null, null, headers); + + assertThat(findPackageCapability(mp.getCapabilities(), "com.example").getAttributes()) + .containsEntry(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version("1.0")); + assertThat(findRequirement(mp.getRequirements(), PackageNamespace.PACKAGE_NAMESPACE).getDirectives()) + .containsEntry(Constants.FILTER_DIRECTIVE, "(&(osgi.wiring.package=com.example)(version>=1.0.0))"); + } + + @Test + void importPackageTypedAttributes() throws Exception + { + BundleCapability pc = findPackageCapability(typedExporter().getCapabilities(), "com.example"); + + ManifestParser mp = parse("com.example.importer", + Constants.IMPORT_PACKAGE, "com.example;version:Version=1.0;" + TYPED_MATCHING_ATTRS); + + assertMatches(findRequirement(mp.getRequirements(), PackageNamespace.PACKAGE_NAMESPACE), pc); + } + + @Test + void importPackageTypedAttributesNotMatching() throws Exception + { + BundleCapability pc = findPackageCapability(typedExporter().getCapabilities(), "com.example"); + + for (String attr : Arrays.asList("longAttr:Long=43", "doubleAttr:Double=4.3", + "versionAttr:Version=4.3", "stringList:List=\"one,four\"", + "versionList:List=\"3.5\"")) + { + ManifestParser mp = parse("com.example.importer", Constants.IMPORT_PACKAGE, "com.example;" + attr); + + assertNotMatches(findRequirement(mp.getRequirements(), PackageNamespace.PACKAGE_NAMESPACE), pc); + } + } + + @Test + void importPackageEmptyListRequiresAttribute() throws Exception + { + BundleCapability pc = findPackageCapability(typedExporter().getCapabilities(), "com.example"); + + ManifestParser present = parse("com.example.importer", + Constants.IMPORT_PACKAGE, "com.example;stringList:List=\"\""); + ManifestParser missing = parse("com.example.importer", + Constants.IMPORT_PACKAGE, "com.example;otherList:List=\"\""); + + assertMatches(findRequirement(present.getRequirements(), PackageNamespace.PACKAGE_NAMESPACE), pc); + assertNotMatches(findRequirement(missing.getRequirements(), PackageNamespace.PACKAGE_NAMESPACE), pc); + } + + @Test + void dynamicImportPackageTypedAttributes() throws Exception + { + BundleCapability pc = findPackageCapability(typedExporter().getCapabilities(), "com.example"); + + List reqs = ManifestParser.parseDynamicImportHeader(null, null, + "com.example;" + TYPED_MATCHING_ATTRS); + + assertMatches(findRequirement(reqs, PackageNamespace.PACKAGE_NAMESPACE), pc); + } + + @Test + void requireBundleTypedAttributes() throws Exception + { + BundleCapability bc = findCapability(typedHost().getCapabilities(), BundleNamespace.BUNDLE_NAMESPACE); + + ManifestParser mp = parse("com.example.requirer", + Constants.REQUIRE_BUNDLE, "com.example.host;" + TYPED_MATCHING_ATTRS); + + assertMatches(findRequirement(mp.getRequirements(), BundleNamespace.BUNDLE_NAMESPACE), bc); + } + + @Test + void requireBundleTypedBundleVersion() throws Exception + { + BundleCapability bc = findCapability(typedHost().getCapabilities(), BundleNamespace.BUNDLE_NAMESPACE); + + ManifestParser matching = parse("com.example.requirer", + Constants.REQUIRE_BUNDLE, "com.example.host;bundle-version:Version=1.0"); + ManifestParser notMatching = parse("com.example.requirer", + Constants.REQUIRE_BUNDLE, "com.example.host;bundle-version:Version=2.0"); + + assertMatches(findRequirement(matching.getRequirements(), BundleNamespace.BUNDLE_NAMESPACE), bc); + assertNotMatches(findRequirement(notMatching.getRequirements(), BundleNamespace.BUNDLE_NAMESPACE), bc); + } + + @Test + void fragmentHostTypedAttributes() throws Exception + { + BundleCapability hc = findCapability(typedHost().getCapabilities(), HostNamespace.HOST_NAMESPACE); + + ManifestParser mp = parse("com.example.fragment", + Constants.FRAGMENT_HOST, "com.example.host;" + TYPED_MATCHING_ATTRS); + + assertMatches(findRequirement(mp.getRequirements(), HostNamespace.HOST_NAMESPACE), hc); + } + + @Test + void fragmentHostTypedBundleVersion() throws Exception + { + BundleCapability hc = findCapability(typedHost().getCapabilities(), HostNamespace.HOST_NAMESPACE); + + ManifestParser matching = parse("com.example.fragment", + Constants.FRAGMENT_HOST, "com.example.host;bundle-version:Version=1.0"); + ManifestParser notMatching = parse("com.example.fragment", + Constants.FRAGMENT_HOST, "com.example.host;bundle-version:Version=2.0"); + + assertMatches(findRequirement(matching.getRequirements(), HostNamespace.HOST_NAMESPACE), hc); + assertNotMatches(findRequirement(notMatching.getRequirements(), HostNamespace.HOST_NAMESPACE), hc); + } + + @Test + void malformedTypedValue() + { + assertManifestError("'longAttr' of type Long : abc", + Constants.EXPORT_PACKAGE, "com.example;longAttr:Long=abc"); + assertManifestError("'versionList' of type List : 1.0,x", + Constants.IMPORT_PACKAGE, "com.example;versionList:List=\"1.0,x\""); + assertManifestError("'doubleAttr' of type Double : x", + Constants.REQUIRE_BUNDLE, "com.example.host;doubleAttr:Double=x"); + } + + @Test + void versionAttributeMustBeStringOrVersion() + { + assertManifestError("'version' must be of type String or Version", + Constants.EXPORT_PACKAGE, "com.example;version:List=\"1.0,2.0\""); + assertManifestError("'version' must be of type String or Version", + Constants.IMPORT_PACKAGE, "com.example;version:Long=1"); + assertManifestError("'bundle-version' must be of type String or Version", + Constants.FRAGMENT_HOST, "com.example.host;bundle-version:List=\"1.0,2.0\""); + } + + @Test + void unknownTypedAttributeType() + { + assertManifestError("Unknown attribute type for 'x' : Foo", + Constants.EXPORT_PACKAGE, "com.example;x:Foo=1"); + assertManifestError("Unknown attribute list type for 'x' : List", + Constants.IMPORT_PACKAGE, "com.example;x:List=1"); + assertManifestError("Invalid attribute list type for 'x' : List headers = new HashMap<>(); + headers.put(Constants.BUNDLE_SYMBOLICNAME, "com.example.r3"); + headers.put(Constants.EXPORT_PACKAGE, "com.example;x:Long=abc"); + headers.put(Constants.IMPORT_PACKAGE, "com.example.other;x:Long=abc"); + + ManifestParser mp = new ManifestParser(new Logger(), null, null, headers); + + assertThat(findPackageCapability(mp.getCapabilities(), "com.example").getAttributes()) + .doesNotContainKey("x"); + assertThat(findRequirement(mp.getRequirements(), PackageNamespace.PACKAGE_NAMESPACE).getDirectives()) + .containsEntry(Constants.FILTER_DIRECTIVE, "(&(osgi.wiring.package=com.example.other)(version>=0.0.0))"); + } + @Test void convertNativeCode() throws InvalidSyntaxException { @@ -214,6 +433,19 @@ private BundleCapability findCapability(Collection capabilitie return null; } + private BundleCapability findPackageCapability(Collection capabilities, String pkgName) + { + for (BundleCapability capability : capabilities) + { + if (PackageNamespace.PACKAGE_NAMESPACE.equals(capability.getNamespace()) + && pkgName.equals(capability.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE))) + { + return capability; + } + } + return null; + } + private BundleRequirement findRequirement(Collection requirements, String namespace) { for(BundleRequirement requirement: requirements) @@ -225,4 +457,56 @@ private BundleRequirement findRequirement(Collection requirem } return null; } + + private static final String TYPED_ATTRS = "longAttr:Long=42;doubleAttr:Double=4.2;versionAttr:Version=4.2;" + + "stringList:List=\"one,two,three\";versionList:List=\"2.3,3.4,4.5\""; + + private static final String TYPED_MATCHING_ATTRS = "longAttr:Long=42;doubleAttr:Double=4.2;versionAttr:Version=4.2;" + + "stringList:List=\"one,three\";versionList:List=\"3.4\""; + + private static ManifestParser typedExporter() throws BundleException + { + return parse("com.example.exporter", Constants.EXPORT_PACKAGE, "com.example;version=1.0;" + TYPED_ATTRS); + } + + private static ManifestParser typedHost() throws BundleException + { + return parse("com.example.host;" + TYPED_ATTRS, Constants.BUNDLE_VERSION, "1.2.0"); + } + + private static ManifestParser parse(String bsn, String... headers) throws BundleException + { + Map map = new HashMap<>(); + map.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + map.put(Constants.BUNDLE_SYMBOLICNAME, bsn); + for (int i = 0; i < headers.length; i += 2) + { + map.put(headers[i], headers[i + 1]); + } + return new ManifestParser(null, null, null, map); + } + + private static void assertMatches(BundleRequirement req, BundleCapability cap) throws InvalidSyntaxException + { + assertThat(req.matches(cap)).as("%s matches %s", req, cap).isTrue(); + String filter = req.getDirectives().get(Constants.FILTER_DIRECTIVE); + assertThat(FrameworkUtil.createFilter(filter).matches(cap.getAttributes())) + .as("%s matches %s", filter, cap.getAttributes()).isTrue(); + } + + private static void assertNotMatches(BundleRequirement req, BundleCapability cap) throws InvalidSyntaxException + { + assertThat(req.matches(cap)).as("%s does not match %s", req, cap).isFalse(); + String filter = req.getDirectives().get(Constants.FILTER_DIRECTIVE); + assertThat(FrameworkUtil.createFilter(filter).matches(cap.getAttributes())) + .as("%s does not match %s", filter, cap.getAttributes()).isFalse(); + } + + private static void assertManifestError(String message, String... headers) + { + assertThatThrownBy(() -> parse("com.example.test", headers)) + .isInstanceOfSatisfying(BundleException.class, + ex -> assertThat(ex.getType()).isEqualTo(BundleException.MANIFEST_ERROR)) + .hasMessageContaining(message); + } }