This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch issue/SLING-10288 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-analyser.git
commit 0fe99889ebfc38c83dde5c1be9456891219b1774 Author: Robert Munteanu <[email protected]> AuthorDate: Wed Jun 12 15:39:01 2024 +0200 SLING-10288 - FelixFrameworkScanner sets framework properties always for current Java version Rough implementation --- pom.xml | 11 +++ readme.md | 25 +++++- .../extensions/AnalyserMetaDataExtension.java | 47 ++++++++++- .../extensions/AnalyserMetaDataHandler.java | 62 ++++++++++++--- .../task/impl/CheckRequirementsCapabilities.java | 4 +- .../org/apache/sling/feature/scanner/Scanner.java | 74 +++++++++++++++++- .../extensions/AnalyserMetaDataHandlerTest.java | 91 +++++++++++++++++++++- .../feature-input-system-bundle.json | 12 +++ 8 files changed, 305 insertions(+), 21 deletions(-) diff --git a/pom.xml b/pom.xml index 146539e..b4199c2 100644 --- a/pom.xml +++ b/pom.xml @@ -212,6 +212,17 @@ <version>1.2.14</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>3.26.0</version> + <scope>test</scope> + </dependency> <dependency> <!-- Used by some tests --> <groupId>org.apache.felix</groupId> diff --git a/readme.md b/readme.md index 7530b1f..93cbd10 100644 --- a/readme.md +++ b/readme.md @@ -116,16 +116,25 @@ Checks bundle requirements/capabilities for consistency and completeness. Generates additional metadata that will be recorded in the feature model definition. It is configured by defining an `analyser-metadata` section in the feature model definition. The section will be processed by the extension when the feature models are aggregated and will be replaced with the required entries for bundles matching the configuration. +### Bundle metadata + The section can have entries that match individual bundle names and entries that match based on regular expressions (if the key contains the "*" character). Each individual entry can contain the following keys: - Configuration key | Allowed values | Description ----- | ----- | ----- `manifest` | `null` or Object | If null, the manifest is not generated. If an object, the values are copied over. If absent, the values are extracted from the OSGi bundle `report` | Object with keys `warning` and `error` | If any of the values are set to `false`, reporting is suppressed for those kind of occurences. +### Framework metadata + +A special case is when an entry with the name `system.bundle` is found. This will record information about the system bundle exported packages and capabilites as present at the time of the feature aggregation. When these are present in an aggregated feature the analysers will use that information instead of the one discovered during analysis time. + +The system bundle information will be extracted from the `execution-environment` extension. In this extension, the `framework` entry is required and is used to gather information about the system bundle. The `javaVersion` property is optional but recommended and is used to validate that the Java version used to generate the metadata matches the one in the execution environment. + +### Example + A typical configuration for platform applications is: ```javascript @@ -138,9 +147,19 @@ A typical configuration for platform applications is: "error": false, "warning": false } - } + }, + "system.bundle": {} + }, + "framework":{ + "id":"org.apache.felix:org.apache.felix.framework:7.0.5" + }, + "javaVersion": "21" } } ``` -This ensures that warnings related to the platform are not reported when the feature is aggregated with downstream (consumer) applications. The manifests should not be inlined under normal circumstances, since it greatly increases the size of the resulting features. +This ensures that + +- warnings related to the platform are not reported when the feature is aggregated with downstream (consumer) applications. The manifests should not be inlined under normal circumstances, since it greatly increases the size of the resulting features. +- metadata related to the system bundle is recorded at the Java 21 compatibility level + diff --git a/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataExtension.java b/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataExtension.java index 40b7172..bc3bdb2 100644 --- a/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataExtension.java +++ b/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataExtension.java @@ -39,6 +39,7 @@ public class AnalyserMetaDataExtension { private final Map<ArtifactId, Map<String, String>> manifests = new HashMap<>(); private final Map<ArtifactId, Boolean> reportWarnings = new HashMap<>(); private final Map<ArtifactId, Boolean> reportErrors = new HashMap<>(); + private SystemBundle systemBundle; public static AnalyserMetaDataExtension getAnalyserMetaDataExtension(Feature feature) { Extension ext = feature == null ? null : feature.getExtensions().getByName(EXTENSION_NAME); @@ -56,10 +57,25 @@ public class AnalyserMetaDataExtension { } private AnalyserMetaDataExtension(JsonObject json) { + for (Map.Entry<String, JsonValue> entry : json.entrySet()) { + + // handle system bundle separately if ( entry.getKey().equals(Constants.SYSTEM_BUNDLE_SYMBOLICNAME) ) { - continue; // not used for now + JsonObject systemBundleConfig = entry.getValue().asJsonObject(); + JsonObject manifestObj = systemBundleConfig.getJsonObject("manifest"); + String artifactId = systemBundleConfig.getJsonString("artifactId").getString(); + String sortedFrameworkProperties = systemBundleConfig.getJsonString("sortedFrameworkProperties").getString(); + + Map<String, String> manifest = new HashMap<>(); + for (String key : manifestObj.keySet()) { + manifest.put(key, manifestObj.getString(key)); + } + systemBundle = new SystemBundle(manifest, ArtifactId.fromMvnId(artifactId), sortedFrameworkProperties); + + continue; } + ArtifactId id = ArtifactId.fromMvnId(entry.getKey()); JsonObject headers = entry.getValue().asJsonObject(); if (headers.containsKey("manifest")) { @@ -89,6 +105,10 @@ public class AnalyserMetaDataExtension { public Map<String, String> getManifest(final ArtifactId artifactId) { return this.manifests.get(artifactId); } + + public SystemBundle getSystemBundle() { + return systemBundle; + } public boolean reportWarning(ArtifactId artifactId) { return !this.reportWarnings.containsKey(artifactId) || this.reportWarnings.get(artifactId); @@ -144,4 +164,29 @@ public class AnalyserMetaDataExtension { public void setReportErrors(ArtifactId id, boolean enabled) { reportErrors.put(id, enabled); } + + public static class SystemBundle { + + private Map<String, String> manifest = new HashMap<>(); + private ArtifactId artifactId; + private String sortedFrameworkProperties; + + public SystemBundle(Map<String, String> manifest, ArtifactId artifactId, String sortedFrameworkProperties) { + this.manifest = manifest; + this.artifactId = artifactId; + this.sortedFrameworkProperties = sortedFrameworkProperties; + } + + public ArtifactId getArtifactId() { + return artifactId; + } + + public Map<String, String> getManifest() { + return manifest; + } + + public String getSortedFrameworkProperties() { + return sortedFrameworkProperties; + } + } } diff --git a/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandler.java b/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandler.java index 06c02d9..9e99598 100644 --- a/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandler.java +++ b/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandler.java @@ -24,6 +24,9 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.ServiceLoader; +import java.util.Set; +import java.util.StringJoiner; +import java.util.TreeMap; import java.util.jar.JarFile; import org.apache.commons.lang3.SystemUtils; @@ -35,11 +38,14 @@ import org.apache.sling.feature.ExtensionType; import org.apache.sling.feature.Feature; import org.apache.sling.feature.builder.HandlerContext; import org.apache.sling.feature.builder.PostProcessHandler; +import org.apache.sling.feature.impl.felix.utils.resource.ResourceUtils; import org.apache.sling.feature.io.IOUtils; import org.apache.sling.feature.scanner.BundleDescriptor; +import org.apache.sling.feature.scanner.PackageInfo; import org.apache.sling.feature.scanner.spi.FrameworkScanner; import org.osgi.framework.Constants; import org.osgi.framework.Version; +import org.osgi.resource.Capability; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,13 +68,13 @@ public class AnalyserMetaDataHandler implements PostProcessHandler { JsonObjectBuilder result = Json.createObjectBuilder(); Map<String, JsonValue> directEntries = new HashMap<>(); Map<String, JsonValue> wildcardEntries = new LinkedHashMap<>(); - JsonObject[] frameworkDefinitionHolder = new JsonObject[1]; + JsonObject[] systemBundleHolder = new JsonObject[1]; extensionJSONStructure.entrySet().forEach( entry -> { if (entry.getKey().contains("*")) { wildcardEntries.put(entry.getKey(), entry.getValue()); } else if (entry.getKey().equals(Constants.SYSTEM_BUNDLE_SYMBOLICNAME)) { - frameworkDefinitionHolder[0] = entry.getValue().asJsonObject(); + systemBundleHolder[0] = entry.getValue().asJsonObject(); } else { directEntries.put(entry.getKey(), entry.getValue()); } @@ -95,12 +101,9 @@ public class AnalyserMetaDataHandler implements PostProcessHandler { ) ); - if (frameworkDefinitionHolder[0] != null) { - JsonObject v = frameworkDefinitionHolder[0]; - // TODO - add test - // TODO - finalise contract - // TODO - make use of the information in the Analyser, if present - FrameworkScanner scanner = ServiceLoader.load(FrameworkScanner.class).iterator().next(); + if (systemBundleHolder[0] != null) { + JsonObject systemBundle = systemBundleHolder[0]; + // TODO - skip if the entries already exist? try { ExecutionEnvironmentExtension executionEnv = ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(feature); if ( executionEnv != null ) { @@ -115,10 +118,16 @@ public class AnalyserMetaDataHandler implements PostProcessHandler { throw new IllegalStateException("Execution environment requires Java " + requiredJavaVersion.getMajor() + ", but running on " + currentJavaVersion.getMajor() + ". Aborting."); } + FrameworkScanner scanner = ServiceLoader.load(FrameworkScanner.class).iterator().next(); + BundleDescriptor fw = scanner.scan(frameworkId, feature.getFrameworkProperties(), handlerContext.getArtifactProvider()); - JsonObjectBuilder wrapper = Json.createObjectBuilder(v); - wrapper.add(Constants.PROVIDE_CAPABILITY, fw.getCapabilities().toString()); - wrapper.add(Constants.EXPORT_PACKAGE, fw.getExportedPackages().toString()); + JsonObjectBuilder wrapper = Json.createObjectBuilder(systemBundle); + JsonObjectBuilder manifest = Json.createObjectBuilder(); + manifest.add(Constants.PROVIDE_CAPABILITY, capabilitiesToString(fw.getCapabilities())); + manifest.add(Constants.EXPORT_PACKAGE, exportedPackagesToString(fw.getExportedPackages())); + wrapper.add(MANIFEST_KEY, manifest); + wrapper.add("artifactId", frameworkId.toMvnId()); + wrapper.add("sortedFrameworkProperties", frameworkPropertiesSortedToString(feature.getFrameworkProperties())); result.add(Constants.SYSTEM_BUNDLE_SYMBOLICNAME, wrapper); } else { LOG.warn("No execution environment found, not creating framework capabilities"); @@ -138,6 +147,37 @@ public class AnalyserMetaDataHandler implements PostProcessHandler { } } + private String frameworkPropertiesSortedToString(Map<String, String> frameworkProperties) { + if (frameworkProperties == null || frameworkProperties.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + // TODO - duplication with Scanner.scan + Map<String, String> sortedMap = new TreeMap<>(frameworkProperties); + for (final Map.Entry<String, String> entry : sortedMap.entrySet()) { + sb.append(":").append(entry.getKey()).append("=").append(entry.getValue()); + } + return sb.toString(); + } + + private static String exportedPackagesToString(Set<PackageInfo> exportedPackages) { + StringJoiner joiner = new StringJoiner(","); + for (PackageInfo packageInfo : exportedPackages) { + joiner.add(packageInfo.getName() + ";version=" + packageInfo.getVersion()); + } + return joiner.toString(); + } + + private static String capabilitiesToString(Set<Capability> capabilities) { + + StringJoiner joiner = new StringJoiner(","); + for (Capability c : capabilities) { + joiner.add(ResourceUtils.toString(null, c.getNamespace(), c.getAttributes(), c.getDirectives())); + } + + return joiner.toString(); + } + private boolean noManifest(JsonObject object) { return manifest(object, null) && !object.getBoolean("no-manifest", false); } diff --git a/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRequirementsCapabilities.java b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRequirementsCapabilities.java index 3492ae3..0979f69 100644 --- a/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRequirementsCapabilities.java +++ b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRequirementsCapabilities.java @@ -155,7 +155,9 @@ public class CheckRequirementsCapabilities implements AnalyserTask { private List<Descriptor> getCandidates(List<Descriptor> artifactDescriptors, Requirement requirement) { return artifactDescriptors.stream() .filter(artifactDescriptor -> artifactDescriptor.getCapabilities() != null) - .filter(artifactDescriptor -> artifactDescriptor.getCapabilities().stream().anyMatch(capability -> CapabilitySet.matches(capability, requirement))) + .filter(artifactDescriptor -> artifactDescriptor.getCapabilities().stream().anyMatch( + capability -> CapabilitySet.matches(capability, requirement)) + ) .collect(Collectors.toList()); } diff --git a/src/main/java/org/apache/sling/feature/scanner/Scanner.java b/src/main/java/org/apache/sling/feature/scanner/Scanner.java index a55f8bb..954af5c 100644 --- a/src/main/java/org/apache/sling/feature/scanner/Scanner.java +++ b/src/main/java/org/apache/sling/feature/scanner/Scanner.java @@ -28,17 +28,24 @@ import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.felix.utils.manifest.Clause; +import org.apache.felix.utils.manifest.Parser; import org.apache.sling.feature.Artifact; import org.apache.sling.feature.ArtifactId; import org.apache.sling.feature.Bundles; import org.apache.sling.feature.Extension; import org.apache.sling.feature.Feature; import org.apache.sling.feature.analyser.extensions.AnalyserMetaDataExtension; +import org.apache.sling.feature.analyser.extensions.AnalyserMetaDataExtension.SystemBundle; import org.apache.sling.feature.builder.ArtifactProvider; +import org.apache.sling.feature.impl.felix.utils.resource.ResourceBuilder; import org.apache.sling.feature.scanner.impl.BundleDescriptorImpl; import org.apache.sling.feature.scanner.impl.FeatureDescriptorImpl; import org.apache.sling.feature.scanner.spi.ExtensionScanner; import org.apache.sling.feature.scanner.spi.FrameworkScanner; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.resource.Capability; /** * The scanner is a service that scans items and provides descriptions for @@ -248,6 +255,71 @@ public class Scanner { } } } + + SystemBundle systemBundle = extension.getSystemBundle(); + if ( systemBundle != null ) { + String key = systemBundle.getArtifactId().toMvnId(); + if ( systemBundle.getSortedFrameworkProperties() != null ) + key += systemBundle.getSortedFrameworkProperties(); + + URL artifactUrl = artifactProvider.provide(systemBundle.getArtifactId()); + if (artifactUrl == null) { + throw new IOException("Unable to find file for " + systemBundle.getArtifactId()); + } + + // TODO - duplicated with FelixFrameworkScanner + BundleDescriptor desc = new BundleDescriptor(systemBundle.getArtifactId().toMvnId()) { + + @Override + public URL getArtifactFile() { + return artifactUrl; + } + + @Override + public Artifact getArtifact() { + return new Artifact(systemBundle.getArtifactId()); + } + + @Override + public Manifest getManifest() { + return new Manifest(); + } + + @Override + public String getBundleVersion() { + return systemBundle.getArtifactId().getOSGiVersion().toString(); + } + + @Override + public String getBundleSymbolicName() { + return Constants.SYSTEM_BUNDLE_SYMBOLICNAME; + } + }; + + String capabilities = systemBundle.getManifest().get(Constants.PROVIDE_CAPABILITY); + if ( capabilities != null ) { + try { + List<Capability> parsedCapabilities = ResourceBuilder.parseCapability(null, capabilities); + desc.getCapabilities().addAll(parsedCapabilities); + } catch (BundleException e) { + throw new IOException("Failed to parse capabilites for the system bundle", e); + } + } + + String exports = systemBundle.getManifest().get(Constants.EXPORT_PACKAGE); + if ( exports != null ) { + Clause[] pcks = Parser.parseHeader(exports); + for (final Clause pck : pcks) { + String version = pck.getAttribute("version"); + PackageInfo info = new PackageInfo(pck.getName(), version, false); + desc.getExportedPackages().add(info); + } + } + desc.lock(); + + this.cache.put(key, desc); + + } } } @@ -263,7 +335,7 @@ public class Scanner { final StringBuilder sb = new StringBuilder(); sb.append(framework.toMvnId()); if (props != null) { - final Map<String, String> sortedMap = new TreeMap<String, String>(props); + final Map<String, String> sortedMap = new TreeMap<>(props); for (final Map.Entry<String, String> entry : sortedMap.entrySet()) { sb.append(":").append(entry.getKey()).append("=").append(entry.getValue()); } diff --git a/src/test/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandlerTest.java b/src/test/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandlerTest.java index 9e4a668..59bb45f 100644 --- a/src/test/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandlerTest.java +++ b/src/test/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandlerTest.java @@ -16,16 +16,22 @@ */ package org.apache.sling.feature.analyser.extensions; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringWriter; import java.io.UncheckedIOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.felix.utils.manifest.Clause; +import org.apache.felix.utils.manifest.Parser; +import org.apache.sling.feature.ArtifactId; import org.apache.sling.feature.Feature; import org.apache.sling.feature.builder.ArtifactProvider; import org.apache.sling.feature.builder.HandlerContext; @@ -34,6 +40,10 @@ import org.apache.sling.feature.io.json.FeatureJSONWriter; import org.junit.Test; import org.mockito.Mockito; +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; + public class AnalyserMetaDataHandlerTest { @Test @@ -45,7 +55,7 @@ public class AnalyserMetaDataHandlerTest { feature = FeatureJSONReader.read(r, url.toString()); } - assertNotNull(feature); + assertThat(feature).isNotNull(); HandlerContext ctx = Mockito.mock(HandlerContext.class); ArtifactProvider provider = artifactId -> { @@ -74,7 +84,80 @@ public class AnalyserMetaDataHandlerTest { FeatureJSONWriter.write(featureWriter, feature); FeatureJSONWriter.write(expectedWriter, expected); - assertEquals(expectedWriter.toString(), featureWriter.toString()); + assertThat(expectedWriter.toString()).isEqualTo(featureWriter.toString()); + } + + @Test + public void testMetadataHandlerSystemBundle() throws IOException { + URL url = getClass().getResource("/analyse-metadata/feature-input-system-bundle.json"); + + Feature feature; + try (Reader r = new InputStreamReader(url.openStream())) { + feature = FeatureJSONReader.read(r, url.toString()); + } + + assertThat(feature).isNotNull(); + + HandlerContext ctx = Mockito.mock(HandlerContext.class); + ArtifactProvider provider = artifactId -> { + try { + return new URL("https://repo1.maven.org/maven2/" + artifactId.toMvnPath()); + } catch (MalformedURLException e) { + throw new UncheckedIOException(e); + } + }; + + Mockito.when(ctx.getArtifactProvider()).thenReturn(provider); + + new AnalyserMetaDataHandler().postProcess(ctx, feature, feature.getExtensions().getByName("analyser-metadata")); + + JsonObject metadata = feature.getExtensions().getByName("analyser-metadata").getJSONStructure().asJsonObject(); + assertThat(metadata).as("analyser-metadata extension").isNotNull(); + + JsonValue systemBundle = metadata.get("system.bundle"); + assertThat(systemBundle).as("system.bundle property of the metadata extension").isNotNull(); + + // ensure the artifactId is recorded and correctly formed + JsonValue artifactId = systemBundle.asJsonObject().get("artifactId"); + assertThat(artifactId).as("artifactId of the system bundle").isNotNull(); + ArtifactId.fromMvnId(artifactId.toString()); + + JsonValue manifest = systemBundle.asJsonObject().get("manifest"); + assertThat(manifest).as("manifest of the system bundle").isNotNull(); + + JsonString systemBundleCapabilities = manifest.asJsonObject().getJsonString("Provide-Capability"); + assertThat(systemBundleCapabilities).as("capabilities for the system bundle").isNotNull(); + String capabilities = systemBundleCapabilities.getString(); + + // validate that the capabilities header is correctly formed + Clause[] clauses = Parser.parseHeader(capabilities); + + // validate that we have exactly one osgi.ee clause with the value "JavaSE" + List<Clause> javaSeEeClauses = Arrays.stream(clauses) + .filter( c -> c.getName().equals("osgi.ee") ) + .filter( c -> c.getAttribute("osgi.ee").equals("JavaSE") ) + .collect(Collectors.toList()); + + assertThat(javaSeEeClauses).as("osgi.ee=JavaSE capabilities") + .hasSize(1) + .element(0).extracting( c -> c.getAttribute("version:List<Version>") ) + .asString().contains("1.8"); + + JsonString systemBundleExports = manifest.asJsonObject().getJsonString("Export-Package"); + assertThat(systemBundleExports).as("exports for the system bundle").isNotNull(); + String exports = systemBundleExports.getString(); + + // validate that the exports header is correctly formed + Clause[] exportClauses = Parser.parseHeader(exports); + List<Clause> javaUtilExports = Arrays.stream(exportClauses) + .filter( c -> c.getName().equals("java.util") ) + .collect(Collectors.toList()); + + // validate that the java.util.function export is present + assertThat(javaUtilExports).as("java.util package exports") + .hasSize(1) + .element(0).extracting( c -> c.getAttribute("version") ) + .isNotNull(); } } diff --git a/src/test/resources/analyse-metadata/feature-input-system-bundle.json b/src/test/resources/analyse-metadata/feature-input-system-bundle.json new file mode 100644 index 0000000..02053eb --- /dev/null +++ b/src/test/resources/analyse-metadata/feature-input-system-bundle.json @@ -0,0 +1,12 @@ +{ + "id": "org.acme:acmefeature:slingosgifeature:metadata-feature:0.0.1", + "execution-environment:JSON|false":{ + "framework":{ + "id":"org.apache.felix:org.apache.felix.framework:7.0.5" + } + }, + "analyser-metadata:JSON|true": { + "system.bundle" : { + } + } +} \ No newline at end of file
