blob: 15340d4b462ea28852f54a8970937e54d817038a [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.sling.testing.mock.osgi;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.OsgiMetadata;
import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.Reference;
import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.ReferenceCardinality;
import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.ReferencePolicy;
import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.ReferencePolicyOption;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
/**
* Helper methods to inject dependencies and activate services.
*/
final class OsgiServiceUtil {
private OsgiServiceUtil() {
// static methods only
}
/**
* Simulate activation or deactivation of OSGi service instance.
* @param target Service instance.
* @param componentContext Component context
* @return true if activation/deactivation method was called. False if it failed.
*/
@SuppressWarnings("unchecked")
public static boolean activateDeactivate(Object target, ComponentContext componentContext, boolean activate) {
Class<?> targetClass = target.getClass();
// get method name for activation/deactivation from osgi metadata
OsgiMetadata metadata = OsgiMetadataUtil.getMetadata(targetClass);
if (metadata == null) {
throw new NoScrMetadataException(targetClass);
}
String methodName;
if (activate) {
methodName = metadata.getActivateMethodName();
} else {
methodName = metadata.getDeactivateMethodName();
}
boolean fallbackDefaultName = false;
if (StringUtils.isEmpty(methodName)) {
fallbackDefaultName = true;
if (activate) {
methodName = "activate";
} else {
methodName = "deactivate";
}
}
// try to find matching activate/deactivate method and execute it
// 1. componentContext
Method method = getMethod(targetClass, methodName, new Class<?>[] { ComponentContext.class });
if (method != null) {
invokeMethod(target, method, new Object[] { componentContext });
return true;
}
// 2. bundleContext
method = getMethod(targetClass, methodName, new Class<?>[] { BundleContext.class });
if (method != null) {
invokeMethod(target, method, new Object[] { componentContext.getBundleContext() });
return true;
}
// 3. map
method = getMethod(targetClass, methodName, new Class<?>[] { Map.class });
if (method != null) {
invokeMethod(target, method, new Object[] { MapUtil.toMap(componentContext.getProperties()) });
return true;
}
// 4. int (deactivation only)
if (!activate) {
method = getMethod(targetClass, methodName, new Class<?>[] { int.class });
if (method != null) {
invokeMethod(target, method, new Object[] { 0 });
return true;
}
}
// 5. Integer (deactivation only)
if (!activate) {
method = getMethod(targetClass, methodName, new Class<?>[] { Integer.class });
if (method != null) {
invokeMethod(target, method, new Object[] { 0 });
return true;
}
}
// 6. mixed arguments of componentContext, bundleContext and map
Class<?>[] mixedArgsAllowed = activate ? new Class<?>[] { ComponentContext.class, BundleContext.class, Map.class }
: new Class<?>[] { ComponentContext.class, BundleContext.class, Map.class, int.class, Integer.class };
method = getMethodWithAnyCombinationArgs(targetClass, methodName, mixedArgsAllowed);
if (method != null) {
Object[] args = new Object[method.getParameterTypes().length];
for (int i=0; i<args.length; i++) {
if (method.getParameterTypes()[i] == ComponentContext.class) {
args[i] = componentContext;
}
else if (method.getParameterTypes()[i] == BundleContext.class) {
args[i] = componentContext.getBundleContext();
}
else if (method.getParameterTypes()[i] == Map.class) {
args[i] = MapUtil.toMap(componentContext.getProperties());
}
else if (method.getParameterTypes()[i] == int.class || method.getParameterTypes()[i] == Integer.class) {
args[i] = 0;
}
}
invokeMethod(target, method, args);
return true;
}
// 7. noargs
method = getMethod(targetClass, methodName, new Class<?>[0]);
if (method != null) {
invokeMethod(target, method, new Object[0]);
return true;
}
if (fallbackDefaultName) {
return false;
}
throw new RuntimeException("No matching " + (activate ? "activation" : "deactivation") + " method with name '" + methodName + "' "
+ " found in class " + targetClass.getName());
}
/**
* Simulate modification of configuration of OSGi service instance.
* @param target Service instance.
* @param properties Updated configuration
* @return true if modified method was called. False if it failed.
*/
public static boolean modified(Object target, BundleContext bundleContext, Map<String,Object> properties) {
Class<?> targetClass = target.getClass();
// get method name for activation/deactivation from osgi metadata
OsgiMetadata metadata = OsgiMetadataUtil.getMetadata(targetClass);
if (metadata == null) {
throw new NoScrMetadataException(targetClass);
}
String methodName = metadata.getModifiedMethodName();
if (StringUtils.isEmpty(methodName)) {
return false;
}
// try to find matching modified method and execute it
Method method = getMethod(targetClass, methodName, new Class<?>[] { Map.class });
if (method != null) {
invokeMethod(target, method, new Object[] { properties });
return true;
}
throw new RuntimeException("No matching modified method with name '" + methodName + "' "
+ " found in class " + targetClass.getName());
}
private static Method getMethod(Class clazz, String methodName, Class<?>[] types) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (StringUtils.equals(method.getName(), methodName)
&& Arrays.equals(method.getParameterTypes(), types)) {
return method;
}
}
// not found? check super classes
Class<?> superClass = clazz.getSuperclass();
if (superClass != null && superClass != Object.class) {
return getMethod(superClass, methodName, types);
}
return null;
}
private static Method getMethodWithAssignableTypes(Class clazz, String methodName, Class<?>[] types) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (StringUtils.equals(method.getName(), methodName) && method.getParameterTypes().length==types.length) {
boolean foundMismatch = false;
for (int i=0; i<types.length; i++) {
if (!method.getParameterTypes()[i].isAssignableFrom(types[i])) {
foundMismatch = false;
break;
}
}
if (!foundMismatch) {
return method;
}
}
}
// not found? check super classes
Class<?> superClass = clazz.getSuperclass();
if (superClass != null && superClass != Object.class) {
return getMethodWithAssignableTypes(superClass, methodName, types);
}
return null;
}
private static Method getMethodWithAnyCombinationArgs(Class clazz, String methodName, Class<?>[] types) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (StringUtils.equals(method.getName(), methodName) && method.getParameterTypes().length > 1) {
for (Class<?> parameterType : method.getParameterTypes()) {
if (!ArrayUtils.contains(types, parameterType)) {
return null;
}
}
return method;
}
}
// not found? check super classes
Class<?> superClass = clazz.getSuperclass();
if (superClass != null && superClass != Object.class) {
return getMethodWithAnyCombinationArgs(superClass, methodName, types);
}
return null;
}
private static void invokeMethod(Object target, Method method, Object[] args) {
try {
method.setAccessible(true);
method.invoke(target, args);
} catch (IllegalAccessException ex) {
throw new RuntimeException("Unable to invoke method '" + method.getName() + "' for class "
+ target.getClass().getName(), ex);
} catch (IllegalArgumentException ex) {
throw new RuntimeException("Unable to invoke method '" + method.getName() + "' for class "
+ target.getClass().getName(), ex);
} catch (InvocationTargetException ex) {
throw new RuntimeException("Unable to invoke method '" + method.getName() + "' for class "
+ target.getClass().getName(), ex.getCause());
}
}
/**
* Simulate OSGi service dependency injection. Injects direct references and
* multiple references.
* @param target Service instance
* @param bundleContext Bundle context from which services are fetched to inject.
* @return true if all dependencies could be injected, false if the service has no dependencies.
*/
public static boolean injectServices(Object target, BundleContext bundleContext) {
// collect all declared reference annotations on class and field level
Class<?> targetClass = target.getClass();
OsgiMetadata metadata = OsgiMetadataUtil.getMetadata(targetClass);
if (metadata == null) {
throw new NoScrMetadataException(targetClass);
}
List<Reference> references = metadata.getReferences();
if (references.isEmpty()) {
return false;
}
// try to inject services
for (Reference reference : references) {
injectServiceReference(reference, target, bundleContext);
}
return true;
}
private static void injectServiceReference(Reference reference, Object target, BundleContext bundleContext) {
Class<?> targetClass = target.getClass();
// get reference type
Class<?> type;
try {
type = Class.forName(reference.getInterfaceType());
} catch (ClassNotFoundException ex) {
throw new RuntimeException("Unable to instantiate reference type: " + reference.getInterfaceType(), ex);
}
// get matching service references
List<ServiceInfo> matchingServices = getMatchingServices(type, bundleContext);
// no references found? check if reference was optional
if (matchingServices.isEmpty()) {
boolean isOptional = (reference.getCardinality() == ReferenceCardinality.OPTIONAL_UNARY || reference
.getCardinality() == ReferenceCardinality.OPTIONAL_MULTIPLE);
if (!isOptional) {
throw new ReferenceViolationException("Unable to inject mandatory reference '" + reference.getName() + "' for class " + targetClass.getName() + " : no matching services were found.");
}
}
// multiple references found? check if reference is not multiple
if (matchingServices.size() > 1
&& (reference.getCardinality() == ReferenceCardinality.MANDATORY_UNARY || reference.getCardinality() == ReferenceCardinality.OPTIONAL_UNARY)) {
throw new ReferenceViolationException("Multiple matches found for unary reference '" + reference.getName() + "' for class "+ targetClass.getName());
}
// try to invoke bind method
for (ServiceInfo matchingService : matchingServices) {
invokeBindUnbindMethod(reference, target, matchingService, true);
}
}
private static void invokeBindUnbindMethod(Reference reference, Object target, ServiceInfo serviceInfo, boolean bind) {
Class<?> targetClass = target.getClass();
// try to invoke bind method
String methodName = bind ? reference.getBind() : reference.getUnbind();
if (StringUtils.isNotEmpty(methodName)) {
// 1. ServiceReference
Method method = getMethod(targetClass, methodName, new Class<?>[] { ServiceReference.class });
if (method != null) {
invokeMethod(target, method, new Object[] { serviceInfo.getServiceReference() });
return;
}
// 2. assignable from service instance
Class<?> interfaceType;
try {
interfaceType = Class.forName(reference.getInterfaceType());
} catch (ClassNotFoundException e) {
throw new RuntimeException("Service reference type not found: " + reference.getInterfaceType());
}
method = getMethodWithAssignableTypes(targetClass, methodName, new Class<?>[] { interfaceType });
if (method != null) {
invokeMethod(target, method, new Object[] { serviceInfo.getServiceInstance() });
return;
}
// 3. assignable from service instance plus map
method = getMethodWithAssignableTypes(targetClass, methodName, new Class<?>[] { interfaceType, Map.class });
if (method != null) {
invokeMethod(target, method, new Object[] { serviceInfo.getServiceInstance(), serviceInfo.getServiceConfig() });
return;
}
}
throw new RuntimeException((bind ? "Bind" : "Unbind") + " method with name " + methodName + " not found "
+ "for reference '" + reference.getName() + "' for class " + targetClass.getName());
}
/**
* Directly invoke bind method on service for the given reference.
* @param reference Reference metadata
* @param target Target object for reference
* @param serviceInfo Service on which to invoke the method
*/
public static void invokeBindMethod(Reference reference, Object target, ServiceInfo serviceInfo) {
invokeBindUnbindMethod(reference, target, serviceInfo, true);
}
/**
* Directly invoke unbind method on service for the given reference.
* @param reference Reference metadata
* @param target Target object for reference
* @param serviceInfo Service on which to invoke the method
*/
public static void invokeUnbindMethod(Reference reference, Object target, ServiceInfo serviceInfo) {
invokeBindUnbindMethod(reference, target, serviceInfo, false);
}
private static List<ServiceInfo> getMatchingServices(Class<?> type, BundleContext bundleContext) {
List<ServiceInfo> matchingServices = new ArrayList<ServiceInfo>();
try {
ServiceReference[] references = bundleContext.getServiceReferences(type.getName(), null);
if (references != null) {
for (ServiceReference serviceReference : references) {
Object serviceInstance = bundleContext.getService(serviceReference);
Map<String, Object> serviceConfig = new HashMap<String, Object>();
String[] keys = serviceReference.getPropertyKeys();
for (String key : keys) {
serviceConfig.put(key, serviceReference.getProperty(key));
}
matchingServices.add(new ServiceInfo(serviceInstance, serviceConfig, serviceReference));
}
}
} catch (InvalidSyntaxException ex) {
// ignore
}
return matchingServices;
}
/**
* Collects all references of any registered service that match with any of the exported interfaces of the given service registration
* and are defined as DYNAMIC.
* @param registeredServices Registered Services
* @param registration Service registration
* @return List of references
*/
public static List<ReferenceInfo> getMatchingDynamicReferences(SortedSet<MockServiceRegistration> registeredServices,
MockServiceRegistration registration) {
List<ReferenceInfo> references = new ArrayList<ReferenceInfo>();
for (MockServiceRegistration existingRegistration : registeredServices) {
OsgiMetadata metadata = OsgiMetadataUtil.getMetadata(existingRegistration.getService().getClass());
if (metadata != null) {
for (Reference reference : metadata.getReferences()) {
if (reference.getPolicy() == ReferencePolicy.DYNAMIC) {
for (String serviceInterface : registration.getClasses()) {
if (StringUtils.equals(serviceInterface, reference.getInterfaceType())) {
references.add(new ReferenceInfo(existingRegistration, reference));
}
}
}
}
}
}
return references;
}
/**
* Collects all references of any registered service that match with any of the exported interfaces of the given service registration
* and are defined as STATIC + GREEDY.
* @param registeredServices Registered Services
* @param registration Service registration
* @return List of references
*/
public static List<ReferenceInfo> getMatchingStaticGreedyReferences(SortedSet<MockServiceRegistration> registeredServices,
MockServiceRegistration registration) {
List<ReferenceInfo> references = new ArrayList<ReferenceInfo>();
for (MockServiceRegistration existingRegistration : registeredServices) {
OsgiMetadata metadata = OsgiMetadataUtil.getMetadata(existingRegistration.getService().getClass());
if (metadata != null) {
for (Reference reference : metadata.getReferences()) {
if (reference.getPolicy() == ReferencePolicy.STATIC && reference.getPolicyOption() == ReferencePolicyOption.GREEDY) {
for (String serviceInterface : registration.getClasses()) {
if (StringUtils.equals(serviceInterface, reference.getInterfaceType())) {
references.add(new ReferenceInfo(existingRegistration, reference));
}
}
}
}
}
}
return references;
}
static class ServiceInfo {
private final Object serviceInstance;
private final Map<String, Object> serviceConfig;
private final ServiceReference serviceReference;
public ServiceInfo(Object serviceInstance, Map<String, Object> serviceConfig, ServiceReference serviceReference) {
this.serviceInstance = serviceInstance;
this.serviceConfig = serviceConfig;
this.serviceReference = serviceReference;
}
public ServiceInfo(MockServiceRegistration registration) {
this.serviceInstance = registration.getService();
this.serviceConfig = MapUtil.toMap(registration.getProperties());
this.serviceReference = registration.getReference();
}
public Object getServiceInstance() {
return this.serviceInstance;
}
public Map<String, Object> getServiceConfig() {
return this.serviceConfig;
}
public ServiceReference getServiceReference() {
return serviceReference;
}
}
static class ReferenceInfo {
private final MockServiceRegistration serviceRegistration;
private final Reference reference;
public ReferenceInfo(MockServiceRegistration serviceRegistration, Reference reference) {
this.serviceRegistration = serviceRegistration;
this.reference = reference;
}
public MockServiceRegistration getServiceRegistration() {
return serviceRegistration;
}
public Reference getReference() {
return reference;
}
}
}