/* * 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.ipojo.handlers.dependency; import org.apache.felix.ipojo.ConfigurationException; import org.apache.felix.ipojo.metadata.Element; import org.apache.felix.ipojo.parser.FieldMetadata; import org.apache.felix.ipojo.parser.MethodMetadata; import org.apache.felix.ipojo.parser.PojoMetadata; import org.apache.felix.ipojo.util.DependencyModel; import org.osgi.framework.ServiceReference; import java.util.*; /** * Utility class checking the configuration of a dependency. */ public class DependencyConfigurationChecker { public static void ensure(Dependency dependency, Element metadata, PojoMetadata manipulation) throws ConfigurationException { ensureThatAtLeastOneInjectionIsSpecified(dependency); ensureThatTheFieldIsInComponentClass(dependency, manipulation); ensureThatTheConstructorParameterIsCoherent(dependency, manipulation); ensureThatCallbacksAreCoherent(dependency, manipulation); deduceAggregationFromTheInjectionPoints(dependency, manipulation); deduceTheServiceSpecification(dependency, manipulation); checkTheServiceUnavailableAction(dependency, metadata); checkTheConsistencyOfTheFromAttribute(dependency, metadata); disableProxyForInconsistentTypes(dependency); } /** * Disables the proxy settings for types that does not support it: Vector, Array, and non interface specification. * If the dependency is disabled, check that we are no constructor injection. * @param dependency the dependency */ private static void disableProxyForInconsistentTypes(Dependency dependency) throws ConfigurationException { if (! dependency.getSpecification().isInterface() || dependency.isAggregate() && dependency.getAggregateType() == AggregateDependencyInjectionType.ARRAY || dependency.isAggregate() && dependency.getAggregateType() == AggregateDependencyInjectionType.VECTOR) { dependency.setProxy(false); if (dependency.getConstructorParameterIndex() != -1) { throw new ConfigurationException("The dependency " + DependencyHandler.getDependencyIdentifier (dependency) + " has an inconsistent configuration. - reason: the service specification " + "or container do not support proxy, which is required for constructor injection"); } dependency.getHandler().info("Proxy disabled for " + DependencyHandler.getDependencyIdentifier (dependency) + " - the service specification or container do not support proxy"); } } /** * Checks that the dependency callbacks are consistent: * <ul> * <li>have a supported 'type'</li> * <li>have a supported signature</li> * </ul> * If the method is not in the component class, a message is logged, as this verification cannot be used. * If the method is found in the manipulation metadata, the callback parameters are set. * @param dependency the dependency * @param manipulation the manipulation * @throws ConfigurationException if the methods do not obey to the previously mentioned rules. */ private static void ensureThatCallbacksAreCoherent(Dependency dependency, PojoMetadata manipulation) throws ConfigurationException { DependencyCallback[] callbacks = dependency.getCallbacks(); if (callbacks != null) { for (DependencyCallback callback : callbacks) { MethodMetadata metadata = manipulation.getMethod(callback.getMethodName()); if (metadata == null) { dependency.getHandler().debug("A dependency callback " + callback.getMethodName() + " of " + DependencyHandler.getDependencyIdentifier(dependency) + " does not " + "exist in the implementation class, will try the parent classes"); } else { String[] parameters = metadata.getMethodArguments(); switch(parameters.length) { case 0 : // Just a notification method. callback.setArgument(parameters); break; case 1 : // Can be the service object, service reference or properties callback.setArgument(parameters); break; case 2 : // Constraints on the second argument, must be a service reference, a dictionary or a map if (!ServiceReference.class.getName().equals(parameters[1]) && ! Dictionary.class.getName().equals(parameters[1]) && ! Map.class.getName().equals(parameters[1])) { throw new ConfigurationException("The method " + callback.getMethodName() + " of " + DependencyHandler.getDependencyIdentifier(dependency) + " is not a valid " + "dependency callback - reason: the second argument (" + parameters[1] + ") must be a service reference, a dictionary or a map."); } callback.setArgument(parameters); break; default: // Invalid signature. throw new ConfigurationException("The method " + callback.getMethodName() + " of " + DependencyHandler.getDependencyIdentifier(dependency) + " is not a valid " + "dependency callback - reason: the signature is invalid"); } } } } } /** * Checks whether the constructor parameter injection is suitable. this check verified that the constructor has * enough parameter. * @param dependency the dependency * @param manipulation the manipulation metadata * @throws ConfigurationException if the constructor is not suitable */ private static void ensureThatTheConstructorParameterIsCoherent(Dependency dependency, PojoMetadata manipulation) throws ConfigurationException { if (dependency.getConstructorParameterIndex() != -1) { MethodMetadata[] constructors = manipulation.getConstructors(); if (constructors == null || constructors.length == 0) { throw new ConfigurationException("The constructor parameter attribute of " + DependencyHandler .getDependencyIdentifier(dependency) + " is inconsistent - reason: there is no constructor in" + " the component class (" + dependency.getHandler().getInstanceManager().getClassName() + ")"); } //TODO Consider only the first constructor. This is a limitation we should think about, // how to determine which constructor to use. Only one constructor should have annotations, // it could be use as hint. MethodMetadata constructor = constructors[0]; if (! (constructor.getMethodArguments().length > dependency.getConstructorParameterIndex())) { throw new ConfigurationException("The constructor parameter attribute of " + DependencyHandler .getDependencyIdentifier(dependency) + " is inconsistent - reason: the constructor with the " + "signature " + Arrays.toString(constructor.getMethodArguments()) + " has not enough " + "parameters"); } } } /** * Checks that the field used to inject the dependency is in the component class. If the dependency has no field, * this method does nothing. * @param dependency the dependency * @param manipulation the manipulation metadata * @throws ConfigurationException if the field used to inject the given dependency is not in the component class. */ private static void ensureThatTheFieldIsInComponentClass(Dependency dependency, PojoMetadata manipulation) throws ConfigurationException { if (dependency.getField() != null) { FieldMetadata field = manipulation.getField(dependency.getField()); if (field == null) { throw new ConfigurationException("Incorrect field injection for " + DependencyHandler .getDependencyIdentifier(dependency) + " - reason: the field " + dependency.getField() + " is" + " not in the component class (" + dependency.getHandler().getInstanceManager().getClassName() + ")"); } } } /** * Determines if the dependency is aggregate from the field or constructor parameter used to inject the dependency. * If the dependency just uses methods, this method does nothing. This method also check that dependencies set to * aggregate have a valid injection type. * @param dependency the dependency * @param manipulation the manipulation metadata * @throws ConfigurationException if the type of the field or constructor parameter used to inject the dependency * is not suitable for aggregate dependencies. */ private static void deduceAggregationFromTheInjectionPoints(Dependency dependency, PojoMetadata manipulation) throws ConfigurationException { if (dependency.getField() != null) { FieldMetadata field = manipulation.getField(dependency.getField()); String type = field.getFieldType(); if (type.endsWith("[]")) { dependency.setAggregateType(AggregateDependencyInjectionType.ARRAY); } else if (Collection.class.getName().equals(type) || List.class.getName().equals(type)) { dependency.setAggregateType(AggregateDependencyInjectionType.LIST); } else if (Set.class.getName().equals(type)) { dependency.setAggregateType(AggregateDependencyInjectionType.SET); } else if (Vector.class.getName().equals(type)) { dependency.setAggregateType(AggregateDependencyInjectionType.VECTOR); } else if (dependency.isAggregate()) { // Something wrong. The dependency has a field that is not suitable for aggregate dependencies throw new ConfigurationException("The dependency " + DependencyHandler.getDependencyIdentifier (dependency) + " cannot be an aggregate dependency - reason: the type " + field.getFieldType () + " of the field " + field.getFieldName() + " is not suitable for aggregate " + "dependencies. Compatible types are array, vector, list, set and collection."); } } if (dependency.getConstructorParameterIndex() != -1) { String type = manipulation.getConstructors()[0].getMethodArguments()[dependency .getConstructorParameterIndex()]; if (type.endsWith("[]")) { dependency.setAggregateType(AggregateDependencyInjectionType.ARRAY); } else if (Collection.class.getName().equals(type) || List.class.getName().equals(type)) { dependency.setAggregateType(AggregateDependencyInjectionType.LIST); } else if (Set.class.getName().equals(type)) { dependency.setAggregateType(AggregateDependencyInjectionType.SET); } else if (Vector.class.getName().equals(type)) { dependency.setAggregateType(AggregateDependencyInjectionType.VECTOR); } else if (dependency.isAggregate()) { // Something wrong. The dependency has a field that is not suitable for aggregate dependencies throw new ConfigurationException("The dependency " + DependencyHandler.getDependencyIdentifier (dependency) + " cannot be an aggregate dependency - reason: the type " + type + " of the constructor parameter " + dependency.getConstructorParameterIndex() + " is not suitable for aggregate " + "dependencies. Compatible types are array, vector, list, set and collection."); } } //TODO We may not cover some cases such as inconsistency between the constructor and the field. However this // should be very rare. } /** * Checks that the dependency has at least one injection point. * @param dependency the dependency * @throws ConfigurationException if the dependency has no injection point */ private static void ensureThatAtLeastOneInjectionIsSpecified(Dependency dependency) throws ConfigurationException { if (dependency.getField() == null && (dependency.getCallbacks() == null || dependency.getCallbacks().length == 0) && dependency.getConstructorParameterIndex() == -1) { throw new ConfigurationException("The dependency " + DependencyHandler.getDependencyIdentifier (dependency) + " is invalid - reason: no injection specified, at least a field, " + "a method or a constructor parameter index must be set"); } } /** * Checks that the `from` attribute is used consistently: * <ul> * <li>Rule 1 : it cannot be used on aggregate dependency</li> * <li>Rule 2 : it cannot be used in combination with the `comparator` attribute</li> * <li>Rule 3 : it cannot be used in combination with the `dynamic-priority` binding policy</li> * </ul> * * @param dependency the dependency * @param metadata the dependency metadata * @throws ConfigurationException if the `from` attribute is used inconsistently. */ private static void checkTheConsistencyOfTheFromAttribute(Dependency dependency, Element metadata) throws ConfigurationException { // Check if we have a from attribute. if (metadata.getAttribute("from") != null) { final String message = "The `from` attribute is not usable in " + DependencyHandler .getDependencyIdentifier(dependency) + " - reason: "; // Rule 1 if (dependency.isAggregate()) { throw new ConfigurationException(message + "the dependency is " + "aggregate"); } // Rule 2 String comparator = metadata.getAttribute("comparator"); if (comparator != null) { throw new ConfigurationException(message + "the dependency uses a comparator"); } // Rule 3 if (dependency.getBindingPolicy() == DependencyModel.DYNAMIC_PRIORITY_BINDING_POLICY) { throw new ConfigurationException(message + "the dependency uses the dynamic-priority " + "binding policy"); } } } /** * Checks that service unavailable actions are consistent. * <ul> * <li>Rule 1: Nullable, Exception, Default-Implementation... can only be used for scalar optional dependency</li> * <li>Rule 2: Only one can be used</li> * <li>Rule 3: Timeout can only be used on optional dependency</li> * </ul> * * @param dependency the dependency * @throws ConfigurationException if the dependency used inconsistent attributes */ private static void checkTheServiceUnavailableAction(Dependency dependency, Element metadata) throws ConfigurationException { if (metadata.containsAttribute("nullable") || dependency.getDefaultImplementation() != null || dependency .getException() != null) { // Rule 1: String message = "The `nullable`, `default-implementation` and `exception` attributes are not " + "usable in " + DependencyHandler.getDependencyIdentifier(dependency) + " - reason: "; if (dependency.isAggregate()) { throw new ConfigurationException(message + "the dependency is aggregate"); } if (! dependency.isOptional()) { throw new ConfigurationException(message + "the dependency is mandatory"); } // At this point, we know that the dependency is scalar and optional, and at least one attribute is set // Rule 2: message = "Inconsistent use of the `nullable`, `default-implementation` and `exception` attributes are " + "not usable in " + DependencyHandler.getDependencyIdentifier(dependency) + " - reason: "; if (metadata.containsAttribute("nullable") && dependency.getDefaultImplementation() != null) { throw new ConfigurationException(message + "`nullable` and `default-implementation` cannot be " + "combined"); } if (metadata.containsAttribute("nullable") && dependency.getException() != null) { throw new ConfigurationException(message + "`nullable` and `exception` cannot be combined"); } if (dependency.getDefaultImplementation() != null && dependency.getException() != null) { throw new ConfigurationException(message + "`exception` and `default-implementation` cannot be " + "combined"); } } // Rule 3: if (dependency.getTimeout() != 0 && ! dependency.isOptional()) { throw new ConfigurationException("The `timeout` attribute is not usable in " + DependencyHandler .getDependencyIdentifier(dependency) + " - reason: the dependency is not optional"); } } /** * Tries to determine the service specification to inject in the dependency. * If the specification is already checked by the dependency, just checks the consistency. * * @param dependency the dependency * @param manipulation the manipulation metadata * @throws ConfigurationException if the specification cannot be deduced, or when the set specification is not * consistent. */ private static void deduceTheServiceSpecification(Dependency dependency, PojoMetadata manipulation) throws ConfigurationException { // Deduction algorithm String fieldType = null; String callbackType = null; String constructorType = null; // First check the field if (dependency.getField() != null) { fieldType = extractSpecificationFromField(dependency.getField(), manipulation); } if (dependency.getCallbacks() != null && dependency.getCallbacks().length != 0) { callbackType = extractSpecificationFromMethods(dependency, dependency.getCallbacks(), manipulation); } if (dependency.getConstructorParameterIndex() != -1) { constructorType = extractSpecificationFromConstructor(dependency.getConstructorParameterIndex(), manipulation); } if (dependency.getSpecification() == null && fieldType == null && callbackType == null && constructorType == null) { throw new ConfigurationException("The deduction of the service specification for " + DependencyHandler .getDependencyIdentifier(dependency) + " has failed - reason: when neither the field, " + "methods and constructor parameter have provided the service specification, " + "the `specification` attribute must be set"); } // The Dependency.setSpecification method check whether the specification coming from the different sources // are consistent. if (fieldType != null) { setSpecification(dependency, fieldType); } if (callbackType != null) { setSpecification(dependency, callbackType); } if (constructorType != null) { setSpecification(dependency, constructorType); } } private static String extractSpecificationFromMethods(Dependency dependency, DependencyCallback[] callbacks, PojoMetadata manipulation) throws ConfigurationException { String type = null; for (DependencyCallback callback : callbacks) { MethodMetadata metadata = manipulation.getMethod(callback.getMethodName()); if (metadata != null) { String[] parameters = metadata.getMethodArguments(); if (parameters.length == 1 || parameters.length == 2) { if (! ServiceReference.class.getName().equals(parameters[0]) && ! Dictionary.class.getName().equals(parameters[0]) && ! Map.class.getName().equals(parameters[0])) { if (type == null) { type = parameters[0]; } else { if (! type.equals(parameters[0])) { throw new ConfigurationException("The callbacks of " + DependencyHandler .getDependencyIdentifier(dependency) + " have inconsistent parameters"); } } } } } } return type; } private static String extractSpecificationFromConstructor(int index, PojoMetadata manipulation) { // We can write the following instructions as everything was previously checked. String type = manipulation.getConstructors()[0].getMethodArguments()[index]; if (type.endsWith("[]")) { return type.substring(0, type.length() - 2); } if (AggregateDependencyInjectionType.AGGREGATE_TYPES.contains(type)) { return null; // It's an aggregate } return type; } /** * Extracts the service specification from the field. * When this method is called, we know that the field is containing in the component class. * @param field the field * @param manipulation the manipulation metadata * @return the service specification, or {@code null} if is cannot be extracted. */ private static String extractSpecificationFromField(String field, PojoMetadata manipulation) { FieldMetadata metadata = manipulation.getField(field); if (metadata.getFieldType().endsWith("[]")) { return metadata.getFieldType().substring(0, metadata.getFieldType().length() - 2); } if (AggregateDependencyInjectionType.AGGREGATE_TYPES.contains(metadata.getFieldType())) { return null; // It's an aggregate } return metadata.getFieldType(); } /** * Sets the dependency specification. If the dependency has already a specification set, * throw an error if the current specification and the given one are not equal. * @param dep the dependency * @param className the service specification * @throws ConfigurationException if the given specification is not loadable or if the dependency has already a * specification set that is not the given one. */ private static void setSpecification(Dependency dep, String className) throws ConfigurationException { if (dep.getSpecification() != null && ! dep.getSpecification().getName().equals(className)) { throw new ConfigurationException("Inconsistent service specification for " + DependencyHandler .getDependencyIdentifier(dep) + " - reason: mismatch between the current specification (" + dep .getSpecification().getName() + ") and the discovered specification (" + className + ")"); } else if (dep.getSpecification() == null) { // Set the specification try { dep.setSpecification(dep.getBundleContext().getBundle().loadClass(className)); } catch (ClassNotFoundException e) { throw new ConfigurationException("Cannot set the service specification of " + DependencyHandler .getDependencyIdentifier(dep) + " - reason: the class " + className + " cannot be loaded from" + " the bundle " + dep.getBundleContext().getBundle().getBundleId(), e); } } } }