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