blob: 5f8dec22d83965a65e1852b060be46855820f9b0 [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.helper;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import org.apache.felix.scrplugin.Options;
import org.apache.felix.scrplugin.Project;
import org.apache.felix.scrplugin.SCRDescriptorException;
import org.apache.felix.scrplugin.SpecVersion;
import org.apache.felix.scrplugin.description.AbstractDescription;
import org.apache.felix.scrplugin.description.ClassDescription;
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.ReferenceCardinality;
import org.apache.felix.scrplugin.description.ReferenceDescription;
import org.apache.felix.scrplugin.description.ReferencePolicy;
import org.apache.felix.scrplugin.description.ReferencePolicyOption;
import org.apache.felix.scrplugin.description.ReferenceStrategy;
import org.apache.felix.scrplugin.description.ServiceDescription;
public class Validator {
private final ComponentContainer container;
private final Options options;
private final Project project;
private final IssueLog iLog;
public Validator(final ComponentContainer container,
final Project project,
final Options options,
final IssueLog iLog) {
this.container = container;
this.project = project;
this.options = options;
this.iLog = iLog;
}
private void logWarn(final AbstractDescription desc, final String message) {
// check if location of description is the same as the class
final String classLocation = this.container.getComponentDescription().getSource();
if ( classLocation.equals(desc.getSource()) ) {
iLog.addWarning(desc.getIdentifier() + " : " + message, desc.getSource());
} else {
iLog.addWarning(desc.getIdentifier() + " (" + desc.getSource() + ") : " + message, classLocation);
}
}
private void logError(final AbstractDescription desc, final String message) {
// check if location of description is the same as the class
final String classLocation = this.container.getComponentDescription().getSource();
if ( classLocation.equals(desc.getSource()) ) {
iLog.addError(desc.getIdentifier() + " : " + message, desc.getSource());
} else {
iLog.addError(desc.getIdentifier() + " (" + desc.getSource() + ") : " + message, classLocation);
}
}
/**
* Validate the component description. If errors occur a message is added to
* the issues list, warnings can be added to the warnings list.
*/
public void validate()
throws SCRDescriptorException {
final ComponentDescription component = this.container.getComponentDescription();
// nothing to check if this is ignored
if (!component.isCreateDs()) {
return;
}
final int currentIssueCount = iLog.getNumberOfErrors();
// if the component is abstract, we do not validate everything
if (!component.isAbstract()) {
// if configuration pid is set and different from name, we need 1.2
if ( component.getConfigurationPid() != null && !component.getConfigurationPid().equals(component.getName())
&& options.getSpecVersion().ordinal() < SpecVersion.VERSION_1_2.ordinal() ) {
this.logError(component, "Different configuration pid requires "
+ SpecVersion.VERSION_1_2.getName() + " or higher.");
}
// ensure non-abstract, public class
if (!Modifier.isPublic(this.container.getClassDescription().getDescribedClass().getModifiers())) {
this.logError(component, "Class must be public: "
+ this.container.getClassDescription().getDescribedClass().getName());
}
if (Modifier.isAbstract(this.container.getClassDescription().getDescribedClass().getModifiers())
|| this.container.getClassDescription().getDescribedClass().isInterface()) {
this.logError(component, "Class must be concrete class (not abstract or interface) : "
+ this.container.getClassDescription().getDescribedClass().getName());
}
// no errors so far, let's continue
if (iLog.getNumberOfErrors() == currentIssueCount) {
final String activateName = component.getActivate() == null ? "activate" : component.getActivate();
final String deactivateName = component.getDeactivate() == null ? "deactivate" : component.getDeactivate();
// check activate and deactivate methods
this.checkLifecycleMethod(activateName, true, component.getActivate() != null);
this.checkLifecycleMethod(deactivateName, false, component.getDeactivate() != null);
if (component.getModified() != null) {
if ( this.options.getSpecVersion().ordinal() >= SpecVersion.VERSION_1_1.ordinal() ) {
this.checkLifecycleMethod(component.getModified(), true, true);
} else {
this.logError(component, "If modified version is specified, spec version must be " +
SpecVersion.VERSION_1_1.name() + " or higher : " + component.getModified());
}
}
// ensure public default constructor
boolean constructorFound = true;
Constructor<?>[] constructors = this.container.getClassDescription().getDescribedClass().getDeclaredConstructors();
for (int i = 0; constructors != null && i < constructors.length; i++) {
// if public default, succeed
if (Modifier.isPublic(constructors[i].getModifiers())
&& (constructors[i].getParameterTypes() == null || constructors[i].getParameterTypes().length == 0)) {
constructorFound = true;
break;
}
// non-public/non-default constructor found, must have
// explicit
constructorFound = false;
}
if (!constructorFound) {
this.logError(component, "Class must have public default constructor: " + this.container.getClassDescription().getDescribedClass().getName());
}
// verify properties
for (final PropertyDescription prop : this.container.getProperties().values()) {
this.validateProperty(prop);
}
// verify service
boolean isServiceFactory = false;
if (this.container.getServiceDescription() != null) {
if (this.container.getServiceDescription().getInterfaces().size() == 0) {
this.logError(component, "Service interface information is missing!");
}
this.validateService(this.container.getServiceDescription());
isServiceFactory = this.container.getServiceDescription().isServiceFactory();
}
// serviceFactory must not be true for immediate of component factory
if (isServiceFactory && component.getImmediate() != null && component.getImmediate().booleanValue()
&& component.getFactory() != null) {
this.logError(component,
"Component must not be a ServiceFactory, if immediate and/or component factory: "
+ this.container.getClassDescription().getDescribedClass().getName());
}
// immediate must not be true for component factory
if (component.getImmediate() != null && component.getImmediate().booleanValue() && component.getFactory() != null) {
this.logError(component,
"Component must not be immediate if component factory: " + this.container.getClassDescription().getDescribedClass().getName());
}
}
// additional check for metatype (FELIX-4035)
if ( this.container.getMetatypeContainer() != null ) {
if ( this.container.getMetatypeContainer().getProperties().size() == 0 ) {
this.logError(component, "Component is defined to generate metatype information, however no properties or only private properties have been " +
"defined; in case no properties or only private properties are wanted, consider to use 'metatype=false'");
}
}
if (iLog.getNumberOfErrors() == currentIssueCount) {
// verify references
for (final ReferenceDescription ref : this.container.getReferences().values()) {
this.validateReference(ref, component.isAbstract());
}
}
}
}
private static final String TYPE_COMPONENT_CONTEXT = "org.osgi.service.component.ComponentContext";
private static final String TYPE_BUNDLE_CONTEXT = "org.osgi.framework.BundleContext";
private static final String TYPE_MAP = "java.util.Map";
private static final String TYPE_INT = "int";
private static final String TYPE_INTEGER = "java.lang.Integer";
private static Method getMethod(final Project project,
final ComponentContainer container,
final String name, final String[] sig)
throws SCRDescriptorException {
Class<?>[] classSig = (sig == null ? null : new Class<?>[sig.length]);
if ( sig != null ) {
for(int i = 0; i<sig.length; i++) {
try {
if ( sig[i].equals("int") ) {
classSig[i] = int.class;
} else {
classSig[i] = project.getClassLoader().loadClass(sig[i]);
}
} catch (final ClassNotFoundException e) {
throw new SCRDescriptorException("Unable to load class.", e);
}
}
}
return getMethod(container.getClassDescription(), name, classSig);
}
/**
* Find a lifecycle methods.
*
* @param methodName
* The method name.
* @param isActivate Whether this is the activate or deactivate method.
*/
public static MethodResult findLifecycleMethod(
final Project project,
final ComponentContainer container,
final String methodName,
final boolean isActivate)
throws SCRDescriptorException {
final MethodResult result = new MethodResult();
result.requiredSpecVersion = SpecVersion.VERSION_1_0;
// first candidate is (de)activate(ComponentContext)
result.method = getMethod(project, container, methodName, new String[] { TYPE_COMPONENT_CONTEXT });
if (result.method == null) {
// Spec 1.1 or higher required
result.requiredSpecVersion = SpecVersion.VERSION_1_1;
// second candidate is (de)activate(BundleContext)
result.method = getMethod(project, container, methodName, new String[] { TYPE_BUNDLE_CONTEXT });
if (result.method == null) {
// third candidate is (de)activate(Map)
result.method = getMethod(project, container, methodName, new String[] { TYPE_MAP });
if (result.method == null) {
// if this is a deactivate method, we have two
// additional possibilities
// a method with parameter of type int and one of type
// Integer
if (!isActivate) {
result.method = getMethod(project, container, methodName, new String[] { TYPE_INT });
if (result.method == null) {
result.method = getMethod(project, container, methodName, new String[] { TYPE_INTEGER });
}
}
}
if (result.method == null) {
// fourth candidate is (de)activate with two or three
// arguments (type must be BundleContext, ComponentCtx
// and Map)
// as we have to iterate now and the fifth candidate is
// zero arguments
// we already store this option
Method zeroArgMethod = null;
Method found = result.method;
final Method[] methods = container.getClassDescription().getDescribedClass().getDeclaredMethods();
int i = 0;
while (i < methods.length) {
if (methodName.equals(methods[i].getName())) {
if (methods[i].getParameterTypes() == null || methods[i].getParameterTypes().length == 0) {
zeroArgMethod = methods[i];
} else if (methods[i].getParameterTypes().length >= 2) {
boolean valid = true;
for (int m = 0; m < methods[i].getParameterTypes().length; m++) {
final String type = methods[i].getParameterTypes()[m].getName();
if (!type.equals(TYPE_BUNDLE_CONTEXT) && !type.equals(TYPE_COMPONENT_CONTEXT)
&& !type.equals(TYPE_MAP)) {
// if this is deactivate, int and
// integer are possible as well
if (isActivate || (!type.equals(TYPE_INT) && !type.equals(TYPE_INTEGER))) {
valid = false;
}
}
}
if (valid) {
if (found == null) {
found = methods[i];
} else {
// print warning
result.additionalWarning = "Lifecycle method " + methods[i].getName()
+ " occurs several times with different matching signature.";
}
}
}
}
i++;
}
if (found != null) {
result.method = found;
} else {
result.method = zeroArgMethod;
}
}
}
}
return result;
}
/**
* Check for existence of lifecycle methods.
*
* @param methodName
* The method name.
* @param isActivate Whether this is the activate or deactivate method.
* @param isSpecified Whether this method has explicitely been specified or is just
* the default
*/
private void checkLifecycleMethod(final String methodName,
final boolean isActivate,
final boolean isSpecified)
throws SCRDescriptorException {
final MethodResult result = findLifecycleMethod(this.project, this.container, methodName, isActivate);
if ( result.additionalWarning != null ) {
this.logWarn(this.container.getComponentDescription(), result.additionalWarning);
}
// if no method is found, we check for any method with that name to print some warnings or errors!
if (result.method == null) {
final Method[] methods = this.container.getClassDescription().getDescribedClass().getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
if (methodName.equals(methods[i].getName())) {
if (methods[i].getParameterTypes() == null || methods[i].getParameterTypes().length != 1) {
final String msg = "Lifecycle method " + methods[i].getName() + " has wrong number of arguments";
if ( isSpecified ) {
this.logError(container.getComponentDescription(), msg);
} else {
this.logWarn(container.getComponentDescription(), msg);
}
} else {
final String msg = "Lifecycle method " + methods[i].getName() + " has wrong argument "
+ methods[i].getParameterTypes()[0].getName();
if ( isSpecified ) {
this.logError(container.getComponentDescription(), msg);
} else {
this.logWarn(container.getComponentDescription(), msg);
}
}
}
}
}
// method must be protected for version 1.0
if (result.method != null && options.getSpecVersion() == SpecVersion.VERSION_1_0) {
// check protected
if (Modifier.isPublic(result.method.getModifiers())) {
this.logWarn(container.getComponentDescription(), "Lifecycle method " + result.method.getName() + " should be declared protected");
} else if (!Modifier.isProtected(result.method.getModifiers())) {
this.logWarn(container.getComponentDescription(), "Lifecycle method " + result.method.getName() +
" has wrong qualifier, public or protected required");
}
}
}
/**
* Validate the service and its interfaces
* If errors occur a message is added to the issues list,
* warnings can be added to the warnings list.
*/
private void validateService(final ServiceDescription service) throws SCRDescriptorException {
for (final String interfaceName : service.getInterfaces()) {
if (this.container.getClassDescription().getDescribedClass().isInterface()) {
this.logError(service, "Must be declared in a Java class - not an interface");
} else {
try {
final Class<?> interfaceClass = project.getClassLoader().loadClass(interfaceName);
if (!interfaceClass.isAssignableFrom(this.container.getClassDescription().getDescribedClass())) {
// interface not implemented
this.logError(service, "Class must implement provided interface " + interfaceName);
}
} catch (final ClassNotFoundException cnfe) {
throw new SCRDescriptorException("Unable to load interface class.", cnfe);
}
}
}
}
/**
* Validate the property.
* If errors occur a message is added to the issues list,
* warnings can be added to the warnings list.
*/
private void validateProperty(final PropertyDescription property) {
if (property.getName() == null || property.getName().trim().length() == 0) {
this.logError(property, "Property name can not be empty.");
}
if (property.getType() != null) {
// now check for old and new char
if (this.options.getSpecVersion() == SpecVersion.VERSION_1_0 && property.getType() == PropertyType.Character) {
property.setType(PropertyType.Char);
}
if (this.options.getSpecVersion().ordinal() >= SpecVersion.VERSION_1_1.ordinal()
&& property.getType() == PropertyType.Char) {
property.setType(PropertyType.Character);
}
// check character property value
if ( property.getType() == PropertyType.Char || property.getType() == PropertyType.Character ) {
if ( property.getValue() != null ) {
if ( property.getValue().length() != 1 ) {
this.logError(property, "Value is not a character: " + property.getValue());
}
}
if ( property.getMultiValue() != null ) {
for(final String value : property.getMultiValue() ) {
if ( value.length() != 1 ) {
this.logError(property, "Value is not a character: " + value);
}
}
}
}
}
// TODO might want to check value
}
/**
* Validate the reference.
* If errors occur a message is added to the issues list,
* warnings can be added to the warnings list.
*/
private void validateReference(final ReferenceDescription ref, final boolean componentIsAbstract)
throws SCRDescriptorException {
final int currentIssueCount = iLog.getNumberOfErrors();
// validate name
if (StringUtils.isEmpty(ref.getName())) {
if (this.options.getSpecVersion().ordinal() < SpecVersion.VERSION_1_1.ordinal() ) {
this.logError(ref, "Reference has no name");
}
}
// validate interface
if (StringUtils.isEmpty(ref.getInterfaceName())) {
this.logError(ref, "Missing interface name");
} else {
try {
this.project.getClassLoader().loadClass(ref.getInterfaceName());
} catch (final ClassNotFoundException e) {
this.logError(ref, "Interface class can't be loaded: " + ref.getInterfaceName());
}
}
// validate cardinality
if (ref.getCardinality() == null) {
ref.setCardinality(ReferenceCardinality.MANDATORY_UNARY);
}
// validate policy
if (ref.getPolicy() == null) {
ref.setPolicy(ReferencePolicy.STATIC);
}
// validate policy option
if ( ref.getPolicyOption() == null ) {
ref.setPolicyOption(ReferencePolicyOption.RELUCTANT);
}
if ( ref.getPolicyOption() != ReferencePolicyOption.RELUCTANT ) {
if ( this.options.getSpecVersion().ordinal() < SpecVersion.VERSION_1_2.ordinal() ) {
this.logError(ref, "ReferencePolicyOption " + ref.getPolicyOption().name() +
" requires spec version " + SpecVersion.VERSION_1_2.getName() + " or higher.");
}
}
// validate strategy
if (ref.getStrategy() == null) {
ref.setStrategy(ReferenceStrategy.EVENT);
}
// validate methods only if interface name is set
if (!StringUtils.isEmpty(ref.getInterfaceName())) {
// validate bind and unbind methods
if (ref.getStrategy() != ReferenceStrategy.LOOKUP) {
String bindName = ref.getBind();
String unbindName = ref.getUnbind();
final boolean canGenerate = this.options.isGenerateAccessors() &&
ref.getField() != null
&& (ref.getCardinality() == ReferenceCardinality.OPTIONAL_UNARY || ref.getCardinality() == ReferenceCardinality.MANDATORY_UNARY);
if (bindName == null && !canGenerate ) {
bindName = "bind";
}
if (unbindName == null && !canGenerate ) {
unbindName = "unbind";
}
if ( bindName != null ) {
bindName = this.validateMethod(ref, bindName, componentIsAbstract);
if ( bindName == null && ref.getField() != null ) {
this.logError(ref, "Something went wrong: " + canGenerate + " - " + this.options.isGenerateAccessors() + " - " + ref.getCardinality());
}
} else {
bindName = "bind" + Character.toUpperCase(ref.getName().charAt(0)) + ref.getName().substring(1);
}
if ( unbindName != null ) {
if ( "-".equals(unbindName) )
{
unbindName = null;
} else {
unbindName = this.validateMethod(ref, unbindName, componentIsAbstract);
}
} else {
unbindName = "unbind" + Character.toUpperCase(ref.getName().charAt(0)) + ref.getName().substring(1);
}
// check for volatile on dynamic field reference with cardinality unary
if ( !this.options.isSkipVolatileCheck() ) {
if ( ref.getField() != null
&& (ref.getCardinality() == ReferenceCardinality.OPTIONAL_UNARY || ref.getCardinality() == ReferenceCardinality.MANDATORY_UNARY)
&& ref.getPolicy() == ReferencePolicy.DYNAMIC ) {
final boolean fieldIsVolatile = Modifier.isVolatile(ref.getField().getModifiers());
if ( ref.isBindMethodCreated() || ref.isUnbindMethodCreated() ) {
// field must be volatile
if (!fieldIsVolatile) {
this.logError(ref, "Dynamic field must be declared volatile for unary references");
}
}
}
}
if (iLog.getNumberOfErrors() == currentIssueCount) {
ref.setBind(bindName);
ref.setUnbind(unbindName);
}
} else {
ref.setBind(null);
ref.setUnbind(null);
}
// validate updated method
if (ref.getUpdated() != null) {
if (this.options.getSpecVersion().ordinal() < SpecVersion.VERSION_1_1_FELIX.ordinal()) {
this.logError(ref, "Updated method declaration requires version "
+ SpecVersion.VERSION_1_1_FELIX.getName() + ", " + SpecVersion.VERSION_1_2.getName() + " or newer");
}
this.validateMethod(ref, ref.getUpdated(), componentIsAbstract);
}
}
}
private String validateMethod(final ReferenceDescription ref, final String methodName, final boolean componentIsAbstract)
throws SCRDescriptorException {
final MethodResult result = findMethod(this.project, this.options, this.container.getClassDescription(), ref, methodName);
if (result == null) {
if (!componentIsAbstract) {
this.logError(ref,
"Missing method " + methodName + " for reference "
+ (ref.getName() == null ? "" : ref.getName()));
}
return null;
}
// method needs to be protected for 1.0
if (this.options.getSpecVersion() == SpecVersion.VERSION_1_0) {
if (Modifier.isPublic(result.method.getModifiers())) {
this.logWarn(ref, "Method " + result.method.getName() + " should be declared protected");
} else if (!Modifier.isProtected(result.method.getModifiers())) {
this.logError(ref, "Method " + result.method.getName() + " has wrong qualifier, public or protected required");
return null;
}
}
if (this.options.getSpecVersion().ordinal() < result.requiredSpecVersion.ordinal() ) {
this.logError(ref, "Method declaration for '" + result.method.getName() + "' requires version "
+ result.requiredSpecVersion + " or newer");
}
return result.method.getName();
}
private static final String TYPE_SERVICE_REFERENCE = "org.osgi.framework.ServiceReference";
private static Method getMethod(final ClassDescription cd, final String name, final Class<?>[] sig) {
Class<?> checkClass = cd.getDescribedClass();
while ( checkClass != null ) {
try {
return checkClass.getDeclaredMethod(name, sig);
} catch (final SecurityException e) {
// ignore
} catch (final NoSuchMethodException e) {
// ignore
}
checkClass = checkClass.getSuperclass();
}
return null;
}
public static final class MethodResult {
public Method method;
public SpecVersion requiredSpecVersion;
public String additionalWarning;
}
/**
* Find the method and the required spec version
* @throws SCRDescriptorException If the class can't be found
*/
public static MethodResult findMethod(final Project project,
final Options options,
final ClassDescription cd,
final ReferenceDescription ref,
final String methodName)
throws SCRDescriptorException {
if ( "-".equals(methodName) ) {
return null;
}
SpecVersion requiredVersion = SpecVersion.VERSION_1_0;
try {
final Class<?>[] sig = new Class<?>[] { project.getClassLoader().loadClass(TYPE_SERVICE_REFERENCE) };
final Class<?>[] sig2 = new Class<?>[] { project.getClassLoader().loadClass(ref.getInterfaceName()) };
final Class<?>[] sig3 = new Class<?>[] { project.getClassLoader().loadClass(ref.getInterfaceName()), Map.class };
// service interface or ServiceReference first
String realMethodName = methodName;
Method method = getMethod(cd, realMethodName, sig);
if (method == null) {
method = getMethod(cd, realMethodName, sig2);
if (method == null && (options.getSpecVersion() == null || options.getSpecVersion().ordinal() >= SpecVersion.VERSION_1_1.ordinal()) ) {
method = getMethod(cd, realMethodName, sig3);
requiredVersion = SpecVersion.VERSION_1_1;
}
}
// append reference name with service interface and ServiceReference
if (method == null) {
final String info;
if (StringUtils.isEmpty(ref.getName())) {
final String interfaceName = ref.getInterfaceName();
final int pos = interfaceName.lastIndexOf('.');
info = interfaceName.substring(pos + 1);
} else {
info = ref.getName();
}
realMethodName = methodName + Character.toUpperCase(info.charAt(0)) + info.substring(1);
method = getMethod(cd, realMethodName, sig);
}
if (method == null) {
method = getMethod(cd, realMethodName, sig2);
if (method == null && (options.getSpecVersion() == null || options.getSpecVersion().ordinal() >= SpecVersion.VERSION_1_1.ordinal()) ) {
method = getMethod(cd, realMethodName, sig3);
requiredVersion = SpecVersion.VERSION_1_1;
}
}
// append type name with service interface and ServiceReference
if (method == null) {
int lastDot = ref.getInterfaceName().lastIndexOf('.');
realMethodName = methodName + ref.getInterfaceName().substring(lastDot + 1);
method = getMethod(cd, realMethodName, sig);
}
if (method == null) {
method = getMethod(cd, realMethodName, sig2);
if (method == null && (options.getSpecVersion() == null || options.getSpecVersion().ordinal() >= SpecVersion.VERSION_1_1.ordinal()) ) {
method = getMethod(cd, realMethodName, sig3);
requiredVersion = SpecVersion.VERSION_1_1;
}
}
if ( method == null ) {
return null;
}
final MethodResult result = new MethodResult();
result.method = method;
result.requiredSpecVersion = requiredVersion;
return result;
} catch (final ClassNotFoundException cnfe) {
throw new SCRDescriptorException("Unable to load class!", cnfe);
}
}
}