| /* |
| * 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); |
| } |
| |
| } |