This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.testing.osgi-mock-1.9.4 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-osgi-mock.git
commit 38aed31e906b64b2c722c15e966522fea8016ef2 Author: Stefan Seifert <[email protected]> AuthorDate: Wed Mar 1 17:29:06 2017 +0000 SLING-6586 Loading SCR metadata fails for components with name != class name git-svn-id: https://svn.apache.org/repos/asf/sling/branches/testing/mocks/osgi-mock-1.x@1785004 13f79535-47bb-0310-9956-ffa450edef68 --- pom.xml | 31 +++ .../testing/mock/osgi/NoScrMetadataException.java | 2 +- .../sling/testing/mock/osgi/OsgiMetadataUtil.java | 277 ++++++++++++++++----- 3 files changed, 242 insertions(+), 68 deletions(-) diff --git a/pom.xml b/pom.xml index 9db0b71..e6f1f5c 100644 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,37 @@ </dependency> <dependency> + <groupId>org.reflections</groupId> + <artifactId>reflections</artifactId> + <!-- Do not use version 0.9.10, it created threading issues when running junit tests in parallel (see SLING-5002) --> + <version>0.9.9</version> + <scope>compile</scope> + <!-- exclude all optional dependencies --> + <exclusions> + <exclusion> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </exclusion> + <exclusion> + <groupId>dom4j</groupId> + <artifactId>dom4j</artifactId> + </exclusion> + <exclusion> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + </exclusion> + <exclusion> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + </exclusion> + <exclusion> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.5</version> diff --git a/src/main/java/org/apache/sling/testing/mock/osgi/NoScrMetadataException.java b/src/main/java/org/apache/sling/testing/mock/osgi/NoScrMetadataException.java index 97b9260..0e9114d 100644 --- a/src/main/java/org/apache/sling/testing/mock/osgi/NoScrMetadataException.java +++ b/src/main/java/org/apache/sling/testing/mock/osgi/NoScrMetadataException.java @@ -25,7 +25,7 @@ public final class NoScrMetadataException extends RuntimeException { private static final long serialVersionUID = 1L; public NoScrMetadataException(Class<?> type) { - super("No OSGi SCR metadata found in classpath at " + OsgiMetadataUtil.getMetadataPath(type)); + super("No OSGi SCR metadata found for class " + OsgiMetadataUtil.cleanupClassName(type.getName())); } } diff --git a/src/main/java/org/apache/sling/testing/mock/osgi/OsgiMetadataUtil.java b/src/main/java/org/apache/sling/testing/mock/osgi/OsgiMetadataUtil.java index 21443f5..c209bab 100644 --- a/src/main/java/org/apache/sling/testing/mock/osgi/OsgiMetadataUtil.java +++ b/src/main/java/org/apache/sling/testing/mock/osgi/OsgiMetadataUtil.java @@ -20,9 +20,7 @@ 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; @@ -30,6 +28,7 @@ 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; @@ -37,10 +36,17 @@ 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.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; @@ -53,7 +59,6 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; -import com.google.common.collect.ImmutableList; /** * Helper methods to parse OSGi metadata. @@ -61,11 +66,16 @@ import com.google.common.collect.ImmutableList; 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(); @@ -76,30 +86,7 @@ final class OsgiMetadataUtil { } private static final OsgiMetadata NULL_METADATA = new OsgiMetadata(); - - /* - * 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 LoadingCache<Class, OsgiMetadata> METADATA_CACHE = CacheBuilder.newBuilder().build(new CacheLoader<Class, OsgiMetadata>() { - @Override - public OsgiMetadata load(Class clazz) throws Exception { - List<Document> metadataDocuments = OsgiMetadataUtil.getMetadataDocument(clazz); - if (metadataDocuments != null) { - for (Document metadataDocument : metadataDocuments) { - if (matchesService(clazz, metadataDocument)) { - return new OsgiMetadata(clazz, metadataDocument); - } - } - } - return NULL_METADATA; - } - }); - - private OsgiMetadataUtil() { - // static methods only - } - + private static final NamespaceContext NAMESPACE_CONTEXT = new NamespaceContext() { @Override public String getNamespaceURI(String prefix) { @@ -117,14 +104,26 @@ final class OsgiMetadataUtil { } }; - public static String getMetadataPath(Class clazz) { - return "OSGI-INF/" + StringUtils.substringBefore(clazz.getName(), "$") + ".xml"; - } + /* + * 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; + } + }); - public static String getOldMetadataMultiPath() { - return "OSGI-INF/serviceComponents.xml"; + 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. @@ -145,36 +144,79 @@ final class OsgiMetadataUtil { 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<String,Document>(); + + 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); + } - private static List<Document> getMetadataDocument(Class clazz) { - String metadataPath = getMetadataPath(clazz); - InputStream metadataStream = OsgiMetadataUtil.class.getClassLoader().getResourceAsStream(metadataPath); - if (metadataStream == null) { - String oldMetadataPath = getOldMetadataMultiPath(); - log.debug("No OSGi metadata found at {}, try to fallback to {}", metadataPath, oldMetadataPath); - - try { - Enumeration<URL> metadataUrls = OsgiMetadataUtil.class.getClassLoader().getResources(oldMetadataPath); - List<Document> docs = new ArrayList<Document>(); - while (metadataUrls.hasMoreElements()) { - URL metadataUrl = metadataUrls.nextElement(); - metadataStream = metadataUrl.openStream(); - docs.add(toXmlDocument(metadataStream, oldMetadataPath)); - } - if (docs.size() == 0) { - return null; + 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) { + InputStream fileStream = null; + try { + fileStream = OsgiMetadataUtil.class.getClassLoader().getResourceAsStream(resourcePath); + if (fileStream != null) { + 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); + } + } + } + } + } + catch (Throwable ex) { + log.warn("Error reading SCR metadata XML document from " + resourcePath, ex); + } + finally { + if (fileStream != null) { + try { + fileStream.close(); } - else { - return docs; + catch (IOException e) { + // ignore } } - catch (IOException ex) { - throw new RuntimeException("Unable to read classpath resource: " + oldMetadataPath, ex); - } } - else { - return ImmutableList.of(toXmlDocument(metadataStream, metadataPath)); + } + + 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) { @@ -202,16 +244,19 @@ final class OsgiMetadataUtil { * @return XPath query fragment to find matching XML node in SCR metadata */ private static String getComponentXPathQuery(Class clazz) { - String className = StringUtils.substringBefore(clazz.getName(), "$$Enhancer"); + String className = cleanupClassName(clazz.getName()); return "//*[implementation/@class='" + className + "' or @name='" + className + "']"; } - - private static boolean matchesService(Class clazz, Document metadata) { - String query = getComponentXPathQuery(clazz); - NodeList nodes = queryNodes(metadata, query); - return nodes != null && nodes.getLength() > 0; + + /** + * 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 Set<String> getServiceInterfaces(Class clazz, Document metadata) { Set<String> serviceInterfaces = new HashSet<String>(); String query = getComponentXPathQuery(clazz) + "/service/provide[@interface!='']"; @@ -389,6 +434,10 @@ final class OsgiMetadataUtil { 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; @@ -399,6 +448,19 @@ final class OsgiMetadataUtil { 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() { @@ -413,9 +475,27 @@ final class OsgiMetadataUtil { 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; @@ -424,7 +504,7 @@ final class OsgiMetadataUtil { public ReferencePolicyOption getPolicyOption() { return policyOption; } - + public String getBind() { return this.bind; } @@ -433,6 +513,25 @@ final class OsgiMetadataUtil { 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)) { @@ -459,7 +558,16 @@ final class OsgiMetadataUtil { } 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; + } + } @@ -528,6 +636,7 @@ final class OsgiMetadataUtil { DYNAMIC; } + /** * Options for {@link Reference#policyOption()} property. */ @@ -551,5 +660,39 @@ final class OsgiMetadataUtil { */ 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; + } + } -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
