You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2024/06/17 19:54:41 UTC
(sling-org-apache-sling-feature-analyser) branch master updated: SLING-10288 - FelixFrameworkScanner sets framework properties always for current Java version (#47)
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-analyser.git
The following commit(s) were added to refs/heads/master by this push:
new 15df3c3 SLING-10288 - FelixFrameworkScanner sets framework properties always for current Java version (#47)
15df3c3 is described below
commit 15df3c30c4c53ad83a647928f9f8b0a0d68b238d
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Mon Jun 17 21:54:36 2024 +0200
SLING-10288 - FelixFrameworkScanner sets framework properties always for current Java version (#47)
Support an optional "reserved:system.bundle:0.0.0" key in the analyser-metadata extension. This key, when present, is used to store the capabilities and exported packages for the system bundle.
---
pom.xml | 11 ++
readme.md | 25 +++-
.../extensions/AnalyserMetaDataExtension.java | 94 +++++++++++---
.../extensions/AnalyserMetaDataHandler.java | 85 ++++++++++++-
.../task/impl/CheckRequirementsCapabilities.java | 4 +-
.../org/apache/sling/feature/scanner/Scanner.java | 55 +++++++--
.../scanner/impl/FelixFrameworkScanner.java | 30 +----
.../scanner/impl/SystemBundleDescriptor.java | 81 ++++++++++++
.../extensions/AnalyserMetaDataExtensionTest.java | 42 ++++++-
.../extensions/AnalyserMetaDataHandlerTest.java | 137 +++++++++++++++++----
.../feature-input-system-bundle.json | 12 ++
.../feature-output-system-bundle.json | 13 ++
12 files changed, 499 insertions(+), 90 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..622ad59 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 `extra-metadata:system.bundle:0` 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
}
- }
+ },
+ "extra-metadata:system.bundle:0": {}
+ },
+ "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 61c574f..8435e3b 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
@@ -16,27 +16,42 @@
*/
package org.apache.sling.feature.analyser.extensions;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.Extension;
import org.apache.sling.feature.ExtensionType;
import org.apache.sling.feature.Feature;
import org.apache.sling.feature.scanner.BundleDescriptor;
+import org.osgi.framework.Constants;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonValue;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.stream.Stream;
public class AnalyserMetaDataExtension {
+
public static final String EXTENSION_NAME = "analyser-metadata";
+ // use ArtifactId.fromMvnId to ensure the string form can be parsed back to an ArtifactId
+ static final String SYSTEM_BUNDLE_KEY = ArtifactId.fromMvnId("extra-metadata:" + Constants.SYSTEM_BUNDLE_SYMBOLICNAME + ":0").toString();
+ static final String MANIFEST_KEY = "manifest";
+ static final String REPORT_KEY = "report";
+ static final String WARNING_KEY = "warning";
+ static final String ERROR_KEY = "error";
+ static final String ARTIFACT_ID_KEY = "artifactId";
+ static final String SCANNER_CACHE_KEY = "scannerCacheKey";
+
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);
@@ -54,24 +69,42 @@ public class AnalyserMetaDataExtension {
}
private AnalyserMetaDataExtension(JsonObject json) {
+
for (Map.Entry<String, JsonValue> entry : json.entrySet()) {
+
+ // handle system bundle separately
+ if ( entry.getKey().equals(SYSTEM_BUNDLE_KEY) ) {
+ JsonObject systemBundleConfig = entry.getValue().asJsonObject();
+ JsonObject manifestObj = systemBundleConfig.getJsonObject(MANIFEST_KEY);
+ String artifactId = systemBundleConfig.getJsonString(ARTIFACT_ID_KEY).getString();
+ String scannerCacheKey = systemBundleConfig.getJsonString(SCANNER_CACHE_KEY).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), scannerCacheKey);
+
+ continue;
+ }
+
ArtifactId id = ArtifactId.fromMvnId(entry.getKey());
JsonObject headers = entry.getValue().asJsonObject();
- if (headers.containsKey("manifest")) {
+ if (headers.containsKey(MANIFEST_KEY)) {
Map<String, String> manifest = new LinkedHashMap<>();
- JsonObject manifestHeaders = headers.getJsonObject("manifest");
+ JsonObject manifestHeaders = headers.getJsonObject(MANIFEST_KEY);
for (String name : manifestHeaders.keySet()) {
manifest.put(name, manifestHeaders.getString(name));
}
this.manifests.put(id, manifest);
}
- if (headers.containsKey("report")) {
- JsonObject report = headers.getJsonObject("report");
- if (report.containsKey("warning")) {
- reportWarnings.put(id, report.getBoolean("warning"));
+ if (headers.containsKey(REPORT_KEY)) {
+ JsonObject report = headers.getJsonObject(REPORT_KEY);
+ if (report.containsKey(WARNING_KEY)) {
+ reportWarnings.put(id, report.getBoolean(WARNING_KEY));
}
- if (report.containsKey("error")) {
- reportErrors.put(id, report.getBoolean("error"));
+ if (report.containsKey(ERROR_KEY)) {
+ reportErrors.put(id, report.getBoolean(ERROR_KEY));
}
}
}
@@ -84,6 +117,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);
@@ -102,17 +139,17 @@ public class AnalyserMetaDataExtension {
if (manifests.containsKey(id)) {
JsonObjectBuilder manifest = Json.createObjectBuilder();
manifests.get(id).forEach(manifest::add);
- metadata.add("manifest", manifest);
+ metadata.add(MANIFEST_KEY, manifest);
}
if (reportErrors.containsKey(id) || reportWarnings.containsKey(id)) {
JsonObjectBuilder report = Json.createObjectBuilder();
if (reportErrors.containsKey(id)) {
- report.add("error", reportErrors.get(id));
+ report.add(ERROR_KEY, reportErrors.get(id));
}
if (reportWarnings.containsKey(id)) {
- report.add("warning", reportWarnings.get(id));
+ report.add(WARNING_KEY, reportWarnings.get(id));
}
- metadata.add("report", report);
+ metadata.add(REPORT_KEY, report);
}
builder.add(id.toMvnId(), metadata);
}
@@ -139,4 +176,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 scannerCacheKey;
+
+ public SystemBundle(Map<String, String> manifest, ArtifactId artifactId, String scannerCacheKey) {
+ this.manifest = manifest;
+ this.artifactId = artifactId;
+ this.scannerCacheKey = scannerCacheKey;
+ }
+
+ public ArtifactId getArtifactId() {
+ return artifactId;
+ }
+
+ public Map<String, String> getManifest() {
+ return manifest;
+ }
+
+ public String getScannerCacheKey() {
+ return scannerCacheKey;
+ }
+ }
}
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 830bc7b..65cda8d 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
@@ -23,16 +23,29 @@ import java.util.HashMap;
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.jar.JarFile;
+import org.apache.commons.lang3.SystemUtils;
import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.ExecutionEnvironmentExtension;
import org.apache.sling.feature.Extension;
import org.apache.sling.feature.ExtensionState;
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.impl.SystemBundleDescriptor;
+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;
@@ -44,20 +57,22 @@ import jakarta.json.JsonValue;
public class AnalyserMetaDataHandler implements PostProcessHandler {
private static final Logger LOG = LoggerFactory.getLogger(AnalyserMetaDataHandler.class);
- private static final String MANIFEST_KEY = "manifest";
-
@Override
public void postProcess(HandlerContext handlerContext, Feature feature, Extension extension) {
+
if (AnalyserMetaDataExtension.EXTENSION_NAME.equals(extension.getName())) {
LOG.debug("Handling analyser-metadata extension {}", extension);
JsonObject extensionJSONStructure = extension.getJSONStructure().asJsonObject();
JsonObjectBuilder result = Json.createObjectBuilder();
Map<String, JsonValue> directEntries = new HashMap<>();
Map<String, JsonValue> wildcardEntries = new LinkedHashMap<>();
+ JsonObject[] systemBundleHolder = new JsonObject[1];
extensionJSONStructure.entrySet().forEach(
entry -> {
if (entry.getKey().contains("*")) {
wildcardEntries.put(entry.getKey(), entry.getValue());
+ } else if (entry.getKey().equals(AnalyserMetaDataExtension.SYSTEM_BUNDLE_KEY)) {
+ systemBundleHolder[0] = entry.getValue().asJsonObject();
} else {
directEntries.put(entry.getKey(), entry.getValue());
}
@@ -70,19 +85,59 @@ public class AnalyserMetaDataHandler implements PostProcessHandler {
json -> {
if (nullManifest(json)) {
JsonObjectBuilder wrapper = Json.createObjectBuilder(json);
- wrapper.remove(MANIFEST_KEY);
+ wrapper.remove(AnalyserMetaDataExtension.MANIFEST_KEY);
result.add(bundle.getId().toMvnId(), wrapper);
} else if (noManifest(json)) {
JsonObjectBuilder wrapper = Json.createObjectBuilder(json);
getManifest(handlerContext, bundle.getId()).ifPresent(manifest ->
- wrapper.add(MANIFEST_KEY, manifest));
+ wrapper.add(AnalyserMetaDataExtension.MANIFEST_KEY, manifest));
result.add(bundle.getId().toMvnId(), wrapper);
} else {
result.add(bundle.getId().toMvnId(), json);
}
}
)
- );
+ );
+
+ // only process if we have an empty system bundle definition
+ if (JsonValue.EMPTY_JSON_OBJECT.equals(systemBundleHolder[0]) ) {
+ JsonObject systemBundle = systemBundleHolder[0];
+ try {
+ ExecutionEnvironmentExtension executionEnv = ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(feature);
+ if ( executionEnv != null ) {
+ ArtifactId frameworkId = executionEnv.getFramework().getId();
+ if ( executionEnv.getJavaVersion() == null ) {
+ LOG.warn("No java version set in execution environment extension, skipping version validation");
+ } else {
+ Version requiredJavaVersion = executionEnv.getJavaVersion();
+ Version currentJavaVersion = new Version(SystemUtils.JAVA_VERSION);
+
+ if ( requiredJavaVersion.getMajor() != currentJavaVersion.getMajor() )
+ 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(systemBundle);
+ JsonObjectBuilder manifest = Json.createObjectBuilder();
+ manifest.add(Constants.PROVIDE_CAPABILITY, capabilitiesToString(fw.getCapabilities()));
+ manifest.add(Constants.EXPORT_PACKAGE, exportedPackagesToString(fw.getExportedPackages()));
+ wrapper.add(AnalyserMetaDataExtension.MANIFEST_KEY, manifest);
+ wrapper.add(AnalyserMetaDataExtension.ARTIFACT_ID_KEY, frameworkId.toMvnId());
+ wrapper.add(AnalyserMetaDataExtension.SCANNER_CACHE_KEY, SystemBundleDescriptor.createCacheKey(frameworkId, feature.getFrameworkProperties()));
+ result.add(AnalyserMetaDataExtension.SYSTEM_BUNDLE_KEY, wrapper);
+ } else {
+ LOG.warn("No execution environment found, not creating framework capabilities");
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ } else if (systemBundleHolder[0] != null) {
+ // preserve the existing system bundle information
+ result.add(AnalyserMetaDataExtension.SYSTEM_BUNDLE_KEY, systemBundleHolder[0]);
+ }
+
feature.getExtensions().remove(extension);
@@ -93,6 +148,24 @@ public class AnalyserMetaDataHandler implements PostProcessHandler {
}
}
+ 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);
}
@@ -102,7 +175,7 @@ public class AnalyserMetaDataHandler implements PostProcessHandler {
}
private boolean manifest(JsonObject object, Object match) {
- return object.get(MANIFEST_KEY) == match;
+ return object.get(AnalyserMetaDataExtension.MANIFEST_KEY) == match;
}
private Optional<JsonObject> findFirst(Map<String, JsonValue> directValues, Map<String, JsonValue> wildcardValues, ArtifactId bundle) {
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..9973736 100644
--- a/src/main/java/org/apache/sling/feature/scanner/Scanner.java
+++ b/src/main/java/org/apache/sling/feature/scanner/Scanner.java
@@ -22,23 +22,30 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
-import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
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.impl.SystemBundleDescriptor;
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,42 @@ public class Scanner {
}
}
}
+
+ SystemBundle systemBundle = extension.getSystemBundle();
+ if ( systemBundle != null ) {
+ URL artifactUrl = artifactProvider.provide(systemBundle.getArtifactId());
+ if (artifactUrl == null) {
+ throw new IOException("Unable to find file for " + systemBundle.getArtifactId());
+ }
+
+ BundleDescriptor desc = new SystemBundleDescriptor(systemBundle.getArtifactId(), artifactUrl);
+
+ 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();
+
+ String key = systemBundle.getScannerCacheKey();
+
+ this.cache.put(key, desc);
+
+ }
}
}
@@ -260,15 +303,7 @@ public class Scanner {
* @throws IOException If something goes wrong or a scanner is missing
*/
public BundleDescriptor scan(final ArtifactId framework, final Map<String,String> props) throws IOException {
- final StringBuilder sb = new StringBuilder();
- sb.append(framework.toMvnId());
- if (props != null) {
- final Map<String, String> sortedMap = new TreeMap<String, String>(props);
- for (final Map.Entry<String, String> entry : sortedMap.entrySet()) {
- sb.append(":").append(entry.getKey()).append("=").append(entry.getValue());
- }
- }
- final String key = sb.toString();
+ final String key = SystemBundleDescriptor.createCacheKey(framework, props);
BundleDescriptor desc = (BundleDescriptor) this.cache.get(key);
if (desc == null) {
for (final FrameworkScanner scanner : this.frameworkScanners) {
diff --git a/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java b/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java
index c7db4b9..d4cab4e 100644
--- a/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java
+++ b/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java
@@ -35,13 +35,11 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
-import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.felix.utils.manifest.Parser;
import org.apache.felix.utils.resource.ResourceBuilder;
-import org.apache.sling.feature.Artifact;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.builder.ArtifactProvider;
import org.apache.sling.feature.io.IOUtils;
@@ -73,33 +71,7 @@ public class FelixFrameworkScanner implements FrameworkScanner {
final Set<PackageInfo> pcks = calculateSystemPackages(fwkProps);
final List<Capability> capabilities = calculateSystemCapabilities(fwkProps);
- final BundleDescriptor d = new BundleDescriptor(framework.toMvnId()) {
-
- @Override
- public String getBundleSymbolicName() {
- return Constants.SYSTEM_BUNDLE_SYMBOLICNAME;
- }
-
- @Override
- public String getBundleVersion() {
- return framework.getOSGiVersion().toString();
- }
-
- @Override
- public URL getArtifactFile() {
- return platformFile;
- }
-
- @Override
- public Artifact getArtifact() {
- return new Artifact(framework);
- }
-
- @Override
- public Manifest getManifest() {
- return new Manifest();
- }
- };
+ final BundleDescriptor d = new SystemBundleDescriptor(framework, platformFile);
d.getCapabilities().addAll(capabilities);
d.getExportedPackages().addAll(pcks);
d.lock();
diff --git a/src/main/java/org/apache/sling/feature/scanner/impl/SystemBundleDescriptor.java b/src/main/java/org/apache/sling/feature/scanner/impl/SystemBundleDescriptor.java
new file mode 100644
index 0000000..3972042
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/scanner/impl/SystemBundleDescriptor.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.scanner.impl;
+
+import java.net.URL;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.jar.Manifest;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.scanner.BundleDescriptor;
+import org.osgi.framework.Constants;
+
+/**
+ * Bundle descriptor that describes the system bundle
+ *
+ * <p>It is based on an existing artifact id and the associated platform file.</p>
+ */
+public final class SystemBundleDescriptor extends BundleDescriptor {
+
+ public static String createCacheKey(final ArtifactId framework, final Map<String, String> props) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(framework.toMvnId());
+ if (props != null) {
+ 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());
+ }
+ }
+ return sb.toString();
+ }
+
+ private final URL platformFile;
+ private final ArtifactId framework;
+
+ public SystemBundleDescriptor(ArtifactId framework, URL platformFile) {
+ super(framework.toMvnId());
+ this.platformFile = platformFile;
+ this.framework = framework;
+ }
+
+ @Override
+ public String getBundleSymbolicName() {
+ return Constants.SYSTEM_BUNDLE_SYMBOLICNAME;
+ }
+
+ @Override
+ public String getBundleVersion() {
+ return framework.getOSGiVersion().toString();
+ }
+
+ @Override
+ public URL getArtifactFile() {
+ return platformFile;
+ }
+
+ @Override
+ public Artifact getArtifact() {
+ return new Artifact(framework);
+ }
+
+ @Override
+ public Manifest getManifest() {
+ return new Manifest();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataExtensionTest.java b/src/test/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataExtensionTest.java
index fbb388a..d417aa8 100644
--- a/src/test/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataExtensionTest.java
+++ b/src/test/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataExtensionTest.java
@@ -18,14 +18,27 @@
*/
package org.apache.sling.feature.analyser.extensions;
-import org.apache.sling.feature.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Objects;
+import java.util.jar.Manifest;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.extensions.AnalyserMetaDataExtension.SystemBundle;
+import org.apache.sling.feature.io.json.FeatureJSONReader;
import org.apache.sling.feature.scanner.BundleDescriptor;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
-import java.util.jar.Manifest;
-
public class AnalyserMetaDataExtensionTest {
@Test
public void testAnalyserMetaDataExtension() throws Exception {
@@ -58,4 +71,27 @@ public class AnalyserMetaDataExtensionTest {
Assert.assertFalse(testAnalyserMetaDataExtension.reportError(artifact.getId()));
}
+
+ @Test
+ public void readSystemBundleInformation() throws IOException {
+ InputStream featureStream = Objects.requireNonNull(getClass().getResourceAsStream("/analyse-metadata/feature-output-system-bundle.json"), "Unable to locate feature file");
+ Feature feature = FeatureJSONReader.read(new InputStreamReader(featureStream), null);
+
+ Extension extension = Objects.requireNonNull(feature.getExtensions().getByName(AnalyserMetaDataExtension.EXTENSION_NAME), "Failed to load analyser-metadata extension");
+
+ AnalyserMetaDataExtension analyserMetaDataExtension = AnalyserMetaDataExtension.getAnalyserMetaDataExtension(extension);
+ assertThat(analyserMetaDataExtension).as("metadata extension").isNotNull();
+
+ SystemBundle systemBundle = analyserMetaDataExtension.getSystemBundle();
+ assertThat(systemBundle).as("system bundle")
+ .isNotNull()
+ .extracting(SystemBundle::getArtifactId)
+ .isEqualTo(ArtifactId.fromMvnId("org.apache.felix:org.apache.felix.framework:7.0.5"));
+
+ assertThat(systemBundle.getScannerCacheKey()).as("system bundle scanner cache key")
+ .isNotBlank();
+
+ assertThat(systemBundle.getManifest()).as("system bundle manifest")
+ .containsKeys("Export-Package", "Provide-Capability");
+ }
}
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..846a620 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,24 @@
*/
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 static org.assertj.core.api.InstanceOfAssertFactories.MAP;
+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.Objects;
+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,18 +42,117 @@ 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
public void testMetaDataHandler() throws Exception {
- URL url = getClass().getResource("/analyse-metadata/feature-input.json");
+
+ Feature feature = postProcessFeature("/analyse-metadata/feature-input.json");
+
+ URL expectedURL = getClass().getResource("/analyse-metadata/feature-expected-output.json");
+
+ Feature expected;
+ try (Reader r = new InputStreamReader(expectedURL.openStream())) {
+ expected = FeatureJSONReader.read(r, expectedURL.toString());
+ }
+
+ StringWriter featureWriter = new StringWriter();
+ StringWriter expectedWriter = new StringWriter();
+
+ FeatureJSONWriter.write(featureWriter, feature);
+ FeatureJSONWriter.write(expectedWriter, expected);
+
+ assertThat(expectedWriter.toString()).isEqualTo(featureWriter.toString());
+ }
+
+ @Test
+ public void testMetadataHandlerSystemBundle() throws IOException {
+
+ Feature feature = postProcessFeature("/analyse-metadata/feature-input-system-bundle.json");
+
+ JsonObject metadata = feature.getExtensions().getByName("analyser-metadata").getJSONStructure().asJsonObject();
+ assertThat(metadata).as("analyser-metadata extension").isNotNull();
+
+ JsonValue systemBundle = metadata.get("extra-metadata:system.bundle:0");
+ 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();
+ }
+
+ @Test
+ public void testMetadataHandlerSystemBundleAlreadyProcessed() throws IOException {
+
+ // the minimal feature used in this test would not be processed successfully, as it does not
+ // have an execution-environment extension . This test guarantees that processing is skipped and the
+ // information preserved
+ Feature feature = postProcessFeature("/analyse-metadata/feature-output-system-bundle.json");
+
+ JsonObject metadata = feature.getExtensions().getByName("analyser-metadata").getJSONStructure().asJsonObject();
+ assertThat(metadata).as("analyser-metadata extension").isNotNull();
+
+ JsonValue systemBundle = metadata.get("extra-metadata:system.bundle:0");
+ assertThat(systemBundle).as("system.bundle property of the metadata extension")
+ .isNotNull()
+ .extracting(JsonValue::asJsonObject)
+ .asInstanceOf(MAP)
+ .containsKeys("manifest", "artifactId", "scannerCacheKey");
+ }
+
+ private Feature postProcessFeature(String featureLocation) throws IOException {
+ URL url = getClass().getResource(featureLocation);
+
+ Objects.requireNonNull(url, "Failed to load feature file from " + featureLocation);
Feature feature;
try (Reader r = new InputStreamReader(url.openStream())) {
feature = FeatureJSONReader.read(r, url.toString());
}
- assertNotNull(feature);
+ assertThat(feature).isNotNull();
HandlerContext ctx = Mockito.mock(HandlerContext.class);
ArtifactProvider provider = artifactId -> {
@@ -58,23 +165,9 @@ public class AnalyserMetaDataHandlerTest {
Mockito.when(ctx.getArtifactProvider()).thenReturn(provider);
- new AnalyserMetaDataHandler().
- postProcess(ctx, feature, feature.getExtensions().getByName("analyser-metadata"));
-
- URL expectedURL = getClass().getResource("/analyse-metadata/feature-expected-output.json");
-
- Feature expected;
- try (Reader r = new InputStreamReader(expectedURL.openStream())) {
- expected = FeatureJSONReader.read(r, expectedURL.toString());
- }
-
- StringWriter featureWriter = new StringWriter();
- StringWriter expectedWriter = new StringWriter();
-
- FeatureJSONWriter.write(featureWriter, feature);
- FeatureJSONWriter.write(expectedWriter, expected);
-
- assertEquals(expectedWriter.toString(), featureWriter.toString());
+ new AnalyserMetaDataHandler().postProcess(ctx, feature, feature.getExtensions().getByName("analyser-metadata"));
+
+ return feature;
}
}
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..b860df1
--- /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": {
+ "extra-metadata:system.bundle:0" : {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/analyse-metadata/feature-output-system-bundle.json b/src/test/resources/analyse-metadata/feature-output-system-bundle.json
new file mode 100644
index 0000000..091cdc3
--- /dev/null
+++ b/src/test/resources/analyse-metadata/feature-output-system-bundle.json
@@ -0,0 +1,13 @@
+{
+ "id": "org.acme:acmefeature:slingosgifeature:metadata-feature:0.0.1",
+ "analyser-metadata:JSON|false": {
+ "extra-metadata:system.bundle:0": {
+ "manifest": {
+ "Provide-Capability": "osgi.ee; osgi.ee=\"JavaSE/compact2\"; version:List<Version>=\"1.8.0,9.0.0,10.0.0,11.0.0,12.0.0,13.0.0,14.0.0,15.0.0,16.0.0,17.0.0,18.0.0,19.0.0,20.0.0,21.0.0\",osgi.ee; osgi.ee=\"JavaSE/compact3\"; version:List<Version>=\"1.8.0,9.0.0,10.0.0,11.0.0,12.0.0,13.0.0,14.0.0,15.0.0,16.0.0,17.0.0,18.0.0,19.0.0,20.0.0,21.0.0\",osgi.ee; osgi.ee=\"OSGi/Minimum\"; version:List<Version>=\"1.0.0,1.1.0,1.2.0\",osgi.ee; osgi.ee=\"JavaSE/compact1\"; version:List<Version [...]
+ "Export-Package": "com.sun.source.util;version=0.0.0.JavaSE_021,java.security.interfaces;version=0.0.0.JavaSE_021,com.sun.security.auth.module;version=0.0.0.JavaSE_021,org.osgi.framework.launch;version=1.2.0,java.time;version=0.0.0.JavaSE_021,org.osgi.framework.hooks.weaving;version=1.1.0,org.w3c.dom.bootstrap;version=0.0.0.JavaSE_021,javax.swing.text.html.parser;version=0.0.0.JavaSE_021,javax.sound.midi.spi;version=0.0.0.JavaSE_021,javax.swing.filechooser;version=0.0.0.JavaS [...]
+ },
+ "artifactId": "org.apache.felix:org.apache.felix.framework:7.0.5",
+ "scannerCacheKey": "org.apache.felix:org.apache.felix.framework:7.0.5:felix.cm.config.plugins=org.apache.felix.configadmin.plugin.interpolation:felix.systempackages.calculate.uses=true:felix.systempackages.substitution=true:localIndexDir=${sling.home}/repository/index:org.osgi.framework.bootdelegation=sun.*,com.sun.*,jdk.internal.reflect,jdk.internal.reflect.*:org.osgi.framework.system.packages.extra={dollar}{sling.jre-{dollar}{java.specification.version}.javax.xml}{dollar}{sli [...]
+ }
+ }
+}
\ No newline at end of file