Skip to content

Commit

Permalink
Parses META-INF/services files for annotations
Browse files Browse the repository at this point in the history
It is possible to add addnotions in the comments of
the  service files of the Service Loader.

Fixes #5903

---
 Signed-off-by: Peter Kriens <Peter.Kriens@aQute.biz>

Signed-off-by: Peter Kriens <Peter.Kriens@aQute.biz>
  • Loading branch information
pkriens committed Dec 1, 2023
1 parent 696ecac commit 16c6497
Show file tree
Hide file tree
Showing 9 changed files with 517 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package test.annotationheaders;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertTrue;

import org.junit.jupiter.api.Test;

import aQute.bnd.header.Parameters;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Domain;
import aQute.lib.io.IO;

public class ServiceProviderFileTest {
@SuppressWarnings("unchecked")
@Test
public void testServiceProviderOnPackage() throws Exception {
try (Builder b = new Builder();) {
b.addClasspath(IO.getFile("bin_test"));
b.setProperty("-includeresource", """
META-INF/services/com.example.service.Type;\
literal='\
#import aQute.bnd.annotation.spi.ServiceProvider;\n\
#@ServiceProvider(attribute:List<String>="a=1,b=2", effective:=foobar)\n\
java.lang.String'
""");
b.build();
assertTrue(b.check());
Domain manifest = Domain.domain(b.getJar()
.getManifest());
Parameters provideCapability = manifest.getProvideCapability();

assertThat(provideCapability.get("osgi.service")).isNotNull();
assertThat(provideCapability.get("osgi.service")).containsEntry("objectClass", "com.example.service.Type")
.containsEntry("a", "1")
.containsEntry("b", "2")
.containsEntry("effective:", "active");

assertThat(provideCapability.get("osgi.serviceloader")).isNotNull();
assertThat(provideCapability.get("osgi.serviceloader")
.get("osgi.serviceloader")).isEqualTo("com.example.service.Type");
assertThat(provideCapability.get("osgi.serviceloader"))
.containsEntry("osgi.serviceloader", "com.example.service.Type")
.containsEntry("a", "1")
.containsEntry("b", "2")
.containsEntry("register:", "java.lang.String");

Parameters requireCapability = manifest.getRequireCapability();

System.out.println(provideCapability.toString()
.replace(',', '\n'));
System.out.println(requireCapability.toString()
.replace(',', '\n'));
}
}

@Test
public void testBothMetaInfoAndAnnotations() throws Exception {
try (Builder b = new Builder();) {
b.addClasspath(IO.getFile("bin_test"));
b.setPrivatePackage("test.annotationheaders.spi.providerF");
b.setProperty("-includeresource", """
META-INF/services/com.example.service.Type;\
literal='\
#import aQute.bnd.annotation.spi.ServiceProvider;\n\
#@ServiceProvider(attribute:List<String>="a=1,b=2")\n\
java.lang.String'
""");
b.build();
assertTrue(b.check());
Domain manifest = Domain.domain(b.getJar()
.getManifest());
Parameters provideCapability = manifest.getProvideCapability();
Parameters requireCapability = manifest.getRequireCapability();
assertThat(provideCapability.size()).isEqualTo(4);
assertThat(requireCapability.size()).isEqualTo(2);
}
}

}
6 changes: 4 additions & 2 deletions biz.aQute.bndlib/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ Import-Package: \
org.osgi.service.repository;version=latest,\
org.osgi.util.function;version=latest,\
org.osgi.util.promise;version=latest,\
aQute.libg, \
aQute.libg,\
biz.aQute.bnd.annotation;version=project,\
biz.aQute.bnd.util;version=latest,\
slf4j.api;version=latest
slf4j.api;version=latest,\
org.osgi.service.serviceloader,\
org.eclipse.jdt.annotation

-testpath: \
${junit},\
Expand Down
28 changes: 28 additions & 0 deletions biz.aQute.bndlib/src/aQute/bnd/header/Attrs.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -462,6 +463,17 @@ public boolean isEqual(Attrs other) {
return true;
}

/**
* Return this attrs as typed valuesx
*/

public Map<String, Object> toTyped() {
Map<String, Object> result = new HashMap<>();
for (String k : keySet()) {
result.put(k, getTyped(k));
}
return result;
}
public Object getTyped(String adname) {
String s = get(adname);
if (s == null)
Expand Down Expand Up @@ -622,4 +634,20 @@ public Attrs select(Predicate<String> predicate) {
});
return attrs;
}

/**
* Add aliases for all directives. In some cases, like annotations,
* directives cannot have their ':' at the end. In that case we create an
* alias without the ':'. We only create an alias for a directive when there
* is no attribute with that value.
*/
public void addDirectiveAliases() {
for (String k : new HashSet<>(keySet())) {
if (k.endsWith(":")) {
String alias = k.substring(0, k.length() - 1);
if (!containsKey(alias))
putTyped(alias, getTyped(k));
}
}
}
}
2 changes: 1 addition & 1 deletion biz.aQute.bndlib/src/aQute/bnd/header/package-info.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@Version("2.6.0")
@Version("2.7.0")
package aQute.bnd.header;

