blob: a269300590f8c975d1800460a92537bca5faaa3d [file] [log] [blame]
/*
* 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);
}
}