You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gn...@apache.org on 2018/04/20 10:49:37 UTC

svn commit: r1829644 - in /felix/trunk/utils/src: main/java/org/apache/felix/utils/repository/ test/java/org/apache/felix/utils/repository/ test/resources/org/ test/resources/org/apache/ test/resources/org/apache/felix/ test/resources/org/apache/felix/...

Author: gnodet
Date: Fri Apr 20 10:49:37 2018
New Revision: 1829644

URL: http://svn.apache.org/viewvc?rev=1829644&view=rev
Log:
[FELIX-5839] Add xml and json repositories implemenation

Added:
    felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/
    felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/AggregateRepository.java
    felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/BaseRepository.java
    felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/JsonRepository.java
    felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/StaxParser.java
    felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/UrlLoader.java
    felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/XmlRepository.java
    felix/trunk/utils/src/test/java/org/apache/felix/utils/repository/
    felix/trunk/utils/src/test/java/org/apache/felix/utils/repository/RepositoryTest.java
    felix/trunk/utils/src/test/resources/org/
    felix/trunk/utils/src/test/resources/org/apache/
    felix/trunk/utils/src/test/resources/org/apache/felix/
    felix/trunk/utils/src/test/resources/org/apache/felix/utils/
    felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/
    felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.json
    felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.xml

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/AggregateRepository.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/AggregateRepository.java?rev=1829644&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/AggregateRepository.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/AggregateRepository.java Fri Apr 20 10:49:37 2018
@@ -0,0 +1,55 @@
+/*
+ * 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.felix.utils.repository;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.service.repository.Repository;
+
+public class AggregateRepository implements Repository {
+
+    private final Collection<Repository> repositories;
+
+    public AggregateRepository(Collection<Repository> repositories) {
+        this.repositories = repositories;
+    }
+
+    @Override
+    public Map<Requirement, Collection<Capability>> findProviders(Collection<? extends Requirement> requirements) {
+        Map<Requirement, Collection<Capability>> result = new HashMap<>();
+        for (Requirement requirement : requirements) {
+            List<Capability> caps = new ArrayList<>();
+            for (Repository repository : repositories) {
+                Map<Requirement, Collection<Capability>> resMap =
+                        repository.findProviders(Collections.singleton(requirement));
+                Collection<Capability> res = resMap != null ? resMap.get(requirement) : null;
+                if (res != null) {
+                    caps.addAll(res);
+                }
+            }
+            result.put(requirement, caps);
+        }
+        return result;
+    }
+}

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/BaseRepository.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/BaseRepository.java?rev=1829644&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/BaseRepository.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/BaseRepository.java Fri Apr 20 10:49:37 2018
@@ -0,0 +1,93 @@
+/*
+ * 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.felix.utils.repository;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.utils.resource.CapabilitySet;
+import org.apache.felix.utils.resource.RequirementImpl;
+import org.apache.felix.utils.resource.SimpleFilter;
+import org.osgi.framework.Constants;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import org.osgi.service.repository.Repository;
+
+/**
+ */
+public class BaseRepository implements Repository {
+
+    protected final List<Resource> resources;
+    protected final Map<String, CapabilitySet> capSets;
+
+    public BaseRepository() {
+        this.resources = new ArrayList<>();
+        this.capSets = new HashMap<>();
+    }
+
+    public BaseRepository(Collection<Resource> resources) {
+        this();
+        for (Resource resource : resources) {
+            addResource(resource);
+        }
+    }
+
+    protected void addResource(Resource resource) {
+        for (Capability cap : resource.getCapabilities(null)) {
+            String ns = cap.getNamespace();
+            CapabilitySet cs = capSets.get(ns);
+            if (cs == null) {
+                cs = new CapabilitySet(Collections.singletonList(ns));
+                capSets.put(ns, cs);
+            }
+            cs.addCapability(cap);
+        }
+        resources.add(resource);
+    }
+
+    public List<Resource> getResources() {
+        return resources;
+    }
+
+    @Override
+    public Map<Requirement, Collection<Capability>> findProviders(Collection<? extends Requirement> requirements) {
+        Map<Requirement, Collection<Capability>> result = new HashMap<>();
+        for (Requirement requirement : requirements) {
+            CapabilitySet set = capSets.get(requirement.getNamespace());
+            if (set != null) {
+                SimpleFilter sf;
+                if (requirement instanceof RequirementImpl) {
+                    sf = ((RequirementImpl) requirement).getFilter();
+                } else {
+                    String filter = requirement.getDirectives().get(Constants.FILTER_DIRECTIVE);
+                    sf = (filter != null)
+                            ? SimpleFilter.parse(filter)
+                            : SimpleFilter.MATCH_ALL_FILTER;
+                }
+                result.put(requirement, set.match(sf, true));
+            } else {
+                result.put(requirement, Collections.<Capability>emptyList());
+            }
+        }
+        return result;
+    }
+}

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/JsonRepository.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/JsonRepository.java?rev=1829644&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/JsonRepository.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/JsonRepository.java Fri Apr 20 10:49:37 2018
@@ -0,0 +1,118 @@
+/*
+ * 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.felix.utils.repository;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.apache.felix.utils.json.JSONParser;
+import org.apache.felix.utils.resource.ResourceBuilder;
+import org.osgi.framework.BundleException;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+
+/**
+ * Repository using a JSON representation of resource metadata.
+ * The json should be a map: the key is the resource uri and the
+ * value is a map of resource headers.
+ * The content of the URL can be gzipped.
+ */
+public class JsonRepository extends BaseRepository {
+
+    private final UrlLoader loader;
+    private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+    public JsonRepository(String url, long expiration) {
+        loader = new UrlLoader(url, expiration) {
+            @Override
+            protected boolean doRead(InputStream is) throws IOException {
+                return JsonRepository.this.doRead(is);
+            }
+        };
+    }
+
+    @Override
+    public List<Resource> getResources() {
+        checkAndLoadCache();
+        lock.readLock().lock();
+        try {
+            return super.getResources();
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    @Override
+    public Map<Requirement, Collection<Capability>> findProviders(Collection<? extends Requirement> requirements) {
+        checkAndLoadCache();
+        lock.readLock().lock();
+        try {
+            return super.findProviders(requirements);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    private void checkAndLoadCache() {
+        loader.checkAndLoadCache();
+    }
+
+    protected boolean doRead(InputStream is) throws IOException {
+        Map<String, Map<String, String>> metadatas = verify(new JSONParser(is).getParsed());
+        lock.writeLock().lock();
+        try {
+            resources.clear();
+            capSets.clear();
+            for (Map.Entry<String, Map<String, String>> metadata : metadatas.entrySet()) {
+                buildResource(metadata.getKey(), metadata.getValue());
+            }
+            return true;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    protected void buildResource(String uri, Map<String, String> headerMap) throws IOException {
+        try {
+            Resource resource = ResourceBuilder.build(uri, headerMap);
+            addResource(resource);
+        } catch (BundleException e) {
+            throw new IOException("Unable to read resource: " + uri, e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Map<String, String>> verify(Object value) {
+        Map<?, ?> obj = Map.class.cast(value);
+        for (Map.Entry<?, ?> entry : obj.entrySet()) {
+            String.class.cast(entry.getKey());
+            Map<?, ?> child = Map.class.cast(entry.getValue());
+            for (Map.Entry<?, ?> ce : child.entrySet()) {
+                String.class.cast(ce.getKey());
+                String.class.cast(ce.getValue());
+            }
+        }
+        return (Map<String, Map<String, String>>) obj;
+    }
+
+}

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/StaxParser.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/StaxParser.java?rev=1829644&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/StaxParser.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/StaxParser.java Fri Apr 20 10:49:37 2018
@@ -0,0 +1,451 @@
+/*
+ * 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.felix.utils.repository;
+
+import java.io.InputStream;
+import java.io.Writer;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.xml.stream.Location;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+import org.apache.felix.utils.resource.CapabilityImpl;
+import org.apache.felix.utils.resource.RequirementImpl;
+import org.apache.felix.utils.resource.ResourceImpl;
+import org.apache.felix.utils.resource.SimpleFilter;
+import org.apache.felix.utils.version.VersionTable;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import org.osgi.service.repository.ContentNamespace;
+
+import static javax.xml.stream.XMLStreamConstants.CHARACTERS;
+import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
+import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
+
+/**
+ * Repository XML xml based on StaX
+ */
+public final class StaxParser {
+
+    public static final String REPOSITORY = "repository";
+    public static final String REPO_NAME = "name";
+    public static final String INCREMENT = "increment";
+    public static final String REFERRAL = "referral";
+    public static final String DEPTH = "depth";
+    public static final String URL = "url";
+    public static final String RESOURCE = "resource";
+    public static final String CAPABILITY = "capability";
+    public static final String REQUIREMENT = "requirement";
+    public static final String NAMESPACE = "namespace";
+    public static final String ATTRIBUTE = "attribute";
+    public static final String DIRECTIVE = "directive";
+    public static final String NAME = "name";
+    public static final String VALUE = "value";
+    public static final String TYPE = "type";
+
+    public static final String REPOSITORY_NAMESPACE = "http://www.osgi.org/xmlns/repository/v1.0.0";
+
+    static XMLInputFactory inputFactory;
+    static XMLOutputFactory outputFactory;
+
+    private StaxParser() {
+    }
+
+    public static class Referral {
+        public String url;
+        public int depth = Integer.MAX_VALUE;
+    }
+
+    public static class XmlRepository {
+        public String name;
+        public long increment;
+        public List<Referral> referrals = new ArrayList<>();
+        public List<Resource> resources = new ArrayList<>();
+    }
+
+    public static void write(XmlRepository repository, Writer os) throws XMLStreamException {
+        XMLStreamWriter writer = getOutputFactory().createXMLStreamWriter(os);
+        try {
+            writer.writeStartDocument();
+            writer.setDefaultNamespace(REPOSITORY_NAMESPACE);
+            // repository element
+            writer.writeStartElement(REPOSITORY_NAMESPACE, REPOSITORY);
+            writer.writeAttribute("xmlns", REPOSITORY_NAMESPACE);
+            writer.writeAttribute(REPO_NAME, repository.name);
+            writer.writeAttribute(INCREMENT, Long.toString(repository.increment));
+            // referrals
+            for (Referral referral : repository.referrals) {
+                writer.writeStartElement(REPOSITORY_NAMESPACE, REFERRAL);
+                writer.writeAttribute(DEPTH, Integer.toString(referral.depth));
+                writer.writeAttribute(URL, referral.url);
+                writer.writeEndElement();
+            }
+            // resources
+            for (Resource resource : repository.resources) {
+                writer.writeStartElement(REPOSITORY_NAMESPACE, RESOURCE);
+                for (Capability cap : resource.getCapabilities(null)) {
+                    writeClause(writer, CAPABILITY, cap.getNamespace(), cap.getDirectives(), cap.getAttributes());
+                }
+                for (Requirement req : resource.getRequirements(null)) {
+                    writeClause(writer, REQUIREMENT, req.getNamespace(), req.getDirectives(), req.getAttributes());
+                }
+                writer.writeEndElement();
+            }
+            writer.writeEndDocument();
+            writer.flush();
+        } finally {
+            writer.close();
+        }
+    }
+
+    private static void writeClause(XMLStreamWriter writer, String element, String namespace, Map<String, String> directives, Map<String, Object> attributes) throws XMLStreamException {
+        writer.writeStartElement(REPOSITORY_NAMESPACE, element);
+        writer.writeAttribute(NAMESPACE, namespace);
+        for (Map.Entry<String, String> dir : directives.entrySet()) {
+            writer.writeStartElement(REPOSITORY_NAMESPACE, DIRECTIVE);
+            writer.writeAttribute(NAME, dir.getKey());
+            writer.writeAttribute(VALUE, dir.getValue());
+            writer.writeEndElement();
+        }
+        for (Map.Entry<String, Object> att : attributes.entrySet()) {
+            String key = att.getKey();
+            Object val = att.getValue();
+            writer.writeStartElement(REPOSITORY_NAMESPACE, ATTRIBUTE);
+            writer.writeAttribute(NAME, key);
+            if (val instanceof Version) {
+                writer.writeAttribute(TYPE, "Version");
+            } else if (val instanceof Long) {
+                writer.writeAttribute(TYPE, "Long");
+            } else if (val instanceof Double) {
+                writer.writeAttribute(TYPE, "Double");
+            } else if (val instanceof Iterable) {
+                Iterable<?> it = (Iterable<?>) att.getValue();
+                String scalar = null;
+                for (Object o : it) {
+                    String ts;
+                    if (o instanceof String) {
+                        ts = "String";
+                    } else if (o instanceof Long) {
+                        ts = "Long";
+                    } else if (o instanceof Double) {
+                        ts = "Double";
+                    } else if (o instanceof Version) {
+                        ts = "Version";
+                    } else {
+                        throw new IllegalArgumentException("Unsupported scalar type: " + o);
+                    }
+                    if (scalar == null) {
+                        scalar = ts;
+                    } else if (!scalar.equals(ts)) {
+                        throw new IllegalArgumentException("Unconsistent list type for attribute " + key);
+                    }
+                }
+                writer.writeAttribute(TYPE, "List<" + scalar + ">");
+                StringBuilder sb = new StringBuilder();
+                boolean first = true;
+                for (Object o : it) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        sb.append(",");
+                    }
+                    sb.append(o.toString().replace(",", "\\,"));
+                }
+                val = sb.toString();
+            }
+            writer.writeAttribute(VALUE, val.toString());
+            writer.writeEndElement();
+        }
+        writer.writeEndElement();
+    }
+
+    public static XmlRepository parse(InputStream is) throws XMLStreamException {
+        return parse(null, is, null);
+    }
+
+    public static XmlRepository parse(URI repositoryUrl, InputStream is) throws XMLStreamException {
+        return parse(repositoryUrl, is, null);
+    }
+
+    public static XmlRepository parse(URI repositoryUrl, InputStream is, XmlRepository previous) throws XMLStreamException {
+        XMLStreamReader reader = getInputFactory().createXMLStreamReader(is);
+        try {
+            int event = reader.nextTag();
+            if (event != START_ELEMENT || !REPOSITORY.equals(reader.getLocalName())) {
+                throw new IllegalStateException("Expected element 'repository' at the root of the document");
+            }
+            XmlRepository repo = new XmlRepository();
+            for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
+                String attrName = reader.getAttributeLocalName(i);
+                String attrValue = reader.getAttributeValue(i);
+                switch (attrName) {
+                case REPO_NAME:
+                    repo.name = attrValue;
+                    break;
+                case INCREMENT:
+                    repo.increment = Long.parseLong(attrValue);
+                    break;
+                default:
+                    throw new IllegalStateException("Unexpected attribute '" + attrName + "'");
+                }
+            }
+            if (previous != null && repo.increment == previous.increment) {
+                return previous;
+            }
+            while ((event = reader.nextTag()) == START_ELEMENT) {
+                String element = reader.getLocalName();
+                switch (element) {
+                case REFERRAL:
+                    Referral referral = new Referral();
+                    for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
+                        String attrName = reader.getAttributeLocalName(i);
+                        String attrValue = reader.getAttributeValue(i);
+                        switch (attrName) {
+                        case DEPTH:
+                            referral.depth = Integer.parseInt(attrValue);
+                            break;
+                        case URL:
+                            referral.url = attrValue;
+                            break;
+                        default:
+                            throw new IllegalStateException("Unexpected attribute '" + attrName + "'");
+                        }
+                    }
+                    if (referral.url == null) {
+                        throw new IllegalStateException("Expected attribute '" + URL + "'");
+                    }
+                    repo.referrals.add(referral);
+                    sanityCheckEndElement(reader, reader.nextTag(), REFERRAL);
+                    break;
+                case RESOURCE:
+                    repo.resources.add(parseResource(repositoryUrl, reader));
+                    break;
+                default:
+                    throw new IllegalStateException("Unsupported element '" + element + "'. Expected 'referral' or 'resource'");
+                }
+            }
+            // Sanity check
+            sanityCheckEndElement(reader, event, REPOSITORY);
+            return repo;
+        } finally {
+            reader.close();
+        }
+    }
+
+    private static void sanityCheckEndElement(XMLStreamReader reader, int event, String element) {
+        if (event != END_ELEMENT || !element.equals(reader.getLocalName())) {
+            throw new IllegalStateException("Unexpected state while finishing element " + element);
+        }
+    }
+
+    private static ResourceImpl parseResource(URI repositoryUrl, XMLStreamReader reader) {
+        try {
+            if (reader.getAttributeCount() > 0) {
+                throw new IllegalStateException("Unexpected attribute '" + reader.getAttributeLocalName(0) + "'");
+            }
+            ResourceImpl resource = new ResourceImpl();
+            int event;
+            while ((event = reader.nextTag()) == START_ELEMENT) {
+                String element = reader.getLocalName();
+                switch (element) {
+                case CAPABILITY:
+                    CapabilityImpl cap = parseCapability(reader, resource);
+                    // Resolve relative resource urls now
+                    if (repositoryUrl != null && ContentNamespace.CONTENT_NAMESPACE.equals(cap.getNamespace())) {
+                        Object url = cap.getAttributes().get(ContentNamespace.CAPABILITY_URL_ATTRIBUTE);
+                        if (url instanceof String) {
+                            url = repositoryUrl.resolve(url.toString()).toString();
+                            cap.getAttributes().put(ContentNamespace.CAPABILITY_URL_ATTRIBUTE, url);
+                        }
+                    }
+                    resource.addCapability(cap);
+                    break;
+                case REQUIREMENT:
+                    resource.addRequirement(parseRequirement(reader, resource));
+                    break;
+                default:
+                    while ((event = reader.next()) != END_ELEMENT) {
+                        switch (event) {
+                        case START_ELEMENT:
+                            throw new IllegalStateException("Unexpected element '" + reader.getLocalName() + "' inside 'resource' element");
+                        case CHARACTERS:
+                            throw new IllegalStateException("Unexpected text inside 'resource' element");
+                        default:
+                            break;
+                        }
+                    }
+                    break;
+                }
+            }
+            // Sanity check
+            sanityCheckEndElement(reader, event, RESOURCE);
+            return resource;
+        } catch (Exception e) {
+            Location loc = reader.getLocation();
+            if (loc != null) {
+                throw new IllegalStateException("Error while parsing resource at line " + loc.getLineNumber() + " and column " + loc.getColumnNumber(), e);
+            } else {
+                throw new IllegalStateException("Error while parsing resource", e);
+            }
+        }
+    }
+
+    private static CapabilityImpl parseCapability(XMLStreamReader reader, ResourceImpl resource) throws XMLStreamException {
+        String[] namespace = new String[1];
+        Map<String, String> directives = new HashMap<>();
+        Map<String, Object> attributes = new HashMap<>();
+        parseClause(reader, namespace, directives, attributes);
+        sanityCheckEndElement(reader, reader.getEventType(), CAPABILITY);
+        return new CapabilityImpl(resource, namespace[0], directives, attributes);
+    }
+
+    private static RequirementImpl parseRequirement(XMLStreamReader reader, ResourceImpl resource) throws XMLStreamException {
+        String[] namespace = new String[1];
+        Map<String, String> directives = new HashMap<>();
+        Map<String, Object> attributes = new HashMap<>();
+        parseClause(reader, namespace, directives, attributes);
+        sanityCheckEndElement(reader, reader.getEventType(), REQUIREMENT);
+        String filterStr = directives.get(Constants.FILTER_DIRECTIVE);
+        SimpleFilter sf = (filterStr != null)
+                ? SimpleFilter.parse(filterStr)
+                : SimpleFilter.convert(attributes);
+        return new RequirementImpl(resource, namespace[0], directives, attributes, sf);
+    }
+
+    private static void parseClause(XMLStreamReader reader, String[] namespace, Map<String, String> directives, Map<String, Object> attributes) throws XMLStreamException {
+        namespace[0] = null;
+        for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
+            String name = reader.getAttributeLocalName(i);
+            String value = reader.getAttributeValue(i);
+            if (NAMESPACE.equals(name)) {
+                namespace[0] = value;
+            } else {
+                throw new IllegalStateException("Unexpected attribute: '" + name + "'. Expected 'namespace'");
+            }
+        }
+        if (namespace[0] == null) {
+            throw new IllegalStateException("Expected attribute 'namespace'");
+        }
+        while (reader.nextTag() == START_ELEMENT) {
+            String element = reader.getLocalName();
+            switch (element) {
+            case DIRECTIVE: {
+                String name = null;
+                String value = null;
+                for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
+                    String attName = reader.getAttributeLocalName(i);
+                    String attValue = reader.getAttributeValue(i);
+                    switch (attName) {
+                    case NAME:
+                        name = attValue;
+                        break;
+                    case VALUE:
+                        value = attValue;
+                        break;
+                    default:
+                        throw new IllegalStateException("Unexpected attribute: '" + attName + "'. Expected 'name', or 'value'.");
+                    }
+                }
+                if (name == null || value == null) {
+                    throw new IllegalStateException("Expected attribute 'name' and 'value'");
+                }
+                directives.put(name, value);
+                sanityCheckEndElement(reader, reader.nextTag(), DIRECTIVE);
+                break;
+            }
+            case ATTRIBUTE: {
+                String name = null;
+                String value = null;
+                String type = "String";
+                for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
+                    String attName = reader.getAttributeLocalName(i);
+                    String attValue = reader.getAttributeValue(i);
+                    switch (attName) {
+                    case NAME:
+                        name = attValue;
+                        break;
+                    case VALUE:
+                        value = attValue;
+                        break;
+                    case TYPE:
+                        type = attValue;
+                        break;
+                    default:
+                        throw new IllegalStateException("Unexpected attribute: '" + attName + "'. Expected 'name', 'value' or 'type'.");
+                    }
+                }
+                if (name == null || value == null) {
+                    throw new IllegalStateException("Expected attribute 'name' and 'value'");
+                }
+                attributes.put(name, parseAttribute(value, type));
+                sanityCheckEndElement(reader, reader.nextTag(), ATTRIBUTE);
+                break;
+            }
+            default:
+                throw new IllegalStateException("Unexpected element: '" + element + ". Expected 'directive' or 'attribute'");
+            }
+        }
+    }
+
+    private static Object parseAttribute(String value, String type) {
+        if ("String".equals(type)) {
+            return value;
+        } else if ("Version".equals(type)) {
+            return VersionTable.getVersion(value);
+        } else if ("Long".equals(type)) {
+            return Long.parseLong(value.trim());
+        } else if ("Double".equals(type)) {
+            return Double.parseDouble(value.trim());
+        } else if (type.startsWith("List<") && type.endsWith(">")) {
+            type = type.substring("List<".length(), type.length() - 1);
+            List<Object> list = new ArrayList<>();
+            for (String s : value.split(",")) {
+                list.add(parseAttribute(s.trim(), type));
+            }
+            return list;
+        } else {
+            throw new IllegalStateException("Unexpected type: '" + type + "'");
+        }
+    }
+
+    private static synchronized XMLInputFactory getInputFactory() {
+        if (StaxParser.inputFactory == null) {
+            XMLInputFactory factory = XMLInputFactory.newInstance();
+            factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);
+            StaxParser.inputFactory = factory;
+        }
+        return StaxParser.inputFactory;
+    }
+
+    private static synchronized XMLOutputFactory getOutputFactory() {
+        if (StaxParser.outputFactory == null) {
+            StaxParser.outputFactory = XMLOutputFactory.newInstance();
+        }
+        return StaxParser.outputFactory;
+    }
+
+}
\ No newline at end of file

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/UrlLoader.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/UrlLoader.java?rev=1829644&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/UrlLoader.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/UrlLoader.java Fri Apr 20 10:49:37 2018
@@ -0,0 +1,163 @@
+/*
+ * 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.felix.utils.repository;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.GZIPInputStream;
+
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
+import static java.net.HttpURLConnection.HTTP_OK;
+
+/**
+ */
+public abstract class UrlLoader {
+
+    public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
+    public static final String GZIP = "gzip";
+
+    private final String url;
+    private final long expiration;
+    private long lastModified;
+    private long lastChecked;
+
+    public UrlLoader(String url, long expiration) {
+        this.url = url;
+        this.expiration = expiration;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    protected boolean checkAndLoadCache() {
+        long time = System.currentTimeMillis();
+        if (lastChecked > 0) {
+            if (expiration < 0 || time - lastChecked < expiration) {
+                return false;
+            }
+        }
+        try {
+            URL u = new URL(url);
+            URLConnection connection = u.openConnection();
+            if (connection instanceof HttpURLConnection) {
+                HttpURLConnection con = (HttpURLConnection) connection;
+                if (lastModified > 0) {
+                    con.setIfModifiedSince(lastModified);
+                }
+                con.setRequestProperty(HEADER_ACCEPT_ENCODING, GZIP);
+                if (u.getUserInfo() != null)  {
+                    String encoded = base64((u.getUserInfo()).getBytes(StandardCharsets.UTF_8));
+                    connection.setRequestProperty("Authorization", "Basic " + encoded);
+                }
+                int rc = con.getResponseCode();
+                if (rc == HTTP_NOT_MODIFIED) {
+                    lastChecked = time;
+                    return false;
+                }
+                if (rc != HTTP_OK) {
+                    throw new IOException("Unexpected http response loading " + url + " : " + rc + " " + con.getResponseMessage());
+                }
+            }
+            
+            if (didNotChange(connection)) {
+                lastChecked = time;
+                return false;
+            }
+            boolean wasRead = read(connection);
+            lastModified = connection.getLastModified();
+            lastChecked = time;
+            return wasRead;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private boolean didNotChange(URLConnection connection) {
+        long lm = connection.getLastModified();
+        return lm > 0 && lm <= lastModified;
+    }
+
+    private boolean read(URLConnection connection) throws IOException {
+        InputStream is = null;
+        try {
+            is = new BufferedInputStream(connection.getInputStream());
+            if (isGzipStream(is)) {
+                is = new GZIPInputStream(is);
+            }
+            return doRead(is);
+        } finally {
+            // cannot be use try-with-resources, as it would not close GZIPInpuStream
+            if (is != null) {
+                is.close();
+            }
+        }
+    }
+
+    private boolean isGzipStream(InputStream is) throws IOException {
+        is.mark(512);
+        int b0 = is.read();
+        int b1 = is.read();
+        is.reset();
+        return (b0 == 0x1f && b1 == 0x8b);
+    }
+
+    protected abstract boolean doRead(InputStream is) throws IOException;
+
+    static final String	alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+    protected static String base64(byte[] in) {
+        StringBuilder sb = new StringBuilder();
+        int idx = 0;
+        int buf = 0;
+        int bits = 0;
+        int out = 0;
+
+        while (true) {
+            if (bits >= 6) {
+                bits -= 6;
+                int v = 0x3F & (buf >> bits);
+                sb.append(alphabet.charAt(v));
+                out++;
+            } else {
+                int c = idx < in.length ? in[idx] : -1;
+                if (c < 0)
+                    break;
+                buf <<= 8;
+                buf |= 0xFF & c;
+                bits += 8;
+            }
+        }
+        if (bits != 0) {// must be less than 7
+            sb.append(alphabet.charAt(0x3F & (buf << (6 - bits))));
+            out++;
+        }
+        int mod = 4 - (out % 4);
+        if (mod != 4) {
+            for (int i = 0; i < mod; i++) {
+                sb.append('=');
+            }
+        }
+        return sb.toString();
+    }
+
+}

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/XmlRepository.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/XmlRepository.java?rev=1829644&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/XmlRepository.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/repository/XmlRepository.java Fri Apr 20 10:49:37 2018
@@ -0,0 +1,178 @@
+/*
+ * 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.felix.utils.repository;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import javax.xml.stream.XMLStreamException;
+
+import org.apache.felix.utils.resource.CapabilitySet;
+import org.apache.felix.utils.resource.SimpleFilter;
+import org.osgi.framework.Version;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+
+import static org.osgi.framework.namespace.IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE;
+import static org.osgi.framework.namespace.IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE;
+import static org.osgi.framework.namespace.IdentityNamespace.IDENTITY_NAMESPACE;
+
+/**
+ * Repository conforming to the OSGi Repository specification.
+ * The content of the URL can be gzipped.
+ */
+public class XmlRepository extends BaseRepository {
+
+    protected final String url;
+    protected final long expiration;
+    protected final Map<String, XmlLoader> loaders = new HashMap<>();
+    protected final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+    public XmlRepository(String url, long expiration) {
+        this.url = url;
+        this.expiration = expiration;
+    }
+
+    @Override
+    public List<Resource> getResources() {
+        checkAndLoadCache();
+        return super.getResources();
+    }
+
+    @Override
+    public Map<Requirement, Collection<Capability>> findProviders(Collection<? extends Requirement> requirements) {
+        checkAndLoadCache();
+        return super.findProviders(requirements);
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    protected Map<String, XmlLoader> getLoaders() {
+        return loaders;
+    }
+
+    @Override
+    protected void addResource(Resource resource) {
+        List<Capability> identities = resource.getCapabilities(IDENTITY_NAMESPACE);
+        if (identities.isEmpty()) {
+            throw new IllegalStateException("Invalid resource: a capability with 'osgi.identity' namespace is required");
+        } else if (identities.size() > 1) {
+            throw new IllegalStateException("Invalid resource: multiple 'osgi.identity' capabilities found");
+        }
+        Capability identity = identities.get(0);
+        Object name = identity.getAttributes().get(IDENTITY_NAMESPACE);
+        Object type = identity.getAttributes().get(CAPABILITY_TYPE_ATTRIBUTE);
+        Object vers = identity.getAttributes().get(CAPABILITY_VERSION_ATTRIBUTE);
+        if (!String.class.isInstance(name)
+                || !String.class.isInstance(type)
+                || !Version.class.isInstance(vers)) {
+            throw new IllegalStateException("Invalid osgi.identity capability: " + identity);
+        }
+        if (!hasResource((String) type, (String) name, (Version) vers)) {
+            super.addResource(resource);
+        }
+    }
+
+    private boolean hasResource(String type, String name, Version version) {
+        CapabilitySet set = capSets.get(IDENTITY_NAMESPACE);
+        if (set != null) {
+            Map<String, Object> attrs = new HashMap<>();
+            attrs.put(CAPABILITY_TYPE_ATTRIBUTE, type);
+            attrs.put(IDENTITY_NAMESPACE, name);
+            attrs.put(CAPABILITY_VERSION_ATTRIBUTE, version);
+            SimpleFilter sf = SimpleFilter.convert(attrs);
+            return !set.match(sf, true).isEmpty();
+        } else {
+            return false;
+        }
+    }
+
+    private void checkAndLoadCache() {
+        if (checkAndLoadReferrals(url, Integer.MAX_VALUE)) {
+            lock.writeLock().lock();
+            try {
+                resources.clear();
+                capSets.clear();
+                populate(loaders.get(url).xml, Integer.MAX_VALUE);
+            } finally {
+                lock.writeLock().unlock();
+            }
+        }
+    }
+
+    private void populate(StaxParser.XmlRepository xml, int hopCount) {
+        if (hopCount > 0) {
+            for (Resource resource : xml.resources) {
+                addResource(resource);
+            }
+            for (StaxParser.Referral referral : xml.referrals) {
+                populate(loaders.get(referral.url).xml, Math.min(referral.depth, hopCount - 1));
+            }
+        }
+    }
+
+    private boolean checkAndLoadReferrals(String url, int hopCount) {
+        boolean modified = false;
+        if (hopCount > 0) {
+            XmlLoader loader = loaders.get(url);
+            if (loader == null) {
+                loader = new XmlLoader(url, expiration);
+                loaders.put(url, loader);
+            }
+            modified = loader.checkAndLoadCache();
+            for (StaxParser.Referral referral : loader.xml.referrals) {
+                modified |= checkAndLoadReferrals(referral.url, Math.min(referral.depth, hopCount - 1));
+            }
+        }
+        return modified;
+    }
+
+    protected static class XmlLoader extends UrlLoader {
+
+        protected StaxParser.XmlRepository xml;
+
+        public XmlLoader(String url, long expiration) {
+            super(url, expiration);
+        }
+
+        public XmlLoader(String url, long expiration, StaxParser.XmlRepository xml) {
+            super(url, expiration);
+            this.xml = xml;
+        }
+
+        @Override
+        protected boolean doRead(InputStream is) throws IOException {
+            try {
+                StaxParser.XmlRepository oldXml = xml;
+                xml = StaxParser.parse(URI.create(getUrl()), is, oldXml);
+                return oldXml != xml;
+            } catch (XMLStreamException e) {
+                throw new IOException("Unable to read xml repository", e);
+            }
+        }
+    }
+
+}

Added: felix/trunk/utils/src/test/java/org/apache/felix/utils/repository/RepositoryTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/java/org/apache/felix/utils/repository/RepositoryTest.java?rev=1829644&view=auto
==============================================================================
--- felix/trunk/utils/src/test/java/org/apache/felix/utils/repository/RepositoryTest.java (added)
+++ felix/trunk/utils/src/test/java/org/apache/felix/utils/repository/RepositoryTest.java Fri Apr 20 10:49:37 2018
@@ -0,0 +1,101 @@
+/*
+ * 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.felix.utils.repository;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.zip.GZIPOutputStream;
+
+import org.junit.Test;
+import org.osgi.resource.Resource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.osgi.framework.namespace.BundleNamespace.BUNDLE_NAMESPACE;
+import static org.osgi.framework.namespace.IdentityNamespace.IDENTITY_NAMESPACE;
+import static org.osgi.framework.namespace.PackageNamespace.PACKAGE_NAMESPACE;
+
+public class RepositoryTest {
+
+    @Test
+    public void testXml() throws Exception {
+        URL url = getClass().getResource("repo.xml");
+        XmlRepository repo = new XmlRepository(url.toExternalForm(), 0);
+        verify(repo);
+    }
+
+    @Test
+    public void testJson() throws Exception {
+        URL url = getClass().getResource("repo.json");
+        JsonRepository repo = new JsonRepository(url.toExternalForm(), 0);
+        verify(repo);
+    }
+
+    @Test
+    public void testXmlGzip() throws Exception {
+        URL url = getClass().getResource("repo.xml");
+        url = gzip(url);
+        XmlRepository repo = new XmlRepository(url.toExternalForm(), 0);
+        verify(repo);
+    }
+
+    @Test
+    public void testJsonGzip() throws Exception {
+        URL url = getClass().getResource("repo.json");
+        url = gzip(url);
+        JsonRepository repo = new JsonRepository(url.toExternalForm(), 0);
+        verify(repo);
+    }
+
+    private void verify(BaseRepository repo) {
+        assertNotNull(repo.getResources());
+        assertEquals(1, repo.getResources().size());
+        Resource resource = repo.getResources().get(0);
+        assertNotNull(resource);
+        assertEquals(1, resource.getCapabilities(IDENTITY_NAMESPACE).size());
+        assertEquals(1, resource.getCapabilities(BUNDLE_NAMESPACE).size());
+        assertEquals(1, resource.getCapabilities(PACKAGE_NAMESPACE).size());
+        assertEquals(1, resource.getRequirements(PACKAGE_NAMESPACE).size());
+    }
+
+    private static URL gzip(URL url) throws IOException {
+        File temp = File.createTempFile("repo", ".tmp");
+        try (
+            OutputStream os = new GZIPOutputStream(new FileOutputStream(temp));
+            InputStream is = url.openStream()
+        ) {
+            copy(is, os);
+        }
+        return temp.toURI().toURL();
+    }
+
+    private static void copy(final InputStream input, final OutputStream output) throws IOException {
+        byte[] buffer = new byte[1024 * 16];
+        int n;
+        while ((n = input.read(buffer)) > 0) {
+            output.write(buffer, 0, n);
+        }
+        output.flush();
+    }
+
+}

Added: felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.json
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.json?rev=1829644&view=auto
==============================================================================
--- felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.json (added)
+++ felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.json Fri Apr 20 10:49:37 2018
@@ -0,0 +1,9 @@
+ {
+	"http://www.acme.com/repository/org/acme/pool/org.acme.pool-1.5.6.jar": {
+		"Bundle-ManifestVersion": "2",
+	 	"Bundle-SymbolicName": "org.acme.pool",
+	 	"Bundle-Version": "1.5.6",
+	 	"Export-Package": "org.acme.pool; version=1.1.2; uses=\"org.acme.pool,org.acme.util\"",
+	 	"Import-Package": "org.apache.commons.pool; version=1.5.6"
+	}
+ }

Added: felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.xml
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.xml?rev=1829644&view=auto
==============================================================================
--- felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.xml (added)
+++ felix/trunk/utils/src/test/resources/org/apache/felix/utils/repository/repo.xml Fri Apr 20 10:49:37 2018
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<repository name='OSGi Repository' increment='13582741'
+            xmlns='http://www.osgi.org/xmlns/repository/v1.0.0'>
+    <resource>
+        <capability namespace='osgi.identity'>
+            <attribute name='osgi.identity' value='org.acme.pool'/>
+            <attribute name='version' type='Version' value='1.5.6'> </attribute>
+            <attribute name='type' value='osgi.bundle'/>
+        </capability>
+        <capability namespace='osgi.content'>
+            <attribute name='osgi.content' value='e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'/>
+            <attribute name='url' value='http://www.acme.com/repository/org/acme/pool/org.acme.pool-1.5.6.jar'/>
+            <attribute name='size' type='Long' value='4405'/>
+            <attribute name='mime' value='application/vnd.osgi.bundle'/>
+        </capability>
+        <capability namespace='osgi.wiring.bundle'>
+            <attribute name='osgi.wiring.bundle' value='org.acme.pool'/>
+            <attribute name='bundle-version' type='Version' value='1.5.6'/>
+        </capability>
+        <capability namespace='osgi.wiring.package'>
+            <attribute name='osgi.wiring.package' value='org.acme.pool'/>
+            <attribute name='version' type='Version' value='1.1.2'/>
+            <attribute name='bundle-version' type='Version' value='1.5.6'/>
+            <attribute name='bundle-symbolic-name' value='org.acme.pool'/>
+            <directive name='uses' value='org.acme.pool,org.acme.util'/>
+        </capability>
+        <requirement namespace='osgi.wiring.package'>
+            <directive name='filter' value='(&amp;(osgi.wiring.package=org.apache.commons.pool)(version&gt;=1.5.6))'/>
+        </requirement>
+        <requirement namespace='osgi.identity'>
+            <directive name='effective' value='meta'/>
+            <directive name='resolution' value='optional'/>
+            <directive name='filter' value='(&amp;(version=1.5.6)(osgi.identity=org.acme.pool-src))'/>
+            <directive name='classifier' value='sources'/>
+        </requirement>
+    </resource>
+</repository>
\ No newline at end of file