Skip to content

Commit 0a2a051

Browse files
committed
Add a FeatureBundle manifest replacement extension
This commit adds a utility extension which can be used to replace the manifest of a Feature Bundle being installed. This can be used, for example, to dynamically add a manifest to an artifact which isn't an OSGi bundle, or to correct some metadata. Signed-off-by: Tim Ward <timothyjward@apache.org>
1 parent 9acaf17 commit 0a2a051

File tree

11 files changed

+558
-1
lines changed

11 files changed

+558
-1
lines changed

extensions/hash.checker/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<artifactId>hash.checker</artifactId>
2525
<packaging>jar</packaging>
2626

27-
<name>OSGi Feature Launcher Maven Artifact Repository - no framework dependencie</name>
27+
<name>OSGi Feature Launcher Extensions - Bundle installation hash checker</name>
2828
<url>https://github.com/eclipse-osgi-technology/feature-launcher</url>
2929

3030
<properties>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target/
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/*********************************************************************
4+
* Copyright (c) 2025 Contributors to the Eclipse Foundation.
5+
*
6+
* This program and the accompanying materials are made
7+
* available under the terms of the Eclipse Public License 2.0
8+
* which is available at https://www.eclipse.org/legal/epl-2.0/
9+
*
10+
* SPDX-License-Identifier: EPL-2.0
11+
**********************************************************************/
12+
-->
13+
<project xmlns="http://maven.apache.org/POM/4.0.0"
14+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
15+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
16+
<modelVersion>4.0.0</modelVersion>
17+
18+
<parent>
19+
<groupId>org.eclipse.osgi-technology.featurelauncher.extensions</groupId>
20+
<artifactId>extensions</artifactId>
21+
<version>1.0.0-SNAPSHOT</version>
22+
</parent>
23+
24+
<artifactId>manifest.replacer</artifactId>
25+
<packaging>jar</packaging>
26+
27+
<name>OSGi Feature Launcher Extensions - Bundle Manifest Replacement</name>
28+
<url>https://github.com/eclipse-osgi-technology/feature-launcher</url>
29+
30+
<properties>
31+
<featurelauncher.dependency.allowed>true</featurelauncher.dependency.allowed>
32+
</properties>
33+
34+
<dependencies>
35+
<dependency>
36+
<groupId>org.osgi</groupId>
37+
<artifactId>org.osgi.service.featurelauncher</artifactId>
38+
<exclusions>
39+
<exclusion>
40+
<groupId>org.osgi</groupId>
41+
<artifactId>org.osgi.framework</artifactId>
42+
</exclusion>
43+
</exclusions>
44+
</dependency>
45+
<dependency>
46+
<groupId>jakarta.json</groupId>
47+
<artifactId>jakarta.json-api</artifactId>
48+
</dependency>
49+
<dependency>
50+
<groupId>org.eclipse.osgi-technology.featurelauncher.repository</groupId>
51+
<artifactId>spi</artifactId>
52+
<version>${project.version}</version>
53+
</dependency>
54+
<dependency>
55+
<groupId>org.slf4j</groupId>
56+
<artifactId>slf4j-api</artifactId>
57+
</dependency>
58+
<dependency>
59+
<groupId>org.glassfish</groupId>
60+
<artifactId>jakarta.json</artifactId>
61+
</dependency>
62+
<dependency>
63+
<groupId>org.apache.felix</groupId>
64+
<artifactId>org.apache.felix.feature</artifactId>
65+
<scope>test</scope>
66+
</dependency>
67+
<dependency>
68+
<groupId>org.apache.felix</groupId>
69+
<artifactId>org.apache.felix.cm.json</artifactId>
70+
<scope>test</scope>
71+
</dependency>
72+
<dependency>
73+
<groupId>org.eclipse.osgi-technology.featurelauncher</groupId>
74+
<artifactId>common</artifactId>
75+
<version>${project.version}</version>
76+
<scope>test</scope>
77+
</dependency>
78+
</dependencies>
79+
80+
<build>
81+
<plugins>
82+
<plugin>
83+
<groupId>biz.aQute.bnd</groupId>
84+
<artifactId>bnd-maven-plugin</artifactId>
85+
</plugin>
86+
<plugin>
87+
<groupId>org.apache.maven.plugins</groupId>
88+
<artifactId>maven-jar-plugin</artifactId>
89+
<executions>
90+
<execution>
91+
<id>generate-test</id>
92+
<phase>generate-test-resources</phase>
93+
<goals>
94+
<goal>test-jar</goal>
95+
</goals>
96+
<configuration>
97+
<archive>
98+
<manifestFile>${project.basedir}/src/test/resources/test-bundle/META-INF/MANIFEST.MF</manifestFile>
99+
</archive>
100+
<testClassesDirectory>${project.basedir}/src/test/resources/test-bundle</testClassesDirectory>
101+
<outputDirectory>${project.build.directory}/test-bundles</outputDirectory>
102+
103+
</configuration>
104+
</execution>
105+
<execution>
106+
<id>generate-test2</id>
107+
<phase>generate-test-resources</phase>
108+
<goals>
109+
<goal>test-jar</goal>
110+
</goals>
111+
<configuration>
112+
<archive>
113+
<manifestFile>${project.basedir}/src/test/resources/test-bundle2/META-INF/MANIFEST.MF</manifestFile>
114+
</archive>
115+
<testClassesDirectory>${project.basedir}/src/test/resources/test-bundle2</testClassesDirectory>
116+
<outputDirectory>${project.build.directory}/test-bundles</outputDirectory>
117+
</configuration>
118+
</execution>
119+
</executions>
120+
</plugin>
121+
</plugins>
122+
</build>
123+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* Copyright (c) 2025 Kentyou and others.
3+
* All rights reserved.
4+
*
5+
* This program and the accompanying materials are made
6+
* available under the terms of the Eclipse Public License 2.0
7+
* which is available at https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Kentyou - initial implementation
13+
*/
14+
package org.eclipse.osgi.technology.featurelauncher.extensions.manifest.replacer;
15+
16+
import java.io.BufferedOutputStream;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.io.StringReader;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.nio.file.Paths;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Objects;
27+
import java.util.Optional;
28+
import java.util.jar.Attributes;
29+
import java.util.jar.JarEntry;
30+
import java.util.jar.JarInputStream;
31+
import java.util.jar.JarOutputStream;
32+
import java.util.jar.Manifest;
33+
import java.util.stream.Collectors;
34+
35+
import org.eclipse.osgi.technology.featurelauncher.repository.spi.FileSystemRepository;
36+
import org.osgi.service.feature.Feature;
37+
import org.osgi.service.feature.FeatureBundle;
38+
import org.osgi.service.feature.FeatureExtension;
39+
import org.osgi.service.feature.FeatureExtension.Type;
40+
import org.osgi.service.feature.ID;
41+
import org.osgi.service.featurelauncher.decorator.AbandonOperationException;
42+
import org.osgi.service.featurelauncher.decorator.DecoratorBuilderFactory;
43+
import org.osgi.service.featurelauncher.decorator.FeatureExtensionHandler;
44+
import org.osgi.service.featurelauncher.repository.ArtifactRepository;
45+
import org.slf4j.Logger;
46+
import org.slf4j.LoggerFactory;
47+
48+
import jakarta.json.Json;
49+
import jakarta.json.JsonObject;
50+
import jakarta.json.JsonReader;
51+
52+
public class BundleManifestReplacer implements FeatureExtensionHandler {
53+
54+
/**
55+
* The recommended extension name for this extension handler
56+
*/
57+
public static final String MANIFEST_REPLACER_EXTENSION_NAME = "eclipse.osgi.technology.manifest.replacer";
58+
59+
public static final String WORKING_DIRECTORY = "working_directory";
60+
61+
private static final Logger LOG = LoggerFactory.getLogger(BundleManifestReplacer.class);
62+
63+
@Override
64+
public Feature handle(Feature feature, FeatureExtension extension,
65+
List<ArtifactRepository> repositories,
66+
FeatureExtensionHandlerBuilder decoratedFeatureBuilder, DecoratorBuilderFactory factory)
67+
throws AbandonOperationException {
68+
if(!MANIFEST_REPLACER_EXTENSION_NAME.equals(extension.getName())) {
69+
LOG.warn("The recommended extension name for using the manifest replacer is {}, but it is being called for extensions named {}");
70+
}
71+
72+
if(extension.getType() != Type.JSON) {
73+
LOG.error("The manifest replacer requires JSON configuration not {}", extension.getType());
74+
throw new AbandonOperationException("The configuration of the manifest replacer feature extension must be JSON.");
75+
}
76+
try {
77+
JsonObject config;
78+
String json = extension.getJSON();
79+
if(json == null || json.isBlank()) {
80+
config = Json.createObjectBuilder().build();
81+
} else {
82+
try (JsonReader reader = Json.createReader(new StringReader(json))) {
83+
config = reader.readObject();
84+
};
85+
}
86+
87+
Path baseFolder = Optional.ofNullable(config.getString(WORKING_DIRECTORY, null))
88+
.map(Paths::get)
89+
.orElse(Files.createTempDirectory(MANIFEST_REPLACER_EXTENSION_NAME))
90+
.resolve(feature.getID().toString());
91+
92+
Map<ID, Path> manifestReplacedBundles = new HashMap<>();
93+
94+
for (FeatureBundle fb : feature.getBundles()) {
95+
ID fbId = fb.getID();
96+
Map<String,String> manifest = fb.getMetadata().entrySet().stream()
97+
.filter(e -> e.getKey().startsWith(MANIFEST_REPLACER_EXTENSION_NAME))
98+
.collect(Collectors.toMap(
99+
e -> e.getKey().substring(MANIFEST_REPLACER_EXTENSION_NAME.length() + 1),
100+
e -> e.getValue().toString()));
101+
102+
if(manifest.isEmpty()) {
103+
continue;
104+
}
105+
106+
Manifest m = new Manifest();
107+
Attributes a = m.getMainAttributes();
108+
a.put(Attributes.Name.MANIFEST_VERSION, "1.0");
109+
manifest.forEach((k,v) -> a.putValue(k,v));
110+
111+
Path outputPath = baseFolder
112+
.resolve(fbId.getGroupId())
113+
.resolve(fbId.getArtifactId())
114+
.resolve(fbId.toString());
115+
Files.createDirectories(outputPath.getParent());
116+
manifestReplacedBundles.put(fbId, outputPath);
117+
118+
try(JarInputStream is = new JarInputStream(repositories.stream()
119+
.map(r -> r.getArtifact(fbId))
120+
.filter(Objects::nonNull)
121+
.findFirst()
122+
.orElseThrow(() -> new AbandonOperationException("Unable to locate feature bundle "
123+
+ fbId + " in a repository")));
124+
JarOutputStream os = new JarOutputStream(
125+
new BufferedOutputStream(Files.newOutputStream(outputPath)), m)) {
126+
127+
JarEntry je;
128+
while((je = is.getNextJarEntry()) != null) {
129+
if("META-INF/MANIFEST.MF".equals(je.getName())) {
130+
continue;
131+
}
132+
os.putNextEntry(new JarEntry(je.getRealName()));
133+
is.transferTo(os);
134+
}
135+
} catch (IOException ioe) {
136+
throw new AbandonOperationException("Failed to generate jar with updated manifest for "
137+
+ fbId, ioe);
138+
}
139+
}
140+
141+
if(!manifestReplacedBundles.isEmpty()) {
142+
ArtifactRepository virtual = new ManifestReplacingArtifactRepository(feature.getID(), manifestReplacedBundles);
143+
repositories.add(0, virtual);
144+
}
145+
} catch(IOException | RuntimeException e) {
146+
throw new AbandonOperationException("Unable to process extension " + extension.getName()
147+
+ " for feature " + feature.getID());
148+
}
149+
150+
return feature;
151+
}
152+
153+
private static class ManifestReplacingArtifactRepository implements ArtifactRepository, FileSystemRepository {
154+
155+
private final ID featureId;
156+
private final Map<ID, Path> manifestReplacedBundles;
157+
158+
public ManifestReplacingArtifactRepository(ID featureId, Map<ID, Path> manifestReplacedBundles) {
159+
this.featureId = featureId;
160+
this.manifestReplacedBundles = Map.copyOf(manifestReplacedBundles);
161+
}
162+
163+
@Override
164+
public InputStream getArtifactData(ID id) {
165+
Path path = manifestReplacedBundles.get(id);
166+
167+
if(path != null) {
168+
try {
169+
return Files.newInputStream(path);
170+
} catch (IOException e) {
171+
throw new RuntimeException("Failed to open manifest replaced file for feature bundle " + id);
172+
}
173+
} else {
174+
return null;
175+
}
176+
}
177+
178+
@Override
179+
public String getName() {
180+
return "Virtual repository for manifest replaced bundles in feature " + featureId;
181+
}
182+
183+
@Override
184+
public Path getArtifactPath(ID id) {
185+
return manifestReplacedBundles.get(id);
186+
}
187+
188+
@Override
189+
public Path getLocalRepositoryPath() {
190+
// We don't expose a root path
191+
return null;
192+
}
193+
194+
@Override
195+
public InputStream getArtifact(ID id) {
196+
return getArtifactData(id);
197+
}
198+
199+
}
200+
}

0 commit comments

Comments
 (0)