import org.osgi.annotation.versioning.Version;
71 changes: 68 additions & 3 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.annotation.RetentionPolicy;
import java.net.URL;
import java.time.ZonedDateTime;
import java.util.ArrayList;
Expand Down Expand Up @@ -62,11 +63,13 @@
import aQute.bnd.header.OSGiHeader;
import aQute.bnd.header.Parameters;
import aQute.bnd.http.HttpClient;
import aQute.bnd.osgi.Annotation.ElementType;
import aQute.bnd.osgi.Clazz.JAVA;
import aQute.bnd.osgi.Clazz.QUERY;
import aQute.bnd.osgi.Descriptors.Descriptor;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.osgi.MetaInfService.Implementation;
import aQute.bnd.service.AnalyzerPlugin;
import aQute.bnd.service.ManifestPlugin;
import aQute.bnd.service.OrderedPlugin;
Expand Down Expand Up @@ -211,8 +214,14 @@ private void analyze0() throws Exception {
analyzed = true;
analyzeContent();

Instructions instructions = new Instructions(
OSGiHeader.parseHeader(getProperty(Constants.BUNDLEANNOTATIONS, "*")));
annotationHeaders = new AnnotationHeaders(this, instructions);

doPlugins();

analyzeMetaInfServices();

//
// calculate class versions in use
//
Expand All @@ -232,9 +241,7 @@ private void analyze0() throws Exception {
// built ins
//

Instructions instructions = new Instructions(
OSGiHeader.parseHeader(getProperty(Constants.BUNDLEANNOTATIONS, "*")));
cds.add(annotationHeaders = new AnnotationHeaders(this, instructions));
cds.add(annotationHeaders);

for (Clazz c : classspace.values()) {
cds.parse(c);
Expand Down Expand Up @@ -408,6 +415,47 @@ private void analyze0() throws Exception {
}
}

/*
* process the META-INF/services/* files. These files can contain bnd
* annotations.
*/

private void analyzeMetaInfServices() {
try {
MetaInfService.getServiceFiles(getJar())
.values()
.stream()
.flatMap(mis -> mis.getImplementations()
.values()
.stream())
.forEach(impl -> {
impl.getAnnotations()
.forEach((annotationName, attrs) -> {
doAnnotationsforMetaInf(impl, Processor.removeDuplicateMarker(annotationName), attrs);
});
});
} catch (Exception e) {
exception(e, "failed to process META-INF/services due to %s", e);
}
}

/*
* Process 1 annotation
*/
private void doAnnotationsforMetaInf(Implementation impl, String annotationName, Attrs attrs) {
try {
Map<String, Object> properties = attrs.toTyped();
properties.putIfAbsent("value", impl.getServiceName()); // default
TypeRef implementation = getTypeRefFromFQN(impl.getImplementationName());
assert implementation != null;
Annotation ann = new Annotation(getTypeRefFromFQN(annotationName), properties, ElementType.TYPE,
RetentionPolicy.CLASS);
addAnnotation(ann, implementation);
} catch (Exception e) {
exception(e, "failed to process %s=%v due to %s", annotationName, attrs, e);
}
}

/**
* Get the export version of a package
*
Expand Down Expand Up @@ -3862,4 +3910,21 @@ public void addDelta(Jar delta) {
}
}

/**
* Useful to reuse the annotation processing. The Annotation
* can be created from other sources than Java code. This
* is mostly useful for the annotations that generate Manifest
* headers.
*/
public void addAnnotation(Annotation ann, TypeRef c) throws Exception {
Clazz clazz = findClass(c);
if (clazz == null) {
error("analyzer processing annotation %s but the associated class %s is not found in the JAR", c);
return;
}
annotationHeaders.classStart(clazz);
annotationHeaders.annotation(ann);
annotationHeaders.classEnd();
}

}
18 changes: 15 additions & 3 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/AnnotationHeaders.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -141,6 +142,10 @@ class AnnotationHeaders extends ClassDataCollector implements Closeable {
static final String STD_HEADER = "org.osgi.annotation.bundle.Header";
static final String STD_HEADERS = "org.osgi.annotation.bundle.Headers";

public static Set<String> BND_ANNOTATIONS = Set.of(BUNDLE_LICENSE, REQUIRE_CAPABILITY,
PROVIDE_CAPABILITY, BUNDLE_CATEGORY, BUNDLE_DOC_URL, BUNDLE_DEVELOPERS, BUNDLE_CONTRIBUTORS, BUNDLE_COPYRIGHT,
STD_REQUIREMENT, STD_CAPABILITY, STD_HEADER);

// Used to detect attributes and directives on Require-Capability and
// Provide-Capability
static final String STD_ATTRIBUTE = "org.osgi.annotation.bundle.Attribute";
Expand Down Expand Up @@ -180,25 +185,28 @@ public boolean classStart(Clazz c) {
//
// Parse any annotated classes except annotations
//
current = c;
if (!c.isAnnotation() && !c.annotations()
.isEmpty()) {

for (Instruction instruction : instructions.keySet()) {
if (instruction.matches(c.getFQN())) {
if (instruction.isNegated()) {
current = null;
return false;
}

current = c;
return true;
}
}
}
current = null;
return false;
}

@Override
public void classEnd() throws Exception {
current = null;
}

/*
* Called when an annotation is found. Dispatch on the known types.
*/
Expand Down Expand Up @@ -490,6 +498,10 @@ private Object getOrDefault(MethodDef method) {
}
}

if (object instanceof Collection col) {
object = col.toArray();
}

if ((object instanceof Object[] typeRefs) && (typeRefs.length > 0) && typeRefs[0] instanceof TypeRef) {
Object[] copy = new Object[typeRefs.length];
for (int i = 0; i < typeRefs.length; i++) {
Expand Down
1 change: 1 addition & 0 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Descriptors.java
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,7 @@ public static String fqnToPath(String s) {

public TypeRef getTypeRefFromFQN(String fqn) {
return switch (fqn) {
case "void" -> VOID;
case "boolean" -> BOOLEAN;
case "byte" -> BOOLEAN;
case "char" -> CHAR;
Expand Down
Loading

0 comments on commit 16c6497

Please sign in to comment.