package aQute.bnd.deployer.repository.providers; import static aQute.bnd.deployer.repository.api.Decision.accept; import static aQute.bnd.deployer.repository.api.Decision.reject; import static aQute.bnd.deployer.repository.api.Decision.undecided; import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; import static javax.xml.stream.XMLStreamConstants.PROCESSING_INSTRUCTION; import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.transform.stream.StreamSource; import org.osgi.framework.Version; import org.osgi.framework.namespace.BundleNamespace; import org.osgi.framework.namespace.ExecutionEnvironmentNamespace; import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.namespace.IdentityNamespace; import org.osgi.framework.namespace.PackageNamespace; import org.osgi.namespace.service.ServiceNamespace; import org.osgi.resource.Namespace; import org.osgi.resource.Resource; import org.osgi.service.bindex.BundleIndexer; import org.osgi.service.log.LogService; import org.osgi.service.repository.ContentNamespace; import aQute.bnd.deployer.repository.api.CheckResult; import aQute.bnd.deployer.repository.api.IRepositoryContentProvider; import aQute.bnd.deployer.repository.api.IRepositoryIndexProcessor; import aQute.bnd.deployer.repository.api.Referral; import aQute.bnd.osgi.resource.CapReqBuilder; import aQute.bnd.osgi.resource.ResourceBuilder; import aQute.bnd.service.Registry; import aQute.lib.io.IO; public class ObrContentProvider implements IRepositoryContentProvider { public static final String NAME = "OBR"; private static final String INDEX_NAME = "repository.xml"; private static final String EMPTY_REPO_TEMPLATE = "<?xml version='1.0' encoding='UTF-8'?>\n<repository name='%s' lastmodified='0' xmlns='http://www.osgi.org/xmlns/obr/v1.0.0'/>"; private static final String NS_URI = "http://www.osgi.org/xmlns/obr/v1.0.0"; private static final String PI_DATA_STYLESHEET = "type='text/xsl' href='http://www2.osgi.org/www/obr2html.xsl'"; private static final String PI_TARGET_STYLESHEET = "xml-stylesheet"; private static final String TAG_REPOSITORY = "repository"; private static final String TAG_RESOURCE = "resource"; private static final String ATTR_RESOURCE_SYMBOLIC_NAME = "symbolicname"; private static final String ATTR_RESOURCE_URI = "uri"; private static final String ATTR_RESOURCE_VERSION = "version"; private static final String TAG_REFERRAL = "referral"; private static final String ATTR_REFERRAL_URL = "url"; private static final String ATTR_REFERRAL_DEPTH = "depth"; private static final String TAG_CAPABILITY = "capability"; private static final String TAG_REQUIRE = "require"; private static final String ATTR_NAME = "name"; private static final String ATTR_EXTEND = "extend"; private static final String ATTR_OPTIONAL = "optional"; private static final String ATTR_FILTER = "filter"; private static final String TAG_PROPERTY = "p"; private static final String ATTR_PROPERTY_NAME = "n"; private static final String ATTR_PROPERTY_TYPE = "t"; private static final String ATTR_PROPERTY_VALUE = "v"; private static final String PROPERTY_USES = "uses"; private static final String TYPE_VERSION = "version"; private BundleIndexer indexer; private static enum ParserState { beforeRoot, inRoot, inResource, inCapability } public ObrContentProvider(BundleIndexer indexer) { this.indexer = indexer; } public String getName() { return NAME; } public String getDefaultIndexName(boolean pretty) { return INDEX_NAME; } public void parseIndex(InputStream stream, URI baseUri, IRepositoryIndexProcessor listener, LogService log) throws Exception { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); inputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true); inputFactory.setProperty(XMLInputFactory.IS_VALIDATING, false); inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); StreamSource source = new StreamSource(stream, baseUri.toString()); XMLStreamReader reader = inputFactory.createXMLStreamReader(source); ResourceBuilder resourceBuilder = null; CapReqBuilder capReqBuilder = null; while (reader.hasNext()) { int type = reader.next(); String localName; switch (type) { case START_ELEMENT : localName = reader.getLocalName(); if (TAG_REFERRAL.equals(localName)) { Referral referral = new Referral(reader.getAttributeValue(null, ATTR_REFERRAL_URL), parseInt(reader.getAttributeValue(null, ATTR_REFERRAL_DEPTH))); listener.processReferral(baseUri, referral, referral.getDepth(), 1); } else if (TAG_RESOURCE.equals(localName)) { resourceBuilder = new ResourceBuilder(); String bsn = reader.getAttributeValue(null, ATTR_RESOURCE_SYMBOLIC_NAME); String versionStr = reader.getAttributeValue(null, ATTR_RESOURCE_VERSION); Version version = Version.parseVersion(versionStr); String uri = reader.getAttributeValue(null, ATTR_RESOURCE_URI); URI resolvedUri = resolveUri(uri, baseUri); addBasicCapabilities(resourceBuilder, bsn, version, resolvedUri); } else if (TAG_CAPABILITY.equals(localName)) { String obrName = reader.getAttributeValue(null, ATTR_NAME); String namespace = mapObrNameToR5Namespace(obrName, false); capReqBuilder = new CapReqBuilder(namespace); } else if (TAG_REQUIRE.equals(localName)) { String obrName = reader.getAttributeValue(null, ATTR_NAME); boolean extend = "true".equalsIgnoreCase(reader.getAttributeValue(null, ATTR_EXTEND)); String namespace = mapObrNameToR5Namespace(obrName, extend); boolean optional = "true".equalsIgnoreCase(reader.getAttributeValue(null, ATTR_OPTIONAL)); capReqBuilder = new CapReqBuilder(namespace); if (optional) capReqBuilder.addDirective(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE, Namespace.RESOLUTION_OPTIONAL); String filter = translateObrFilter(namespace, reader.getAttributeValue(null, ATTR_FILTER), log); capReqBuilder.addDirective(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter); } else if (TAG_PROPERTY.equals(localName)) { String name = reader.getAttributeValue(null, ATTR_PROPERTY_NAME); String typeStr = reader.getAttributeValue(null, ATTR_PROPERTY_TYPE); String valueStr = reader.getAttributeValue(null, ATTR_PROPERTY_VALUE); if (capReqBuilder != null) { name = mapObrPropertyToR5(capReqBuilder.getNamespace(), name); if (PROPERTY_USES.equals(name)) capReqBuilder.addDirective(PROPERTY_USES, valueStr); else { Object value = convertProperty(valueStr, typeStr); capReqBuilder.addAttribute(name, value); } } } break; case END_ELEMENT : localName = reader.getLocalName(); if (TAG_RESOURCE.equals(localName)) { if (resourceBuilder != null) { Resource resource = resourceBuilder.build(); listener.processResource(resource); } } else if (TAG_CAPABILITY.equals(localName)) { if (resourceBuilder != null && capReqBuilder != null) resourceBuilder.addCapability(capReqBuilder); capReqBuilder = null; } else if (TAG_REQUIRE.equals(localName)) { if (resourceBuilder != null && capReqBuilder != null) resourceBuilder.addRequirement(capReqBuilder); capReqBuilder = null; } } } } private static Object convertProperty(String value, String typeName) { final Object result; if (TYPE_VERSION.equals(typeName)) result = Version.parseVersion(value); else result = value; return result; } private static String mapObrNameToR5Namespace(String obrName, boolean extend) { if ("bundle".equals(obrName)) return extend ? HostNamespace.HOST_NAMESPACE : BundleNamespace.BUNDLE_NAMESPACE; if ("package".equals(obrName)) return PackageNamespace.PACKAGE_NAMESPACE; if ("service".equals(obrName)) return ServiceNamespace.SERVICE_NAMESPACE; if ("ee".equals(obrName)) return ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE; return obrName; } private static String translateObrFilter(String namespace, String filter, LogService log) { filter = ObrUtil.processFilter(filter, log); if (PackageNamespace.PACKAGE_NAMESPACE.equals(namespace)) return filter.replaceAll("\\(package", "(" + PackageNamespace.PACKAGE_NAMESPACE); if (ServiceNamespace.SERVICE_NAMESPACE.equals(namespace)) return filter.replaceAll("\\(service", "(" + ServiceNamespace.SERVICE_NAMESPACE); if (BundleNamespace.BUNDLE_NAMESPACE.equals(namespace)) { filter = filter.replaceAll("\\(symbolicname", "(" + BundleNamespace.BUNDLE_NAMESPACE); return filter.replaceAll("\\(version", "(" + BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); } if (ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE.equals(namespace)) return filter.replaceAll("\\(ee", "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE); return filter; } private static String mapObrPropertyToR5(String namespace, String propName) { if (BundleNamespace.BUNDLE_NAMESPACE.equals(namespace)) { if ("symbolicname".equals(propName)) return BundleNamespace.BUNDLE_NAMESPACE; if ("version".equals(propName)) return BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE; } if (PackageNamespace.PACKAGE_NAMESPACE.equals(namespace)) { if ("package".equals(propName)) return PackageNamespace.PACKAGE_NAMESPACE; } if (ServiceNamespace.SERVICE_NAMESPACE.equals(namespace)) { if ("service".equals(propName)) return ServiceNamespace.SERVICE_NAMESPACE; } return propName; } private static URI resolveUri(String uriStr, URI baseUri) throws URISyntaxException { URI resolved; URI resourceUri = new URI(uriStr); if (resourceUri.isAbsolute()) resolved = resourceUri; else resolved = baseUri.resolve(resourceUri); return resolved; } private static void addBasicCapabilities(ResourceBuilder builder, String bsn, Version version, URI resolvedUri) throws Exception { CapReqBuilder identity = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE) .addAttribute(IdentityNamespace.IDENTITY_NAMESPACE, bsn) .addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, IdentityNamespace.TYPE_BUNDLE) .addAttribute(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, version); CapReqBuilder content = new CapReqBuilder(ContentNamespace.CONTENT_NAMESPACE) // null attributes are skipped anyway // Setting this to a "reasonable" value got the testGetHttp to // failed utterly because it interpreted the value as the SHA? // .addAttribute(ContentNamespace.CONTENT_NAMESPACE, null) .addAttribute(ContentNamespace.CAPABILITY_URL_ATTRIBUTE, resolvedUri); CapReqBuilder host = new CapReqBuilder(HostNamespace.HOST_NAMESPACE) .addAttribute(HostNamespace.HOST_NAMESPACE, bsn) .addAttribute(HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, version); builder.addCapability(identity).addCapability(content).addCapability(host); } private static int parseInt(String value) { if (value == null || "".equals(value)) return 0; return Integer.parseInt(value); } public CheckResult checkStream(String name, InputStream stream) throws IOException { XMLStreamReader reader = null; try { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); inputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true); inputFactory.setProperty(XMLInputFactory.IS_VALIDATING, false); inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); reader = inputFactory.createXMLStreamReader(stream); ParserState state = ParserState.beforeRoot; while (reader.hasNext()) { int type = reader.next(); String localName; switch (type) { case PROCESSING_INSTRUCTION : if (PI_TARGET_STYLESHEET.equals(reader.getPITarget()) && PI_DATA_STYLESHEET.equals(reader.getPIData())) return new CheckResult(accept, "Recognised stylesheet", null); break; case START_ELEMENT : localName = reader.getLocalName(); switch (state) { case beforeRoot : String nsUri = reader.getNamespaceURI(); if (nsUri != null) return CheckResult.fromBool(NS_URI.equals(nsUri), "Correct namespace on root element", "Incorrect namespace on root element: " + nsUri, null); if (!TAG_REPOSITORY.equals(localName)) return new CheckResult(reject, "Incorrect root element name", null); state = ParserState.inRoot; break; case inRoot : if (TAG_RESOURCE.equals(localName)) { state = ParserState.inResource; } else if (!TAG_REFERRAL.equals(localName)) { return new CheckResult(reject, String.format("Incorrect element '%s', expected '%s' or '%s'.", localName, TAG_RESOURCE, TAG_REFERRAL), null); } break; case inResource : if (TAG_CAPABILITY.equals(localName)) { state = ParserState.inCapability; } break; case inCapability : return CheckResult.fromBool(TAG_PROPERTY.equals(localName), "Found 'p' tag inside 'capability'", String.format("Incorrect element '%s' inside '%s'; expected '%s'.", localName, TAG_CAPABILITY, TAG_PROPERTY), null); } break; case END_ELEMENT : localName = reader.getLocalName(); if (state == ParserState.inResource && TAG_RESOURCE.equals(localName)) state = ParserState.inRoot; if (state == ParserState.inCapability && TAG_CAPABILITY.equals(localName)) state = ParserState.inResource; break; } } return new CheckResult(undecided, "Reached end of stream", null); } catch (XMLStreamException e) { return new CheckResult(reject, "Invalid XML", e); } finally { if (reader != null) try { reader.close(); } catch (XMLStreamException e) {} } } public boolean supportsGeneration() { return true; } public void generateIndex(Set<File> files, OutputStream output, String repoName, URI rootUri, boolean pretty, Registry registry, LogService log) throws Exception { if (!files.isEmpty()) { if (indexer == null) throw new IllegalStateException("Cannot index repository: no Bundle Indexer provided."); Map<String,String> config = new HashMap<String,String>(); config.put(BundleIndexer.REPOSITORY_NAME, repoName); config.put(BundleIndexer.ROOT_URL, rootUri.toString()); indexer.index(files, output, config); } else { String content = String.format(EMPTY_REPO_TEMPLATE, repoName); IO.store(content, output); } } }