blob: 6ee2c2c67c457e6194cc23428c26a69c321fef97 [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.description;
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.helper.IssueLog;
import org.apache.felix.scrplugin.helper.StringUtils;
import org.apache.felix.scrplugin.om.Component;
import org.apache.felix.scrplugin.om.Interface;
import org.apache.felix.scrplugin.om.Property;
import org.apache.felix.scrplugin.om.Reference;
import org.apache.felix.scrplugin.om.Service;
public class Validator {
private final ClassDescription classDescription;
private final Component component;
private final SpecVersion specVersion;
private final Options options;
private final Project project;
public Validator(final ClassDescription cd,
final Component component,
final SpecVersion specVersion,
final Project project,
final Options options) {
this.classDescription = cd;
this.project = project;
this.specVersion = specVersion;
this.component = component;
this.options = options;
}
/**
* 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(final IssueLog iLog)
throws SCRDescriptorException {
// nothing to check if this is ignored
if (!component.isDs()) {
return;
}
final int currentIssueCount = iLog.getNumberOfErrors();
// if the component is abstract, we do not validate everything
if (!this.component.isAbstract()) {
// if configuration pid is set and different from name, we need 1.2
if ( this.component.getConfigurationPid() != null && !this.component.getConfigurationPid().equals(this.component.getName())
&& this.specVersion.ordinal() < SpecVersion.VERSION_1_2.ordinal() ) {
this.component.logError(iLog, "Different configuration pid requires "
+ SpecVersion.VERSION_1_2.getName() + " or higher.");
}
// ensure non-abstract, public class
if (!Modifier.isPublic(this.classDescription.getDescribedClass().getModifiers())) {
this.component.logError(iLog, "Class must be public: "
+ this.classDescription.getDescribedClass().getName());
}
if (Modifier.isAbstract(this.classDescription.getDescribedClass().getModifiers())
|| this.classDescription.getDescribedClass().isInterface()) {
this.component.logError(iLog, "Class must be concrete class (not abstract or interface) : "
+ this.classDescription.getDescribedClass().getName());
}
// no errors so far, let's continue
if (iLog.getNumberOfErrors() == currentIssueCount) {
final String activateName = this.component.getActivate() == null ? "activate" : this.component.getActivate();
final String deactivateName = this.component.getDeactivate() == null ? "deactivate" : this.component.getDeactivate();
// check activate and deactivate methods
this.checkLifecycleMethod(iLog, activateName, true);
this.checkLifecycleMethod(iLog, deactivateName, false);
if (this.component.getModified() != null) {
if ( this.specVersion.ordinal() >= SpecVersion.VERSION_1_1.ordinal() ) {
this.checkLifecycleMethod(iLog, this.component.getModified(), true);
} else {
this.component.logError(iLog, "If modified version is specified, spec version must be " +
SpecVersion.VERSION_1_1.name() + " or higher : " + this.component.getModified());
}
}
// ensure public default constructor
boolean constructorFound = true;
Constructor<?>[] constructors = this.classDescription.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.component.logError(iLog, "Class must have public default constructor: " + this.classDescription.getDescribedClass().getName());
}
// verify properties
for (final Property prop : this.component.getProperties()) {
this.validateProperty(iLog, prop);
}
// verify service
boolean isServiceFactory = false;
if (this.component.getService() != null) {
if (this.component.getService().getInterfaces().size() == 0) {
this.component.logError(iLog, "Service interface information is missing!");
}
this.validateService(iLog, component.getService());
isServiceFactory = this.component.getService().isServiceFactory();
}
// serviceFactory must not be true for immediate of component factory
if (isServiceFactory && this.component.isImmediate() != null && this.component.isImmediate().booleanValue()
&& this.component.getFactory() != null) {
this.component.logError(iLog,
"Component must not be a ServiceFactory, if immediate and/or component factory: "
+ this.classDescription.getDescribedClass().getName());
}
// immediate must not be true for component factory
if (this.component.isImmediate() != null && this.component.isImmediate().booleanValue() && this.component.getFactory() != null) {
this.component.logError(iLog,
"Component must not be immediate if component factory: " + this.classDescription.getDescribedClass().getName());
}
}
}
if (iLog.getNumberOfErrors() == currentIssueCount) {
// verify references
for (final Reference ref : this.component.getReferences()) {
this.validateReference(iLog, ref, this.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 Method getMethod(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] = this.project.getClassLoader().loadClass(sig[i]);
}
} catch (final ClassNotFoundException e) {
throw new SCRDescriptorException("Unable to load class.", e);
}
}
}
try {
return this.classDescription.getDescribedClass().getDeclaredMethod(name, classSig);
} catch (final SecurityException e) {
// ignore
} catch (final NoSuchMethodException e) {
// ignore
}
return null;
}
/**
* Check for existence of lifecycle methods.
*
* @param specVersion
* The spec version
* @param javaClass
* The java class to inspect.
* @param methodName
* The method name.
* @param warnings
* The list of warnings used to add new warnings.
*/
private void checkLifecycleMethod(final IssueLog iLog,
final String methodName,
final boolean isActivate)
throws SCRDescriptorException {
// first candidate is (de)activate(ComponentContext)
Method method = this.getMethod(methodName, new String[] { TYPE_COMPONENT_CONTEXT });
if (method == null) {
if (this.specVersion.ordinal() >= SpecVersion.VERSION_1_1.ordinal() ) {
// second candidate is (de)activate(BundleContext)
method = this.getMethod(methodName, new String[] { TYPE_BUNDLE_CONTEXT });
if (method == null) {
// third candidate is (de)activate(Map)
method = this.getMethod(methodName, new String[] { TYPE_MAP });
if (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) {
method = this.getMethod(methodName, new String[] { TYPE_INT });
if (method == null) {
method = this.getMethod(methodName, new String[] { TYPE_INTEGER });
}
}
}
if (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 = method;
final Method[] methods = this.classDescription.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
this.component.logWarn(iLog, "Lifecycle method " + methods[i].getName()
+ " occurs several times with different matching signature.");
}
}
}
}
i++;
}
if (found != null) {
method = found;
} else {
method = zeroArgMethod;
}
}
}
}
}
// if no method is found, we check for any method with that name to print some warnings!
if (method == null) {
final Method[] methods = this.classDescription.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) {
this.component.logWarn(iLog, "Lifecycle method " + methods[i].getName() + " has wrong number of arguments");
} else {
this.component.logWarn(iLog,
"Lifecycle method " + methods[i].getName() + " has wrong argument "
+ methods[i].getParameterTypes()[0].getName());
}
}
}
}
// method must be protected for version 1.0
if (method != null && specVersion == SpecVersion.VERSION_1_0) {
// check protected
if (Modifier.isPublic(method.getModifiers())) {
this.component.logWarn(iLog, "Lifecycle method " + method.getName() + " should be declared protected");
} else if (!Modifier.isProtected(method.getModifiers())) {
this.component.logWarn(iLog, "Lifecycle method " + method.getName() +
" has wrong qualifier, public or protected required");
}
}
}
/**
* Validate the service.
* If errors occur a message is added to the issues list,
* warnings can be added to the warnings list.
*/
private void validateService(final IssueLog iLog, final Service service) throws SCRDescriptorException {
for (final Interface interf : service.getInterfaces()) {
this.validateInterface(iLog, interf);
}
}
/**
* Validate the interface.
* If errors occur a message is added to the issues list,
* warnings can be added to the warnings list.
*/
private void validateInterface(final IssueLog iLog, final Interface iFace)
throws SCRDescriptorException {
if (this.classDescription.getDescribedClass().isInterface()) {
iFace.logError(iLog, "Must be declared in a Java class - not an interface");
} else {
try {
final Class<?> interfaceClass = project.getClassLoader().loadClass(iFace.getInterfaceName());
if (!interfaceClass.isAssignableFrom(this.classDescription.getDescribedClass())) {
// interface not implemented
iFace.logError(iLog, "Class must implement provided interface " + iFace.getInterfaceName());
}
} 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 IssueLog iLog, final Property property) {
if (property.getName() == null || property.getName().trim().length() == 0) {
property.logError(iLog, "Property name can not be empty.");
}
if (property.getType() != null) {
// now check for old and new char
if (this.specVersion == SpecVersion.VERSION_1_0 && property.getType() == PropertyType.Character) {
property.setType(PropertyType.Char);
}
if (this.specVersion.ordinal() >= SpecVersion.VERSION_1_1.ordinal()
&& property.getType() == PropertyType.Char) {
property.setType(PropertyType.Character);
}
}
// 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 IssueLog iLog, final Reference ref, final boolean componentIsAbstract)
throws SCRDescriptorException {
final int currentIssueCount = iLog.getNumberOfErrors();
// validate name
if (StringUtils.isEmpty(ref.getName())) {
if (this.specVersion.ordinal() < SpecVersion.VERSION_1_1.ordinal() ) {
ref.logError(iLog, "Reference has no name");
}
}
// validate interface
if (StringUtils.isEmpty(ref.getInterfacename())) {
ref.logError(iLog, "Missing interface name");
}
try {
this.project.getClassLoader().loadClass(ref.getInterfacename());
} catch (final ClassNotFoundException e) {
ref.logError(iLog, "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.specVersion.ordinal() < SpecVersion.VERSION_1_2.ordinal() ) {
ref.logError(iLog, "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 bind and unbind methods
if (!ref.isLookupStrategy()) {
String bindName = ref.getBind();
String unbindName = ref.getUnbind();
final boolean canGenerate = this.options.isGenerateAccessors() &&
!ref.isLookupStrategy() && 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(iLog, ref, bindName, componentIsAbstract);
} else {
bindName = "bind" + Character.toUpperCase(ref.getName().charAt(0)) + ref.getName().substring(1);
}
if ( unbindName != null ) {
unbindName = this.validateMethod(iLog, ref, unbindName, componentIsAbstract);
} else {
unbindName = "unbind" + Character.toUpperCase(ref.getName().charAt(0)) + ref.getName().substring(1);
}
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.specVersion.ordinal() < SpecVersion.VERSION_1_1_FELIX.ordinal()) {
ref.logError(iLog, "Updated method declaration requires version "
+ SpecVersion.VERSION_1_1_FELIX.getName() + ", " + SpecVersion.VERSION_1_2.getName() + " or newer");
}
}
}
private String validateMethod(final IssueLog iLog, final Reference ref, final String methodName, final boolean componentIsAbstract)
throws SCRDescriptorException {
final Method method = this.findMethod(iLog, ref, methodName);
if (method == null) {
if (!componentIsAbstract) {
ref.logError(iLog,
"Missing method " + methodName + " for reference "
+ (ref.getName() == null ? "" : ref.getName()));
}
return null;
}
// method needs to be protected for 1.0
if (this.specVersion == SpecVersion.VERSION_1_0) {
if (Modifier.isPublic(method.getModifiers())) {
ref.logWarn(iLog, "Method " + method.getName() + " should be declared protected");
} else if (!Modifier.isProtected(method.getModifiers())) {
ref.logError(iLog, "Method " + method.getName() + " has wrong qualifier, public or protected required");
return null;
}
}
return method.getName();
}
private static final String TYPE_SERVICE_REFERENCE = "org.osgi.framework.ServiceReference";
private Method getMethod(final String name, final Class<?>[] sig) {
try {
return this.classDescription.getDescribedClass().getDeclaredMethod(name, sig);
} catch (final SecurityException e) {
// ignore
} catch (final NoSuchMethodException e) {
// ignore
}
return null;
}
public Method findMethod(final IssueLog iLog, final Reference ref, final String methodName)
throws SCRDescriptorException {
try {
final Class<?>[] sig = new Class<?>[] { this.project.getClassLoader().loadClass(TYPE_SERVICE_REFERENCE) };
final Class<?>[] sig2 = new Class<?>[] { this.project.getClassLoader().loadClass(ref.getInterfacename()) };
final Class<?>[] sig3 = new Class<?>[] { this.project.getClassLoader().loadClass(ref.getInterfacename()), Map.class };
// service interface or ServiceReference first
String realMethodName = methodName;
Method method = getMethod(realMethodName, sig);
if (method == null) {
method = getMethod(realMethodName, sig2);
if (method == null && this.specVersion.ordinal() >= SpecVersion.VERSION_1_1.ordinal() ) {
method = getMethod(realMethodName, sig3);
}
}
// 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(realMethodName, sig);
}
if (method == null) {
method = getMethod(realMethodName, sig2);
if (method == null && this.specVersion.ordinal() >= SpecVersion.VERSION_1_1.ordinal() ) {
method = getMethod(realMethodName, sig3);
}
}
// 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(realMethodName, sig);
}
if (method == null) {
method = getMethod(realMethodName, sig2);
if (method == null && this.specVersion.ordinal() >= SpecVersion.VERSION_1_1.ordinal() ) {
method = getMethod(realMethodName, sig3);
}
}
return method;
} catch (final ClassNotFoundException cnfe) {
throw new SCRDescriptorException("Unable to load class!", cnfe);
}
}
}