blob: 19ad81403385a395eea3b88e1bc89bb19c96126a [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.scrplugin;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.felix.scrplugin.annotations.AnnotationProcessor;
import org.apache.felix.scrplugin.description.ClassDescription;
import org.apache.felix.scrplugin.description.ComponentConfigurationPolicy;
import org.apache.felix.scrplugin.description.ComponentDescription;
import org.apache.felix.scrplugin.description.PropertyDescription;
import org.apache.felix.scrplugin.description.PropertyType;
import org.apache.felix.scrplugin.description.PropertyUnbounded;
import org.apache.felix.scrplugin.description.ReferenceCardinality;
import org.apache.felix.scrplugin.description.ReferenceDescription;
import org.apache.felix.scrplugin.description.ReferencePolicyOption;
import org.apache.felix.scrplugin.description.ReferenceStrategy;
import org.apache.felix.scrplugin.description.ServiceDescription;
import org.apache.felix.scrplugin.helper.AnnotationProcessorManager;
import org.apache.felix.scrplugin.helper.ClassModifier;
import org.apache.felix.scrplugin.helper.ClassScanner;
import org.apache.felix.scrplugin.helper.ComponentContainer;
import org.apache.felix.scrplugin.helper.DescriptionContainer;
import org.apache.felix.scrplugin.helper.IssueLog;
import org.apache.felix.scrplugin.helper.MetatypeAttributeDefinition;
import org.apache.felix.scrplugin.helper.MetatypeContainer;
import org.apache.felix.scrplugin.helper.StringUtils;
import org.apache.felix.scrplugin.helper.Validator;
import org.apache.felix.scrplugin.xml.ComponentDescriptorIO;
import org.apache.felix.scrplugin.xml.MetaTypeIO;
import org.osgi.service.cm.ConfigurationAdmin;
/**
* The <code>SCRDescriptorGenerator</code> class does the hard work of
* generating the SCR descriptors. This class is being instantiated and
* configured by clients and the {@link #execute()} method called to generate
* the descriptor files.
* <p>
* When using this class carefully consider calling <i>all</i> setter methods to properly configure the generator. All setter
* method document, which default value is assumed for the respective property if the setter is not called.
* <p>
* Instances of this class are not thread save and should not be reused.
*/
public class SCRDescriptorGenerator {
private final Log logger;
/** The project. */
private Project project;
/** The options. */
private Options options = new Options();
/** The annotation scanner. */
private ClassScanner scanner;
/** The issue log. */
private IssueLog iLog;
/**
* Create an instance of this generator using the given {@link Log} instance
* of logging.
*/
public SCRDescriptorGenerator(final Log logger) {
this.logger = logger;
}
/**
* Set the project. This is required.
*/
public void setProject(final Project p) {
this.project = p;
}
/**
* Set the options.
*/
public void setOptions(final Options p) {
this.options = p;
}
/**
* Actually generates the Declarative Services and Metatype descriptors
* scanning the java sources provided by the {@link #setProject(Project)}
*
* @return A list of generated file names, relative to the output directory
*
* @throws SCRDescriptorException
* @throws SCRDescriptorFailureException
*/
public Result execute() throws SCRDescriptorException, SCRDescriptorFailureException {
this.logger.debug("Starting SCR Descriptor Generator....");
if (this.project == null) {
throw new SCRDescriptorFailureException("Project has not been set!");
}
if (this.options == null) {
// use default options
this.options = new Options();
}
if (this.options.getOutputDirectory() == null) {
throw new SCRDescriptorFailureException("Output directory has not been set!");
}
this.logger.debug("..using output directory: " + this.options.getOutputDirectory());
this.logger.debug("..strict mode: " + this.options.isStrictMode());
this.logger.debug("..generating accessors: " + this.options.isGenerateAccessors());
// check speck version configuration
SpecVersion specVersion = options.getSpecVersion();
if (specVersion == null) {
this.logger.debug("..auto detecting spec version");
} else {
this.logger.debug("..using spec version " + specVersion.getName());
}
// create a log
this.iLog = new IssueLog(this.options.isStrictMode());
// create the annotation processor manager
final AnnotationProcessor aProcessor = new AnnotationProcessorManager(this.logger,
this.project.getClassLoader());
// create the class scanner - and start scanning
this.scanner = new ClassScanner(logger, iLog, project, aProcessor);
final List<ClassDescription> scannedDescriptions = scanner.scanSources();
// create the result to hold the list of processed source files
final Result result = new Result();
final List<ComponentContainer> processedContainers = new ArrayList<ComponentContainer>();
for (final ClassDescription desc : scannedDescriptions) {
this.logger.debug("Processing component class " + desc.getSource());
result.addProcessedSourceFile(desc.getSource());
// check if there is more than one component definition
if (desc.getDescriptions(ComponentDescription.class).size() > 1) {
iLog.addError("Class has more than one component definition." +
" Check the annotations and merge the definitions to a single definition.",
desc.getSource());
} else {
final ComponentContainer container = this.createComponent(desc, iLog);
if (container.getComponentDescription().getSpecVersion() != null) {
if ( specVersion == null ) {
specVersion = container.getComponentDescription().getSpecVersion();
logger.debug("Setting used spec version to " + specVersion);
} else if (container.getComponentDescription().getSpecVersion().ordinal() > specVersion.ordinal() ) {
if ( this.options.getSpecVersion() != null) {
// if a spec version has been configured and a component requires a higher
// version, this is considered an error!
iLog.addError("Component " + container + " requires spec version " + container.getComponentDescription().getSpecVersion().name()
+ " but plugin is configured to use version " + this.options.getSpecVersion(),
desc.getSource());
} else {
specVersion = container.getComponentDescription().getSpecVersion();
logger.debug("Setting used spec version to " + specVersion);
}
}
} else {
if ( this.options.getSpecVersion() != null ) {
container.getComponentDescription().setSpecVersion(options.getSpecVersion());
} else {
container.getComponentDescription().setSpecVersion(SpecVersion.VERSION_1_0);
}
}
processedContainers.add(container);
}
}
// if spec version is still not set, we're using lowest available
if ( specVersion == null ) {
specVersion = SpecVersion.VERSION_1_0;
logger.debug("Using default spec version " + specVersion);
}
this.logger.debug("Generating descriptor for spec version: " + specVersion);
options.setSpecVersion(specVersion);
// before we can validate we should check the references for bind/unbind method
// in order to create them if possible
if ( this.options.isGenerateAccessors() ) {
for(final ComponentContainer container : processedContainers) {
this.generateMethods(container);
}
}
// now validate
final DescriptionContainer module = new DescriptionContainer(this.options);
for (final ComponentContainer container : processedContainers) {
final int errorCount = iLog.getNumberOfErrors();
final Validator validator = new Validator(container, project, options, iLog);
validator.validate();
// ignore component if it has errors
if (iLog.getNumberOfErrors() == errorCount) {
module.add(container);
}
}
// log issues
iLog.logMessages(logger);
// after checking all classes, throw if there were any failures
if (iLog.hasErrors()) {
throw new SCRDescriptorFailureException("SCR Descriptor parsing had failures (see log)");
}
// and generate files
result.setMetatypeFiles(MetaTypeIO.generateDescriptors(module, this.project, this.options, this.logger));
result.setScrFiles(ComponentDescriptorIO.generateDescriptorFiles(module, this.options, logger));
return result;
}
private void generateMethods(final ComponentContainer container) throws SCRDescriptorException {
for (final ReferenceDescription ref : container.getReferences().values()) {
// skip refs without a interface name (validate will be called next)
if (StringUtils.isEmpty(ref.getInterfaceName())) {
continue;
}
// if this is a field with a single cardinality,
// we look for the bind/unbind methods
// and create them if they are not availabe
if (ref.getStrategy() != ReferenceStrategy.LOOKUP && ref.getField() != null
&& ref.getField().getDeclaringClass().getName().equals(container.getClassDescription().getDescribedClass().getName())
&& (ref.getCardinality() == ReferenceCardinality.OPTIONAL_UNARY || ref.getCardinality() == ReferenceCardinality.MANDATORY_UNARY)) {
final String bindValue = ref.getBind();
final String unbindValue = ref.getUnbind();
final String name = ref.getName();
final String type = ref.getInterfaceName();
boolean createBind = false;
boolean createUnbind = false;
// Only create method if no bind name has been specified
if (bindValue == null && Validator.findMethod(this.project, this.options, container.getClassDescription(), ref, "bind") == null) {
// create bind method
createBind = true;
}
if (unbindValue == null && Validator.findMethod(this.project, this.options, container.getClassDescription(), ref, "unbind") == null) {
// create unbind method
createUnbind = true;
}
if (createBind || createUnbind) {
// logging
if ( createBind && createUnbind ) {
this.logger.debug("Generating bind and unbind method for " + name + " in " + container.getClassDescription().getDescribedClass().getName());
} else if ( createBind ) {
this.logger.debug("Generating bind method for " + name + " in " + container.getClassDescription().getDescribedClass().getName());
} else {
this.logger.debug("Generating unbind method for " + name + " in " + container.getClassDescription().getDescribedClass().getName());
}
ClassModifier.addMethods(container.getClassDescription().getDescribedClass().getName(),
name,
ref.getField().getName(),
type,
createBind,
createUnbind,
this.project.getClassLoader(),
this.project.getClassesDirectory(),
this.logger);
// set a flag for validation
ref.setBindMethodCreated(createBind);
ref.setUnbindMethodCreated(createUnbind);
}
}
}
}
/**
* Create the SCR objects based on the descriptions
*/
private ComponentContainer createComponent(final ClassDescription desc,
final IssueLog iLog)
throws SCRDescriptorException {
final ComponentDescription componentDesc = desc.getDescription(ComponentDescription.class);
final SpecVersion intitialComponentSpecVersion = componentDesc.getSpecVersion();
// configuration pid in 1.2
if ( componentDesc.getConfigurationPid() != null && !componentDesc.getConfigurationPid().equals(componentDesc.getName())) {
componentDesc.setSpecVersion(SpecVersion.VERSION_1_2);
}
final ComponentContainer container = new ComponentContainer(desc, componentDesc);
// Create metatype (if required)
final MetatypeContainer ocd;
if ( !componentDesc.isAbstract() && componentDesc.isCreateMetatype() ) {
// OCD
ocd = new MetatypeContainer();
container.setMetatypeContainer( ocd );
ocd.setId( componentDesc.getName() );
if ( componentDesc.getLabel() != null ) {
ocd.setName( componentDesc.getLabel() );
}
if ( componentDesc.getDescription() != null ) {
ocd.setDescription( componentDesc.getDescription() );
}
// Factory pid
if ( componentDesc.isSetMetatypeFactoryPid() ) {
if ( componentDesc.getFactory() == null ) {
ocd.setFactoryPid( componentDesc.getName() );
} else {
iLog.addWarning( "Component factory " + componentDesc.getName()
+ " should not set metatype factory pid.", desc.getSource() );
}
}
} else {
ocd = null;
}
// metatype checks if metatype is not generated (FELIX-4033)
if ( !componentDesc.isAbstract() && !componentDesc.isCreateMetatype() ) {
if ( componentDesc.getLabel() != null && componentDesc.getLabel().trim().length() > 0 ) {
iLog.addWarning(" Component " + componentDesc.getName() + " has set a label. However metatype is set to false. This label is ignored.",
desc.getSource());
}
if ( componentDesc.getDescription() != null && componentDesc.getDescription().trim().length() > 0 ) {
iLog.addWarning(" Component " + componentDesc.getName() + " has set a description. However metatype is set to false. This description is ignored.",
desc.getSource());
}
}
ClassDescription current = desc;
boolean inherit;
do {
final ComponentDescription cd = current.getDescription(ComponentDescription.class);
inherit = (cd == null ? true : cd.isInherit());
if ( cd != null ) {
if ( current != desc ) {
iLog.addWarning(" Component " + componentDesc.getName() + " is using the " +
"deprecated inheritance feature and inherits from " + current.getDescribedClass().getName() +
". This feature will be removed in future versions.",
desc.getSource());
}
// handle enabled and immediate
if ( componentDesc.getEnabled() == null ) {
componentDesc.setEnabled(cd.getEnabled());
}
if ( componentDesc.getImmediate() == null ) {
componentDesc.setImmediate(cd.getImmediate());
}
// lifecycle methods
if ( componentDesc.getActivate() == null && cd.getActivate() != null ) {
componentDesc.setActivate(cd.getActivate());
}
if ( componentDesc.getDeactivate() == null && cd.getDeactivate() != null ) {
componentDesc.setDeactivate(cd.getDeactivate());
}
if ( componentDesc.getModified() == null && cd.getModified() != null ) {
componentDesc.setModified(cd.getModified());
}
if ( componentDesc.getActivate() != null || componentDesc.getDeactivate() != null || componentDesc.getModified() != null ) {
// spec version must be at least 1.1
componentDesc.setSpecVersion(SpecVersion.VERSION_1_1);
}
if ( componentDesc.getConfigurationPolicy() != ComponentConfigurationPolicy.OPTIONAL ) {
// policy requires 1.1
componentDesc.setSpecVersion(SpecVersion.VERSION_1_1);
}
}
// services, properties, references
this.processServices(current, container);
this.processProperties(current, container, ocd);
this.processReferences(current, container);
// go up in the class hierarchy
if ( !inherit || current.getDescribedClass().getSuperclass() == null ) {
current = null;
} else {
try {
current = this.scanner.getDescription(current.getDescribedClass().getSuperclass());
} catch ( final SCRDescriptorFailureException sde) {
this.logger.debug(sde.getMessage(), sde);
iLog.addError(sde.getMessage(), current.getSource());
} catch ( final SCRDescriptorException sde) {
this.logger.debug(sde.getSourceLocation() + " : " + sde.getMessage(), sde);
iLog.addError(sde.getMessage(), sde.getSourceLocation());
}
}
} while ( current != null);
// check service interfaces for properties
if ( container.getServiceDescription() != null ) {
for(final String interfaceName : container.getServiceDescription().getInterfaces()) {
try {
final Class<?> interfaceClass = project.getClassLoader().loadClass(interfaceName);
final ClassDescription interfaceDesc = this.scanner.getDescription(interfaceClass);
if ( interfaceDesc != null ) {
this.processProperties(interfaceDesc, container, ocd);
}
} catch ( final SCRDescriptorFailureException sde) {
this.logger.debug(sde.getMessage(), sde);
iLog.addError(sde.getMessage(), interfaceName);
} catch ( final SCRDescriptorException sde) {
this.logger.debug(sde.getSourceLocation() + " : " + sde.getMessage(), sde);
iLog.addError(sde.getMessage(), sde.getSourceLocation());
} catch (ClassNotFoundException e) {
this.logger.debug(e.getMessage(), e);
iLog.addError(e.getMessage(), interfaceName);
}
}
}
// global properties
this.processGlobalProperties(desc, container.getProperties());
// check lifecycle methods
if ( componentDesc.getActivate() == null ) {
final Validator.MethodResult result = Validator.findLifecycleMethod(project, container, "activate", true);
if ( result.method != null ) {
componentDesc.setSpecVersion(result.requiredSpecVersion);
}
}
if ( componentDesc.getDeactivate() == null ) {
final Validator.MethodResult result = Validator.findLifecycleMethod(project, container, "deactivate", false);
if ( result.method != null ) {
componentDesc.setSpecVersion(result.requiredSpecVersion);
}
}
// check if component has spec version configured but requires a higher one
if ( intitialComponentSpecVersion != null && componentDesc.getSpecVersion().ordinal() > intitialComponentSpecVersion.ordinal() ) {
iLog.addError("Component " + container + " requires spec version " + container.getComponentDescription().getSpecVersion().name()
+ " but component is configured to use version " + intitialComponentSpecVersion.name(),
desc.getSource());
}
return container;
}
/**
* Process service directives
*/
private void processServices(final ClassDescription current, final ComponentContainer component) {
final ServiceDescription serviceDesc = current.getDescription(ServiceDescription.class);
if ( serviceDesc != null ) {
ServiceDescription service = component.getServiceDescription();
if ( service == null ) {
service = new ServiceDescription(serviceDesc.getAnnotation());
service.setServiceFactory(false);
component.setServiceDescription(service);
}
if ( serviceDesc.isServiceFactory() ) {
service.setServiceFactory(true);
}
for(final String className : serviceDesc.getInterfaces()) {
service.addInterface(className);
}
}
}
private boolean isPrivateProperty(final String name) {
final boolean isPrivate;
if (org.osgi.framework.Constants.SERVICE_RANKING.equals(name)
|| org.osgi.framework.Constants.SERVICE_PID.equals(name)
|| org.osgi.framework.Constants.SERVICE_DESCRIPTION.equals(name)
|| org.osgi.framework.Constants.SERVICE_VENDOR.equals(name)
|| ConfigurationAdmin.SERVICE_BUNDLELOCATION.equals(name)
|| ConfigurationAdmin.SERVICE_FACTORYPID.equals(name) ) {
isPrivate = true;
} else {
isPrivate = false;
}
return isPrivate;
}
/**
* Process property directives
*/
private void processProperties(
final ClassDescription current,
final ComponentContainer component,
final MetatypeContainer ocd) {
for(final PropertyDescription pd : current.getDescriptions(PropertyDescription.class)) {
if ( this.testProperty(current, component.getProperties(), pd, current == component.getClassDescription()) ) {
final String name = pd.getName();
if ( org.osgi.framework.Constants.SERVICE_ID.equals(name) ) {
iLog.addError("Class " + current.getDescribedClass().getName() + " is declaring " +
"the protected property 'service.id'.", current.getSource() );
continue;
}
if ( ocd != null) {
// metatype - is this property private?
final boolean isPrivate;
if ( pd.isPrivate() != null ) {
isPrivate = pd.isPrivate();
} else {
if (isPrivateProperty(name) ) {
isPrivate = true;
} else {
isPrivate = false;
}
}
if ( !isPrivate ) {
final MetatypeAttributeDefinition ad = new MetatypeAttributeDefinition();
ocd.getProperties().add(ad);
ad.setId(pd.getName());
ad.setType(pd.getType().name());
if (pd.getLabel() != null ) {
ad.setName(pd.getLabel());
}
if (pd.getDescription() != null ) {
ad.setDescription(pd.getDescription());
}
if ( pd.getUnbounded() == PropertyUnbounded.DEFAULT ) {
if ( pd.getCardinality() != 0 ) {
ad.setCardinality(pd.getCardinality());
}
} else if ( pd.getUnbounded() == PropertyUnbounded.ARRAY ) {
// unlimited array
ad.setCardinality(new Integer(Integer.MAX_VALUE));
} else {
// unlimited vector
ad.setCardinality(new Integer(Integer.MIN_VALUE));
}
ad.setDefaultValue(pd.getValue());
ad.setDefaultMultiValue(pd.getMultiValue());
// check options
final String[] parameters = pd.getOptions();
if ( parameters != null && parameters.length > 0 ) {
final Map<String, String> options = new LinkedHashMap<String, String>();
for (int j=0; j < parameters.length; j=j+2) {
final String optionLabel = parameters[j];
final String optionValue = (j < parameters.length-1) ? parameters[j+1] : null;
if (optionValue != null) {
options.put(optionLabel, optionValue);
}
}
ad.setOptions(options);
}
}
} else {
// additional metatype checks (FELIX-4033)
if ( pd.isPrivate() != null && pd.isPrivate() ) {
iLog.addWarning("Property " + pd.getName() + " in class "
+ current.getDescribedClass().getName() + " is set as private. " +
"This is redundant as no metatype will be generated.", current.getSource() );
}
}
}
}
}
/**
* Add global properties (if not already defined in the component)
*/
private void processGlobalProperties(final ClassDescription desc,
final Map<String, PropertyDescription> allProperties) {
// apply pre configured global properties
if ( this.options.getProperties() != null ) {
for(final Map.Entry<String, String> entry : this.options.getProperties().entrySet()) {
final String propName = entry.getKey();
final String value = entry.getValue();
// check if the service already provides this property
if ( value != null && !allProperties.containsKey(propName) ) {
final PropertyDescription p = new PropertyDescription(null);
p.setName(propName);
p.setValue(value);
p.setType(PropertyType.String);
allProperties.put(propName, p);
}
}
}
}
/**
* Test a newly found property
*/
private boolean testProperty(final ClassDescription current,
final Map<String, PropertyDescription> allProperties,
final PropertyDescription newProperty,
final boolean isInspectedClass ) {
final String propName = newProperty.getName();
if ( !StringUtils.isEmpty(propName) ) {
if ( allProperties.containsKey(propName) ) {
// if the current class is the class we are currently inspecting, we
// have found a duplicate definition
if ( isInspectedClass ) {
iLog.addError("Duplicate definition for property " + propName + " in class "
+ current.getDescribedClass().getName(), current.getSource() );
}
return false;
}
allProperties.put(propName, newProperty);
} else {
// no name - generate a unique one
allProperties.put(UUID.randomUUID().toString(), newProperty);
}
return true;
}
/**
* Process reference directives
* @throws SCRDescriptorException
*/
private void processReferences(final ClassDescription current,
final ComponentContainer component) {
for(final ReferenceDescription rd : current.getDescriptions(ReferenceDescription.class)) {
if ( rd.getPolicyOption() != ReferencePolicyOption.RELUCTANT ) {
component.getComponentDescription().setSpecVersion(SpecVersion.VERSION_1_2);
}
if ( rd.getUpdated() != null ) {
// updated requires 1.2 or 1.1_FELIX, if nothing is set, we use 1.2
if ( component.getComponentDescription().getSpecVersion() == null
|| component.getComponentDescription().getSpecVersion().ordinal() < SpecVersion.VERSION_1_1_FELIX.ordinal() ) {
component.getComponentDescription().setSpecVersion(SpecVersion.VERSION_1_2);
}
}
this.testReference(current, component.getReferences(), rd, component.getClassDescription() == current);
// check for method signature - if interface name is set (empty interface name will fail during validate)
if (!StringUtils.isEmpty(rd.getInterfaceName())) {
try {
final Validator.MethodResult bindMethod = Validator.findMethod(this.project, this.options, current, rd,
rd.getBind() == null ? "bind" : rd.getBind());
if ( bindMethod != null ) {
component.getComponentDescription().setSpecVersion(bindMethod.requiredSpecVersion);
}
final Validator.MethodResult unbindMethod = Validator.findMethod(this.project, this.options, current, rd,
rd.getUnbind() == null ? "unbind" : rd.getUnbind());
if ( unbindMethod != null ) {
component.getComponentDescription().setSpecVersion(unbindMethod.requiredSpecVersion);
}
} catch (final SCRDescriptorException sde) {
// this happens only if a class not found exception occurs, so we can ignore this at this point!
}
}
}
}
/**
* Test a newly found reference
*/
private void testReference(final ClassDescription current,
final Map<String, ReferenceDescription> allReferences,
final ReferenceDescription newReference,
final boolean isInspectedClass ) {
String refName = newReference.getName();
if ( refName == null) {
refName = newReference.getInterfaceName();
}
if ( refName != null ) {
if ( allReferences.containsKey( refName ) ) {
// if the current class is the class we are currently inspecting, we
// have found a duplicate definition
if ( isInspectedClass ) {
iLog.addError("Duplicate definition for reference " + refName + " in class "
+ current.getDescribedClass().getName(), current.getSource() );
}
} else {
allReferences.put(refName, newReference);
}
} else {
// no name - generate a unique one
allReferences.put(UUID.randomUUID().toString(), newReference);
}
}
}