/*
* 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 WARRANTIESOR 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.aries.util.manifest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.aries.util.ManifestHeaderUtils;
import org.apache.aries.util.VersionRange;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
public class ManifestHeaderProcessor
{
public static final String NESTED_FILTER_ATTRIBUTE = "org.apache.aries.application.filter.attribute";
private static final Pattern FILTER_ATTR = Pattern.compile("(\\(!)?\\((.*?)([<>]?=)(.*?)\\)\\)?");
private static final String LESS_EQ_OP = "<=";
private static final String GREATER_EQ_OP = ">=";
/**
* A GenericMetadata is either a Generic Capability or a Generic Requirement
*/
public static class GenericMetadata {
private final String namespace;
private final Map<String, Object> attributes = new HashMap<String, Object>();
private final Map<String, String> directives = new HashMap<String, String>();
public GenericMetadata(String namespace) {
this.namespace = namespace;
}
public String getNamespace() {
return namespace;
}
public Map<String, Object> getAttributes() {
return attributes;
}
public Map<String, String> getDirectives() {
return directives;
}
}
/**
* A simple class to associate two types.
*/
public static class NameValuePair {
private String name;
private Map<String,String> attributes;
public NameValuePair(String name, Map<String,String> value)
{
this.name = name;
this.attributes = value;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public Map<String,String> getAttributes()
{
return attributes;
}
public void setAttributes(Map<String,String> value)
{
this.attributes = value;
}
@Override
public String toString(){
return "{"+name.toString()+"::"+attributes.toString()+"}";
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((attributes == null) ? 0 : attributes.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final NameValuePair other = (NameValuePair) obj;
if (name == null) {
if (other.name != null) return false;
} else if (!name.equals(other.name)) return false;
if (attributes == null) {
if (other.attributes != null) return false;
} else if (!attributes.equals(other.attributes)) return false;
return true;
}
}
/**
* Intended to provide a standard way to add Name/Value's to
* aggregations of Name/Value's.
*/
public static interface NameValueCollection {
/**
* Add this Name & Value to the collection.
* @param n
* @param v
*/
public void addToCollection(String n, Map<String,String> v);
}
/**
* Map of Name -> Value.
*/
public static class NameValueMap extends HashMap<String, Map<String,String>> implements NameValueCollection, Map<String, Map<String,String>>{
private static final long serialVersionUID = -6446338858542599141L;
public void addToCollection(String n, Map<String,String> v){
this.put(n,v);
}
@Override
public String toString(){
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first=true;
for(Map.Entry<String, Map<String,String>> entry : this.entrySet()){
if(!first)sb.append(",");
first=false;
sb.append(entry.getKey()+"->"+entry.getValue());
}
sb.append("}");
return sb.toString();
}
}
/**
* List of Name/Value
*/
public static class NameValueList extends ArrayList<NameValuePair> implements NameValueCollection, List<NameValuePair> {
private static final long serialVersionUID = 1808636823825029983L;
public void addToCollection(String n, Map<String,String> v){
this.add(new NameValuePair(n,v));
}
@Override
public String toString(){
StringBuffer sb = new StringBuffer();
sb.append("{");
boolean first = true;
for(NameValuePair nvp : this){
if(!first)sb.append(",");
first=false;
sb.append(nvp.toString());
}
sb.append("}");
return sb.toString();
}
}
/**
*
* Splits a delimiter separated string, tolerating presence of non separator commas
* within double quoted segments.
*
* Eg.
* com.ibm.ws.eba.helloWorldService;version="[1.0.0, 1.0.0]" &
* com.ibm.ws.eba.helloWorldService;version="1.0.0"
* com.ibm.ws.eba.helloWorld;version="2";bundle-version="[2,30)"
* com.acme.foo;weirdAttr="one;two;three";weirdDir:="1;2;3"
* @param value the value to be split
* @param delimiter the delimiter string such as ',' etc.
* @return List<String> the components of the split String in a list
*/
public static List<String> split(String value, String delimiter)
{
return ManifestHeaderUtils.split(value, delimiter);
}
/**
* Internal method to parse headers with the format<p>
* [Name](;[Name])*(;[attribute-name]=[attribute-value])*<br>
* Eg.<br>
* rumplestiltskin;thing=value;other=something<br>
* littleredridinghood
* bundle1;bundle2;other=things
* bundle1;bundle2
*
* @param s data to parse
* @return a list of NameValuePair, with the Name being the name component,
* and the Value being a NameValueMap of key->value mappings.
*/
private static List<NameValuePair> genericNameWithNameValuePairProcess(String s){
String name;
Map<String,String> params = null;
List<NameValuePair> nameValues = new ArrayList<NameValuePair>();
List<String> pkgs = new ArrayList<String>();
int index = s.indexOf(";");
if(index==-1){
name = s;
params = new HashMap<String, String>();
pkgs.add(name);
}else{
name = s.substring(0,index).trim();
String tail = s.substring(index+1).trim();
pkgs.add(name); // add the first package
StringBuilder parameters = new StringBuilder();
// take into consideration of multiple packages separated by ';'
// while they share the same attributes or directives
List<String> tailParts = split(tail, ";");
boolean firstParameter =false;
for (String part : tailParts) {
// if it is not a parameter and no parameter appears in front of it, it must a package
if (!!!(part.contains("="))) {
// Need to make sure no parameter appears before the package, otherwise ignore this string
// as this syntax is invalid
if (!!!(firstParameter))
pkgs.add(part);
} else {
if (!!!(firstParameter))
firstParameter = true;
parameters.append(part + ";");
}
}
if (parameters.length() != 0) {
//remove the final ';' if there is one
if (parameters.toString().endsWith(";")) {
parameters = parameters.deleteCharAt(parameters.length() -1);
}
params = genericNameValueProcess(parameters.toString());
}
}
for (String pkg : pkgs) {
nameValues.add(new NameValuePair(pkg,params));
}
return nameValues;
}
/**
* Internal method to parse headers with the format<p>
* [attribute-name]=[attribute-value](;[attribute-name]=[attribute-value])*<br>
* Eg.<br>
* thing=value;other=something<br>
* <p>
* Note. Directives (name:=value) are represented in the map with name suffixed by ':'
*
* @param s data to parse
* @return a NameValueMap, with attribute-name -> attribute-value.
*/
private static Map<String,String> genericNameValueProcess(String s){
Map<String,String> params = new HashMap<String,String>();
List<String> parameters = split(s, ";");
for(String parameter : parameters) {
List<String> parts = split(parameter,"=");
// do a check, otherwise we might get NPE
if (parts.size() ==2) {
String second = parts.get(1).trim();
if (second.startsWith("\"") && second.endsWith("\""))
second = second.substring(1,second.length()-1);
String first = parts.get(0).trim();
// make sure for directives we clear out any space as in "directive :=value"
if (first.endsWith(":")) {
first = first.substring(0, first.length()-1).trim()+":";
}
params.put(first, second);
}
}
return params;
}
/**
* Processes an import/export style header.. <p>
* pkg1;attrib=value;attrib=value,pkg2;attrib=value,pkg3;attrib=value
*
* @param out The collection to add each package name + attrib map to.
* @param s The data to parse
*/
private static void genericImportExportProcess(NameValueCollection out, String s){
List<String> packages = split(s, ",");
for(String pkg : packages){
List<NameValuePair> ps = genericNameWithNameValuePairProcess(pkg);
for (NameValuePair p : ps) {
out.addToCollection(p.getName(), p.getAttributes());
}
}
}
/**
* Parse an export style header.<p>
* pkg1;attrib=value;attrib=value,pkg2;attrib=value,pkg3;attrib=value2
* <p>
* Result is returned as a list, as export does allow duplicate package exports.
*
* @param s The data to parse.
* @return List of NameValuePairs, where each Name in the list is an exported package,
* with its associated Value being a NameValueMap of any attributes declared.
*/
public static List<NameValuePair> parseExportString(String s){
NameValueList retval = new NameValueList();
genericImportExportProcess(retval, s);
return retval;
}
/**
* Parse an export style header in a list.<p>
* pkg1;attrib=value;attrib=value
* pkg2;attrib=value
* pkg3;attrib=value2
* <p>
* Result is returned as a list, as export does allow duplicate package exports.
*
* @param list The data to parse.
* @return List of NameValuePairs, where each Name in the list is an exported package,
* with its associated Value being a NameValueMap of any attributes declared.
*/
public static List<NameValuePair> parseExportList(List<String> list){
NameValueList retval = new NameValueList();
for(String pkg : list){
List<NameValuePair> ps = genericNameWithNameValuePairProcess(pkg);
for (NameValuePair p : ps) {
retval.addToCollection(p.getName(), p.getAttributes());
}
}
return retval;
}
/**
* Parse an import style header.<p>
* pkg1;attrib=value;attrib=value,pkg2;attrib=value,pkg3;attrib=value
* <p>
* Result is returned as a set, as import does not allow duplicate package imports.
*
* @param s The data to parse.
* @return Map of NameValuePairs, where each Key in the Map is an imported package,
* with its associated Value being a NameValueMap of any attributes declared.
*/
public static Map<String, Map<String, String>> parseImportString(String s){
NameValueMap retval = new NameValueMap();
genericImportExportProcess(retval, s);
return retval;
}
/**
* Parse a generic capability header. For example<br/>
* com.acme.myns;mylist:List<String>="nl,be,fr,uk";myver:Version=1.3;long:Long="1234";d:Double="3.14";myattr=xyz,
* com.acme.myns;myattr=abc
* @param s The header to be parsed
* @return A list of GenericMetadata objects each representing an individual capability. The values in the attribute map
* are of the specified datatype.
*/
public static List<GenericMetadata> parseCapabilityString(String s) {
return parseGenericMetadata(s);
}
/**
* Parse a generic capability header. For example<br/>
* com.acme.myns;mylist:List<String>="nl,be,fr,uk";myver:Version=1.3;long:Long="1234";d:Double="3.14";myattr=xyz,
* com.acme.myns;myattr=abc
* @param s The header to be parsed
* @return A list of GenericMetadata objects each representing an individual capability. The values in the attribute map
* are of the specified datatype.
*/
public static List<GenericMetadata> parseRequirementString(String s) {
return parseGenericMetadata(s);
}
private static List<GenericMetadata> parseGenericMetadata(String s) {
List<GenericMetadata> capabilities = new ArrayList<GenericMetadata>();
List<String> entries = split(s, ",");
for(String e : entries){
List<NameValuePair> nvpList = genericNameWithNameValuePairProcess(e);
for(NameValuePair nvp : nvpList) {
String namespace = nvp.getName();
GenericMetadata cap = new GenericMetadata(namespace);
capabilities.add(cap);
Map<String, String> attrMap = nvp.getAttributes();
for (Map.Entry<String, String> entry : attrMap.entrySet()) {
String k = entry.getKey();
String v = entry.getValue();
if (k.contains(":")) {
if (k.endsWith(":")) {
// a directive
cap.getDirectives().put(k.substring(0, k.length() - 1), v);
} else {
// an attribute with its datatype specified
parseTypedAttribute(k, v, cap);
}
} else {
// ordinary (String) attribute
cap.getAttributes().put(k, v);
}
}
}
}
return capabilities;
}
private static void parseTypedAttribute(String k, String v, GenericMetadata cap) {
int idx = k.indexOf(':');
String name = k.substring(0, idx);
String type = k.substring(idx + 1);
if (type.startsWith("List<") && type.endsWith(">")) {
String subtype = type.substring("List<".length(), type.length() - 1).trim();
List<Object> l = new ArrayList<Object>();
for (String s : v.split(",")) {
l.add(getTypedValue(k, subtype, s));
}
cap.getAttributes().put(name, l);
} else {
cap.getAttributes().put(name, getTypedValue(k, type.trim(), v));
}
}
private static Object getTypedValue(String k, String type, String v) {
if ("String".equals(type)) {
return v;
} else if ("Long".equals(type)) {
return Long.parseLong(v);
} else if ("Double".equals(type)) {
return Double.parseDouble(v);
} else if ("Version".equals(type)) {
return Version.parseVersion(v);
}
throw new IllegalArgumentException(k + "=" + v);
}
/**
* Parse a bundle symbolic name.<p>
* bundlesymbolicname;attrib=value;attrib=value
* <p>
*
* @param s The data to parse.
* @return NameValuePair with Name being the BundleSymbolicName,
* and Value being any attribs declared for the name.
*/
public static NameValuePair parseBundleSymbolicName(String s){
return genericNameWithNameValuePairProcess(s).get(0); // should just return the first one
}
/**
* Parse a version range..
*
* @param s
* @return VersionRange object.
* @throws IllegalArgumentException if the String could not be parsed as a VersionRange
*/
public static VersionRange parseVersionRange(String s) throws IllegalArgumentException{
return new VersionRange(s);
}
/**
* Parse a version range and indicate if the version is an exact version
*
* @param s
* @param exactVersion
* @return VersionRange object.
* @throws IllegalArgumentException if the String could not be parsed as a VersionRange
*/
public static VersionRange parseVersionRange(String s, boolean exactVersion) throws IllegalArgumentException{
return new VersionRange(s, exactVersion);
}
/**
* Generate a filter from a set of attributes. This filter will be suitable
* for presentation to OBR This means that, due to the way OBR works, it
* will include a stanza of the form, (mandatory:<*mandatoryAttribute)
* Filter strings generated by this method will therefore tend to break the
* standard OSGi Filter class. The OBR stanza can be stripped out later if
* required.
*
* @param attribs
* @return filter string
*/
public static String generateFilter(Map<String, String> attribs) {
StringBuilder filter = new StringBuilder("(&");
boolean realAttrib = false;
StringBuffer realAttribs = new StringBuffer();
if (attribs == null) {
attribs = new HashMap<String, String>();
}
for (Map.Entry<String, String> attrib : attribs.entrySet()) {
String attribName = attrib.getKey();
if (attribName.endsWith(":")) {
// skip all directives. It is used to affect the attribs on the
// filter xml.
} else if ((Constants.VERSION_ATTRIBUTE.equals(attribName))
|| (Constants.BUNDLE_VERSION_ATTRIBUTE.equals(attribName))) {
// version and bundle-version attrib requires special
// conversion.
realAttrib = true;
VersionRange vr = ManifestHeaderProcessor
.parseVersionRange(attrib.getValue());
filter.append("(" + attribName + ">=" + vr.getMinimumVersion());
if (vr.getMaximumVersion() != null) {
filter.append(")(" + attribName + "<=");
filter.append(vr.getMaximumVersion());
}
if (vr.getMaximumVersion() != null && vr.isMinimumExclusive()) {
filter.append(")(!(" + attribName + "=");
filter.append(vr.getMinimumVersion());
filter.append(")");
}
if (vr.getMaximumVersion() != null && vr.isMaximumExclusive()) {
filter.append(")(!(" + attribName + "=");
filter.append(vr.getMaximumVersion());
filter.append(")");
}
filter.append(")");
} else if (NESTED_FILTER_ATTRIBUTE.equals(attribName)) {
// Filters go in whole, no formatting needed
realAttrib = true;
filter.append(attrib.getValue());
} else if (Constants.OBJECTCLASS.equals(attribName)) {
realAttrib = true;
// objectClass has a "," separated list of interfaces
String[] values = attrib.getValue().split(",");
for (String s : values)
filter.append("(" + Constants.OBJECTCLASS + "=" + s + ")");
} else {
// attribName was not version..
realAttrib = true;
filter.append("(" + attribName + "=" + attrib.getValue() + ")");
// store all attributes in order to build up the mandatory
// filter and separate them with ", "
// skip bundle-symbolic-name in the mandatory directive query
if (!!!Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE
.equals(attribName)) {
realAttribs.append(attribName);
realAttribs.append(", ");
}
}
}
/*
* The following is how OBR makes mandatory attributes work, we require
* that the set of mandatory attributes on the export is a subset of (or
* equal to) the set of the attributes we supply.
*/
if (realAttribs.length() > 0) {
String attribStr = (realAttribs.toString()).trim();
// remove the final ,
if ((attribStr.length() > 0) && (attribStr.endsWith(","))) {
attribStr = attribStr.substring(0, attribStr.length() - 1);
}
// build the mandatory filter, e.g.(mandatory:<*company, local)
filter.append("(" + Constants.MANDATORY_DIRECTIVE + ":" + "<*"
+ attribStr + ")");
}
// Prune (& off the front and ) off end
String filterString = filter.toString();
int openBraces = 0;
for (int i = 0; openBraces < 3; i++) {
i = filterString.indexOf('(', i);
if (i == -1) {
break;
} else {
openBraces++;
}
}
if (openBraces < 3 && filterString.length() > 2) {
filter.delete(0, 2);
} else {
filter.append(")");
}
String result = "";
if (realAttrib != false) {
result = filter.toString();
}
return result;
}
/**
* Generate a filter from a set of attributes. This filter will be suitable
* for presentation to OBR. This means that, due to the way OBR works, it will
* include a stanza of the form, (mandatory:<*mandatoryAttribute) Filter
* strings generated by this method will therefore tend to break the standard
* OSGi Filter class. The OBR stanza can be stripped out later if required.
*
* We may wish to consider relocating this method since VersionRange has its
* own top level class.
*
* @param type
* @param name
* @param attribs
* @return filter string
*/
public static String generateFilter(String type, String name,
Map<String, String> attribs) {
StringBuffer filter = new StringBuffer();
String result;
// shortcut for the simple case with no attribs.
if (attribs == null || attribs.isEmpty())
filter.append("(" + type + "=" + name + ")");
else {
// process all the attribs passed.
// find out whether there are attributes on the filter
filter.append("(&(" + type + "=" + name + ")");
String filterString = generateFilter(attribs);
int start = 0;
int end = filterString.length();
if (filterString.startsWith("(&")) {
start = 2;
end--;
}
if ("".equals(filterString)) {
filter.delete(0, 2);
} else {
filter.append(filterString, start, end);
filter.append(")");
}
}
result = filter.toString();
return result;
}
private static Map<String, String> parseFilterList(String filter) {
Map<String, String> result = new HashMap<String, String>();
Set<String> negatedVersions = new HashSet<String>();
Set<String> negatedBundleVersions = new HashSet<String>();
String lowerVersion = null;
String upperVersion = null;
String lowerBundleVersion = null;
String upperBundleVersion = null;
Matcher m = FILTER_ATTR.matcher(filter);
while (m.find()) {
boolean negation = m.group(1) != null;
String attr = m.group(2);
String op = m.group(3);
String value = m.group(4);
if (Constants.VERSION_ATTRIBUTE.equals(attr)) {
if (negation) {
negatedVersions.add(value);
} else {
if (GREATER_EQ_OP.equals(op))
lowerVersion = value;
else if (LESS_EQ_OP.equals(op))
upperVersion = value;
else
throw new IllegalArgumentException();
}
} else if (Constants.BUNDLE_VERSION_ATTRIBUTE.equals(attr)) {
// bundle-version is like version, but may be specified at the
// same time
// therefore we have similar code with separate variables
if (negation) {
negatedBundleVersions.add(value);
} else {
if (GREATER_EQ_OP.equals(op))
lowerBundleVersion = value;
else if (LESS_EQ_OP.equals(op))
upperBundleVersion = value;
else
throw new IllegalArgumentException();
}
} else {
result.put(attr, value);
}
}
if (lowerVersion != null) {
StringBuilder versionAttr = new StringBuilder(lowerVersion);
if (upperVersion != null) {
versionAttr.append(",").append(upperVersion).insert(0,
negatedVersions.contains(lowerVersion) ? '(' : '[').append(
negatedVersions.contains(upperVersion) ? ')' : ']');
}
result.put(Constants.VERSION_ATTRIBUTE, versionAttr.toString());
}
// Do it again for bundle-version
if (lowerBundleVersion != null) {
StringBuilder versionAttr = new StringBuilder(lowerBundleVersion);
if (upperBundleVersion != null) {
versionAttr.append(",").append(upperBundleVersion).insert(0,
negatedBundleVersions.contains(lowerBundleVersion) ? '(' : '[')
.append(
negatedBundleVersions.contains(upperBundleVersion) ? ')' : ']');
}
result.put(Constants.BUNDLE_VERSION_ATTRIBUTE, versionAttr.toString());
}
return result;
}
public static Map<String,String> parseFilter(String filter)
{
Map<String,String> result;
if (filter.startsWith("(&")) {
result = parseFilterList(filter.substring(2, filter.length()-1));
} else {
result = parseFilterList(filter);
}
return result;
}
}