blob: 78f7130e71ad131e0f8e74ec1cbcbe623691665e [file] [log] [blame]
/*
* 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.felix.dm.annotation.plugin.bnd;
import static aQute.bnd.osgi.Clazz.QUERY.ANNOTATED;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.util.AbstractMap;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.felix.dm.annotation.api.AdapterService;
import org.apache.felix.dm.annotation.api.AspectService;
import org.apache.felix.dm.annotation.api.BundleAdapterService;
import org.apache.felix.dm.annotation.api.BundleDependency;
import org.apache.felix.dm.annotation.api.Component;
import org.apache.felix.dm.annotation.api.Composition;
import org.apache.felix.dm.annotation.api.ConfigurationDependency;
import org.apache.felix.dm.annotation.api.Destroy;
import org.apache.felix.dm.annotation.api.Init;
import org.apache.felix.dm.annotation.api.Inject;
import org.apache.felix.dm.annotation.api.LifecycleController;
import org.apache.felix.dm.annotation.api.Property;
import org.apache.felix.dm.annotation.api.PropertyType;
import org.apache.felix.dm.annotation.api.Registered;
import org.apache.felix.dm.annotation.api.RepeatableProperty;
import org.apache.felix.dm.annotation.api.ServiceDependency;
import org.apache.felix.dm.annotation.api.ServiceScope;
import org.apache.felix.dm.annotation.api.Start;
import org.apache.felix.dm.annotation.api.Stop;
import org.apache.felix.dm.annotation.api.Unregistered;
import org.osgi.framework.Bundle;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Annotation;
import aQute.bnd.osgi.ClassDataCollector;
import aQute.bnd.osgi.Clazz;
import aQute.bnd.osgi.Clazz.FieldDef;
import aQute.bnd.osgi.Clazz.MethodDef;
import aQute.bnd.osgi.Descriptors;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.osgi.Instruction;
import aQute.bnd.osgi.Verifier;
import aQute.lib.collections.MultiMap;
/**
* This is the scanner which does all the annotation parsing on a given class.
* To start the parsing, just invoke the parseClassFileWithCollector and finish methods.
* Once parsed, the corresponding component descriptors can be built using the "writeTo" method.
*
* @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
*/
public class AnnotationCollector extends ClassDataCollector
{
private final static String A_INIT = Init.class.getName();
private final static String A_START = Start.class.getName();
private final static String A_STOP = Stop.class.getName();
private final static String A_DESTROY = Destroy.class.getName();
private final static String A_COMPOSITION = Composition.class.getName();
private final static String A_LIFCLE_CTRL = LifecycleController.class.getName();
private final static String A_COMPONENT = Component.class.getName();
private final static String A_PROPERTY = Property.class.getName();
private final static String A_REPEATABLE_PROPERTY = RepeatableProperty.class.getName();
private final static String A_SERVICE_DEP = ServiceDependency.class.getName();
private final static String A_CONFIGURATION_DEPENDENCY = ConfigurationDependency.class.getName();
private final static String A_BUNDLE_DEPENDENCY = BundleDependency.class.getName();
private final static String A_ASPECT_SERVICE = AspectService.class.getName();
private final static String A_ADAPTER_SERVICE = AdapterService.class.getName();
private final static String A_BUNDLE_ADAPTER_SERVICE = BundleAdapterService.class.getName();
private final static String A_INJECT = Inject.class.getName();
private final static String A_REGISTERED = Registered.class.getName();
private final static String A_UNREGISTERED = Unregistered.class.getName();
private final Logger m_logger;
private String[] m_interfaces;
private boolean m_isField;
private String m_field;
private FieldDef m_fieldDef;
private String m_method;
private MethodDef m_methodDef;
private String m_descriptor;
private final Set<String> m_dependencyNames = new HashSet<String>();
private final List<EntryWriter> m_writers = new ArrayList<EntryWriter>();
private String m_startMethod;
private String m_stopMethod;
private String m_initMethod;
private String m_destroyMethod;
private String m_compositionMethod;
private String m_starter;
private String m_stopper;
private final Set<String> m_importService = new HashSet<String>();
private final Set<String> m_exportService = new HashSet<String>();
private String m_bundleContextField;
private String m_dependencyManagerField;
private String m_registrationField;
private String m_bundleField;
private String m_componentField;
private String m_registeredMethod;
private String m_unregisteredMethod;
private TypeRef m_superClass;
private boolean m_baseClass = true;
private final static String[] EMPTY_STRING_ARRAY = new String[0];
/**
* Name of the class annotated with @Component (or other kind of components, like aspect, adapters).
*/
private String m_componentClassName;
/*
* Name of class currently being parsed (the component class name at first, then the inherited classes).
* See DescriptorGenerator class, which first calls parseClassFileWithCollector method with the component class, then it calls
* again the parseClassFileWithCollector method with all inherited component super classes.
*/
private String m_currentClassName;
/**
* Contains all bind methods annotated with a dependency.
* Each entry has the format: "methodName/method signature".
*/
private final Set<String> m_bindMethods = new HashSet<>();
/**
* When more than one @Property annotation are declared on a component type (outside of the @Component annotation), then a @Repeatable
* annotation is used as the container for the @Property annotations. When such annotation is found, it is stored in this attribute, which
* will be parsed in our finish() method.
*/
private Annotation m_repeatableProperty;
/**
* If a Single @Property is declared on the component type (outside of the @Component annotation), then there is no @Repeatable annotation.
* When such single @Property annotation is found, it is stored in this attribute, which will be parsed in our finish() method.
*/
private Annotation m_singleProperty;
/**
* List of all possible DM components.
*/
private final List<EntryType> m_componentTypes = Arrays.asList(EntryType.Component, EntryType.AspectService, EntryType.AdapterService,
EntryType.BundleAdapterService, EntryType.ResourceAdapterService, EntryType.FactoryConfigurationAdapterService);
/**
* Class Analyzer used to scan component property type annotation.
*/
private final Analyzer m_analyzer;
/**
* List of component property type annotation applied on component class.
*/
private final List<Annotation> m_componentPropertyTypes = new ArrayList<>();
/**
* List of all parsed methods. Key = method name, value is list of signatures.
* Example: if a component has two methods update(Dictionary) and updated(MyConfig), then the map will contain the following:
* "updated" -> ["(Ljava/util/Dictionary;)V", "(Lfoo.bar.MyConfig;)V"]
*/
final Map<String, List<Descriptors.Descriptor>> m_methods = new HashMap<>();
/**
* Object used to see if an annotation is matching a declarative service ComponentPropertyType annotation.
*/
private static final Instruction DS_PROPERTY_TYPE = new Instruction("org.osgi.service.component.annotations.ComponentPropertyType");
/**
* Object used to see if an annotation is matching a DM PropertyType annotation.
*/
private static final Instruction DM_PROPERTY_TYPE = new Instruction(PropertyType.class.getName());
/**
* Mapping between type strings -> type class
*/
private final static Map<String,Class<?>>m_types = Collections.unmodifiableMap(Stream.of(
new SimpleEntry<>("boolean", Boolean.class),
new SimpleEntry<>("byte", Byte.class),
new SimpleEntry<>("short", Short.class),
new SimpleEntry<>("char", Character.class),
new SimpleEntry<>("int", Integer.class),
new SimpleEntry<>("long", Long.class),
new SimpleEntry<>("float", Float.class),
new SimpleEntry<>("double", Double.class))
.collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)));
/**
* Marker used when parsing component property types annotations.
* (To make sure the output is an array, we must make sure there is more than one entry)
*/
private final static String MARKER = new String("|marker");
/**
* Pattern used when parsing component property type annotations.
*/
private static final Pattern IDENTIFIERTOPROPERTY = Pattern.compile("(__)|(_)|(\\$_\\$)|(\\$\\$)|(\\$)");
/**
* Makes a new Collector for parsing a given class.
* @param reporter the object used to report logs.
* @param m_analyzer
*/
public AnnotationCollector(Logger reporter, Analyzer analyzer)
{
m_logger = reporter;
m_analyzer = analyzer;
}
/**
* Indicates that we are parsing a superclass of a given component class.
*/
public void baseClass(boolean baseClass) {
m_logger.debug("baseClass:%b", baseClass);
m_baseClass = baseClass;
}
/**
* Parses the name of the class.
* @param access the class access
* @param name the class name (package are "/" separated).
*/
@Override
public void classBegin(int access, TypeRef name)
{
if (m_baseClass)
{
m_componentClassName = name.getFQN();
m_logger.debug("Parsing class: %s", m_componentClassName);
}
m_currentClassName = name.getFQN();
}
/**
* Parses the implemented interfaces ("/" separated).
*/
@Override
public void implementsInterfaces(TypeRef[] interfaces)
{
if (m_baseClass)
{
List<String> result = new ArrayList<String>();
for (int i = 0; i < interfaces.length; i++)
{
if (!interfaces[i].getBinary().equals("scala/ScalaObject"))
{
result.add(interfaces[i].getFQN());
}
}
m_interfaces = result.toArray(new String[result.size()]);
m_logger.debug("implements: %s", Arrays.toString(m_interfaces));
}
}
/**
* Parses a method. Always invoked BEFORE eventual method annotation.
*/
@Override
public void method(Clazz.MethodDef method)
{
m_logger.debug("Parsed method %s descriptor=%s signature=%s", method.getName(), method.getDescriptor(), method.getSignature());
m_isField = false;
m_method = method.getName();
m_methodDef = method;
m_descriptor = method.getDescriptor().toString();
m_methods.computeIfAbsent(method.getName(), k -> new ArrayList<Descriptors.Descriptor>()).add(method.getDescriptor());
}
/**
* Parses a field. Always invoked BEFORE eventual field annotation
*/
@Override
public void field(Clazz.FieldDef field)
{
m_logger.debug("Parsed field %s descriptor=%s", field.getName(), field.getDescriptor());
m_isField = true;
m_field = field.getName();
m_fieldDef = field;
m_descriptor = field.getDescriptor().toString();
}
@Override
public void extendsClass(TypeRef name) {
m_superClass = name;
}
public TypeRef getSuperClass() {
return m_superClass;
}
/**
* An annotation has been parsed. Always invoked AFTER the "method"/"field"/"classBegin" callbacks.
*/
@Override
public void annotation(Annotation annotation)
{
m_logger.debug("Parsing annotation: %s", annotation.getName());
// if we are parsing a superclass of a given component, then ignore any component annotations.
String name = annotation.getName().getFQN();
if (! m_baseClass) {
String simpleName = name.indexOf(".") != -1 ? name.substring(name.lastIndexOf(".")+1) : name;
Optional<EntryType> type = m_componentTypes.stream().filter(writer -> writer.name().equals(simpleName)).findFirst();
if (type.isPresent()) {
m_logger.debug("Ignoring annotation %s from super class %s of component class %s", name, m_currentClassName, m_componentClassName);
return;
}
}
if (name.equals(A_COMPONENT))
{
parseComponentAnnotation(annotation);
}
else if (name.equals(A_ASPECT_SERVICE))
{
parseAspectService(annotation);
}
else if (name.equals(A_ADAPTER_SERVICE))
{
parseAdapterService(annotation);
}
else if (name.equals(A_BUNDLE_ADAPTER_SERVICE))
{
parseBundleAdapterService(annotation);
}
else if (name.equals(A_INIT))
{
checkAlreadyDeclaredSingleAnnot(() -> m_initMethod, "@Init");
m_initMethod = m_method;
}
else if (name.equals(A_START))
{
checkAlreadyDeclaredSingleAnnot(() -> m_startMethod, "@Start");
m_startMethod = m_method;
}
else if (name.equals(A_REGISTERED))
{
checkAlreadyDeclaredSingleAnnot(() -> m_registeredMethod, "@Registered");
m_registeredMethod = m_method;
}
else if (name.equals(A_STOP))
{
checkAlreadyDeclaredSingleAnnot(() -> m_stopMethod, "@Stop");
m_stopMethod = m_method;
}
else if (name.equals(A_UNREGISTERED))
{
checkAlreadyDeclaredSingleAnnot(() -> m_unregisteredMethod, "@Unregistered");
m_unregisteredMethod = m_method;
}
else if (name.equals(A_DESTROY))
{
checkAlreadyDeclaredSingleAnnot(() -> m_destroyMethod, "@Destroy");
m_destroyMethod = m_method;
}
else if (name.equals(A_COMPOSITION))
{
checkAlreadyDeclaredSingleAnnot(() -> m_compositionMethod, "@Composition");
Patterns.parseMethod(m_method, m_descriptor, Patterns.COMPOSITION);
m_compositionMethod = m_method;
}
else if (name.equals(A_LIFCLE_CTRL))
{
parseLifecycleAnnotation(annotation);
}
else if (name.equals(A_SERVICE_DEP))
{
parseServiceDependencyAnnotation(annotation);
}
else if (name.equals(A_CONFIGURATION_DEPENDENCY))
{
parseConfigurationDependencyAnnotation(annotation);
}
else if (name.equals(A_BUNDLE_DEPENDENCY))
{
parseBundleDependencyAnnotation(annotation);
}
else if (name.equals(A_INJECT))
{
parseInject(annotation);
}
else if (name.equals(A_REPEATABLE_PROPERTY))
{
parseRepeatableProperties(annotation);
}
else if (annotation.getName().getFQN().equals(A_PROPERTY))
{
m_singleProperty = annotation;
}
else {
handlePossibleComponentPropertyAnnotation(annotation);
}
}
/**
* Finishes up the class parsing. This method must be called once the parseClassFileWithCollector method has returned.
* @return true if some annotations have been parsed, false if not.
*/
public boolean finish()
{
m_logger.info("finish %s", m_componentClassName);
// check if we have a component (or adapter) annotation.
Optional<EntryWriter> componentWriter = m_writers.stream()
.filter(writer -> m_componentTypes.indexOf(writer.getEntryType()) != -1)
.findFirst();
if (! componentWriter.isPresent() || m_writers.size() == 0) {
m_logger.info("No components found for class %s", m_componentClassName);
return false;
}
finishComponentAnnotation(componentWriter.get());
// log all meta data for component annotations, dependencies, etc ...
StringBuilder sb = new StringBuilder();
sb.append("Parsed annotation for class ");
sb.append(m_componentClassName);
for (int i = m_writers.size() - 1; i >= 0; i--)
{
sb.append("\n\t").append(m_writers.get(i).toString());
}
m_logger.info(sb.toString());
return true;
}
private void finishComponentAnnotation(EntryWriter componentWriter) {
// Register previously parsed Init/Start/Stop/Destroy/Composition annotations
addCommonServiceParams(componentWriter);
// Add any repeated @Property annotations to the component (or to the aspect, or adapter).
if (m_repeatableProperty != null)
{
Object[] properties = m_repeatableProperty.get("value");
for (Object property : properties)
{
// property is actually a @Property annotation.
parseProperty((Annotation) property, componentWriter);
}
}
// handle component property types
parseComponentPropertyAnnotation(componentWriter);
// Handle a single Property declared on the component type (in this case, there is no @Repeatable annotation).
if (m_singleProperty != null) {
parseProperty(m_singleProperty, componentWriter);
}
if (componentWriter.getEntryType() == EntryType.FactoryConfigurationAdapterService) {
m_logger.debug("finishFactoryComponent ...");
finishFactoryComponent(componentWriter);
}
}
/**
* Finish to parse a Factory Pid Component annotation.
* @param componentWriter the datastructure which holds the parsed annotation attributes.
*/
private void finishFactoryComponent(EntryWriter componentWriter) {
if (componentWriter.getParameter(EntryParam.configType) == null) {
// No configtype defined in the annotation, then try to detect if the method signature contains some config types.
// First, see if there is an updated callback without a config type:
String method = componentWriter.getParameter(EntryParam.updated).toString(); // can't be null
List<Descriptors.Descriptor> methodSigs = m_methods.get(method);
if (methodSigs == null) {
throw new IllegalStateException("can't find callback " + method + " on class" + m_componentClassName);
}
boolean parseConfigTypesFromArgs = true;
for (Descriptors.Descriptor desc : methodSigs) {
Matcher m = Patterns.UPDATED_NO_CONFIG_TYPES.matcher(desc.toString());
if (m.matches()) {
// Found an updated callback which does not contain any config type
m_logger.debug("updated callback %s does not contain any config type in accepted arguments", method);
parseConfigTypesFromArgs = false;
break;
}
}
// Now, try to find an updated callback having some config types accepted as arguments
if (parseConfigTypesFromArgs) {
for (Descriptors.Descriptor desc : methodSigs) {
Matcher m = Patterns.UPDATED_CONFIG_TYPES.matcher(desc.toString());
String[] configTypes = parsePossibleConfigTypesFromUpdatedCallback(componentWriter,
desc.toString());
if (configTypes.length > 0) {
// If factory pid not specified, derive the factory pid from the config type
// (and if only one config type is specified)
if (componentWriter.getParameter(EntryParam.factoryPid) == null) {
if (configTypes.length == 1) {
componentWriter.put(EntryParam.factoryPid, configTypes[0]);
}
}
break;
}
}
}
}
// set factory pid to component class name if it has not been derived from any annotation attributes or from updated callback parameters.
if (componentWriter.getParameter(EntryParam.factoryPid) == null) {
componentWriter.put(EntryParam.factoryPid, m_componentClassName);
}
}
/**
* Auto Detect if an updated callback method contains some configuration types.
* @param writer the writer where the detected configration types are written in case some are detected
* @param callbackDescriptor the descriptor of the udpated callback method.
* @param propagateConfigTypeDefVal true if any found config type default values should be parsed and propagated to service properties
* @return an empty string array in case no config types has been detected, or the array of parsed config types.
*/
private String[] parsePossibleConfigTypesFromUpdatedCallback(EntryWriter writer, String callbackDescriptor) {
Matcher m = Patterns.UPDATED_CONFIG_TYPES.matcher(callbackDescriptor);
List<String> types = new ArrayList<>();
while (m.find()) {
String type = m.group(5);
if (type != null) {
m_logger.debug("checking if type %s is an interface or an annotation", type);
TypeRef typeRef = m_analyzer.getTypeRef(type);
try {
Clazz clazz = m_analyzer.findClass(typeRef);
if (! clazz.isAnnotation() && ! clazz.isInterface()) {
m_logger.debug("ignoring updated callback signature %s (argument type not an interface or an annotation", callbackDescriptor);
continue;
}
types.add(type.replace("/", "."));
/*
if (propagateConfigTypeDefVal) {
// set component service properties with config type default values
if (clazz.isAnnotation()) {
clazz.parseClassFileWithCollector (new ComponentPropertyTypeDataCollector(writer));
}
}
*/
} catch (Exception e) {
throw new IllegalStateException("could not find config type class " + type);
}
}
}
if (types.size() > 0) {
m_logger.debug("detected config types for updated callback %s", types);
if (types.size() == 1) {
writer.put(EntryParam.configType, types.get(0));
} else {
writer.put(EntryParam.configTypes, types);
}
return types.stream().toArray(String[]::new);
}
return EMPTY_STRING_ARRAY;
}
/**
* Writes the generated component descriptor in the given print writer.
* The first line must be the component descriptor (@Component or AspectService, etc ..).
* @param pw the writer where the component descriptor will be written.
*/
public void writeTo(PrintWriter pw)
{
// write first the component descriptor (@Component, @AspectService, ...)
EntryWriter componentWriter = m_writers.stream()
.filter(writer -> m_componentTypes.indexOf(writer.getEntryType()) != -1)
.findFirst()
.orElseThrow(() -> new IllegalStateException("Component type not found while scanning class " + m_componentClassName));
pw.println(componentWriter);
// and write other component descriptors (dependencies, and other annotations)
m_writers.stream()
.filter(writer -> m_componentTypes.indexOf(writer.getEntryType()) == -1)
.forEach(dependency -> pw.println(dependency.toString()));
}
/**
* Returns list of all imported services. Imported services are deduced from every
* @ServiceDependency annotations.
* @return the list of imported services, or null
*/
public Set<String> getImportService()
{
return m_importService;
}
/**
* Returns list of all exported services. Imported services are deduced from every
* annotations which provides a service (@Component, etc ...)
* @return the list of exported services, or null
*/
public Set<String> getExportService()
{
return m_exportService;
}
/**
* Parses a Property annotation that is applied on the component class.
* @param property the Property annotation.
*/
private void parseRepeatableProperties(Annotation repeatedProperties)
{
m_repeatableProperty = repeatedProperties;
}
/**
* Checks double declaration of an annotation, which normally must be declared only one time.
* @param field the field supplier (if not null, means the annotation is already declared)
* @param annot the annotation name.
* @throws IllegalStateException if annotation is already declared.
*/
private void checkAlreadyDeclaredSingleAnnot(Supplier<Object> field, String annot) {
if (field.get() != null) {
throw new IllegalStateException("detected multiple " + annot + " annotation from class " + m_currentClassName + " (on from child classes)");
}
}
private void parseComponentAnnotation(Annotation annotation)
{
String factoryPid = annotation.get(EntryParam.factoryPid.toString());
if (factoryPid != null) {
parseFactoryComponent(annotation);
return;
}
EntryWriter writer = new EntryWriter(EntryType.Component);
m_writers.add(writer);
// impl attribute
writer.put(EntryParam.impl, m_componentClassName);
// Parse Adapter properties, and other params common to all kind of component
parseCommonComponentAttributes(annotation, writer);
// provides attribute.
if (writer.putClassArray(annotation, EntryParam.provides, m_interfaces, m_exportService) == 0)
{
// no service provided: check if @Registered/@Unregistered annotation are used
// and raise an error if true.
checkRegisteredUnregisteredNotPresent();
}
// factoryMethod attribute
writer.putString(annotation, EntryParam.factoryMethod, null);
}
private void addCommonServiceParams(EntryWriter writer)
{
if (m_initMethod != null)
{
writer.put(EntryParam.init, m_initMethod);
}
if (m_startMethod != null)
{
writer.put(EntryParam.start, m_startMethod);
}
if (m_registeredMethod != null)
{
writer.put(EntryParam.registered, m_registeredMethod);
}
if (m_stopMethod != null)
{
writer.put(EntryParam.stop, m_stopMethod);
}
if (m_unregisteredMethod != null)
{
writer.put(EntryParam.unregistered, m_unregisteredMethod);
}
if (m_destroyMethod != null)
{
writer.put(EntryParam.destroy, m_destroyMethod);
}
if (m_compositionMethod != null)
{
writer.put(EntryParam.composition, m_compositionMethod);
}
if (m_starter != null)
{
writer.put(EntryParam.starter, m_starter);
}
if (m_stopper != null)
{
writer.put(EntryParam.stopper, m_stopper);
if (m_starter == null)
{
throw new IllegalArgumentException("Can't use a @LifecycleController annotation for stopping a service without declaring a " +
"@LifecycleController that starts the component in class " + m_currentClassName);
}
}
if (m_bundleContextField != null)
{
writer.put(EntryParam.bundleContextField, m_bundleContextField);
}
if (m_dependencyManagerField != null)
{
writer.put(EntryParam.dependencyManagerField, m_dependencyManagerField);
}
if (m_componentField != null)
{
writer.put(EntryParam.componentField, m_componentField);
}
if (m_registrationField != null)
{
writer.put(EntryParam.registrationField, m_registrationField);
}
if (m_bundleField != null)
{
Object scope = writer.getParameter(EntryParam.scope);
if (scope == null || scope == ServiceScope.SINGLETON.name()) {
throw new IllegalStateException("can't inject a bundle field on a component without prototype or bundle scope");
}
writer.put(EntryParam.bundle, m_bundleField);
}
}
/**
* Check if a dependency is already declared in another same bindMethod (or class field) on another child class.
*/
private void checkDependencyAlreadyDeclaredInChild(Annotation annotation, String methodOrField, boolean method) {
if (! m_baseClass && m_bindMethods.contains(methodOrField + "/" + m_descriptor)) {
throw new IllegalStateException("Annotation " + annotation.getName().getShortName()
+ " declared on " + m_currentClassName + "." + methodOrField + (method ? " method" : " field") + " is already declared in child classe(s)");
}
m_bindMethods.add(methodOrField + "/" + m_descriptor);
}
/**
* Parses a ServiceDependency Annotation.
* @param annotation the ServiceDependency Annotation.
*/
private void parseServiceDependencyAnnotation(Annotation annotation)
{
EntryWriter writer = new EntryWriter(EntryType.ServiceDependency);
boolean doDereference = true; // false means dependency manager must not internally dereference the service dependency
m_writers.add(writer);
// service attribute
String service = parseClassAttrValue(annotation.get(EntryParam.service.toString()));
if (service == null)
{
if (m_isField)
{
checkDependencyAlreadyDeclaredInChild(annotation, m_field, false);
//service = Patterns.parseClass(m_descriptor, Patterns.CLASS, 1);
m_logger.debug("parsing field service type %s", m_field);
service = FieldTypeGetter.determineFieldType(m_logger, m_fieldDef);
m_logger.debug("field service type=%s", service);
}
else
{
// if we are parsing some inherited classes, detect if the bind method is already declared in child classes
checkDependencyAlreadyDeclaredInChild(annotation, m_method, true);
// parse "bind(Component, ServiceReference, Service)" signature
service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS1, 3, false);
if (service == null) {
// parse "bind(Component, Service)" signature
service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS2, 2, false);
}
if (service == null) {
// parse "bind(ServiceReference<T>)" or "bind(ServiceObjects<T>)" signatures
service = Patterns.inferTypeFromGenericType(m_methodDef.getDescriptor().toString(),
m_methodDef.getSignature(),
m_logger);
// dm must not internally dereference the service, since it is injected as a ServiceRef or a ServiceObjects
doDereference = false;
}
if (service == null) {
// parse "bind(Component, Map, Service)" signature
service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS3, 3, false);
}
if (service == null) {
// parse "bind(ServiceReference, Service)" signature
service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS4, 2, false);
}
if (service == null) {
// parse "bind(Service)" signature
service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS5, 1, false);
}
if (service == null) {
// parse "bind(Service, Map)" signature
service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS6, 1, false);
}
if (service == null) {
// parse "bind(Map, Service)" signature
service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS7, 2, false);
}
if (service == null) {
// parse "bind(Service, Dictionary)" signature
service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS8, 1, false);
}
if (service == null) {
// parse "bind(Dictionary, Service)" signature
service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS9, 2, true);
}
}
}
boolean matchAnyService = service != null && service.equals(ServiceDependency.Any.class.getName());
if (matchAnyService) {
service = null;
}
writer.put(EntryParam.service, service);
// Store this service in list of imported services.
m_importService.add(service);
// autoConfig attribute
if (m_isField)
{
writer.put(EntryParam.autoConfig, m_field);
}
// filter attribute
String filter = annotation.get(EntryParam.filter.toString());
if (filter != null)
{
Verifier.verifyFilter(filter, 0);
if (matchAnyService) {
filter = "(&(objectClass=*)" + filter + ")";
}
writer.put(EntryParam.filter, filter);
} else if (matchAnyService) {
filter = "(objectClass=*)";
writer.put(EntryParam.filter, filter);
}
// defaultImpl attribute
writer.putClass(annotation, EntryParam.defaultImpl);
// added callback
writer.putString(annotation, EntryParam.added, (!m_isField) ? m_method : null);
// timeout parameter
writer.putString(annotation, EntryParam.timeout, null);
Long t = (Long) annotation.get(EntryParam.timeout.toString());
if (t != null && t.longValue() < -1)
{
throw new IllegalArgumentException("Invalid timeout value " + t + " in ServiceDependency annotation from class " + m_currentClassName);
}
// required attribute (not valid if parsing a temporal service dependency)
writer.putString(annotation, EntryParam.required, null);
// changed callback
writer.putString(annotation, EntryParam.changed, null);
// removed callback
writer.putString(annotation, EntryParam.removed, null);
// swap callback
writer.putString(annotation, EntryParam.swap, null);
// name attribute
parseDependencyName(writer, annotation);
// propagate attribute
writer.putString(annotation, EntryParam.propagate, null);
// dereference flag
writer.putString(annotation, EntryParam.dereference, String.valueOf(doDereference));
if (writer.getParameter(EntryParam.defaultImpl) != null) {
// If the defaultImpl attribute is set, then check if the required flag is set to false.
if ("true".equals(writer.getParameter(EntryParam.required))) {
throw new IllegalArgumentException("ServiceDependency defaultImpl attribute can't be used on a required dependency from class " + m_currentClassName);
}
// If the defaultImpl attribute is set, then check if dependency is applied on a class field
if (! m_isField) {
throw new IllegalArgumentException("ServiceDependency defaultImpl attribute can only be used when the ServiceDependency is applied on a class field from class " + m_currentClassName);
}
}
}
/**
* Parse the value of a given annotation attribute (which is of type 'class').
* This method is compatible with bndtools 2.4.1 (where the annotation.get() method returns a String of the form "Lfull/class/name;"),
* and with bndtools 3.x.x (where the annotation.get() method returns a TypeRef).
*
* @param annot the annotation which contains the given attribute
* @param attr the attribute name (of 'class' type).
* @return the annotation class attribute value
*/
public static String parseClassAttrValue(Object value) {
if (value instanceof String)
{
return Patterns.parseClass((String) value, Patterns.CLASS, 1);
}
else if (value instanceof TypeRef)
{
return ((TypeRef) value).getFQN();
}
else if (value == null) {
return null;
}
else {
throw new IllegalStateException("can't parse class attribute value from " + value);
}
}
/**
* Parses a ConfigurationDependency annotation.
* @param annotation the ConfigurationDependency annotation.
*/
private void parseConfigurationDependencyAnnotation(Annotation annotation) {
checkDependencyAlreadyDeclaredInChild(annotation, m_method, true);
EntryWriter writer = new EntryWriter(EntryType.ConfigurationDependency);
m_writers.add(writer);
// propagate attribute
writer.putString(annotation, EntryParam.propagate, null);
// required flag (true by default)
writer.putString(annotation, EntryParam.required, Boolean.TRUE.toString());
// Parse possible config types specified in the updated callback.
// First, check if the update callback does not contain any config type parameters
String[] configTypes = EMPTY_STRING_ARRAY;
Matcher m = Patterns.UPDATED_NO_CONFIG_TYPES.matcher(m_descriptor.toString());
if (! m.matches()) {
// the updated callback may contain some configuration types.
configTypes = parsePossibleConfigTypesFromUpdatedCallback(writer, m_descriptor);
}
// Calculate the the pid, which value is either:
//
// - the value of the pid attribute, if specified
// - or the fqdn of the class specified by the pidFromClass attribute, if specified
// - or the fqdn of one configuration proxy type found from the updated callback method (if multiple config types are specified in the callback,
// then we can't derive the pid)
// - or by default the fdqn of the class where the annotation is found
String pid = get(annotation, EntryParam.pid.toString(), null);
if (pid == null) {
pid = parseClassAttrValue(annotation.get(EntryParam.pidClass.toString()));
}
if (pid == null) {
if (configTypes.length == 1) {
pid = configTypes[0];
}
}
if (pid == null) {
pid = m_componentClassName;
}
writer.put(EntryParam.pid, pid);
// the method on which the annotation is applied
writer.put(EntryParam.updated, m_method);
// name attribute
parseDependencyName(writer, annotation);
}
/**
* Parses an AspectService annotation.
* @param annotation
*/
private void parseAspectService(Annotation annotation)
{
EntryWriter writer = new EntryWriter(EntryType.AspectService);
m_writers.add(writer);
// Parse service filter
String filter = annotation.get(EntryParam.filter.toString());
if (filter != null)
{
Verifier.verifyFilter(filter, 0);
writer.put(EntryParam.filter, filter);
}
// Parse service aspect ranking
Integer ranking = annotation.get(EntryParam.ranking.toString());
writer.put(EntryParam.ranking, ranking.toString());
// Generate Aspect Implementation
writer.put(EntryParam.impl, m_componentClassName);
// Parse Adapter properties, and other params common to all kind of component
parseCommonComponentAttributes(annotation, writer);
// Parse field/added/changed/removed attributes
parseAspectOrAdapterCallbackMethods(annotation, writer);
// Parse service interface this aspect is applying to
Object service = annotation.get(EntryParam.service.toString());
if (service == null)
{
if (m_interfaces == null)
{
throw new IllegalStateException("Invalid AspectService annotation: " +
"the service attribute has not been set and the class " + m_componentClassName
+ " does not implement any interfaces");
}
if (m_interfaces.length != 1)
{
throw new IllegalStateException("Invalid AspectService annotation: " +
"the service attribute has not been set and the class " + m_componentClassName
+ " implements more than one interface");
}
writer.put(EntryParam.service, m_interfaces[0]);
}
else
{
writer.putClass(annotation, EntryParam.service);
}
// Parse factoryMethod attribute
writer.putString(annotation, EntryParam.factoryMethod, null);
}
private void parseAspectOrAdapterCallbackMethods(Annotation annotation, EntryWriter writer)
{
String field = annotation.get(EntryParam.field.toString());
String added = annotation.get(EntryParam.added.toString());
String changed = annotation.get(EntryParam.changed.toString());
String removed = annotation.get(EntryParam.removed.toString());
String swap = annotation.get(EntryParam.swap.toString());
// "field" and "added/changed/removed/swap" attributes can't be mixed
if (field != null && (added != null || changed != null || removed != null || swap != null))
{
throw new IllegalStateException("Annotation " + annotation + "can't applied on " + m_componentClassName
+ " can't mix \"field\" attribute with \"added/changed/removed\" attributes");
}
// Parse aspect impl field where to inject the original service.
writer.putString(annotation, EntryParam.field, null);
// Parse aspect impl callback methods.
writer.putString(annotation, EntryParam.added, null);
writer.putString(annotation, EntryParam.changed, null);
writer.putString(annotation, EntryParam.removed, null);
writer.putString(annotation, EntryParam.swap, null);
}
/**
* Parses an AspectService annotation.
* @param annotation
*/
private void parseAdapterService(Annotation annotation)
{
EntryWriter writer = new EntryWriter(EntryType.AdapterService);
m_writers.add(writer);
// Generate Adapter Implementation
writer.put(EntryParam.impl, m_componentClassName);
// Parse adaptee filter
String adapteeFilter = annotation.get(EntryParam.adapteeFilter.toString());
if (adapteeFilter != null)
{
Verifier.verifyFilter(adapteeFilter, 0);
writer.put(EntryParam.adapteeFilter, adapteeFilter);
}
// Parse the mandatory adapted service interface.
writer.putClass(annotation, EntryParam.adapteeService);
// Parse Adapter properties, and other params common to all kind of component
parseCommonComponentAttributes(annotation, writer);
// Parse the provided adapter service (use directly implemented interface by default).
if (writer.putClassArray(annotation, EntryParam.provides, m_interfaces, m_exportService) == 0)
{
checkRegisteredUnregisteredNotPresent();
}
// Parse factoryMethod attribute
writer.putString(annotation, EntryParam.factoryMethod, null);
// Parse propagate flag.
// Parse factoryMethod attribute
writer.putString(annotation, EntryParam.propagate, null);
// Parse field/added/changed/removed attributes
parseAspectOrAdapterCallbackMethods(annotation, writer);
}
/**
* Parses a BundleAdapterService annotation.
* @param annotation
*/
private void parseBundleAdapterService(Annotation annotation)
{
EntryWriter writer = new EntryWriter(EntryType.BundleAdapterService);
m_writers.add(writer);
// Generate Adapter Implementation
writer.put(EntryParam.impl, m_componentClassName);
// Parse bundle filter
String filter = annotation.get(EntryParam.filter.toString());
if (filter != null)
{
Verifier.verifyFilter(filter, 0);
writer.put(EntryParam.filter, filter);
}
// Parse stateMask attribute
writer.putString(annotation, EntryParam.stateMask, Integer.valueOf(
Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE).toString());
// Parse Adapter properties, and other params common to all kind of component
parseCommonComponentAttributes(annotation, writer);
// Parse the optional adapter service (use directly implemented interface by default).
if (writer.putClassArray(annotation, EntryParam.provides, m_interfaces, m_exportService) == 0)
{
checkRegisteredUnregisteredNotPresent();
}
// Parse propagate attribute
writer.putString(annotation, EntryParam.propagate, Boolean.FALSE.toString());
// Parse factoryMethod attribute
writer.putString(annotation, EntryParam.factoryMethod, null);
}
/**
* Parses a Factory Pid Component
* @param annotation
*/
private void parseFactoryComponent(Annotation annotation)
{
EntryWriter writer = new EntryWriter(EntryType.FactoryConfigurationAdapterService);
m_writers.add(writer);
// Generate Adapter Implementation
writer.put(EntryParam.impl, m_componentClassName);
String factoryPid = get(annotation, EntryParam.factoryPid.toString(), null);
if (factoryPid != null) {
writer.put(EntryParam.factoryPid, factoryPid);
} else {
// The finishFactoryComponent will set later the factoryPid (either to a config type specified in the updated callback,
// or to the component class name). See finishFactoryComponent() method.
}
// Parse updated callback
writer.putString(annotation, EntryParam.updated, "updated");
// propagate attribute
writer.putString(annotation, EntryParam.propagate, Boolean.FALSE.toString());
// Parse the provided adapter service (use directly implemented interface by default).
if (writer.putClassArray(annotation, EntryParam.provides, m_interfaces, m_exportService) == 0)
{
checkRegisteredUnregisteredNotPresent();
}
// Parse Adapter properties, and other params common to all kind of component
parseCommonComponentAttributes(annotation, writer);
// Parse factoryMethod attribute
writer.putString(annotation, EntryParam.factoryMethod, null);
m_logger.debug("Parsed factory configuration adapter annotations, methods=%s", m_methods);
}
private void parseBundleDependencyAnnotation(Annotation annotation)
{
checkDependencyAlreadyDeclaredInChild(annotation, m_method, true);
EntryWriter writer = new EntryWriter(EntryType.BundleDependency);
m_writers.add(writer);
String filter = annotation.get(EntryParam.filter.toString());
if (filter != null)
{
Verifier.verifyFilter(filter, 0);
writer.put(EntryParam.filter, filter);
}
writer.putString(annotation, EntryParam.added, m_method);
writer.putString(annotation, EntryParam.changed, null);
writer.putString(annotation, EntryParam.removed, null);
writer.putString(annotation, EntryParam.required, null);
writer.putString(annotation, EntryParam.stateMask, null);
writer.putString(annotation, EntryParam.propagate, null);
parseDependencyName(writer, annotation);
}
/**
* Parse the name of a given dependency.
* @param writer The writer where to write the dependency name
* @param annotation the dependency to be parsed
*/
private void parseDependencyName(EntryWriter writer, Annotation annotation)
{
String name = annotation.get(EntryParam.name.toString());
if (name != null)
{
if(! m_dependencyNames.add(name))
{
throw new IllegalArgumentException("Duplicate dependency name " + name + " in Dependency " + annotation + " from class " + m_currentClassName);
}
writer.put(EntryParam.name, name);
}
}
private void parseLifecycleAnnotation(Annotation annotation)
{
Patterns.parseField(m_field, m_descriptor, Patterns.RUNNABLE);
if ("true".equals(get(annotation,EntryParam.start.name(), "true")))
{
if (m_starter != null) {
throw new IllegalStateException("Lifecycle annotation already defined on field " +
m_starter + " in class (or super class of) " + m_componentClassName);
}
m_starter = m_field;
} else {
if (m_stopper != null) {
throw new IllegalStateException("Lifecycle annotation already defined on field " +
m_stopper + " in class (or super class of) " + m_componentClassName);
}
m_stopper = m_field;
}
}
/**
* Parses attributes common to all kind of components.
* First, Property annotation is parsed which represents a list of key-value pair.
* The properties are encoded using the following json form:
*
* properties ::= key-value-pair*
* key-value-pair ::= key value
* value ::= String | String[] | value-type
* value-type ::= jsonObject with value-type-info
* value-type-info ::= "type"=primitive java type
* "value"=String|String[]
*
* Exemple:
*
* "properties" : {
* "string-param" : "string-value",
* "string-array-param" : ["str1", "str2],
* "long-param" : {"type":"java.lang.Long", "value":"1"}}
* "long-array-param" : {"type":"java.lang.Long", "value":["1"]}}
* }
* }
*
* @param component the component annotation which contains a "properties" attribute. The component can be either a @Component, or an aspect, or an adapter.
* @param writer the object where the parsed attributes are written.
*/
private void parseCommonComponentAttributes(Annotation component, EntryWriter writer)
{
// Parse properties attribute.
Object[] properties = component.get(EntryParam.properties.toString());
if (properties != null)
{
for (Object property : properties)
{
Annotation propertyAnnotation = (Annotation) property;
parseProperty(propertyAnnotation, writer);
}
}
// scope attribute
String scope = component.get("scope");
if (scope == null) {
scope = ServiceScope.SINGLETON.name();
}
writer.put(EntryParam.scope, scope);
}
/**
* Parses a Property annotation. The result is added to the associated writer object
* @param annotation the @Property annotation.
* @param writer the writer object where the parsed property will be added to.
*/
private void parseProperty(Annotation property, EntryWriter writer)
{
String name = (String) property.get("name");
String type = parseClassAttrValue(property.get("type"));
Class<?> classType;
try
{
classType = (type == null) ? String.class : Class.forName(type);
}
catch (ClassNotFoundException e)
{
// Theorically impossible
throw new IllegalArgumentException("Invalid Property type " + type
+ " from annotation " + property + " in class " + m_componentClassName);
}
Object[] values;
if ((values = property.get("value")) != null)
{
values = checkPropertyType(name, classType, values);
writer.addProperty(name, values, classType);
}
else if ((values = property.get("values")) != null)
{ // deprecated
values = checkPropertyType(name, classType, values);
writer.addProperty(name, values, classType);
}
else if ((values = property.get("longValue")) != null)
{
writer.addProperty(name, values, Long.class);
}
else if ((values = property.get("doubleValue")) != null)
{
writer.addProperty(name, values, Double.class);
}
else if ((values = property.get("floatValue")) != null)
{
writer.addProperty(name, values, Float.class);
}
else if ((values = property.get("intValue")) != null)
{
writer.addProperty(name, values, Integer.class);
}
else if ((values = property.get("byteValue")) != null)
{
writer.addProperty(name, values, Byte.class);
}
else if ((values = property.get("charValue")) != null)
{
writer.addProperty(name, values, Character.class);
}
else if ((values = property.get("booleanValue")) != null)
{
writer.addProperty(name, values, Boolean.class);
}
else if ((values = property.get("shortValue")) != null)
{
writer.addProperty(name, values, Short.class);
}
else
{
throw new IllegalArgumentException(
"Missing Property value from annotation " + property + " in class " + m_componentClassName);
}
}
/**
* Checks if a property contains values that are compatible with a give primitive type.
*
* @param name the property name
* @param values the values for the property name
* @param type the primitive type.
* @return the same property values, possibly modified if the type is 'Character' (the strings are converted to their character integer values).
*/
private Object[] checkPropertyType(String name, Class<?> type, Object ... values)
{
if (type.equals(String.class))
{
return values;
} else if (type.equals(Long.class)) {
for (Object value : values) {
try {
Long.valueOf(value.toString());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName
+ " does not contain a valid Long value (" + value.toString() + ")");
}
}
} else if (type.equals(Double.class)) {
for (Object value : values) {
try {
Double.valueOf(value.toString());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName
+ " does not contain a valid Double value (" + value.toString() + ")");
}
}
} else if (type.equals(Float.class)) {
for (Object value : values) {
try {
Float.valueOf(value.toString());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName
+ " does not contain a valid Float value (" + value.toString() + ")");
}
}
} else if (type.equals(Integer.class)) {
for (Object value : values) {
try {
Integer.valueOf(value.toString());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName
+ " does not contain a valid Integer value (" + value.toString() + ")");
}
}
} else if (type.equals(Byte.class)) {
for (Object value : values) {
try {
Byte.valueOf(value.toString());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName
+ " does not contain a valid Byte value (" + value.toString() + ")");
}
}
} else if (type.equals(Character.class)) {
for (int i = 0; i < values.length; i++)
{
try
{
// If the string is already an integer, don't modify it
Integer.valueOf(values[i].toString());
}
catch (NumberFormatException e)
{
// Converter the character string to its corresponding integer code.
if (values[i].toString().length() != 1)
{
throw new IllegalArgumentException("Property \"" + name + "\" in class "
+ m_componentClassName + " does not contain a valid Character value (" + values[i] + ")");
}
try
{
values[i] = Integer.valueOf(values[i].toString().charAt(0));
}
catch (NumberFormatException e2)
{
throw new IllegalArgumentException("Property \"" + name + "\" in class "
+ m_componentClassName + " does not contain a valid Character value (" + values[i].toString()
+ ")");
}
}
}
} else if (type.equals(Boolean.class)) {
for (Object value : values) {
if (! value.toString().equalsIgnoreCase("false") && ! value.toString().equalsIgnoreCase("true")) {
throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName
+ " does not contain a valid Boolean value (" + value.toString() + ")");
}
}
} else if (type.equals(Short.class)) {
for (Object value : values) {
try {
Short.valueOf(value.toString());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName
+ " does not contain a valid Short value (" + value.toString() + ")");
}
}
}
return values;
}
/**
* Parse Inject annotation, used to inject some special classes in some fields
* (BundleContext/DependencyManager etc ...)
* @param annotation the Inject annotation
*/
private void parseInject(Annotation annotation)
{
if (Patterns.BUNDLE_CONTEXT.matcher(m_descriptor).matches())
{
if (m_bundleContextField != null) {
throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)");
}
m_bundleContextField = m_field;
}
else if (Patterns.DEPENDENCY_MANAGER.matcher(m_descriptor).matches())
{
if (m_dependencyManagerField != null) {
throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)");
}
m_dependencyManagerField = m_field;
}
else if (Patterns.COMPONENT.matcher(m_descriptor).matches())
{
if (m_componentField != null) {
throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)");
}
m_componentField = m_field;
}
else if (Patterns.SERVICE_REGISTRATION.matcher(m_descriptor).matches())
{
if (m_registrationField != null) {
throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)");
}
m_registrationField = m_field;
}
else if (Patterns.BUNDLE.matcher(m_descriptor).matches())
{
if (m_bundleField != null) {
throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)");
}
m_bundleField = m_field;
}
else
{
throw new IllegalArgumentException("@Inject annotation can't be applied on the field \"" + m_field
+ "\" in class " + m_currentClassName);
}
}
/**
* This method checks if the @Registered and/or @Unregistered annotations have been defined
* while they should not, because the component does not provide a service.
*/
private void checkRegisteredUnregisteredNotPresent()
{
if (m_registeredMethod != null)
{
throw new IllegalStateException("@Registered annotation can't be used on a Component " +
" which does not provide a service (class=" + m_currentClassName + ")");
}
if (m_unregisteredMethod != null)
{
throw new IllegalStateException("@Unregistered annotation can't be used on a Component " +
" which does not provide a service (class=" + m_currentClassName + ")");
}
}
/**
* Get an annotation attribute, and return a default value if its not present.
* @param <T> the type of the variable which is assigned to the return value of this method.
* @param annotation The annotation we are parsing
* @param name the attribute name to get from the annotation
* @param defaultValue the default value to return if the attribute is not found in the annotation
* @return the annotation attribute value, or the defaultValue if not found
*/
@SuppressWarnings("unchecked")
private <T> T get(Annotation annotation, String name, T defaultValue)
{
T value = (T) annotation.get(name);
return value != null ? value : defaultValue;
}
/**
* Check if an annotation applied on the component class is a component property type.
* (the following is adapted from the original bnd DS AnnotationReader class)
*/
private void handlePossibleComponentPropertyAnnotation(Annotation annotation) {
m_logger.debug("Checking possible component property annotations %s", annotation);
try {
Clazz clazz = m_analyzer.findClass(annotation.getName());
if (clazz == null) {
m_logger.debug(
"Unable to find the annotation %s applied to type %s from project build path (ignoring it).",
annotation.getName().getFQN(), m_currentClassName);
return;
}
if (clazz.is(ANNOTATED, DS_PROPERTY_TYPE, m_analyzer) || clazz.is(ANNOTATED, DM_PROPERTY_TYPE, m_analyzer)) {
m_logger.debug("Found component property type %s", annotation);
m_componentPropertyTypes.add(annotation);
} else {
m_logger.debug("The annotation %s on component type %s will not be used for properties as the annotation is not annotated with @ComponentPropertyType",
clazz.getFQN(), m_currentClassName);
return;
}
} catch (Exception e) {
m_logger.error("An error occurred when attempting to process annotation %s, applied to component %s", e,
annotation.getName().getFQN(), m_currentClassName);
}
}
/**
* Parse a component property type annotation applied on the component class.
* (the following is adapted from the original bnd DS AnnotationReader class)
*/
private void parseComponentPropertyAnnotation(EntryWriter componentWriter) {
for (Annotation annotation : m_componentPropertyTypes) {
try {
Clazz clazz = m_analyzer.findClass(annotation.getName());
if (clazz == null) {
m_logger.warn(
"Unable to determine whether the annotation %s applied to type %s is a component property type as it is not on the project build path. If this annotation is a component property type then it must be present on the build path in order to be processed",
annotation.getName().getFQN(), m_currentClassName);
continue;
}
m_logger.debug("Parsing component property type %s", annotation);
clazz.parseClassFileWithCollector(new ComponentPropertyTypeDataCollector(annotation, componentWriter));
} catch (Exception e) {
m_logger.error("An error occurred when attempting to process annotation %s, applied to component %s", e,
annotation.getName().getFQN(), m_currentClassName);
}
}
}
private final class ComponentPropertyTypeDataCollector extends ClassDataCollector {
private final MultiMap<String,String> props = new MultiMap<String,String>();
private final Map<String,Class> propertyTypes = new HashMap<>();
private int hasNoDefault = 0;
private boolean hasValue = false;
private boolean hasMethods = false;
private FieldDef prefixField = null;
private TypeRef typeRef = null;
private final EntryWriter m_componentWriter;
private ComponentPropertyTypeDataCollector(EntryWriter componentWriter) {
m_componentWriter = componentWriter;
}
private ComponentPropertyTypeDataCollector(Annotation componentPropertyAnnotation, EntryWriter componentWriter) {
m_componentWriter = componentWriter;
// Add in the defined attributes
for (String key : componentPropertyAnnotation.keySet()) {
Object value = componentPropertyAnnotation.get(key);
m_logger.debug("ComponentPropertyTypeDataCollector: handle value %s %s", key, value);
handleValue(key, value, value instanceof TypeRef, null);
}
}
@Override
public void classBegin(int access, TypeRef name) {
m_logger.debug("PropertyType: class begin %s", name);
typeRef = name;
}
@Override
public void field(FieldDef defined) {
m_logger.debug("PropertyType: field %s", defined);
if (defined.isStatic() && defined.getName().equals("PREFIX_")) {
prefixField = defined;
}
}
@Override
public void method(MethodDef defined) {
m_logger.debug("PropertyType: method %s", defined);
if (defined.isStatic()) {
return;
}
hasMethods = true;
if (defined.getName().equals("value")) {
hasValue = true;
} else {
hasNoDefault++;
}
}
@Override
public void annotationDefault(MethodDef defined, Object value) {
m_logger.debug("PropertyType: annotationDefault method %s value %s", defined, value);
if (!defined.getName().equals("value")) {
hasNoDefault--;
}
// check type, exit with warning if annotation
// or annotation array
boolean isClass = false;
Class< ? > typeClass = null;
TypeRef type = defined.getType().getClassRef();
if (!type.isPrimitive()) {
if (type == m_analyzer.getTypeRef("java/lang/Class")) {
isClass = true;
} else {
try {
Clazz r = m_analyzer.findClass(type);
if (r.isAnnotation()) {
m_logger.warn("Nested annotation type found in member %s, %s",
defined.getName(), type.getFQN());
return;
}
} catch (Exception e) {
m_logger.error("Exception looking at annotation type to lifecycle method with type %s", e, type);
}
}
} else {
m_logger.debug("PropertyType: type.getFQN()=%s", type.getFQN());
typeClass = m_types.get(type.getFQN());
}
if (value != null) {
String name = defined.getName();
// only add the default value if the user has not specified the property name
if (! props.containsKey(name)) {
handleValue(name, value, isClass, typeClass);
}
}
}
private void handleValue(String name, Object value, boolean isClass, Class< ? > typeClass) {
if (value.getClass().isArray()) {
// add element individually
int len = Array.getLength(value);
for (int i = 0; i < len; i++) {
Object element = Array.get(value, i);
valueToProperty(name, element, isClass, typeClass);
}
// if (len == 1) {
// // To make sure the output is an array, we must make
// // sure there is more than one entry
// props.add(name, MARKER);
// }
} else {
valueToProperty(name, value, isClass, typeClass);
}
}
@Override
public void classEnd() throws Exception {
m_logger.debug("PropertyType: classEnd");
String prefix = null;
if (prefixField != null) {
Object c = prefixField.getConstant();
if (prefixField.isFinal() && (prefixField.getType() == m_analyzer.getTypeRef("java/lang/String"))
&& (c instanceof String)) {
prefix = (String) c;
} else {
m_logger.warn(
"Field PREFIX_ in %s is not a static final String field with a compile-time constant value: %s",
typeRef.getFQN(), c);
}
m_logger.debug("PropertyType: classEnd prefix = %s", prefix);
}
if (!hasMethods) {
// This is a marker annotation so treat it like it is a single
// element annotation with a value of Boolean.TRUE
hasValue = true;
handleValue("value", Boolean.TRUE, false, Boolean.class);
}
String singleElementAnnotation = null;
if (hasValue && (hasNoDefault == 0)) {
m_logger.debug("PropertyType: classEnd hasValue && hasNoDefault == 0");
StringBuilder sb = new StringBuilder(typeRef.getShorterName());
boolean lastLowerCase = false;
for (int i = 0; i < sb.length(); i++) {
char c = sb.charAt(i);
if (Character.isUpperCase(c)) {
sb.setCharAt(i, Character.toLowerCase(c));
if (lastLowerCase) {
sb.insert(i++, '.');
}
lastLowerCase = false;
} else {
lastLowerCase = Character.isLowerCase(c);
}
}
singleElementAnnotation = sb.toString();
m_logger.debug("PropertyType: classEnd singleElementAnnotation=%s", singleElementAnnotation);
}
m_logger.debug("PropertyType: classEnd props=" + props);
for (Entry<String,List<String>> entry : props.entrySet()) {
String key = entry.getKey();
List<String> value = entry.getValue();
Class<?> type = propertyTypes.get(key);
if ((singleElementAnnotation != null) && key.equals("value")) {
key = singleElementAnnotation;
} else {
key = identifierToPropertyName(key);
}
if (prefix != null) {
key = prefix + key;
}
m_componentWriter.addProperty(key, value.toArray(), type);
m_logger.info("PropertyType: parsed property key:%s, value:%s, type:%s", key, value, type);
}
}
private void valueToProperty(String name, Object value, boolean isClass, Class<?> typeClass) {
if (isClass)
value = ((TypeRef) value).getFQN();
if (typeClass == null)
typeClass = value.getClass();
// enums already come out as the enum name, no
// processing needed.
//String type = typeClass.getSimpleName();
String type = typeClass.getName();
propertyTypes.put(name, typeClass);
props.add(name, value.toString());
}
private String identifierToPropertyName(String name) {
name = derivePropertyNameUsingJavaBeanConvention(name);
name = derivePropertyNameUsingCamelCaseConvention(name);
name = derivePropertyNameUsingMetatypeConvention(name);
return name;
}
// getFoo -> foo; isFoo -> foo
private String derivePropertyNameUsingJavaBeanConvention(String methodName) {
StringBuilder sb = new StringBuilder(methodName);
if (methodName.startsWith("get")) {
sb.delete(0, 3);
} else if (methodName.startsWith("is")) {
sb.delete(0, 2);
}
char c = sb.charAt(0);
if (Character.isUpperCase(c)) {
sb.setCharAt(0, Character.toLowerCase(c));
}
return (sb.toString());
}
// fooBarZoo -> foo.bar.zoo
private String derivePropertyNameUsingCamelCaseConvention(String methodName) {
StringBuilder sb = new StringBuilder(methodName);
for (int i = 0; i < sb.length(); i++) {
char c = sb.charAt(i);
if (Character.isUpperCase(c)) {
// camel casing: replace fooBar -> foo.bar
sb.setCharAt(i, Character.toLowerCase(c));
sb.insert(i, ".");
}
}
return sb.toString();
}
// foo__bar -> foo_bar; foo_bar -> foo.bar
private String derivePropertyNameUsingMetatypeConvention(String name) {
Matcher m = IDENTIFIERTOPROPERTY.matcher(name);
StringBuffer b = new StringBuffer();
while (m.find()) {
String replace;
if (m.group(1) != null) // __ to _
replace = "_";
else if (m.group(2) != null) // _ to .
replace = ".";
else if (m.group(3) != null) { // $_$ to -
replace = "-";
}
else if (m.group(4) != null) // $$ to $
replace = "\\$";
else // $ removed.
replace = "";
m.appendReplacement(b, replace);
}
m.appendTail(b);
return b.toString();
}
}
public static <K, V> Map.Entry<K, V> entry(K key, V value) {
return new AbstractMap.SimpleEntry<>(key, value);
}
}