| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.felix.utils.resource; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.felix.utils.collections.StringArrayMap; |
| import org.apache.felix.utils.version.VersionRange; |
| import org.apache.felix.utils.version.VersionTable; |
| import org.osgi.framework.BundleException; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.Version; |
| import org.osgi.framework.namespace.ExecutionEnvironmentNamespace; |
| import org.osgi.framework.namespace.IdentityNamespace; |
| import org.osgi.framework.wiring.BundleRevision; |
| import org.osgi.namespace.service.ServiceNamespace; |
| import org.osgi.resource.Capability; |
| import org.osgi.resource.Requirement; |
| import org.osgi.resource.Resource; |
| import org.osgi.service.repository.ContentNamespace; |
| |
| public final class ResourceBuilder { |
| |
| public static final String RESOLUTION_DYNAMIC = "dynamic"; |
| |
| private static final char EOF = (char) -1; |
| |
| 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; |
| |
| private static final int CHAR = 1; |
| private static final int DELIMITER = 2; |
| private static final int STARTQUOTE = 4; |
| private static final int ENDQUOTE = 8; |
| |
| private static final Map<String, String> DEFAULT_DIRECTIVES = Collections.singletonMap(ServiceNamespace.CAPABILITY_EFFECTIVE_DIRECTIVE, ServiceNamespace.EFFECTIVE_ACTIVE); |
| |
| private ResourceBuilder() { |
| } |
| |
| public static ResourceImpl build(String uri, Map<String, String> headerMap) throws BundleException { |
| return build(new ResourceImpl(), uri, headerMap, false); |
| } |
| |
| public static ResourceImpl build(String uri, Map<String, String> headerMap, boolean removeServiceRequirements) throws BundleException { |
| return build(new ResourceImpl(), uri, headerMap, removeServiceRequirements); |
| } |
| |
| public static ResourceImpl build(ResourceImpl resource, String uri, Map<String, String> headerMap) throws BundleException { |
| return build(resource, uri, headerMap, false); |
| } |
| |
| public static ResourceImpl build(ResourceImpl resource, String uri, Map<String, String> headerMap, boolean removeServiceRequirements) throws BundleException { |
| try { |
| return doBuild(resource, uri, headerMap, removeServiceRequirements); |
| } catch (Exception e) { |
| throw new BundleException("Unable to build resource for " + uri + ": " + e.getMessage(), e); |
| } |
| } |
| |
| private static ResourceImpl doBuild(ResourceImpl resource, String uri, Map<String, String> headerMap, boolean removeServiceRequirements) 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 = VersionTable.getVersion(headerMap.get(Constants.BUNDLE_VERSION)); |
| } |
| |
| // |
| // Parse bundle symbolic name. |
| // |
| |
| String bundleSymbolicName; |
| 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; |
| { |
| Map<String, Object> attrs = new StringArrayMap<>(3); |
| attrs.put(IdentityNamespace.IDENTITY_NAMESPACE, bundleSymbolicName); |
| attrs.put(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, type); |
| attrs.put(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, bundleVersion); |
| CapabilityImpl identity = new CapabilityImpl(resource, IdentityNamespace.IDENTITY_NAMESPACE, Collections.<String, String>emptyMap(), attrs); |
| resource.addCapability(identity); |
| } |
| if (uri != null) { |
| resource.addCapability(new CapabilityImpl(resource, ContentNamespace.CONTENT_NAMESPACE, |
| Collections.<String, String>emptyMap(), |
| Collections.<String, Object>singletonMap(ContentNamespace.CAPABILITY_URL_ATTRIBUTE, uri))); |
| } |
| |
| // 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<String, Object> hostAttrs = new StringArrayMap<>(bundleCap.attrs.size()); |
| for (Map.Entry<String, Object> e : bundleCap.attrs.entrySet()) { |
| String k = e.getKey(); |
| if (BundleRevision.BUNDLE_NAMESPACE.equals(k)) { |
| k = BundleRevision.HOST_NAMESPACE; |
| } |
| hostAttrs.put(k, e.getValue()); |
| } |
| resource.addCapability(new CapabilityImpl( |
| resource, BundleRevision.HOST_NAMESPACE, |
| bundleCap.dirs, |
| hostAttrs)); |
| } |
| } |
| |
| // |
| // Parse Fragment-Host. |
| // |
| |
| List<RequirementImpl> hostReqs = parseFragmentHost(resource, headerMap); |
| |
| // |
| // Parse Require-Bundle |
| // |
| |
| List<ParsedHeaderClause> rbClauses = parseStandardHeader(headerMap.get(Constants.REQUIRE_BUNDLE)); |
| rbClauses = normalizeRequireClauses(rbClauses); |
| List<Requirement> rbReqs = convertRequires(rbClauses, resource); |
| |
| // |
| // Parse Import-Package. |
| // |
| |
| List<ParsedHeaderClause> importClauses = parseStandardHeader(headerMap.get(Constants.IMPORT_PACKAGE)); |
| importClauses = normalizeImportClauses(importClauses); |
| List<Requirement> importReqs = convertImports(importClauses, resource); |
| |
| // |
| // Parse DynamicImport-Package. |
| // |
| |
| List<ParsedHeaderClause> dynamicClauses = parseStandardHeader(headerMap.get(Constants.DYNAMICIMPORT_PACKAGE)); |
| dynamicClauses = normalizeDynamicImportClauses(dynamicClauses); |
| List<Requirement> dynamicReqs = convertImports(dynamicClauses, resource); |
| |
| // |
| // Parse Require-Capability. |
| // |
| |
| List<ParsedHeaderClause> requireClauses = parseStandardHeader(headerMap.get(Constants.REQUIRE_CAPABILITY)); |
| requireClauses = normalizeRequireCapabilityClauses(requireClauses); |
| List<Requirement> requireReqs = convertRequireCapabilities(requireClauses, resource); |
| |
| // |
| // Parse Bundle-RequiredExecutionEnvironment. |
| // |
| List<Requirement> breeReqs = |
| parseBreeHeader(headerMap.get(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT), resource); |
| |
| // |
| // Parse Export-Package. |
| // |
| |
| List<ParsedHeaderClause> exportClauses = parseStandardHeader(headerMap.get(Constants.EXPORT_PACKAGE)); |
| exportClauses = normalizeExportClauses(exportClauses, bundleSymbolicName, bundleVersion); |
| List<Capability> exportCaps = convertExports(exportClauses, resource); |
| |
| // |
| // Parse Provide-Capability. |
| // |
| |
| List<ParsedHeaderClause> provideClauses = parseStandardHeader(headerMap.get(Constants.PROVIDE_CAPABILITY)); |
| provideClauses = normalizeProvideCapabilityClauses(provideClauses); |
| List<Capability> 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<ParsedHeaderClause> exportServices = parseStandardHeader(headerMap.get(Constants.EXPORT_SERVICE)); |
| List<Capability> caps = convertExportService(exportServices, resource); |
| provideCaps.addAll(caps); |
| } |
| |
| boolean hasServiceReferenceRequirement = false; |
| for (Requirement req : requireReqs) { |
| hasServiceReferenceRequirement |= ServiceNamespace.SERVICE_NAMESPACE.equals(req.getNamespace()); |
| } |
| if (!hasServiceReferenceRequirement) { |
| List<ParsedHeaderClause> importServices = parseStandardHeader(headerMap.get(Constants.IMPORT_SERVICE)); |
| List<Requirement> reqs = convertImportService(importServices, resource); |
| if (!reqs.isEmpty()) { |
| requireReqs.addAll(reqs); |
| hasServiceReferenceRequirement = true; |
| } |
| } |
| |
| if (hasServiceReferenceRequirement && removeServiceRequirements) { |
| for (Iterator<Requirement> iterator = requireReqs.iterator(); iterator.hasNext();) { |
| Requirement req = iterator.next(); |
| if (ServiceNamespace.SERVICE_NAMESPACE.equals(req.getNamespace())) { |
| iterator.remove(); |
| } |
| } |
| } |
| |
| // 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<Requirement> parseRequirement(Resource resource, String requirement) throws BundleException { |
| List<ParsedHeaderClause> requireClauses = parseStandardHeader(requirement); |
| requireClauses = normalizeRequireCapabilityClauses(requireClauses); |
| return convertRequireCapabilities(requireClauses, resource); |
| } |
| |
| public static List<Capability> parseCapability(Resource resource, String capability) throws BundleException { |
| List<ParsedHeaderClause> provideClauses = parseStandardHeader(capability); |
| provideClauses = normalizeProvideCapabilityClauses(provideClauses); |
| return convertProvideCapabilities(provideClauses, resource); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private static List<ParsedHeaderClause> normalizeImportClauses(List<ParsedHeaderClause> clauses) throws BundleException { |
| // Verify that the values are equals if the package specifies |
| // both version and specification-version attributes. |
| Set<String> 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 no duplicate imports, nor '.' or empty packages. |
| for (String pkgName : clause.paths) { |
| if (!dupeSet.contains(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. |
| 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<Capability> convertExportService(List<ParsedHeaderClause> clauses, Resource resource) { |
| List<Capability> capList = new ArrayList<>(); |
| for (ParsedHeaderClause clause : clauses) { |
| for (String path : clause.paths) { |
| Map<String, Object> attrs = new LinkedHashMap<>(); |
| attrs.put(Constants.OBJECTCLASS, path); |
| attrs.putAll(clause.attrs); |
| capList.add(new CapabilityImpl( |
| resource, |
| ServiceNamespace.SERVICE_NAMESPACE, |
| DEFAULT_DIRECTIVES, |
| attrs)); |
| } |
| } |
| return capList; |
| } |
| |
| private static List<Requirement> convertImportService(List<ParsedHeaderClause> clauses, Resource resource) throws BundleException { |
| try { |
| List<Requirement> 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<String, String> dirs = new LinkedHashMap<>(2); |
| 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, |
| null, |
| SimpleFilter.parse(filter))); |
| } |
| } |
| return reqList; |
| } catch (Exception ex) { |
| throw new BundleException("Error creating requirement: " + ex, ex); |
| } |
| } |
| |
| private static List<Requirement> convertImports(List<ParsedHeaderClause> clauses, Resource resource) { |
| // Now convert generic header clauses into requirements. |
| List<Requirement> reqList = new ArrayList<>(); |
| for (ParsedHeaderClause clause : clauses) { |
| for (String path : clause.paths) { |
| // Prepend the package name to the array of attributes. |
| Map<String, Object> 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<String, Object> 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<String, String> dirs = clause.dirs; |
| Map<String, String> newDirs = new StringArrayMap<>(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, |
| null, |
| sf) |
| ); |
| } |
| } |
| |
| return reqList; |
| } |
| |
| @SuppressWarnings("deprecation") |
| private static List<ParsedHeaderClause> normalizeDynamicImportClauses(List<ParsedHeaderClause> 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<ParsedHeaderClause> normalizeRequireCapabilityClauses( |
| List<ParsedHeaderClause> clauses) throws BundleException { |
| |
| // Convert attributes into specified types. |
| for (ParsedHeaderClause clause : clauses) { |
| for (Map.Entry<String, Object> entry : clause.attrs.entrySet()) { |
| if (entry.getKey().equals("version")) { |
| clause.attrs.put(entry.getKey(), new VersionRange(entry.getValue().toString())); |
| } |
| } |
| for (Map.Entry<String, String> 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<String> tokens = parseDelimitedString( |
| clause.attrs.get(entry.getKey()).toString(), ",", false); |
| List<Object> values = new ArrayList<>(tokens.size()); |
| for (String token : tokens) { |
| switch (listType) { |
| case "String": |
| values.add(token); |
| break; |
| case "Double": |
| values.add(new Double(token.trim())); |
| break; |
| case "Version": |
| values.add(new Version(token.trim())); |
| break; |
| case "Long": |
| values.add(new Long(token.trim())); |
| break; |
| default: |
| 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<ParsedHeaderClause> normalizeProvideCapabilityClauses( |
| List<ParsedHeaderClause> clauses) throws BundleException { |
| |
| // Convert attributes into specified types. |
| for (ParsedHeaderClause clause : clauses) { |
| for (Map.Entry<String, String> 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<String> tokens = parseDelimitedString( |
| clause.attrs.get(entry.getKey()).toString(), ",", false); |
| List<Object> values = new ArrayList<>(tokens.size()); |
| for (String token : tokens) { |
| switch (listType) { |
| case "String": |
| values.add(token); |
| break; |
| case "Double": |
| values.add(new Double(token.trim())); |
| break; |
| case "Version": |
| values.add(new Version(token.trim())); |
| break; |
| case "Long": |
| values.add(new Long(token.trim())); |
| break; |
| default: |
| 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<Requirement> convertRequireCapabilities( |
| List<ParsedHeaderClause> clauses, Resource resource) throws BundleException { |
| |
| // Now convert generic header clauses into requirements. |
| List<Requirement> reqList = new ArrayList<>(); |
| for (ParsedHeaderClause clause : clauses) { |
| try { |
| String filterStr = clause.dirs.get(Constants.FILTER_DIRECTIVE); |
| SimpleFilter sf = (filterStr != null) |
| ? SimpleFilter.parse(filterStr) |
| : SimpleFilter.convert(clause.attrs); |
| 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<Capability> convertProvideCapabilities( |
| List<ParsedHeaderClause> clauses, Resource resource) throws BundleException { |
| |
| List<Capability> 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<ParsedHeaderClause> normalizeExportClauses( |
| List<ParsedHeaderClause> 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, VersionTable.getVersion(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<Capability> convertExports(List<ParsedHeaderClause> clauses, Resource resource) { |
| List<Capability> capList = new ArrayList<>(); |
| for (ParsedHeaderClause clause : clauses) { |
| for (String pkgName : clause.paths) { |
| // Prepend the package name to the array of attributes. |
| Map<String, Object> attrs = clause.attrs; |
| Map<String, Object> newAttrs = new StringArrayMap<>(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<String, String> headerMap) { |
| String manifestVersion = headerMap.get(Constants.BUNDLE_MANIFESTVERSION); |
| return (manifestVersion == null) ? "1" : manifestVersion.trim(); |
| } |
| |
| private static ParsedHeaderClause parseBundleSymbolicName(Map<String, String> headerMap) throws BundleException { |
| List<ParsedHeaderClause> 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 = VersionTable.getVersion(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<RequirementImpl> parseFragmentHost(Resource resource, Map<String, String> headerMap) throws BundleException { |
| List<RequirementImpl> reqs = new ArrayList<>(); |
| List<ParsedHeaderClause> 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<String, Object> attrs = clauses.get(0).attrs; |
| Map<String, Object> 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<String, String> dirs = clauses.get(0).dirs; |
| Map<String, String> newDirs = new StringArrayMap<>(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<Requirement> parseBreeHeader(String header, Resource resource) { |
| List<String> filters = new ArrayList<>(); |
| for (String entry : parseDelimitedString(header, ",")) { |
| List<String> names = parseDelimitedString(entry, "/"); |
| List<String> left = parseDelimitedString(names.get(0), "-"); |
| |
| String lName = left.get(0); |
| Version lVer; |
| try { |
| lVer = Version.parseVersion(left.get(1)); |
| } catch (Exception ex) { |
| // Version doesn't parse. Make it part of the name. |
| lName = names.get(0); |
| lVer = null; |
| } |
| |
| String rName = null; |
| Version rVer = null; |
| if (names.size() > 1) { |
| List<String> right = parseDelimitedString(names.get(1), "-"); |
| rName = right.get(0); |
| try { |
| rVer = Version.parseVersion(right.get(1)); |
| } catch (Exception ex) { |
| rName = names.get(1); |
| rVer = null; |
| } |
| } |
| |
| String versionClause; |
| if (lVer != null) { |
| if ((rVer != null) && (!rVer.equals(lVer))) { |
| // Both versions are defined, but different. Make each of them part of the name |
| lName = names.get(0); |
| rName = names.get(1); |
| versionClause = null; |
| } else { |
| versionClause = getBreeVersionClause(lVer); |
| } |
| } else { |
| versionClause = getBreeVersionClause(rVer); |
| } |
| |
| if ("J2SE".equals(lName)) { |
| // J2SE is not used in the Capability variant of BREE, use JavaSE here |
| // This can only happen with the lName part... |
| lName = "JavaSE"; |
| } |
| |
| String nameClause; |
| if (rName != null) { |
| nameClause = "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE + "=" + lName + "/" + rName + ")"; |
| } else { |
| nameClause = "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE + "=" + lName + ")"; |
| } |
| |
| String filter; |
| if (versionClause != null) { |
| filter = "(&" + nameClause + versionClause + ")"; |
| } else { |
| filter = nameClause; |
| } |
| |
| filters.add(filter); |
| } |
| |
| if (filters.size() == 0) { |
| return Collections.emptyList(); |
| } else { |
| String reqFilter; |
| if (filters.size() == 1) { |
| reqFilter = filters.get(0); |
| } else { |
| // If there are more BREE filters, we need to or them together |
| StringBuilder sb = new StringBuilder("(|"); |
| for (String f : filters) { |
| sb.append(f); |
| } |
| sb.append(")"); |
| reqFilter = sb.toString(); |
| } |
| |
| SimpleFilter sf = SimpleFilter.parse(reqFilter); |
| return Collections.<Requirement>singletonList(new RequirementImpl( |
| resource, |
| ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE, |
| Collections.singletonMap(ExecutionEnvironmentNamespace.REQUIREMENT_FILTER_DIRECTIVE, reqFilter), |
| null, |
| sf)); |
| } |
| } |
| |
| private static String getBreeVersionClause(Version ver) { |
| if (ver == null) { |
| return null; |
| } |
| |
| return "(" + ExecutionEnvironmentNamespace.CAPABILITY_VERSION_ATTRIBUTE + "=" + ver + ")"; |
| } |
| |
| private static List<ParsedHeaderClause> normalizeRequireClauses(List<ParsedHeaderClause> 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<Requirement> convertRequires(List<ParsedHeaderClause> clauses, Resource resource) { |
| List<Requirement> reqList = new ArrayList<>(); |
| for (ParsedHeaderClause clause : clauses) { |
| for (String path : clause.paths) { |
| // Prepend the bundle symbolic name to the array of attributes. |
| Map<String, Object> 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<String, Object> 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<String, String> dirs = clause.dirs; |
| Map<String, String> newDirs = new StringArrayMap<>(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 char charAt(int pos, String headers, int length) { |
| if (pos >= length) { |
| return EOF; |
| } |
| return headers.charAt(pos); |
| } |
| |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| private static List<ParsedHeaderClause> parseStandardHeader(String header) { |
| List<ParsedHeaderClause> 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; |
| do { |
| currentChar = charAt(currentPosition, header, length); |
| switch (state) { |
| case CLAUSE_START: |
| clause = new ParsedHeaderClause(); |
| clauses.add(clause); |
| // Fall through |
| case PARAMETER_START: |
| startPosition = currentPosition; |
| state = KEY; |
| // Fall through |
| 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; |
| 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<String> 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. |
| * @param trim true to trim the string, false else. |
| * @return a list of string or an empty list if there are none. |
| */ |
| public static List<String> parseDelimitedString(String value, String delim, boolean trim) { |
| if (value == null) { |
| value = ""; |
| } |
| |
| List<String> list = new ArrayList<>(); |
| |
| StringBuilder sb = new StringBuilder(); |
| |
| 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<String> paths = new ArrayList<>(); |
| public final Map<String, String> dirs = new StringArrayMap<>(0); |
| public final Map<String, Object> attrs = new StringArrayMap<>(0); |
| public final Map<String, String> types = new StringArrayMap<>(0); |
| } |
| } |