/* * 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.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedSet; import org.apache.commons.lang3.StringUtils; import org.apache.felix.scr.impl.inject.Annotations; import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.FieldCollectionType; 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.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. */ 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 if (invokeLifecycleMethod(target, targetClass, methodName, !activate, componentContext, MapUtil.toMap(componentContext.getProperties()))) { 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, ComponentContext componentContext, 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 if (invokeLifecycleMethod(target, targetClass, methodName, false, componentContext, properties)) { return true; } throw new RuntimeException("No matching modified method with name '" + methodName + "' " + " found in class " + targetClass.getName()); } /** * Invokes a lifecycle method (activation, deactivation or modified) with variable method arguments. * @param target Target object * @param targetClass Target object class * @param methodName Method name * @param allowIntegerArgument Allow int or Integer as arguments (only decactivate) * @param componentContext Component context * @param properties Component properties * @return true if a method was found and invoked */ private static boolean invokeLifecycleMethod(Object target, Class<?> targetClass, String methodName, boolean allowIntegerArgument, ComponentContext componentContext, Map<String,Object> properties) { // 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. Component property type (annotation lass) method = getMethod(targetClass, methodName, new Class<?>[] { Annotation.class }); if (method != null) { invokeMethod(target, method, new Object[] { Annotations.toObject(method.getParameterTypes()[0], MapUtil.toMap(componentContext.getProperties()), componentContext.getBundleContext().getBundle(), false) }); return true; } // 5. int (deactivation only) if (allowIntegerArgument) { method = getMethod(targetClass, methodName, new Class<?>[] { int.class }); if (method != null) { invokeMethod(target, method, new Object[] { 0 }); return true; } } // 6. Integer (deactivation only) if (allowIntegerArgument) { method = getMethod(targetClass, methodName, new Class<?>[] { Integer.class }); if (method != null) { invokeMethod(target, method, new Object[] { 0 }); return true; } } // 7. mixed arguments Class<?>[] mixedArgsAllowed = allowIntegerArgument ? new Class<?>[] { ComponentContext.class, BundleContext.class, Map.class, Annotation.class, int.class, Integer.class } : new Class<?>[] { ComponentContext.class, BundleContext.class, Map.class, Annotation.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].isAnnotation()) { args[i] = Annotations.toObject(method.getParameterTypes()[i], MapUtil.toMap(componentContext.getProperties()), componentContext.getBundleContext().getBundle(), false); } else if (method.getParameterTypes()[i] == int.class || method.getParameterTypes()[i] == Integer.class) { args[i] = 0; } } invokeMethod(target, method, args); return true; } // 8. noargs method = getMethod(targetClass, methodName, new Class<?>[0]); if (method != null) { invokeMethod(target, method, new Object[0]); return true; } return false; } private static Method getMethod(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]==types[i]) || (types[i]==Annotation.class && method.getParameterTypes()[i].isAnnotation()))) { foundMismatch = true; break; } } if (!foundMismatch) { 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 = true; 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) { boolean foundMismatch = false; for (Class<?> parameterType : method.getParameterTypes()) { boolean foundAnyMatch = false; for (int i=0; i<types.length; i++) { if ((parameterType==types[i]) || (types[i]==Annotation.class && parameterType.isAnnotation())) { foundAnyMatch = true; break; } } if (!foundAnyMatch) { foundMismatch = true; break; } } if (!foundMismatch) { 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()); } } private static Field getField(Class clazz, String fieldName, Class<?> type) { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (StringUtils.equals(field.getName(), fieldName) && field.getType().equals(type)) { return field; } } // not found? check super classes Class<?> superClass = clazz.getSuperclass(); if (superClass != null && superClass != Object.class) { return getField(superClass, fieldName, type); } return null; } private static Field getFieldWithAssignableType(Class clazz, String fieldName, Class<?> type) { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (StringUtils.equals(field.getName(), fieldName) && field.getType().isAssignableFrom(type)) { return field; } } // not found? check super classes Class<?> superClass = clazz.getSuperclass(); if (superClass != null && superClass != Object.class) { return getFieldWithAssignableType(superClass, fieldName, type); } return null; } private static void setField(Object target, Field field, Object value) { try { field.setAccessible(true); field.set(target, value); } catch (IllegalAccessException ex) { throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + target.getClass().getName(), ex); } catch (IllegalArgumentException ex) { throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + target.getClass().getName(), ex); } } /** * 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 = reference.getInterfaceTypeAsClass(); // get matching service references List<ServiceInfo> matchingServices = getMatchingServices(type, bundleContext, reference.getTarget()); // no references found? check if reference was optional if (matchingServices.isEmpty()) { if (!reference.isCardinalityOptional()) { throw new ReferenceViolationException("Unable to inject mandatory reference '" + reference.getName() + "' for class " + targetClass.getName() + " : no matching services were found."); } if (reference.isCardinalityMultiple()) { // make sure at least empty array is set invokeBindUnbindMethod(reference, target, null, true); } } // multiple references found? check if reference is not multiple if (matchingServices.size() > 1 && !reference.isCardinalityMultiple()) { 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(); String fieldName = reference.getField(); if (StringUtils.isEmpty(methodName) && StringUtils.isEmpty(fieldName)) { throw new RuntimeException("No bind/unbind method name or file name defined " + "for reference '" + reference.getName() + "' for class " + targetClass.getName()); } if (StringUtils.isNotEmpty(methodName) && serviceInfo != null) { // 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 = reference.getInterfaceTypeAsClass(); 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()); } // in OSGi declarative services 1.3 there are no bind/unbind methods - modify the field directly else if (StringUtils.isNotEmpty(fieldName)) { // check for field with list/collection reference if (reference.isCardinalityMultiple()) { switch (reference.getFieldCollectionType()) { case SERVICE: case REFERENCE: Object item = null; if (serviceInfo != null) { item = serviceInfo.getServiceInstance(); if (reference.getFieldCollectionType() == FieldCollectionType.REFERENCE) { item = serviceInfo.getServiceReference(); } } // 1. collection Field field = getFieldWithAssignableType(targetClass, fieldName, Collection.class); if (field != null) { if (bind) { addToCollection(target, field, item); } else { removeFromCollection(target, field, item); } return; } // 2. list field = getField(targetClass, fieldName, List.class); if (field != null) { if (bind) { addToCollection(target, field, item); } else { removeFromCollection(target, field, item); } return; } break; default: throw new RuntimeException("Field collection type '" + reference.getFieldCollectionType() + "' not supported " + "for reference '" + reference.getName() + "' for class " + targetClass.getName()); } } // check for single field reference else { // 1. assignable from service instance Class<?> interfaceType = reference.getInterfaceTypeAsClass(); Field field = getFieldWithAssignableType(targetClass, fieldName, interfaceType); if (field != null) { setField(target, field, bind && serviceInfo != null ? serviceInfo.getServiceInstance() : null); return; } // 2. ServiceReference field = getField(targetClass, fieldName, ServiceReference.class); if (field != null) { setField(target, field, bind && serviceInfo != null ? serviceInfo.getServiceReference() : null); return; } } } } @SuppressWarnings("unchecked") private static void addToCollection(Object target, Field field, Object item) { try { field.setAccessible(true); Collection<Object> collection = (Collection<Object>)field.get(target); if (collection == null) { collection = new ArrayList<Object>(); } if (item != null) { collection.add(item); } field.set(target, collection); } catch (IllegalAccessException ex) { throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + target.getClass().getName(), ex); } catch (IllegalArgumentException ex) { throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + target.getClass().getName(), ex); } } @SuppressWarnings("unchecked") private static void removeFromCollection(Object target, Field field, Object item) { try { field.setAccessible(true); Collection<Object> collection = (Collection<Object>)field.get(target); if (collection == null) { collection = new ArrayList<Object>(); } if (item != null) { collection.remove(item); } field.set(target, collection); } catch (IllegalAccessException ex) { throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + target.getClass().getName(), ex); } catch (IllegalArgumentException ex) { throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + target.getClass().getName(), ex); } } /** * 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, String filter) { List<ServiceInfo> matchingServices = new ArrayList<ServiceInfo>(); try { ServiceReference[] references = bundleContext.getServiceReferences(type.getName(), filter); 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; } @SuppressWarnings("unchecked") 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; } } }