[KARAF-2930] Refactory repository support and fully support for the xml format defined by the OSGi Repository specification
Project: http://git-wip-us.apache.org/repos/asf/karaf/repo Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/3574b440 Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/3574b440 Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/3574b440 Branch: refs/heads/master Commit: 3574b440f45093802507e5fdd427289a5b4e9984 Parents: 7456445 Author: Guillaume Nodet <[email protected]> Authored: Wed Apr 23 11:30:45 2014 +0200 Committer: Guillaume Nodet <[email protected]> Committed: Wed Apr 23 15:48:42 2014 +0200 ---------------------------------------------------------------------- .../region/SubsystemResolveContext.java | 4 +- .../internal/repository/BaseRepository.java | 7 + .../internal/repository/CacheRepository.java | 59 ---- .../repository/HttpMetadataProvider.java | 88 ------ .../internal/repository/JsonRepository.java | 117 ++++++++ .../internal/repository/MetadataProvider.java | 29 -- .../internal/repository/MetadataRepository.java | 43 --- .../internal/repository/StaticRepository.java | 33 --- .../internal/repository/StaxParser.java | 289 +++++++++++++++++++ .../features/internal/repository/UrlLoader.java | 88 ++++++ .../internal/repository/XmlRepository.java | 163 +++++++++++ .../internal/resolver/ResourceImpl.java | 9 + .../internal/repository/RepositoryTest.java | 58 ++++ .../features/internal/repository/repo.json | 9 + .../karaf/features/internal/repository/repo.xml | 53 ++++ pom.xml | 2 + 16 files changed, 797 insertions(+), 254 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java index d5a3113..949e37a 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Map; import org.apache.felix.resolver.Util; -import org.apache.karaf.features.internal.repository.StaticRepository; +import org.apache.karaf.features.internal.repository.BaseRepository; import org.eclipse.equinox.region.Region; import org.eclipse.equinox.region.RegionDigraph; import org.eclipse.equinox.region.RegionFilter; @@ -61,7 +61,7 @@ public class SubsystemResolveContext extends ResolveContext { this.digraph = digraph; prepare(root); - repository = new StaticRepository(resToSub.keySet()); + repository = new BaseRepository(resToSub.keySet()); } void prepare(Subsystem subsystem) { http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/BaseRepository.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/BaseRepository.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/BaseRepository.java index c4c0d16..e4befcb 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/repository/BaseRepository.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/repository/BaseRepository.java @@ -44,6 +44,13 @@ public class BaseRepository implements Repository { this.capSets = new HashMap<String, CapabilitySet>(); } + 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(); http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/CacheRepository.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/CacheRepository.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/CacheRepository.java deleted file mode 100644 index 7916821..0000000 --- a/features/core/src/main/java/org/apache/karaf/features/internal/repository/CacheRepository.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.karaf.features.internal.repository; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.osgi.resource.Capability; -import org.osgi.resource.Requirement; -import org.osgi.service.repository.Repository; - -public class CacheRepository implements Repository { - - private final Repository repository; - private final Map<Requirement, Collection<Capability>> cache = - new ConcurrentHashMap<Requirement, Collection<Capability>>(); - - public CacheRepository(Repository repository) { - this.repository = repository; - } - - @Override - public Map<Requirement, Collection<Capability>> findProviders(Collection<? extends Requirement> requirements) { - List<Requirement> missing = new ArrayList<Requirement>(); - Map<Requirement, Collection<Capability>> result = new HashMap<Requirement, Collection<Capability>>(); - for (Requirement requirement : requirements) { - Collection<Capability> caps = cache.get(requirement); - if (caps == null) { - missing.add(requirement); - } else { - result.put(requirement, caps); - } - } - Map<Requirement, Collection<Capability>> newCache = repository.findProviders(missing); - for (Requirement requirement : newCache.keySet()) { - cache.put(requirement, newCache.get(requirement)); - result.put(requirement, newCache.get(requirement)); - } - return result; - } -} http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/HttpMetadataProvider.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/HttpMetadataProvider.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/HttpMetadataProvider.java deleted file mode 100644 index 1aecef1..0000000 --- a/features/core/src/main/java/org/apache/karaf/features/internal/repository/HttpMetadataProvider.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.karaf.features.internal.repository; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Map; -import java.util.zip.GZIPInputStream; - -import org.apache.karaf.features.internal.util.JsonReader; - -/** - */ -public class HttpMetadataProvider implements MetadataProvider { - - public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding"; - public static final String HEADER_CONTENT_ENCODING = "Content-Encoding"; - public static final String GZIP = "gzip"; - - private final String url; - private long lastModified; - private Map<String, Map<String, String>> metadatas; - - public HttpMetadataProvider(String url) { - this.url = url; - } - - @Override - public long getLastModified() { - return lastModified; - } - - @Override - public Map<String, Map<String, String>> getMetadatas() { - try { - HttpURLConnection.setFollowRedirects(false); - HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); - if (lastModified > 0) { - con.setIfModifiedSince(lastModified); - } - con.setRequestProperty(HEADER_ACCEPT_ENCODING, GZIP); - if (con.getResponseCode() == HttpURLConnection.HTTP_OK) { - lastModified = con.getLastModified(); - InputStream is = con.getInputStream(); - if (GZIP.equals(con.getHeaderField(HEADER_CONTENT_ENCODING))) { - is = new GZIPInputStream(is); - } - metadatas = verify(JsonReader.read(is)); - } else if (con.getResponseCode() != HttpURLConnection.HTTP_NOT_MODIFIED) { - throw new IOException("Unexpected http response: " - + con.getResponseCode() + " " + con.getResponseMessage()); - } - return metadatas; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - 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; - } - -} http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/JsonRepository.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/JsonRepository.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/JsonRepository.java new file mode 100644 index 0000000..f1f3c0b --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/internal/repository/JsonRepository.java @@ -0,0 +1,117 @@ +/* + * 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.karaf.features.internal.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.karaf.features.internal.resolver.ResourceBuilder; +import org.apache.karaf.features.internal.util.JsonReader; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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 static final Logger LOGGER = LoggerFactory.getLogger(JsonRepository.class); + + private final UrlLoader loader; + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public JsonRepository(String url) { + loader = new UrlLoader(url) { + @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(JsonReader.read(is)); + lock.writeLock().lock(); + try { + resources.clear(); + capSets.clear(); + for (Map.Entry<String, Map<String, String>> metadata : metadatas.entrySet()) { + try { + Resource resource = ResourceBuilder.build(metadata.getKey(), metadata.getValue()); + addResource(resource); + } catch (Exception e) { + LOGGER.info("Unable to build resource for " + metadata.getKey(), e); + } + } + return true; + } finally { + lock.writeLock().unlock(); + } + } + + @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; + } + +} http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/MetadataProvider.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/MetadataProvider.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/MetadataProvider.java deleted file mode 100644 index 9ac54a1..0000000 --- a/features/core/src/main/java/org/apache/karaf/features/internal/repository/MetadataProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.karaf.features.internal.repository; - -import java.util.Map; - -/** - */ -public interface MetadataProvider { - - long getLastModified(); - - Map<String, Map<String, String>> getMetadatas(); - -} http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/MetadataRepository.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/MetadataRepository.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/MetadataRepository.java deleted file mode 100644 index 2d4fbba..0000000 --- a/features/core/src/main/java/org/apache/karaf/features/internal/repository/MetadataRepository.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.karaf.features.internal.repository; - -import java.util.Map; - -import org.apache.karaf.features.internal.resolver.ResourceBuilder; -import org.osgi.resource.Resource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - */ -public class MetadataRepository extends BaseRepository { - - private static final Logger LOGGER = LoggerFactory.getLogger(MetadataRepository.class); - - public MetadataRepository(MetadataProvider provider) { - Map<String, Map<String, String>> metadatas = provider.getMetadatas(); - for (Map.Entry<String, Map<String, String>> metadata : metadatas.entrySet()) { - try { - Resource resource = ResourceBuilder.build(metadata.getKey(), metadata.getValue()); - addResource(resource); - } catch (Exception e) { - LOGGER.info("Unable to build resource for " + metadata.getKey(), e); - } - } - } -} http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/StaticRepository.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/StaticRepository.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/StaticRepository.java deleted file mode 100644 index f289c8d..0000000 --- a/features/core/src/main/java/org/apache/karaf/features/internal/repository/StaticRepository.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.karaf.features.internal.repository; - -import java.util.Collection; - -import org.osgi.resource.Resource; - -/** - */ -public class StaticRepository extends BaseRepository { - - public StaticRepository(Collection<Resource> resources) { - for (Resource resource : resources) { - addResource(resource); - } - } - -} http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/StaxParser.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/StaxParser.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/StaxParser.java new file mode 100644 index 0000000..9e13e8e --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/internal/repository/StaxParser.java @@ -0,0 +1,289 @@ +/* + * 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.karaf.features.internal.repository; + +import java.io.InputStream; +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.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.apache.karaf.features.internal.resolver.CapabilityImpl; +import org.apache.karaf.features.internal.resolver.RequirementImpl; +import org.apache.karaf.features.internal.resolver.ResourceImpl; +import org.osgi.framework.Version; +import org.osgi.resource.Resource; + +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 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 class Referral { + String url; + int depth = Integer.MAX_VALUE; + } + + public static class XmlRepository { + String name; + long increment; + List<Referral> referrals = new ArrayList<Referral>(); + List<Resource> resources = new ArrayList<Resource>(); + } + + public static XmlRepository parse(InputStream is) throws XMLStreamException { + return parse(is, null); + } + + public static XmlRepository parse(InputStream is, XmlRepository previous) throws XMLStreamException { + XMLStreamReader reader = getFactory().createXMLStreamReader(is); + 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); + if (REPO_NAME.equals(attrName)) { + repo.name = attrName; + } else if (INCREMENT.equals(attrName)) { + repo.increment = Integer.parseInt(attrValue); + } else { + 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(); + if (REFERRAL.equals(element)) { + Referral referral = new Referral(); + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) { + String attrName = reader.getAttributeLocalName(i); + String attrValue = reader.getAttributeValue(i); + if (DEPTH.equals(attrName)) { + referral.depth = Integer.parseInt(attrValue); + } else if (URL.equals(attrName)) { + referral.url = attrValue; + } else { + 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); + } else if (RESOURCE.equals(element)) { + repo.resources.add(parseResource(reader)); + } else { + throw new IllegalStateException("Unsupported element '" + element + "'. Expected 'referral' or 'resource'"); + } + } + // Sanity check + sanityCheckEndElement(reader, event, REPOSITORY); + return repo; + } + + 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(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(); + if (CAPABILITY.equals(element)) { + resource.addCapability(parseCapability(reader, resource)); + } else if (REQUIREMENT.equals(element)) { + resource.addRequirement(parseRequirement(reader, resource)); + } else { + 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"); + } + } + } + } + // 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<String, String>(); + Map<String, Object> attributes = new HashMap<String, Object>(); + 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<String, String>(); + Map<String, Object> attributes = new HashMap<String, Object>(); + parseClause(reader, namespace, directives, attributes); + sanityCheckEndElement(reader, reader.getEventType(), REQUIREMENT); + return new RequirementImpl(resource, namespace[0], directives, attributes); + } + + 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(); + if (DIRECTIVE.equals(element)) { + 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); + if (NAME.equals(attName)) { + name = attValue; + } else if (VALUE.equals(attName)) { + value = attValue; + } else { + 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); + } else if (ATTRIBUTE.equals(element)) { + 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); + if (NAME.equals(attName)) { + name = attValue; + } else if (VALUE.equals(attName)) { + value = attValue; + } else if (TYPE.equals(attName)) { + type = attValue; + } else { + 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); + } else { + 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 Version.parseVersion(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<Object>(); + for (String s : value.split(",")) { + list.add(parseAttribute(s.trim(), type)); + } + return list; + } else { + throw new IllegalStateException("Unexpected type: '" + type + "'"); + } + } + + static XMLInputFactory factory; + + private static synchronized XMLInputFactory getFactory() { + if (StaxParser.factory == null) { + XMLInputFactory factory = XMLInputFactory.newInstance(); + factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true); + StaxParser.factory = factory; + } + return StaxParser.factory; + } + + private StaxParser() { + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/UrlLoader.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/UrlLoader.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/UrlLoader.java new file mode 100644 index 0000000..8bed863 --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/internal/repository/UrlLoader.java @@ -0,0 +1,88 @@ +/* + * 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.karaf.features.internal.repository; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URLConnection; +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 long lastModified; + + public UrlLoader(String url) { + this.url = url; + } + + protected boolean checkAndLoadCache() { + try { + URLConnection connection = new java.net.URL(url).openConnection(); + if (connection instanceof HttpURLConnection) { + HttpURLConnection con = (HttpURLConnection) connection; + if (lastModified > 0) { + con.setIfModifiedSince(lastModified); + } + con.setRequestProperty(HEADER_ACCEPT_ENCODING, GZIP); + int rc = con.getResponseCode(); + if (rc == HTTP_NOT_MODIFIED) { + return false; + } + if (rc != HTTP_OK) { + throw new IOException("Unexpected http response: " + rc + " " + con.getResponseMessage()); + } + } + long lm = connection.getLastModified(); + if (lm <= lastModified) { + return false; + } + BufferedInputStream bis = new BufferedInputStream(connection.getInputStream()); + try { + // Auto-detect gzipped streams + InputStream is = bis; + bis.mark(512); + int b0 = bis.read(); + int b1 = bis.read(); + bis.reset(); + if (b0 == 0x1f && b1 == 0x8b) { + is = new GZIPInputStream(bis); + } + boolean r = doRead(is); + lastModified = lm; + return r; + } finally { + bis.close(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected abstract boolean doRead(InputStream is) throws IOException; + +} http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/repository/XmlRepository.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/repository/XmlRepository.java b/features/core/src/main/java/org/apache/karaf/features/internal/repository/XmlRepository.java new file mode 100644 index 0000000..6661fb1 --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/internal/repository/XmlRepository.java @@ -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.karaf.features.internal.repository; + +import java.io.IOException; +import java.io.InputStream; +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.karaf.features.internal.resolver.CapabilitySet; +import org.apache.karaf.features.internal.resolver.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 { + + private final String url; + private final Map<String, XmlLoader> loaders = new HashMap<String, XmlLoader>(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public XmlRepository(String url) { + this.url = url; + } + + @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); + } + + @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<String, Object>(); + 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); + 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; + } + + static class XmlLoader extends UrlLoader { + + StaxParser.XmlRepository xml; + + XmlLoader(String url) { + super(url); + } + + @Override + protected boolean doRead(InputStream is) throws IOException { + try { + StaxParser.XmlRepository oldXml = xml; + xml = StaxParser.parse(is, oldXml); + return oldXml != xml; + } catch (XMLStreamException e) { + throw new IOException("Unable to read xml repository", e); + } + } + } + +} http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java index 873720e..cd48ca2 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java @@ -34,6 +34,15 @@ public class ResourceImpl implements Resource { private final List<Capability> m_caps; private final List<Requirement> m_reqs; + /** + * CAUTION: This constructor does not ensure that the resource + * has the required identity capability + */ + public ResourceImpl() { + m_caps = new ArrayList<Capability>(); + m_reqs = new ArrayList<Requirement>(); + } + public ResourceImpl(String name, Version version) { this(name, IdentityNamespace.TYPE_BUNDLE, version); } http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/test/java/org/apache/karaf/features/internal/repository/RepositoryTest.java ---------------------------------------------------------------------- diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/repository/RepositoryTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/repository/RepositoryTest.java new file mode 100644 index 0000000..c05eea0 --- /dev/null +++ b/features/core/src/test/java/org/apache/karaf/features/internal/repository/RepositoryTest.java @@ -0,0 +1,58 @@ +/* + * 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.karaf.features.internal.repository; + +import java.net.URL; + +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()); + verify(repo); + } + + @Test + public void testJson() throws Exception { + URL url = getClass().getResource("repo.json"); + JsonRepository repo = new JsonRepository(url.toExternalForm()); + 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()); + } +} http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/test/resources/org/apache/karaf/features/internal/repository/repo.json ---------------------------------------------------------------------- diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/repository/repo.json b/features/core/src/test/resources/org/apache/karaf/features/internal/repository/repo.json new file mode 100644 index 0000000..7249594 --- /dev/null +++ b/features/core/src/test/resources/org/apache/karaf/features/internal/repository/repo.json @@ -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" + } + } http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/features/core/src/test/resources/org/apache/karaf/features/internal/repository/repo.xml ---------------------------------------------------------------------- diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/repository/repo.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/repository/repo.xml new file mode 100644 index 0000000..dc1318b --- /dev/null +++ b/features/core/src/test/resources/org/apache/karaf/features/internal/repository/repo.xml @@ -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='(&(osgi.wiring.package=org.apache.commons.pool)(version>=1.5.6))'/> + </requirement> + <requirement namespace='osgi.identity'> + <directive name='effective' value='meta'/> + <directive name='resolution' value='optional'/> + <directive name='filter' value='(&(version=1.5.6)(osgi.identity=org.acme.pool-src))'/> + <directive name='classifier' value='sources'/> + </requirement> + </resource> +</repository> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/karaf/blob/3574b440/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index 82427e0..51a1eb3 100644 --- a/pom.xml +++ b/pom.xml @@ -2250,6 +2250,8 @@ <exclude>manual/**/*.conf</exclude> <!-- test manifests --> <exclude>**/*.mf</exclude> + <!-- test json files --> + <exclude>**/*.json</exclude> </excludes> </configuration> </plugin>
