Skip to content

Commit 28b503d

Browse files
authored
fix(exposition): export internal package for OSGi resolution (#2415)
1 parent 5aed579 commit 28b503d

4 files changed

Lines changed: 247 additions & 0 deletions

File tree

prometheus-metrics-exposition-formats-shaded/pom.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,32 @@
6161

6262
<build>
6363
<plugins>
64+
<plugin>
65+
<groupId>org.apache.felix</groupId>
66+
<artifactId>maven-bundle-plugin</artifactId>
67+
<configuration>
68+
<instructions>
69+
<Export-Package>
70+
io.prometheus.metrics.expositionformats.generated*
71+
</Export-Package>
72+
<!-- Required for OSGi: textformats loads protobuf impl via Class.forName -->
73+
<_exportcontents>
74+
io.prometheus.metrics.expositionformats.internal
75+
</_exportcontents>
76+
<Import-Package>
77+
!com.google.protobuf,*
78+
</Import-Package>
79+
</instructions>
80+
</configuration>
81+
<executions>
82+
<execution>
83+
<!-- Write MANIFEST.MF in process-classes for OSGi header tests. -->
84+
<goals>
85+
<goal>manifest</goal>
86+
</goals>
87+
</execution>
88+
</executions>
89+
</plugin>
6490
<plugin>
6591
<groupId>org.apache.maven.plugins</groupId>
6692
<artifactId>maven-resources-plugin</artifactId>

prometheus-metrics-exposition-formats/pom.xml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,29 @@
6161

6262
<build>
6363
<plugins>
64+
<plugin>
65+
<groupId>org.apache.felix</groupId>
66+
<artifactId>maven-bundle-plugin</artifactId>
67+
<configuration>
68+
<instructions>
69+
<Export-Package>
70+
io.prometheus.metrics.expositionformats.generated*
71+
</Export-Package>
72+
<!-- Required for OSGi: textformats loads protobuf impl via Class.forName -->
73+
<_exportcontents>
74+
io.prometheus.metrics.expositionformats.internal
75+
</_exportcontents>
76+
</instructions>
77+
</configuration>
78+
<executions>
79+
<execution>
80+
<!-- Write MANIFEST.MF in process-classes for OSGi header tests. -->
81+
<goals>
82+
<goal>manifest</goal>
83+
</goals>
84+
</execution>
85+
</executions>
86+
</plugin>
6487
<plugin>
6588
<groupId>org.codehaus.mojo</groupId>
6689
<artifactId>build-helper-maven-plugin</artifactId>
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package io.prometheus.metrics.expositionformats;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl;
6+
import java.io.InputStream;
7+
import java.net.URI;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.security.CodeSource;
11+
import java.util.ArrayList;
12+
import java.util.LinkedHashMap;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.jar.JarFile;
16+
import java.util.jar.Manifest;
17+
import org.junit.jupiter.api.Test;
18+
19+
class OsgiBundleManifestTest {
20+
21+
private static final String INTERNAL_PACKAGE = "io.prometheus.metrics.expositionformats.internal";
22+
private static final String GENERATED_PREFIX =
23+
"io.prometheus.metrics.expositionformats.generated";
24+
private static final String SHADED_BSN = "io.prometheus.metrics-exposition-formats";
25+
private static final String TEXTFORMATS_BSN = "io.prometheus.metrics-exposition-textformats";
26+
27+
@Test
28+
void formatsBundleExportsInternalPackage() throws Exception {
29+
Manifest manifest = loadBundleManifest(PrometheusProtobufWriterImpl.class);
30+
assertThat(bundleSymbolicName(manifest)).contains("exposition-formats");
31+
List<PackageClause> exported = parsePackageHeader(manifest, "Export-Package");
32+
assertThat(names(exported)).contains(INTERNAL_PACKAGE);
33+
assertThat(exported).anyMatch(clause -> clause.name.startsWith(GENERATED_PREFIX));
34+
for (PackageClause clause : exported) {
35+
if (!INTERNAL_PACKAGE.equals(clause.name) && !clause.name.startsWith(GENERATED_PREFIX)) {
36+
continue;
37+
}
38+
assertThat(clause.attributes.get("version"))
39+
.as("version of %s", clause.name)
40+
.isNotBlank()
41+
.doesNotContain(".SNAPSHOT")
42+
.doesNotContain("-SNAPSHOT");
43+
}
44+
}
45+
46+
@Test
47+
void textformatsBundleImportsInternalPackageOptionally() throws Exception {
48+
Manifest manifest = loadBundleManifest(PrometheusProtobufWriter.class);
49+
assertThat(bundleSymbolicName(manifest)).isEqualTo(TEXTFORMATS_BSN);
50+
PackageClause internal =
51+
requireClause(parsePackageHeader(manifest, "Import-Package"), INTERNAL_PACKAGE);
52+
assertThat(internal.directives.get("resolution")).isEqualTo("optional");
53+
}
54+
55+
@Test
56+
void protobufImportMatchesShading() throws Exception {
57+
Manifest manifest = loadBundleManifest(PrometheusProtobufWriterImpl.class);
58+
List<String> imported = names(parsePackageHeader(manifest, "Import-Package"));
59+
if (SHADED_BSN.equals(bundleSymbolicName(manifest))) {
60+
assertThat(imported).doesNotContain("com.google.protobuf");
61+
} else {
62+
assertThat(imported).contains("com.google.protobuf");
63+
}
64+
}
65+
66+
private static String bundleSymbolicName(Manifest manifest) {
67+
return manifest.getMainAttributes().getValue("Bundle-SymbolicName");
68+
}
69+
70+
private static Manifest loadBundleManifest(Class<?> type) throws Exception {
71+
CodeSource codeSource = type.getProtectionDomain().getCodeSource();
72+
assertThat(codeSource).as("code source for %s", type.getName()).isNotNull();
73+
URI location = codeSource.getLocation().toURI();
74+
Path path = Path.of(location);
75+
if (Files.isDirectory(path)) {
76+
Path manifestFile = path.resolve("META-INF/MANIFEST.MF");
77+
assertThat(Files.exists(manifestFile))
78+
.as("bnd MANIFEST.MF for %s at %s", type.getName(), manifestFile)
79+
.isTrue();
80+
try (InputStream in = Files.newInputStream(manifestFile)) {
81+
return new Manifest(in);
82+
}
83+
}
84+
try (JarFile jar = new JarFile(path.toFile())) {
85+
Manifest manifest = jar.getManifest();
86+
assertThat(manifest).as("MANIFEST.MF in %s", path).isNotNull();
87+
return manifest;
88+
}
89+
}
90+
91+
private static PackageClause requireClause(List<PackageClause> clauses, String packageName) {
92+
return clauses.stream()
93+
.filter(clause -> packageName.equals(clause.name))
94+
.findFirst()
95+
.orElseThrow(() -> new AssertionError("missing package clause " + packageName));
96+
}
97+
98+
private static List<String> names(List<PackageClause> clauses) {
99+
List<String> names = new ArrayList<>();
100+
for (PackageClause clause : clauses) {
101+
names.add(clause.name);
102+
}
103+
return names;
104+
}
105+
106+
/** OSGi headers are comma-separated clauses; attributes may contain quoted commas. */
107+
private static List<PackageClause> parsePackageHeader(Manifest manifest, String header) {
108+
String value = manifest.getMainAttributes().getValue(header);
109+
assertThat(value).as("%s", header).isNotBlank();
110+
List<PackageClause> clauses = new ArrayList<>();
111+
for (String rawClause : splitRespectingQuotes(value, ',')) {
112+
List<String> parts = splitRespectingQuotes(rawClause, ';');
113+
if (parts.isEmpty()) {
114+
continue;
115+
}
116+
String name = parts.get(0).trim();
117+
if (name.isEmpty()) {
118+
continue;
119+
}
120+
Map<String, String> attributes = new LinkedHashMap<>();
121+
Map<String, String> directives = new LinkedHashMap<>();
122+
for (int i = 1; i < parts.size(); i++) {
123+
String part = parts.get(i).trim();
124+
int directiveEq = part.indexOf(":=");
125+
int attributeEq = part.indexOf('=');
126+
if (directiveEq >= 0 && (attributeEq < 0 || directiveEq <= attributeEq)) {
127+
directives.put(
128+
part.substring(0, directiveEq).trim(),
129+
unquote(part.substring(directiveEq + 2).trim()));
130+
} else if (attributeEq >= 0) {
131+
attributes.put(
132+
part.substring(0, attributeEq).trim(),
133+
unquote(part.substring(attributeEq + 1).trim()));
134+
}
135+
}
136+
clauses.add(new PackageClause(name, attributes, directives));
137+
}
138+
return clauses;
139+
}
140+
141+
private static List<String> splitRespectingQuotes(String value, char separator) {
142+
List<String> parts = new ArrayList<>();
143+
StringBuilder current = new StringBuilder();
144+
boolean inQuote = false;
145+
for (int i = 0; i < value.length(); i++) {
146+
char c = value.charAt(i);
147+
if (c == '"') {
148+
inQuote = !inQuote;
149+
current.append(c);
150+
} else if (!inQuote && c == separator) {
151+
parts.add(current.toString());
152+
current.setLength(0);
153+
} else {
154+
current.append(c);
155+
}
156+
}
157+
parts.add(current.toString());
158+
return parts;
159+
}
160+
161+
private static String unquote(String value) {
162+
if (value.length() >= 2 && value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"') {
163+
return value.substring(1, value.length() - 1);
164+
}
165+
return value;
166+
}
167+
168+
static final class PackageClause {
169+
final String name;
170+
final Map<String, String> attributes;
171+
final Map<String, String> directives;
172+
173+
PackageClause(String name, Map<String, String> attributes, Map<String, String> directives) {
174+
this.name = name;
175+
this.attributes = attributes;
176+
this.directives = directives;
177+
}
178+
}
179+
}

prometheus-metrics-exposition-textformats/pom.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@
4242

4343
<build>
4444
<plugins>
45+
<plugin>
46+
<groupId>org.apache.felix</groupId>
47+
<artifactId>maven-bundle-plugin</artifactId>
48+
<configuration>
49+
<instructions>
50+
<Import-Package>
51+
io.prometheus.metrics.expositionformats.internal;resolution:=optional,*
52+
</Import-Package>
53+
</instructions>
54+
</configuration>
55+
<executions>
56+
<execution>
57+
<!-- Write MANIFEST.MF in process-classes for OSGi header tests. -->
58+
<goals>
59+
<goal>manifest</goal>
60+
</goals>
61+
</execution>
62+
</executions>
63+
</plugin>
4564
<plugin>
4665
<groupId>org.apache.maven.plugins</groupId>
4766
<artifactId>maven-jar-plugin</artifactId>

0 commit comments

Comments
 (0)