blob: ab84903edb88d926a13e6ab21044544eb0ab67f5 [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.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);
}
}
}
}