/* * 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; } }