Return-Path: X-Original-To: apmail-karaf-commits-archive@minotaur.apache.org Delivered-To: apmail-karaf-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id ECBEC10F18 for ; Thu, 10 Apr 2014 14:15:40 +0000 (UTC) Received: (qmail 70148 invoked by uid 500); 10 Apr 2014 14:15:31 -0000 Delivered-To: apmail-karaf-commits-archive@karaf.apache.org Received: (qmail 70021 invoked by uid 500); 10 Apr 2014 14:15:27 -0000 Mailing-List: contact commits-help@karaf.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@karaf.apache.org Delivered-To: mailing list commits@karaf.apache.org Received: (qmail 69941 invoked by uid 99); 10 Apr 2014 14:15:24 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 10 Apr 2014 14:15:24 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 08191950F61; Thu, 10 Apr 2014 14:15:24 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: gnodet@apache.org To: commits@karaf.apache.org Date: Thu, 10 Apr 2014 14:15:27 -0000 Message-Id: <3991b984978c4173a53fda3323ea93c4@git.apache.org> In-Reply-To: <133b5a477c6541708f1f1bc939cc368f@git.apache.org> References: <133b5a477c6541708f1f1bc939cc368f@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [05/59] [abbrv] [KARAF-2852] Merge features/core and features/command http://git-wip-us.apache.org/repos/asf/karaf/blob/999f4970/features/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java ---------------------------------------------------------------------- diff --git a/features/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java b/features/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java new file mode 100644 index 0000000..cb2c36a --- /dev/null +++ b/features/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java @@ -0,0 +1,1129 @@ +/* + * 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.resolver; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.utils.version.VersionRange; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +public class ResourceBuilder { + + public static final String RESOLUTION_DYNAMIC = "dynamic"; + + public static Resource build(String uri, Map headerMap) + throws BundleException { + + // Verify that only manifest version 2 is specified. + String manifestVersion = getManifestVersion(headerMap); + if (manifestVersion == null || !manifestVersion.equals("2")) { + throw new BundleException("Unsupported 'Bundle-ManifestVersion' value: " + manifestVersion); + } + + // + // Parse bundle version. + // + + Version bundleVersion = Version.emptyVersion; + if (headerMap.get(Constants.BUNDLE_VERSION) != null) { + bundleVersion = Version.parseVersion(headerMap.get(Constants.BUNDLE_VERSION)); + } + + // + // Parse bundle symbolic name. + // + + String bundleSymbolicName = null; + ParsedHeaderClause bundleCap = parseBundleSymbolicName(headerMap); + if (bundleCap == null) { + throw new BundleException("Bundle manifest must include bundle symbolic name"); + } + bundleSymbolicName = (String) bundleCap.attrs.get(BundleRevision.BUNDLE_NAMESPACE); + + // Now that we have symbolic name and version, create the resource + String type = headerMap.get(Constants.FRAGMENT_HOST) == null ? IdentityNamespace.TYPE_BUNDLE : IdentityNamespace.TYPE_FRAGMENT; + ResourceImpl resource = new ResourceImpl(bundleSymbolicName, type, bundleVersion); + if (uri != null) { + Map attrs = new HashMap(); + attrs.put(UriNamespace.URI_NAMESPACE, uri); + resource.addCapability(new CapabilityImpl(resource, UriNamespace.URI_NAMESPACE, Collections.emptyMap(), attrs)); + } + + // Add a bundle and host capability to all + // non-fragment bundles. A host capability is the same + // as a require capability, but with a different capability + // namespace. Bundle capabilities resolve required-bundle + // dependencies, while host capabilities resolve fragment-host + // dependencies. + if (headerMap.get(Constants.FRAGMENT_HOST) == null) { + // All non-fragment bundles have bundle capability. + resource.addCapability(new CapabilityImpl(resource, BundleRevision.BUNDLE_NAMESPACE, bundleCap.dirs, bundleCap.attrs)); + // A non-fragment bundle can choose to not have a host capability. + String attachment = bundleCap.dirs.get(Constants.FRAGMENT_ATTACHMENT_DIRECTIVE); + attachment = (attachment == null) ? Constants.FRAGMENT_ATTACHMENT_RESOLVETIME : attachment; + if (!attachment.equalsIgnoreCase(Constants.FRAGMENT_ATTACHMENT_NEVER)) { + Map hostAttrs = new HashMap(bundleCap.attrs); + Object value = hostAttrs.remove(BundleRevision.BUNDLE_NAMESPACE); + hostAttrs.put(BundleRevision.HOST_NAMESPACE, value); + resource.addCapability(new CapabilityImpl( + resource, BundleRevision.HOST_NAMESPACE, + bundleCap.dirs, + hostAttrs)); + } + } + + // + // Parse Fragment-Host. + // + + List hostReqs = parseFragmentHost(resource, headerMap); + + // + // Parse Require-Bundle + // + + List rbClauses = parseStandardHeader(headerMap.get(Constants.REQUIRE_BUNDLE)); + rbClauses = normalizeRequireClauses(rbClauses); + List rbReqs = convertRequires(rbClauses, resource); + + // + // Parse Import-Package. + // + + List importClauses = parseStandardHeader(headerMap.get(Constants.IMPORT_PACKAGE)); + importClauses = normalizeImportClauses(importClauses); + List importReqs = convertImports(importClauses, resource); + + // + // Parse DynamicImport-Package. + // + + List dynamicClauses = parseStandardHeader(headerMap.get(Constants.DYNAMICIMPORT_PACKAGE)); + dynamicClauses = normalizeDynamicImportClauses(dynamicClauses); + List dynamicReqs = convertImports(dynamicClauses, resource); + + // + // Parse Require-Capability. + // + + List requireClauses = parseStandardHeader(headerMap.get(Constants.REQUIRE_CAPABILITY)); + requireClauses = normalizeRequireCapabilityClauses(requireClauses); + List requireReqs = convertRequireCapabilities(requireClauses, resource); + + // + // Parse Export-Package. + // + + List exportClauses = parseStandardHeader(headerMap.get(Constants.EXPORT_PACKAGE)); + exportClauses = normalizeExportClauses(exportClauses, bundleSymbolicName, bundleVersion); + List exportCaps = convertExports(exportClauses, resource); + + // + // Parse Provide-Capability. + // + + List provideClauses = parseStandardHeader(headerMap.get(Constants.PROVIDE_CAPABILITY)); + provideClauses = normalizeProvideCapabilityClauses(provideClauses); + List provideCaps = convertProvideCapabilities(provideClauses, resource); + + // + // Parse Import-Service and Export-Service + // if Require-Capability and Provide-Capability are not set for services + // + + boolean hasServiceReferenceCapability = false; + for (Capability cap : exportCaps) { + hasServiceReferenceCapability |= ServiceNamespace.SERVICE_NAMESPACE.equals(cap.getNamespace()); + } + if (!hasServiceReferenceCapability) { + List exportServices = parseStandardHeader(headerMap.get(Constants.EXPORT_SERVICE)); + List caps = convertExportService(exportServices, resource); + provideCaps.addAll(caps); + } + + boolean hasServiceReferenceRequirement = false; + for (Requirement req : requireReqs) { + hasServiceReferenceRequirement |= ServiceNamespace.SERVICE_NAMESPACE.equals(req.getNamespace()); + } + if (!hasServiceReferenceRequirement) { + List importServices = parseStandardHeader(headerMap.get(Constants.IMPORT_SERVICE)); + List reqs = convertImportService(importServices, resource); + requireReqs.addAll(reqs); + } + + // Combine all capabilities. + resource.addCapabilities(exportCaps); + resource.addCapabilities(provideCaps); + + // Combine all requirements. + resource.addRequirements(hostReqs); + resource.addRequirements(importReqs); + resource.addRequirements(rbReqs); + resource.addRequirements(requireReqs); + resource.addRequirements(dynamicReqs); + + return resource; + } + + public static List parseImport(Resource resource, String imports) throws BundleException { + List importClauses = parseStandardHeader(imports); + importClauses = normalizeImportClauses(importClauses); + List importReqs = convertImports(importClauses, resource); + return importReqs; + } + + public static List parseRequirement(Resource resource, String requirement) throws BundleException { + List requireClauses = parseStandardHeader(requirement); + requireClauses = normalizeRequireCapabilityClauses(requireClauses); + List requireReqs = convertRequireCapabilities(requireClauses, resource); + return requireReqs; + } + + public static List parseExport(Resource resource, String bundleSymbolicName, Version bundleVersion, String exports) throws BundleException { + List exportClauses = parseStandardHeader(exports); + exportClauses = normalizeExportClauses(exportClauses, bundleSymbolicName, bundleVersion); + List exportCaps = convertExports(exportClauses, resource); + return exportCaps; + } + + public static List parseCapability(Resource resource, String capability) throws BundleException { + List provideClauses = parseStandardHeader(capability); + provideClauses = normalizeProvideCapabilityClauses(provideClauses); + List provideCaps = convertProvideCapabilities(provideClauses, resource); + return provideCaps; + } + + @SuppressWarnings( "deprecation" ) + private static List normalizeImportClauses( + List clauses) + throws BundleException { + // Verify that the values are equals if the package specifies + // both version and specification-version attributes. + Set dupeSet = new HashSet(); + for (ParsedHeaderClause clause : clauses) { + // Check for "version" and "specification-version" attributes + // and verify they are the same if both are specified. + Object v = clause.attrs.get(Constants.VERSION_ATTRIBUTE); + Object sv = clause.attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION); + if ((v != null) && (sv != null)) { + // Verify they are equal. + if (!((String) v).trim().equals(((String) sv).trim())) { + throw new IllegalArgumentException( + "Both version and specification-version are specified, but they are not equal."); + } + } + + // Ensure that only the "version" attribute is used and convert + // it to the VersionRange type. + if ((v != null) || (sv != null)) { + clause.attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); + v = (v == null) ? sv : v; + clause.attrs.put(Constants.VERSION_ATTRIBUTE, VersionRange.parseVersionRange(v.toString())); + } + + // If bundle version is specified, then convert its type to VersionRange. + v = clause.attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); + if (v != null) { + clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, VersionRange.parseVersionRange(v.toString())); + } + + // Verify java.* is not imported, nor any duplicate imports. + for (String pkgName : clause.paths) { + if (!dupeSet.contains(pkgName)) { + // Verify that java.* packages are not imported. + if (pkgName.startsWith("java.")) { + throw new BundleException("Importing java.* packages not allowed: " + pkgName); + } + // The character "." has no meaning in the OSGi spec except + // when placed on the bundle class path. Some people, however, + // mistakenly think it means the default package when imported + // or exported. This is not correct. It is invalid. + else if (pkgName.equals(".")) { + throw new BundleException("Importing '.' is invalid."); + } + // Make sure a package name was specified. + else if (pkgName.length() == 0) { + throw new BundleException( + "Imported package names cannot be zero length."); + } + dupeSet.add(pkgName); + } else { + throw new BundleException("Duplicate import: " + pkgName); + } + } + } + + return clauses; + } + + private static List convertExportService(List clauses, Resource resource) { + List capList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) { + for (String path : clause.paths) { + Map dirs = new LinkedHashMap(); + dirs.put(ServiceNamespace.CAPABILITY_EFFECTIVE_DIRECTIVE, ServiceNamespace.EFFECTIVE_ACTIVE); + Map attrs = new LinkedHashMap(); + attrs.put(Constants.OBJECTCLASS, path); + attrs.putAll(clause.attrs); + capList.add(new CapabilityImpl( + resource, + ServiceNamespace.SERVICE_NAMESPACE, + dirs, + attrs)); + } + } + return capList; + } + + private static List convertImportService(List clauses, Resource resource) throws BundleException { + try { + List reqList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) { + for (String path : clause.paths) { + String multiple = clause.dirs.get("multiple"); + String avail = clause.dirs.get("availability"); + String filter = (String) clause.attrs.get("filter"); + Map dirs = new LinkedHashMap(); + dirs.put(ServiceNamespace.REQUIREMENT_EFFECTIVE_DIRECTIVE, ServiceNamespace.EFFECTIVE_ACTIVE); + if ("optional".equals(avail)) { + dirs.put(ServiceNamespace.REQUIREMENT_RESOLUTION_DIRECTIVE, ServiceNamespace.RESOLUTION_OPTIONAL); + } + if ("true".equals(multiple)) { + dirs.put(ServiceNamespace.REQUIREMENT_CARDINALITY_DIRECTIVE, ServiceNamespace.CARDINALITY_MULTIPLE); + } + if (filter == null) { + filter = "(" + Constants.OBJECTCLASS + "=" + path + ")"; + } else if (!filter.startsWith("(") && !filter.endsWith(")")) { + filter = "(&(" + Constants.OBJECTCLASS + "=" + path + ")(" + filter + "))"; + } else { + filter = "(&(" + Constants.OBJECTCLASS + "=" + path + ")" + filter + ")"; + } + dirs.put(ServiceNamespace.REQUIREMENT_FILTER_DIRECTIVE, filter); + reqList.add(new RequirementImpl( + resource, + ServiceNamespace.SERVICE_NAMESPACE, + dirs, + Collections.emptyMap(), + SimpleFilter.parse(filter))); + } + } + return reqList; + } catch (Exception ex) { + throw new BundleException("Error creating requirement: " + ex, ex); + } + } + + private static List convertImports(List clauses, Resource resource) { + // Now convert generic header clauses into requirements. + List reqList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) { + for (String path : clause.paths) { + // Prepend the package name to the array of attributes. + Map attrs = clause.attrs; + // Note that we use a linked hash map here to ensure the + // package attribute is first, which will make indexing + // more efficient. + // TODO: OSGi R4.3 - This is ordering is kind of hacky. + // Prepend the package name to the array of attributes. + Map newAttrs = new LinkedHashMap(attrs.size() + 1); + // We want this first from an indexing perspective. + newAttrs.put(BundleRevision.PACKAGE_NAMESPACE, path); + newAttrs.putAll(attrs); + // But we need to put it again to make sure it wasn't overwritten. + newAttrs.put(BundleRevision.PACKAGE_NAMESPACE, path); + + // Create filter now so we can inject filter directive. + SimpleFilter sf = SimpleFilter.convert(newAttrs); + + // Inject filter directive. + // TODO: OSGi R4.3 - Can we insert this on demand somehow? + Map dirs = clause.dirs; + Map newDirs = new HashMap(dirs.size() + 1); + newDirs.putAll(dirs); + newDirs.put(Constants.FILTER_DIRECTIVE, sf.toString()); + + // Create package requirement and add to requirement list. + reqList.add( + new RequirementImpl( + resource, + BundleRevision.PACKAGE_NAMESPACE, + newDirs, + Collections.emptyMap(), + sf)); + } + } + + return reqList; + } + + @SuppressWarnings( "deprecation" ) + private static List normalizeDynamicImportClauses( + List clauses) + throws BundleException { + // Verify that the values are equals if the package specifies + // both version and specification-version attributes. + for (ParsedHeaderClause clause : clauses) { + // Add the resolution directive to indicate that these are + // dynamic imports. + clause.dirs.put(Constants.RESOLUTION_DIRECTIVE, RESOLUTION_DYNAMIC); + + // Check for "version" and "specification-version" attributes + // and verify they are the same if both are specified. + Object v = clause.attrs.get(Constants.VERSION_ATTRIBUTE); + Object sv = clause.attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION); + if ((v != null) && (sv != null)) { + // Verify they are equal. + if (!((String) v).trim().equals(((String) sv).trim())) { + throw new IllegalArgumentException( + "Both version and specification-version are specified, but they are not equal."); + } + } + + // Ensure that only the "version" attribute is used and convert + // it to the VersionRange type. + if ((v != null) || (sv != null)) { + clause.attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); + v = (v == null) ? sv : v; + clause.attrs.put(Constants.VERSION_ATTRIBUTE, VersionRange.parseVersionRange(v.toString())); + } + + // If bundle version is specified, then convert its type to VersionRange. + v = clause.attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); + if (v != null) { + clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, VersionRange.parseVersionRange(v.toString())); + } + + // Dynamic imports can have duplicates, so verify that java.* + // packages are not imported. + for (String pkgName : clause.paths) { + if (pkgName.startsWith("java.")) { + throw new BundleException("Dynamically importing java.* packages not allowed: " + pkgName); + } else if (!pkgName.equals("*") && pkgName.endsWith("*") && !pkgName.endsWith(".*")) { + throw new BundleException("Partial package name wild carding is not allowed: " + pkgName); + } + } + } + + return clauses; + } + + private static List normalizeRequireCapabilityClauses( + List clauses) + throws BundleException { + + return clauses; + } + + private static List normalizeProvideCapabilityClauses( + List clauses) + throws BundleException + { + + // Convert attributes into specified types. + for (ParsedHeaderClause clause : clauses) + { + for (Map.Entry entry : clause.types.entrySet()) + { + String type = entry.getValue(); + if (!type.equals("String")) + { + if (type.equals("Double")) + { + clause.attrs.put( + entry.getKey(), + new Double(clause.attrs.get(entry.getKey()).toString().trim())); + } + else if (type.equals("Version")) + { + clause.attrs.put( + entry.getKey(), + new Version(clause.attrs.get(entry.getKey()).toString().trim())); + } + else if (type.equals("Long")) + { + clause.attrs.put( + entry.getKey(), + new Long(clause.attrs.get(entry.getKey()).toString().trim())); + } + else if (type.startsWith("List")) + { + int startIdx = type.indexOf('<'); + int endIdx = type.indexOf('>'); + if (((startIdx > 0) && (endIdx <= startIdx)) + || ((startIdx < 0) && (endIdx > 0))) + { + throw new BundleException( + "Invalid Provide-Capability attribute list type for '" + + entry.getKey() + + "' : " + + type); + } + + String listType = "String"; + if (endIdx > startIdx) + { + listType = type.substring(startIdx + 1, endIdx).trim(); + } + + List tokens = parseDelimitedString( + clause.attrs.get(entry.getKey()).toString(), ",", false); + List values = new ArrayList(tokens.size()); + for (String token : tokens) + { + if (listType.equals("String")) + { + values.add(token); + } + else if (listType.equals("Double")) + { + values.add(new Double(token.trim())); + } + else if (listType.equals("Version")) + { + values.add(new Version(token.trim())); + } + else if (listType.equals("Long")) + { + values.add(new Long(token.trim())); + } + else + { + throw new BundleException( + "Unknown Provide-Capability attribute list type for '" + + entry.getKey() + + "' : " + + type); + } + } + clause.attrs.put( + entry.getKey(), + values); + } + else + { + throw new BundleException( + "Unknown Provide-Capability attribute type for '" + + entry.getKey() + + "' : " + + type); + } + } + } + } + + return clauses; + } + + private static List convertRequireCapabilities( + List clauses, Resource resource) + throws BundleException { + // Now convert generic header clauses into requirements. + List reqList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) { + try { + String filterStr = clause.dirs.get(Constants.FILTER_DIRECTIVE); + SimpleFilter sf = (filterStr != null) + ? SimpleFilter.parse(filterStr) + : new SimpleFilter(null, null, SimpleFilter.MATCH_ALL); + for (String path : clause.paths) { + // Create requirement and add to requirement list. + reqList.add(new RequirementImpl( + resource, path, clause.dirs, clause.attrs, sf)); + } + } catch (Exception ex) { + throw new BundleException("Error creating requirement: " + ex, ex); + } + } + + return reqList; + } + + private static List convertProvideCapabilities( + List clauses, Resource resource) + throws BundleException { + List capList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) { + for (String path : clause.paths) { + if (path.startsWith("osgi.wiring.")) { +// throw new BundleException("Manifest cannot use Provide-Capability for '" + path + "' namespace."); + } + + // Create package capability and add to capability list. + capList.add(new CapabilityImpl(resource, path, clause.dirs, clause.attrs)); + } + } + + return capList; + } + + @SuppressWarnings( "deprecation" ) + private static List normalizeExportClauses( + List clauses, + String bsn, Version bv) + throws BundleException { + // Verify that "java.*" packages are not exported. + for (ParsedHeaderClause clause : clauses) { + // Verify that the named package has not already been declared. + for (String pkgName : clause.paths) { + // Verify that java.* packages are not exported. + if (pkgName.startsWith("java.")) { + throw new BundleException("Exporting java.* packages not allowed: " + pkgName); + } + // The character "." has no meaning in the OSGi spec except + // when placed on the bundle class path. Some people, however, + // mistakenly think it means the default package when imported + // or exported. This is not correct. It is invalid. + else if (pkgName.equals(".")) { + throw new BundleException("Exporing '.' is invalid."); + } + // Make sure a package name was specified. + else if (pkgName.length() == 0) { + throw new BundleException("Exported package names cannot be zero length."); + } + } + + // Check for "version" and "specification-version" attributes + // and verify they are the same if both are specified. + Object v = clause.attrs.get(Constants.VERSION_ATTRIBUTE); + Object sv = clause.attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION); + if ((v != null) && (sv != null)) { + // Verify they are equal. + if (!((String) v).trim().equals(((String) sv).trim())) { + throw new IllegalArgumentException("Both version and specification-version are specified, but they are not equal."); + } + } + + // Always add the default version if not specified. + if ((v == null) && (sv == null)) { + v = Version.emptyVersion; + } + + // Ensure that only the "version" attribute is used and convert + // it to the appropriate type. + if ((v != null) || (sv != null)) { + // Convert version attribute to type Version. + clause.attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); + v = (v == null) ? sv : v; + clause.attrs.put(Constants.VERSION_ATTRIBUTE, Version.parseVersion(v.toString())); + } + + // Find symbolic name and version attribute, if present. + if (clause.attrs.containsKey(Constants.BUNDLE_VERSION_ATTRIBUTE) + || clause.attrs.containsKey(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE)) { + throw new BundleException("Exports must not specify bundle symbolic name or bundle version."); + } + + // Now that we know that there are no bundle symbolic name and version + // attributes, add them since the spec says they are there implicitly. + clause.attrs.put(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE, bsn); + clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, bv); + } + + return clauses; + } + + private static List convertExports( + List clauses, Resource resource) { + List capList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) { + for (String pkgName : clause.paths) { + // Prepend the package name to the array of attributes. + Map attrs = clause.attrs; + Map newAttrs = new HashMap(attrs.size() + 1); + newAttrs.putAll(attrs); + newAttrs.put(BundleRevision.PACKAGE_NAMESPACE, pkgName); + + // Create package capability and add to capability list. + capList.add(new CapabilityImpl(resource, BundleRevision.PACKAGE_NAMESPACE, clause.dirs, newAttrs)); + } + } + + return capList; + } + + private static String getManifestVersion(Map headerMap) { + String manifestVersion = headerMap.get(Constants.BUNDLE_MANIFESTVERSION); + return (manifestVersion == null) ? "1" : manifestVersion.trim(); + } + + private static List calculateImplicitImports( + List exports, List imports) + throws BundleException { + List clauseList = new ArrayList(); + + // Since all R3 exports imply an import, add a corresponding + // requirement for each existing export capability. Do not + // duplicate imports. + Map map = new HashMap(); + // Add existing imports. + for (ParsedHeaderClause anImport : imports) { + for (int pathIdx = 0; pathIdx < anImport.paths.size(); pathIdx++) { + map.put(anImport.paths.get(pathIdx), anImport.paths.get(pathIdx)); + } + } + // Add import requirement for each export capability. + for (BundleCapability export : exports) { + if (map.get(export.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE).toString()) == null) { + // Convert Version to VersionRange. + Object version = export.getAttributes().get(Constants.VERSION_ATTRIBUTE); + ParsedHeaderClause clause = new ParsedHeaderClause(); + if (version != null) { + clause.attrs.put(Constants.VERSION_ATTRIBUTE, VersionRange.parseVersionRange(version.toString())); + } + clause.paths.add((String) export.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE)); + clauseList.add(clause); + } + } + + return clauseList; + } + + private static List calculateImplicitUses( + List exports, List imports) + throws BundleException { + // Add a "uses" directive onto each export of R3 bundles + // that references every other import (which will include + // exports, since export implies import); this is + // necessary since R3 bundles assumed a single class space, + // but R4 allows for multiple class spaces. + String usesValue = ""; + for (ParsedHeaderClause anImport : imports) { + for (int pathIdx = 0; pathIdx < anImport.paths.size(); pathIdx++) { + usesValue = usesValue + + ((usesValue.length() > 0) ? "," : "") + + anImport.paths.get(pathIdx); + } + } + for (int i = 0; i < exports.size(); i++) { + Map dirs = new HashMap(1); + dirs.put(Constants.USES_DIRECTIVE, usesValue); + exports.set(i, new CapabilityImpl( + exports.get(i).getResource(), + BundleRevision.PACKAGE_NAMESPACE, + dirs, + exports.get(i).getAttributes())); + } + + return exports; + } + + private static ParsedHeaderClause parseBundleSymbolicName(Map headerMap) + throws BundleException { + List clauses = parseStandardHeader(headerMap.get(Constants.BUNDLE_SYMBOLICNAME)); + if (clauses.size() > 0) { + if (clauses.size() > 1 || clauses.get(0).paths.size() > 1) { + throw new BundleException("Cannot have multiple symbolic names: " + headerMap.get(Constants.BUNDLE_SYMBOLICNAME)); + } + + // Get bundle version. + Version bundleVersion = Version.emptyVersion; + if (headerMap.get(Constants.BUNDLE_VERSION) != null) { + bundleVersion = Version.parseVersion(headerMap.get(Constants.BUNDLE_VERSION)); + } + + // Create a require capability and return it. + ParsedHeaderClause clause = clauses.get(0); + String symName = clause.paths.get(0); + clause.attrs.put(BundleRevision.BUNDLE_NAMESPACE, symName); + clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, bundleVersion); + return clause; + } + + return null; + } + + private static List parseFragmentHost( + Resource resource, Map headerMap) + throws BundleException { + List reqs = new ArrayList(); + + List clauses = parseStandardHeader(headerMap.get(Constants.FRAGMENT_HOST)); + if (clauses.size() > 0) { + // Make sure that only one fragment host symbolic name is specified. + if (clauses.size() > 1 || clauses.get(0).paths.size() > 1) { + throw new BundleException("Fragments cannot have multiple hosts: " + headerMap.get(Constants.FRAGMENT_HOST)); + } + + // If the bundle-version attribute is specified, then convert + // it to the proper type. + Object value = clauses.get(0).attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); + value = (value == null) ? "0.0.0" : value; + clauses.get(0).attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, VersionRange.parseVersionRange(value.toString())); + + // Note that we use a linked hash map here to ensure the + // host symbolic name is first, which will make indexing + // more efficient. + // TODO: OSGi R4.3 - This is ordering is kind of hacky. + // Prepend the host symbolic name to the map of attributes. + Map attrs = clauses.get(0).attrs; + Map newAttrs = new LinkedHashMap(attrs.size() + 1); + // We want this first from an indexing perspective. + newAttrs.put(BundleRevision.HOST_NAMESPACE, clauses.get(0).paths.get(0)); + newAttrs.putAll(attrs); + // But we need to put it again to make sure it wasn't overwritten. + newAttrs.put(BundleRevision.HOST_NAMESPACE, clauses.get(0).paths.get(0)); + + // Create filter now so we can inject filter directive. + SimpleFilter sf = SimpleFilter.convert(newAttrs); + + // Inject filter directive. + // TODO: OSGi R4.3 - Can we insert this on demand somehow? + Map dirs = clauses.get(0).dirs; + Map newDirs = new HashMap(dirs.size() + 1); + newDirs.putAll(dirs); + newDirs.put(Constants.FILTER_DIRECTIVE, sf.toString()); + + reqs.add(new RequirementImpl( + resource, BundleRevision.HOST_NAMESPACE, + newDirs, + newAttrs)); + } + + return reqs; + } + + private static List normalizeRequireClauses(List clauses) { + // Convert bundle version attribute to VersionRange type. + for (ParsedHeaderClause clause : clauses) { + Object value = clause.attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); + if (value != null) { + clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, VersionRange.parseVersionRange(value.toString())); + } + } + + return clauses; + } + + private static List convertRequires(List clauses, Resource resource) { + List reqList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) { + for (String path : clause.paths) { + // Prepend the bundle symbolic name to the array of attributes. + Map attrs = clause.attrs; + // Note that we use a linked hash map here to ensure the + // symbolic name attribute is first, which will make indexing + // more efficient. + // TODO: OSGi R4.3 - This is ordering is kind of hacky. + // Prepend the symbolic name to the array of attributes. + Map newAttrs = new LinkedHashMap(attrs.size() + 1); + // We want this first from an indexing perspective. + newAttrs.put(BundleRevision.BUNDLE_NAMESPACE, path); + newAttrs.putAll(attrs); + // But we need to put it again to make sure it wasn't overwritten. + newAttrs.put(BundleRevision.BUNDLE_NAMESPACE, path); + + // Create filter now so we can inject filter directive. + SimpleFilter sf = SimpleFilter.convert(newAttrs); + + // Inject filter directive. + // TODO: OSGi R4.3 - Can we insert this on demand somehow? + Map dirs = clause.dirs; + Map newDirs = new HashMap(dirs.size() + 1); + newDirs.putAll(dirs); + newDirs.put(Constants.FILTER_DIRECTIVE, sf.toString()); + + // Create package requirement and add to requirement list. + reqList.add(new RequirementImpl(resource, BundleRevision.BUNDLE_NAMESPACE, newDirs, newAttrs)); + } + } + + return reqList; + } + + private static final char EOF = (char) -1; + + private static char charAt(int pos, String headers, int length) + { + if (pos >= length) + { + return EOF; + } + return headers.charAt(pos); + } + + private static final int CLAUSE_START = 0; + private static final int PARAMETER_START = 1; + private static final int KEY = 2; + private static final int DIRECTIVE_OR_TYPEDATTRIBUTE = 4; + private static final int ARGUMENT = 8; + private static final int VALUE = 16; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static List parseStandardHeader(String header) + { + List clauses = new ArrayList(); + if (header == null) + { + return clauses; + } + ParsedHeaderClause clause = null; + String key = null; + Map targetMap = null; + int state = CLAUSE_START; + int currentPosition = 0; + int startPosition = 0; + int length = header.length(); + boolean quoted = false; + boolean escaped = false; + + char currentChar = EOF; + do + { + currentChar = charAt(currentPosition, header, length); + switch (state) + { + case CLAUSE_START: + clause = new ParsedHeaderClause(); + clauses.add(clause); + state = PARAMETER_START; + case PARAMETER_START: + startPosition = currentPosition; + state = KEY; + case KEY: + switch (currentChar) + { + case ':': + case '=': + key = header.substring(startPosition, currentPosition).trim(); + startPosition = currentPosition + 1; + targetMap = clause.attrs; + state = currentChar == ':' ? DIRECTIVE_OR_TYPEDATTRIBUTE : ARGUMENT; + break; + case EOF: + case ',': + case ';': + clause.paths.add(header.substring(startPosition, currentPosition).trim()); + state = currentChar == ',' ? CLAUSE_START : PARAMETER_START; + break; + default: + break; + } + currentPosition++; + break; + case DIRECTIVE_OR_TYPEDATTRIBUTE: + switch(currentChar) + { + case '=': + if (startPosition != currentPosition) + { + clause.types.put(key, header.substring(startPosition, currentPosition).trim()); + } + else + { + targetMap = clause.dirs; + } + state = ARGUMENT; + startPosition = currentPosition + 1; + break; + default: + break; + } + currentPosition++; + break; + case ARGUMENT: + if (currentChar == '\"') + { + quoted = true; + currentPosition++; + } + else + { + quoted = false; + } + if (!Character.isWhitespace(currentChar)) { + state = VALUE; + } + else { + currentPosition++; + } + break; + case VALUE: + if (escaped) + { + escaped = false; + } + else + { + if (currentChar == '\\' ) + { + escaped = true; + } + else if (quoted && currentChar == '\"') + { + quoted = false; + } + else if (!quoted) + { + String value = null; + switch(currentChar) + { + case EOF: + case ';': + case ',': + value = header.substring(startPosition, currentPosition).trim(); + if (value.startsWith("\"") && value.endsWith("\"")) + { + value = value.substring(1, value.length() - 1); + } + if (targetMap.put(key, value) != null) + { + throw new IllegalArgumentException( + "Duplicate '" + key + "' in: " + header); + } + state = currentChar == ';' ? PARAMETER_START : CLAUSE_START; + break; + default: + break; + } + } + } + currentPosition++; + break; + default: + break; + } + } while ( currentChar != EOF); + + if (state > PARAMETER_START) + { + throw new IllegalArgumentException("Unable to parse header: " + header); + } + return clauses; + } + + public static List parseDelimitedString(String value, String delim) + { + return parseDelimitedString(value, delim, true); + } + + /** + * Parses delimited string and returns an array containing the tokens. This + * parser obeys quotes, so the delimiter character will be ignored if it is + * inside of a quote. This method assumes that the quote character is not + * included in the set of delimiter characters. + * @param value the delimited string to parse. + * @param delim the characters delimiting the tokens. + * @return a list of string or an empty list if there are none. + **/ + public static List parseDelimitedString(String value, String delim, boolean trim) + { + if (value == null) + { + value = ""; + } + + List list = new ArrayList(); + + int CHAR = 1; + int DELIMITER = 2; + int STARTQUOTE = 4; + int ENDQUOTE = 8; + + StringBuffer sb = new StringBuffer(); + + int expecting = (CHAR | DELIMITER | STARTQUOTE); + + boolean isEscaped = false; + for (int i = 0; i < value.length(); i++) + { + char c = value.charAt(i); + + boolean isDelimiter = (delim.indexOf(c) >= 0); + + if (!isEscaped && (c == '\\')) + { + isEscaped = true; + continue; + } + + if (isEscaped) + { + sb.append(c); + } + else if (isDelimiter && ((expecting & DELIMITER) > 0)) + { + if (trim) + { + list.add(sb.toString().trim()); + } + else + { + list.add(sb.toString()); + } + sb.delete(0, sb.length()); + expecting = (CHAR | DELIMITER | STARTQUOTE); + } + else if ((c == '"') && ((expecting & STARTQUOTE) > 0)) + { + sb.append(c); + expecting = CHAR | ENDQUOTE; + } + else if ((c == '"') && ((expecting & ENDQUOTE) > 0)) + { + sb.append(c); + expecting = (CHAR | STARTQUOTE | DELIMITER); + } + else if ((expecting & CHAR) > 0) + { + sb.append(c); + } + else + { + throw new IllegalArgumentException("Invalid delimited string: " + value); + } + + isEscaped = false; + } + + if (sb.length() > 0) + { + if (trim) + { + list.add(sb.toString().trim()); + } + else + { + list.add(sb.toString()); + } + } + + return list; + } + + + static class ParsedHeaderClause { + public final List paths = new ArrayList(); + public final Map dirs = new LinkedHashMap(); + public final Map attrs = new LinkedHashMap(); + public final Map types = new LinkedHashMap(); + } +} http://git-wip-us.apache.org/repos/asf/karaf/blob/999f4970/features/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java ---------------------------------------------------------------------- diff --git a/features/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java b/features/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java new file mode 100644 index 0000000..18e0dc3 --- /dev/null +++ b/features/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java @@ -0,0 +1,110 @@ +/* + * 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.resolver; + +import java.util.ArrayList; +import java.util.List; + +import org.osgi.framework.Version; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +/** + */ +public class ResourceImpl implements Resource { + + private final List m_caps; + private final List m_reqs; + + public ResourceImpl(String name, Version version) { + this(name, IdentityNamespace.TYPE_BUNDLE, version); + } + + public ResourceImpl(String name, String type, Version version) + { + m_caps = new ArrayList(); + m_caps.add(0, new IdentityCapability(this, name, type, version)); + m_reqs = new ArrayList(); + } + + public void addCapability(Capability capability) { + assert capability.getResource() == this; + m_caps.add(capability); + } + + public void addCapabilities(Iterable capabilities) { + for (Capability cap : capabilities) { + addCapability(cap); + } + } + + public void addRequirement(Requirement requirement) { + assert requirement.getResource() == this; + m_reqs.add(requirement); + } + + public void addRequirements(Iterable requirements) { + for (Requirement req : requirements) { + addRequirement(req); + } + } + + public List getCapabilities(String namespace) + { + List result = m_caps; + if (namespace != null) + { + result = new ArrayList(); + for (Capability cap : m_caps) + { + if (cap.getNamespace().equals(namespace)) + { + result.add(cap); + } + } + } + return result; + } + + public List getRequirements(String namespace) + { + List result = m_reqs; + if (namespace != null) + { + result = new ArrayList(); + for (Requirement req : m_reqs) + { + if (req.getNamespace().equals(namespace)) + { + result.add(req); + } + } + } + return result; + } + + @Override + public String toString() + { + Capability cap = getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE).get(0); + return cap.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE) + "/" + + cap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE); + } + +} http://git-wip-us.apache.org/repos/asf/karaf/blob/999f4970/features/src/main/java/org/apache/karaf/features/internal/resolver/ServiceNamespace.java ---------------------------------------------------------------------- diff --git a/features/src/main/java/org/apache/karaf/features/internal/resolver/ServiceNamespace.java b/features/src/main/java/org/apache/karaf/features/internal/resolver/ServiceNamespace.java new file mode 100644 index 0000000..4fe3bf8 --- /dev/null +++ b/features/src/main/java/org/apache/karaf/features/internal/resolver/ServiceNamespace.java @@ -0,0 +1,30 @@ +/* + * 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.resolver; + +import org.osgi.resource.Namespace; + +/** + */ +public final class ServiceNamespace extends Namespace { + + public static final String SERVICE_NAMESPACE = "service-reference"; + + private ServiceNamespace() { + } + +} http://git-wip-us.apache.org/repos/asf/karaf/blob/999f4970/features/src/main/java/org/apache/karaf/features/internal/resolver/SimpleFilter.java ---------------------------------------------------------------------- diff --git a/features/src/main/java/org/apache/karaf/features/internal/resolver/SimpleFilter.java b/features/src/main/java/org/apache/karaf/features/internal/resolver/SimpleFilter.java new file mode 100644 index 0000000..ae10441 --- /dev/null +++ b/features/src/main/java/org/apache/karaf/features/internal/resolver/SimpleFilter.java @@ -0,0 +1,649 @@ +/* + * 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.resolver; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.felix.utils.version.VersionRange; + +public class SimpleFilter +{ + public static final int MATCH_ALL = 0; + public static final int AND = 1; + public static final int OR = 2; + public static final int NOT = 3; + public static final int EQ = 4; + public static final int LTE = 5; + public static final int GTE = 6; + public static final int SUBSTRING = 7; + public static final int PRESENT = 8; + public static final int APPROX = 9; + + private final String m_name; + private final Object m_value; + private final int m_op; + + public SimpleFilter(String attr, Object value, int op) + { + m_name = attr; + m_value = value; + m_op = op; + } + + public String getName() + { + return m_name; + } + + public Object getValue() + { + return m_value; + } + + public int getOperation() + { + return m_op; + } + + public String toString() + { + String s = null; + switch (m_op) + { + case AND: + s = "(&" + toString((List) m_value) + ")"; + break; + case OR: + s = "(|" + toString((List) m_value) + ")"; + break; + case NOT: + s = "(!" + toString((List) m_value) + ")"; + break; + case EQ: + s = "(" + m_name + "=" + toEncodedString(m_value) + ")"; + break; + case LTE: + s = "(" + m_name + "<=" + toEncodedString(m_value) + ")"; + break; + case GTE: + s = "(" + m_name + ">=" + toEncodedString(m_value) + ")"; + break; + case SUBSTRING: + s = "(" + m_name + "=" + unparseSubstring((List) m_value) + ")"; + break; + case PRESENT: + s = "(" + m_name + "=*)"; + break; + case APPROX: + s = "(" + m_name + "~=" + toEncodedString(m_value) + ")"; + break; + case MATCH_ALL: + s = "(*)"; + break; + } + return s; + } + + private static String toString(List list) + { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < list.size(); i++) + { + sb.append(list.get(i).toString()); + } + return sb.toString(); + } + + private static String toDecodedString(String s, int startIdx, int endIdx) + { + StringBuffer sb = new StringBuffer(endIdx - startIdx); + boolean escaped = false; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = s.charAt(startIdx + i); + if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + sb.append(c); + } + } + + return sb.toString(); + } + + private static String toEncodedString(Object o) + { + if (o instanceof String) + { + String s = (String) o; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + if ((c == '\\') || (c == '(') || (c == ')') || (c == '*')) + { + sb.append('\\'); + } + sb.append(c); + } + + o = sb.toString(); + } + + return o.toString(); + } + + public static SimpleFilter parse(String filter) + { + int idx = skipWhitespace(filter, 0); + + if ((filter == null) || (filter.length() == 0) || (idx >= filter.length())) + { + throw new IllegalArgumentException("Null or empty filter."); + } + else if (filter.charAt(idx) != '(') + { + throw new IllegalArgumentException("Missing opening parenthesis: " + filter); + } + + SimpleFilter sf = null; + List stack = new ArrayList(); + boolean isEscaped = false; + while (idx < filter.length()) + { + if (sf != null) + { + throw new IllegalArgumentException( + "Only one top-level operation allowed: " + filter); + } + + if (!isEscaped && (filter.charAt(idx) == '(')) + { + // Skip paren and following whitespace. + idx = skipWhitespace(filter, idx + 1); + + if (filter.charAt(idx) == '&') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.AND)); + } + else + { + stack.add(0, new Integer(idx)); + } + } + else if (filter.charAt(idx) == '|') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.OR)); + } + else + { + stack.add(0, new Integer(idx)); + } + } + else if (filter.charAt(idx) == '!') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT)); + } + else + { + stack.add(0, new Integer(idx)); + } + } + else + { + stack.add(0, new Integer(idx)); + } + } + else if (!isEscaped && (filter.charAt(idx) == ')')) + { + Object top = stack.remove(0); + if (top instanceof SimpleFilter) + { + if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value).add(top); + } + else + { + sf = (SimpleFilter) top; + } + } + else if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value).add( + SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx)); + } + else + { + sf = SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx); + } + } + else if (!isEscaped && (filter.charAt(idx) == '\\')) + { + isEscaped = true; + } + else + { + isEscaped = false; + } + + idx = skipWhitespace(filter, idx + 1); + } + + if (sf == null) + { + throw new IllegalArgumentException("Missing closing parenthesis: " + filter); + } + + return sf; + } + + private static SimpleFilter subfilter(String filter, int startIdx, int endIdx) + { + final String opChars = "=<>~"; + + // Determine the ending index of the attribute name. + int attrEndIdx = startIdx; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = filter.charAt(startIdx + i); + if (opChars.indexOf(c) >= 0) + { + break; + } + else if (!Character.isWhitespace(c)) + { + attrEndIdx = startIdx + i + 1; + } + } + if (attrEndIdx == startIdx) + { + throw new IllegalArgumentException( + "Missing attribute name: " + filter.substring(startIdx, endIdx)); + } + String attr = filter.substring(startIdx, attrEndIdx); + + // Skip the attribute name and any following whitespace. + startIdx = skipWhitespace(filter, attrEndIdx); + + // Determine the operator type. + int op = -1; + switch (filter.charAt(startIdx)) + { + case '=': + op = EQ; + startIdx++; + break; + case '<': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + op = LTE; + startIdx += 2; + break; + case '>': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + op = GTE; + startIdx += 2; + break; + case '~': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + op = APPROX; + startIdx += 2; + break; + default: + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + + // Parse value. + Object value = toDecodedString(filter, startIdx, endIdx); + + // Check if the equality comparison is actually a substring + // or present operation. + if (op == EQ) + { + String valueStr = filter.substring(startIdx, endIdx); + List values = parseSubstring(valueStr); + if ((values.size() == 2) + && (values.get(0).length() == 0) + && (values.get(1).length() == 0)) + { + op = PRESENT; + } + else if (values.size() > 1) + { + op = SUBSTRING; + value = values; + } + } + + return new SimpleFilter(attr, value, op); + } + + public static List parseSubstring(String value) + { + List pieces = new ArrayList(); + StringBuffer ss = new StringBuffer(); + // int kind = SIMPLE; // assume until proven otherwise + boolean wasStar = false; // indicates last piece was a star + boolean leftstar = false; // track if the initial piece is a star + boolean rightstar = false; // track if the final piece is a star + + int idx = 0; + + // We assume (sub)strings can contain leading and trailing blanks + boolean escaped = false; + loop: for (;;) + { + if (idx >= value.length()) + { + if (wasStar) + { + // insert last piece as "" to handle trailing star + rightstar = true; + } + else + { + pieces.add(ss.toString()); + // accumulate the last piece + // note that in the case of + // (cn=); this might be + // the string "" (!=null) + } + ss.setLength(0); + break loop; + } + + // Read the next character and account for escapes. + char c = value.charAt(idx++); + if (!escaped && (c == '*')) + { + // If we have successive '*' characters, then we can + // effectively collapse them by ignoring succeeding ones. + if (!wasStar) + { + if (ss.length() > 0) + { + pieces.add(ss.toString()); // accumulate the pieces + // between '*' occurrences + } + ss.setLength(0); + // if this is a leading star, then track it + if (pieces.isEmpty()) + { + leftstar = true; + } + wasStar = true; + } + } + else if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + wasStar = false; + ss.append(c); + } + } + if (leftstar || rightstar || pieces.size() > 1) + { + // insert leading and/or trailing "" to anchor ends + if (rightstar) + { + pieces.add(""); + } + if (leftstar) + { + pieces.add(0, ""); + } + } + return pieces; + } + + public static String unparseSubstring(List pieces) + { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < pieces.size(); i++) + { + if (i > 0) + { + sb.append("*"); + } + sb.append(toEncodedString(pieces.get(i))); + } + return sb.toString(); + } + + public static boolean compareSubstring(List pieces, String s) + { + // Walk the pieces to match the string + // There are implicit stars between each piece, + // and the first and last pieces might be "" to anchor the match. + // assert (pieces.length > 1) + // minimal case is * + + boolean result = true; + int len = pieces.size(); + + // Special case, if there is only one piece, then + // we must perform an equality test. + if (len == 1) + { + return s.equals(pieces.get(0)); + } + + // Otherwise, check whether the pieces match + // the specified string. + + int index = 0; + + loop: for (int i = 0; i < len; i++) + { + String piece = pieces.get(i); + + // If this is the first piece, then make sure the + // string starts with it. + if (i == 0) + { + if (!s.startsWith(piece)) + { + result = false; + break loop; + } + } + + // If this is the last piece, then make sure the + // string ends with it. + if (i == (len - 1)) + { + if (s.endsWith(piece) && (s.length() >= (index + piece.length()))) + { + result = true; + } + else + { + result = false; + } + break loop; + } + + // If this is neither the first or last piece, then + // make sure the string contains it. + if ((i > 0) && (i < (len - 1))) + { + index = s.indexOf(piece, index); + if (index < 0) + { + result = false; + break loop; + } + } + + // Move string index beyond the matching piece. + index += piece.length(); + } + + return result; + } + + private static int skipWhitespace(String s, int startIdx) + { + int len = s.length(); + while ((startIdx < len) && Character.isWhitespace(s.charAt(startIdx))) + { + startIdx++; + } + return startIdx; + } + + /** + * Converts a attribute map to a filter. The filter is created by iterating + * over the map's entry set. If ordering of attributes is important (e.g., + * for hitting attribute indices), then the map's entry set should iterate + * in the desired order. Equality testing is assumed for all attribute types + * other than version ranges, which are handled appropriated. If the attribute + * map is empty, then a filter that matches anything is returned. + * @param attrs Map of attributes to convert to a filter. + * @return A filter corresponding to the attributes. + */ + public static SimpleFilter convert(Map attrs) + { + // Rather than building a filter string to be parsed into a SimpleFilter, + // we will just create the parsed SimpleFilter directly. + + List filters = new ArrayList(); + + for (Entry entry : attrs.entrySet()) + { + if (entry.getValue() instanceof VersionRange) + { + VersionRange vr = (VersionRange) entry.getValue(); + if (!vr.isOpenFloor()) + { + filters.add( + new SimpleFilter( + entry.getKey(), + vr.getFloor().toString(), + SimpleFilter.GTE)); + } + else + { + SimpleFilter not = + new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT); + ((List) not.getValue()).add( + new SimpleFilter( + entry.getKey(), + vr.getFloor().toString(), + SimpleFilter.LTE)); + filters.add(not); + } + + if (vr.getCeiling() != null) + { + if (!vr.isOpenCeiling()) + { + filters.add( + new SimpleFilter( + entry.getKey(), + vr.getCeiling().toString(), + SimpleFilter.LTE)); + } + else + { + SimpleFilter not = + new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT); + ((List) not.getValue()).add( + new SimpleFilter( + entry.getKey(), + vr.getCeiling().toString(), + SimpleFilter.GTE)); + filters.add(not); + } + } + } + else + { + List values = SimpleFilter.parseSubstring(entry.getValue().toString()); + if (values.size() > 1) + { + filters.add( + new SimpleFilter( + entry.getKey(), + values, + SimpleFilter.SUBSTRING)); + } + else + { + filters.add( + new SimpleFilter( + entry.getKey(), + values.get(0), + SimpleFilter.EQ)); + } + } + } + + SimpleFilter sf = null; + + if (filters.size() == 1) + { + sf = filters.get(0); + } + else if (attrs.size() > 1) + { + sf = new SimpleFilter(null, filters, SimpleFilter.AND); + } + else if (filters.isEmpty()) + { + sf = new SimpleFilter(null, null, SimpleFilter.MATCH_ALL); + } + + return sf; + } +} http://git-wip-us.apache.org/repos/asf/karaf/blob/999f4970/features/src/main/java/org/apache/karaf/features/internal/resolver/Slf4jResolverLog.java ---------------------------------------------------------------------- diff --git a/features/src/main/java/org/apache/karaf/features/internal/resolver/Slf4jResolverLog.java b/features/src/main/java/org/apache/karaf/features/internal/resolver/Slf4jResolverLog.java new file mode 100644 index 0000000..2f4a1f3 --- /dev/null +++ b/features/src/main/java/org/apache/karaf/features/internal/resolver/Slf4jResolverLog.java @@ -0,0 +1,49 @@ +/* + * 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.resolver; + +import org.slf4j.Logger; + +/** + */ +public class Slf4jResolverLog extends org.apache.felix.resolver.Logger { + + private final Logger logger; + + public Slf4jResolverLog(Logger logger) { + super(LOG_DEBUG); + this.logger = logger; + } + + @Override + protected void doLog(int level, String msg, Throwable throwable) { + switch (level) { + case LOG_ERROR: + logger.error(msg, throwable); + break; + case LOG_WARNING: + logger.warn(msg, throwable); + break; + case LOG_INFO: + logger.info(msg, throwable); + break; + case LOG_DEBUG: + logger.debug(msg, throwable); + break; + } + } +} http://git-wip-us.apache.org/repos/asf/karaf/blob/999f4970/features/src/main/java/org/apache/karaf/features/internal/resolver/UriNamespace.java ---------------------------------------------------------------------- diff --git a/features/src/main/java/org/apache/karaf/features/internal/resolver/UriNamespace.java b/features/src/main/java/org/apache/karaf/features/internal/resolver/UriNamespace.java new file mode 100644 index 0000000..b5158bf --- /dev/null +++ b/features/src/main/java/org/apache/karaf/features/internal/resolver/UriNamespace.java @@ -0,0 +1,47 @@ +/* + * 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.resolver; + +import java.util.List; + +import org.osgi.resource.Capability; +import org.osgi.resource.Namespace; +import org.osgi.resource.Resource; + +/** + */ +public final class UriNamespace extends Namespace { + + public static final String URI_NAMESPACE = "karaf.uri"; + + public static String getUri(Resource resource) + { + List caps = resource.getCapabilities(null); + for (Capability cap : caps) + { + if (cap.getNamespace().equals(UriNamespace.URI_NAMESPACE)) + { + return cap.getAttributes().get(UriNamespace.URI_NAMESPACE).toString(); + } + } + return null; + } + + + private UriNamespace() { + } +} http://git-wip-us.apache.org/repos/asf/karaf/blob/999f4970/features/src/main/java/org/apache/karaf/features/internal/service/Artifact.java ---------------------------------------------------------------------- diff --git a/features/src/main/java/org/apache/karaf/features/internal/service/Artifact.java b/features/src/main/java/org/apache/karaf/features/internal/service/Artifact.java new file mode 100644 index 0000000..44e9a7c --- /dev/null +++ b/features/src/main/java/org/apache/karaf/features/internal/service/Artifact.java @@ -0,0 +1,56 @@ +/* + * 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.service; + +import java.net.URI; + +/** + * Simple abstraction of a maven artifact to avoid external deps + */ +public class Artifact { + String groupId; + String artifactId; + String version; + String extension; + String classifier; + + public Artifact(String coords) { + String[] coordsAr = coords.split(":"); + if (coordsAr.length != 5) { + throw new IllegalArgumentException("Maven URL " + coords + " is malformed or not complete"); + } + this.groupId = coordsAr[0]; + this.artifactId = coordsAr[1]; + this.version = coordsAr[4]; + this.extension = coordsAr[2]; + this.classifier = coordsAr[3]; + } + + public Artifact(String coords, String version) { + this(coords); + this.version = version; + } + + public URI getMavenUrl(String version) { + String uriSt = "mvn:" + this.groupId + "/" + this.artifactId + "/" + version + "/" + this.extension + "/" + this.classifier; + try { + return new URI(uriSt); + } catch (Exception e) { + return null; + } + } +}