| /* |
| * 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.io.IOException; |
| import java.io.InputStream; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ExecutionException; |
| import java.util.regex.Pattern; |
| |
| import javax.xml.namespace.NamespaceContext; |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.xpath.XPath; |
| import javax.xml.xpath.XPathConstants; |
| import javax.xml.xpath.XPathExpression; |
| import javax.xml.xpath.XPathExpressionException; |
| import javax.xml.xpath.XPathFactory; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.felix.framework.FilterImpl; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.Filter; |
| import org.osgi.framework.InvalidSyntaxException; |
| import org.osgi.framework.ServiceReference; |
| import org.reflections.Reflections; |
| import org.reflections.scanners.ResourcesScanner; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.SAXException; |
| |
| import com.google.common.cache.CacheBuilder; |
| import com.google.common.cache.CacheLoader; |
| import com.google.common.cache.LoadingCache; |
| import com.google.common.collect.BiMap; |
| import com.google.common.collect.HashBiMap; |
| |
| /** |
| * Helper methods to parse OSGi metadata. |
| */ |
| final class OsgiMetadataUtil { |
| |
| private static final Logger log = LoggerFactory.getLogger(OsgiMetadataUtil.class); |
| |
| private static final String METADATA_PATH = "OSGI-INF"; |
| |
| private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY; |
| static { |
| DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); |
| DOCUMENT_BUILDER_FACTORY.setNamespaceAware(true); |
| |
| // suppress log entries from Reflections library |
| Reflections.log = null; |
| } |
| |
| private static final XPathFactory XPATH_FACTORY = XPathFactory.newInstance(); |
| |
| private static final BiMap<String, String> NAMESPACES = HashBiMap.create(); |
| static { |
| NAMESPACES.put("scr", "http://www.osgi.org/xmlns/scr/v1.1.0"); |
| } |
| |
| private static final OsgiMetadata NULL_METADATA = new OsgiMetadata(); |
| |
| private static final NamespaceContext NAMESPACE_CONTEXT = new NamespaceContext() { |
| @Override |
| public String getNamespaceURI(String prefix) { |
| return NAMESPACES.get(prefix); |
| } |
| |
| @Override |
| public String getPrefix(String namespaceURI) { |
| return NAMESPACES.inverse().get(namespaceURI); |
| } |
| |
| @Override |
| public Iterator getPrefixes(String namespaceURI) { |
| return NAMESPACES.keySet().iterator(); |
| } |
| }; |
| |
| /* |
| * The OSGI metadata XML files do not change during the unit test runs because static part of classpath. |
| * So we can cache the parsing step if we need them multiple times. |
| */ |
| private static final Map<String,Document> METADATA_DOCUMENT_CACHE = initMetadataDocumentCache(); |
| private static final LoadingCache<Class, OsgiMetadata> METADATA_CACHE = CacheBuilder.newBuilder().build(new CacheLoader<Class, OsgiMetadata>() { |
| @Override |
| public OsgiMetadata load(Class clazz) throws Exception { |
| Document metadataDocument = METADATA_DOCUMENT_CACHE.get(cleanupClassName(clazz.getName())); |
| if (metadataDocument != null) { |
| return new OsgiMetadata(clazz, metadataDocument); |
| } |
| return NULL_METADATA; |
| } |
| }); |
| |
| private OsgiMetadataUtil() { |
| // static methods only |
| } |
| |
| /** |
| * Try to read OSGI-metadata from /OSGI-INF and read all implemented interfaces and service properties. |
| * The metadata is cached after initial read, so it's no problem to call this method multiple time for the same class. |
| * @param clazz OSGi service implementation class |
| * @return Metadata object or null if no metadata present in classpath |
| */ |
| public static OsgiMetadata getMetadata(Class clazz) { |
| try { |
| OsgiMetadata metadata = METADATA_CACHE.get(clazz); |
| if (metadata == NULL_METADATA) { |
| return null; |
| } |
| else { |
| return metadata; |
| } |
| } |
| catch (ExecutionException ex) { |
| throw new RuntimeException("Error loading OSGi metadata from loader cache.", ex); |
| } |
| } |
| |
| /** |
| * Reads all SCR metadata XML documents located at OSGI-INF/ and caches them with quick access by implementation class. |
| * @return Cache map |
| */ |
| private static Map<String,Document> initMetadataDocumentCache() { |
| Map<String,Document> cacheMap = new HashMap<>(); |
| |
| XPath xpath = XPATH_FACTORY.newXPath(); |
| xpath.setNamespaceContext(NAMESPACE_CONTEXT); |
| XPathExpression xpathExpression; |
| try { |
| xpathExpression = xpath.compile("//*[implementation/@class]"); |
| } |
| catch (XPathExpressionException ex) { |
| throw new RuntimeException("Compiling XPath expression failed.", ex); |
| } |
| |
| Reflections reflections = new Reflections(METADATA_PATH, new ResourcesScanner()); |
| Set<String> paths = reflections.getResources(Pattern.compile("^.*\\.xml$")); |
| for (String path : paths) { |
| parseMetadataDocuments(cacheMap, path, xpathExpression); |
| } |
| |
| return cacheMap; |
| } |
| |
| private static void parseMetadataDocuments(Map<String,Document> cacheMap, String resourcePath, XPathExpression xpathExpression) { |
| try { |
| Enumeration<URL> resourceUrls = OsgiMetadataUtil.class.getClassLoader().getResources(resourcePath); |
| while (resourceUrls.hasMoreElements()) { |
| URL resourceUrl = resourceUrls.nextElement(); |
| try (InputStream fileStream = resourceUrl.openStream()) { |
| parseMetadataDocument(cacheMap, resourcePath, fileStream, xpathExpression); |
| } |
| } |
| } |
| catch (Exception ex) { |
| log.warn("Error reading SCR metadata XML document from " + resourcePath, ex); |
| } |
| } |
| |
| private static void parseMetadataDocument(Map<String,Document> cacheMap, String resourcePath, |
| InputStream fileStream, XPathExpression xpathExpression) throws XPathExpressionException { |
| Document metadata = toXmlDocument(fileStream, resourcePath); |
| NodeList nodes = (NodeList)xpathExpression.evaluate(metadata, XPathConstants.NODESET); |
| if (nodes != null) { |
| for (int i = 0; i < nodes.getLength(); i++) { |
| Node node = nodes.item(i); |
| String implementationClass = getImplementationClassName(node); |
| if (implementationClass != null) { |
| cacheMap.put(implementationClass, metadata); |
| } |
| } |
| } |
| } |
| |
| private static String getImplementationClassName(Node componentNode) { |
| NodeList childNodes = componentNode.getChildNodes(); |
| for (int j = 0; j < childNodes.getLength(); j++) { |
| Node childNode = childNodes.item(j); |
| if (childNode.getNodeName().equals("implementation")) { |
| String implementationClass = getAttributeValue(childNode, "class"); |
| if (!StringUtils.isBlank(implementationClass)) { |
| return implementationClass; |
| } |
| break; |
| } |
| } |
| return null; |
| } |
| |
| private static Document toXmlDocument(InputStream inputStream, String path) { |
| try { |
| DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); |
| DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); |
| return documentBuilder.parse(inputStream); |
| } catch (ParserConfigurationException ex) { |
| throw new RuntimeException("Unable to read classpath resource: " + path, ex); |
| } catch (SAXException ex) { |
| throw new RuntimeException("Unable to read classpath resource: " + path, ex); |
| } catch (IOException ex) { |
| throw new RuntimeException("Unable to read classpath resource: " + path, ex); |
| } finally { |
| try { |
| inputStream.close(); |
| } catch (IOException ex) { |
| // ignore |
| } |
| } |
| } |
| |
| /** |
| * @param clazz OSGi component |
| * @return XPath query fragment to find matching XML node in SCR metadata |
| */ |
| private static String getComponentXPathQuery(Class clazz) { |
| String className = cleanupClassName(clazz.getName()); |
| return "//*[implementation/@class='" + className + "' or @name='" + className + "']"; |
| } |
| |
| /** |
| * Remove extensions from class names added e.g. by mockito. |
| * @param className Class name |
| * @return Cleaned up class name |
| */ |
| public static final String cleanupClassName(String className) { |
| return StringUtils.substringBefore(className, "$$Enhancer"); |
| } |
| |
| private static String getComponentName(Class clazz, Document metadata) { |
| String query = getComponentXPathQuery(clazz); |
| NodeList nodes = queryNodes(metadata, query); |
| if (nodes != null && nodes.getLength() > 0) { |
| return getAttributeValue(nodes.item(0), "name"); |
| } |
| return null; |
| } |
| |
| private static Set<String> getServiceInterfaces(Class clazz, Document metadata) { |
| Set<String> serviceInterfaces = new HashSet<String>(); |
| String query = getComponentXPathQuery(clazz) + "/service/provide[@interface!='']"; |
| NodeList nodes = queryNodes(metadata, query); |
| if (nodes != null) { |
| for (int i = 0; i < nodes.getLength(); i++) { |
| Node node = nodes.item(i); |
| String serviceInterface = getAttributeValue(node, "interface"); |
| if (StringUtils.isNotBlank(serviceInterface)) { |
| serviceInterfaces.add(serviceInterface); |
| } |
| } |
| } |
| return serviceInterfaces; |
| } |
| |
| private static Map<String, Object> getProperties(Class clazz, Document metadata) { |
| Map<String, Object> props = new HashMap<String, Object>(); |
| String query = getComponentXPathQuery(clazz) + "/property[@name!='' and @value!='']"; |
| NodeList nodes = queryNodes(metadata, query); |
| if (nodes != null) { |
| for (int i = 0; i < nodes.getLength(); i++) { |
| Node node = nodes.item(i); |
| String name = getAttributeValue(node, "name"); |
| String value = getAttributeValue(node, "value"); |
| String type = getAttributeValue(node, "type"); |
| if (StringUtils.equals("Integer", type)) { |
| props.put(name, Integer.parseInt(value)); |
| } |
| else if (StringUtils.equals("Long", type)) { |
| props.put(name, Long.parseLong(value)); |
| } |
| else if (StringUtils.equals("Boolean", type)) { |
| props.put(name, Boolean.parseBoolean(value)); |
| } |
| else { |
| props.put(name, value); |
| } |
| } |
| } |
| query = getComponentXPathQuery(clazz) + "/property[@name!='' and text()!='']"; |
| nodes = queryNodes(metadata, query); |
| if (nodes != null) { |
| for (int i = 0; i < nodes.getLength(); i++) { |
| Node node = nodes.item(i); |
| String name = getAttributeValue(node, "name"); |
| String[] value = StringUtils.split(StringUtils.trim(node.getTextContent()), "\n\r"); |
| for (int j = 0; j<value.length; j++) { |
| value[j] = StringUtils.trim(value[j]); |
| } |
| props.put(name, value); |
| } |
| } |
| return props; |
| } |
| |
| private static List<Reference> getReferences(Class clazz, Document metadata) { |
| List<Reference> references = new ArrayList<Reference>(); |
| String query = getComponentXPathQuery(clazz) + "/reference[@name!='']"; |
| NodeList nodes = queryNodes(metadata, query); |
| if (nodes != null) { |
| for (int i = 0; i < nodes.getLength(); i++) { |
| Node node = nodes.item(i); |
| references.add(new Reference(clazz, node)); |
| } |
| } |
| return references; |
| } |
| |
| private static String getLifecycleMethodName(Class clazz, Document metadata, String methodName) { |
| String query = getComponentXPathQuery(clazz); |
| Node node = queryNode(metadata, query); |
| if (node != null) { |
| return getAttributeValue(node, methodName); |
| } |
| return null; |
| } |
| |
| private static NodeList queryNodes(Document metadata, String xpathQuery) { |
| try { |
| XPath xpath = XPATH_FACTORY.newXPath(); |
| xpath.setNamespaceContext(NAMESPACE_CONTEXT); |
| return (NodeList) xpath.evaluate(xpathQuery, metadata, XPathConstants.NODESET); |
| } catch (XPathExpressionException ex) { |
| throw new RuntimeException("Error evaluating XPath: " + xpathQuery, ex); |
| } |
| } |
| |
| private static Node queryNode(Document metadata, String xpathQuery) { |
| try { |
| XPath xpath = XPATH_FACTORY.newXPath(); |
| xpath.setNamespaceContext(NAMESPACE_CONTEXT); |
| return (Node) xpath.evaluate(xpathQuery, metadata, XPathConstants.NODE); |
| } catch (XPathExpressionException ex) { |
| throw new RuntimeException("Error evaluating XPath: " + xpathQuery, ex); |
| } |
| } |
| |
| private static String getAttributeValue(Node node, String attributeName) { |
| Node namedItem = node.getAttributes().getNamedItem(attributeName); |
| if (namedItem != null) { |
| return namedItem.getNodeValue(); |
| } else { |
| return null; |
| } |
| } |
| |
| static class OsgiMetadata { |
| |
| private final Class<?> clazz; |
| private final String name; |
| private final Set<String> serviceInterfaces; |
| private final Map<String, Object> properties; |
| private final List<Reference> references; |
| private final String activateMethodName; |
| private final String deactivateMethodName; |
| private final String modifiedMethodName; |
| |
| private OsgiMetadata(Class<?> clazz, Document metadataDocument) { |
| this.clazz = clazz; |
| this.name = OsgiMetadataUtil.getComponentName(clazz, metadataDocument); |
| this.serviceInterfaces = OsgiMetadataUtil.getServiceInterfaces(clazz, metadataDocument); |
| this.properties = OsgiMetadataUtil.getProperties(clazz, metadataDocument); |
| this.references = OsgiMetadataUtil.getReferences(clazz, metadataDocument); |
| this.activateMethodName = OsgiMetadataUtil.getLifecycleMethodName(clazz, metadataDocument, "activate"); |
| this.deactivateMethodName = OsgiMetadataUtil.getLifecycleMethodName(clazz, metadataDocument, "deactivate"); |
| this.modifiedMethodName = OsgiMetadataUtil.getLifecycleMethodName(clazz, metadataDocument, "modified"); |
| } |
| |
| private OsgiMetadata() { |
| this.clazz = null; |
| this.name = null; |
| this.serviceInterfaces = null; |
| this.properties = null; |
| this.references = null; |
| this.activateMethodName = null; |
| this.deactivateMethodName = null; |
| this.modifiedMethodName = null; |
| } |
| |
| public Class<?> getServiceClass() { |
| return clazz; |
| } |
| |
| public String getName() { |
| return name; |
| } |
| |
| public String getPID() { |
| String pid = null; |
| if (properties != null) { |
| pid = (String)properties.get(Constants.SERVICE_PID); |
| } |
| return StringUtils.defaultString(pid, name); |
| } |
| |
| public Set<String> getServiceInterfaces() { |
| return serviceInterfaces; |
| } |
| |
| public Map<String, Object> getProperties() { |
| return properties; |
| } |
| |
| public List<Reference> getReferences() { |
| return references; |
| } |
| |
| public String getActivateMethodName() { |
| return activateMethodName; |
| } |
| |
| public String getDeactivateMethodName() { |
| return deactivateMethodName; |
| } |
| |
| public String getModifiedMethodName() { |
| return modifiedMethodName; |
| } |
| |
| } |
| |
| static class Reference { |
| |
| private final Class<?> clazz; |
| private final String name; |
| private final String interfaceType; |
| private final ReferenceCardinality cardinality; |
| private final ReferencePolicy policy; |
| private final ReferencePolicyOption policyOption; |
| private final String bind; |
| private final String unbind; |
| private final String field; |
| private final FieldCollectionType fieldCollectionType; |
| private final String target; |
| private final Filter targetFilter; |
| |
| private Reference(Class<?> clazz, Node node) { |
| this.clazz = clazz; |
| this.name = getAttributeValue(node, "name"); |
| this.interfaceType = getAttributeValue(node, "interface"); |
| this.cardinality = toCardinality(getAttributeValue(node, "cardinality")); |
| this.policy = toPolicy(getAttributeValue(node, "policy")); |
| this.policyOption = toPolicyOption(getAttributeValue(node, "policy-option")); |
| this.bind = getAttributeValue(node, "bind"); |
| this.unbind = getAttributeValue(node, "unbind"); |
| this.field = getAttributeValue(node, "field"); |
| this.fieldCollectionType = toFieldCollectionType(getAttributeValue(node, "field-collection-type")); |
| this.target = getAttributeValue(node, "target"); |
| if (StringUtils.isNotEmpty(this.target)) { |
| try { |
| this.targetFilter = new FilterImpl(this.target); |
| } catch (InvalidSyntaxException ex) { |
| throw new RuntimeException("Invalid target filter in reference '" + this.name + "' of class " + clazz.getName(), ex); |
| } |
| } |
| else { |
| this.targetFilter = null; |
| } |
| } |
| |
| public Class<?> getServiceClass() { |
| return clazz; |
| } |
| |
| public String getName() { |
| return this.name; |
| } |
| |
| public String getInterfaceType() { |
| return this.interfaceType; |
| } |
| |
| public Class getInterfaceTypeAsClass() { |
| try { |
| return Class.forName(getInterfaceType()); |
| } catch (ClassNotFoundException e) { |
| throw new RuntimeException("Service reference type not found: " + getInterfaceType()); |
| } |
| } |
| |
| public ReferenceCardinality getCardinality() { |
| return this.cardinality; |
| } |
| |
| public boolean isCardinalityMultiple() { |
| return this.cardinality == ReferenceCardinality.OPTIONAL_MULTIPLE |
| || this.cardinality == ReferenceCardinality.MANDATORY_MULTIPLE; |
| } |
| |
| public boolean isCardinalityOptional() { |
| return this.cardinality == ReferenceCardinality.OPTIONAL_UNARY |
| || this.cardinality == ReferenceCardinality.OPTIONAL_MULTIPLE; |
| } |
| |
| public ReferencePolicy getPolicy() { |
| return policy; |
| } |
| |
| public ReferencePolicyOption getPolicyOption() { |
| return policyOption; |
| } |
| |
| public String getBind() { |
| return this.bind; |
| } |
| |
| public String getUnbind() { |
| return this.unbind; |
| } |
| |
| public String getField() { |
| return this.field; |
| } |
| |
| public String getTarget() { |
| return this.target; |
| } |
| |
| public boolean matchesTargetFilter(ServiceReference<?> serviceReference) { |
| if (targetFilter == null) { |
| return true; |
| } |
| return targetFilter.match(serviceReference); |
| } |
| |
| public FieldCollectionType getFieldCollectionType() { |
| return this.fieldCollectionType; |
| } |
| |
| private static ReferenceCardinality toCardinality(String value) { |
| for (ReferenceCardinality item : ReferenceCardinality.values()) { |
| if (StringUtils.equals(item.getCardinalityString(), value)) { |
| return item; |
| } |
| } |
| return ReferenceCardinality.MANDATORY_UNARY; |
| } |
| |
| private static ReferencePolicy toPolicy(String value) { |
| for (ReferencePolicy item : ReferencePolicy.values()) { |
| if (StringUtils.equalsIgnoreCase(item.name(), value)) { |
| return item; |
| } |
| } |
| return ReferencePolicy.STATIC; |
| } |
| |
| private static ReferencePolicyOption toPolicyOption(String value) { |
| for (ReferencePolicyOption item : ReferencePolicyOption.values()) { |
| if (StringUtils.equalsIgnoreCase(item.name(), value)) { |
| return item; |
| } |
| } |
| return ReferencePolicyOption.RELUCTANT; |
| } |
| |
| private static FieldCollectionType toFieldCollectionType(String value) { |
| for (FieldCollectionType item : FieldCollectionType.values()) { |
| if (StringUtils.equalsIgnoreCase(item.name(), value)) { |
| return item; |
| } |
| } |
| return FieldCollectionType.SERVICE; |
| } |
| |
| } |
| |
| |
| /** |
| * Options for {@link Reference#cardinality()} property. |
| */ |
| enum ReferenceCardinality { |
| |
| /** |
| * Optional, unary reference: No service required to be available for the |
| * reference to be satisfied. Only a single service is available through this |
| * reference. |
| */ |
| OPTIONAL_UNARY("0..1"), |
| |
| /** |
| * Mandatory, unary reference: At least one service must be available for |
| * the reference to be satisfied. Only a single service is available through |
| * this reference. |
| */ |
| MANDATORY_UNARY("1..1"), |
| |
| /** |
| * Optional, multiple reference: No service required to be available for the |
| * reference to be satisfied. All matching services are available through |
| * this reference. |
| */ |
| OPTIONAL_MULTIPLE("0..n"), |
| |
| /** |
| * Mandatory, multiple reference: At least one service must be available for |
| * the reference to be satisfied. All matching services are available |
| * through this reference. |
| */ |
| MANDATORY_MULTIPLE("1..n"); |
| |
| private final String cardinalityString; |
| |
| private ReferenceCardinality(final String cardinalityString) { |
| this.cardinalityString = cardinalityString; |
| } |
| |
| /** |
| * @return String representation of cardinality |
| */ |
| public String getCardinalityString() { |
| return this.cardinalityString; |
| } |
| |
| } |
| |
| /** |
| * Options for {@link Reference#policy()} property. |
| */ |
| enum ReferencePolicy { |
| |
| /** |
| * The component will be deactivated and re-activated if the service comes |
| * and/or goes away. |
| */ |
| STATIC, |
| |
| /** |
| * The service will be made available to the component as it comes and goes. |
| */ |
| DYNAMIC; |
| } |
| |
| |
| /** |
| * Options for {@link Reference#policyOption()} property. |
| */ |
| enum ReferencePolicyOption { |
| |
| /** |
| * The reluctant policy option is the default policy option. |
| * When a new target service for a reference becomes available, |
| * references having the reluctant policy option for the static |
| * policy or the dynamic policy with a unary cardinality will |
| * ignore the new target service. References having the dynamic |
| * policy with a multiple cardinality will bind the new |
| * target service |
| */ |
| RELUCTANT, |
| |
| /** |
| * When a new target service for a reference becomes available, |
| * references having the greedy policy option will bind the new |
| * target service. |
| */ |
| GREEDY; |
| } |
| |
| /** |
| * Options for {@link Reference#policyOption()} property. |
| */ |
| enum FieldCollectionType { |
| |
| /** |
| * The bound service object. This is the default field collection type. |
| */ |
| SERVICE, |
| |
| /** |
| * A Service Reference for the bound service. |
| */ |
| REFERENCE, |
| |
| /** |
| * A Component Service Objects for the bound service. |
| */ |
| SERVICEOBJECTS, |
| |
| /** |
| * An unmodifiable Map containing the service properties of the bound service. |
| * This Map must implement Comparable. |
| */ |
| PROPERTIES, |
| |
| /** |
| * An unmodifiable Map.Entry whose key is an unmodifiable Map containing the |
| * service properties of the bound service, as above, and whose value is the |
| * bound service object. This Map.Entry must implement Comparable. |
| */ |
| TUPLE; |
| } |
| |
| } |