package org.osgi.service.indexer.impl;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.indexer.Builder;
import org.osgi.service.indexer.Capability;
import org.osgi.service.indexer.Namespaces;
import org.osgi.service.indexer.Requirement;
import org.osgi.service.indexer.Resource;
import org.osgi.service.indexer.ResourceAnalyzer;
import org.osgi.service.indexer.impl.types.ScalarType;
import org.osgi.service.indexer.impl.types.TypedValue;
import org.osgi.service.indexer.impl.types.VersionKey;
import org.osgi.service.indexer.impl.types.VersionRange;
import org.osgi.service.log.LogService;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class SCRAnalyzer implements ResourceAnalyzer {
static final Pattern URI_VERSION_P = Pattern.compile("/scr/v(\\d+\\.\\d+\\.\\d+)$");
public static final String NS_1_0 = Namespaces.NS_OSGI + "/scr/v1.0.0";
public static final String NS_1_1 = Namespaces.NS_OSGI + "/scr/v1.1.0";
public static final String NS_1_2 = Namespaces.NS_OSGI + "/scr/v1.2.0";
public static final String NS_1_2_1 = Namespaces.NS_OSGI + "/scr/v1.2.1";
public static final String NS_1_3 = Namespaces.NS_OSGI + "/scr/v1.3.0";
public static final String ELEMENT_COMPONENT = "component";
public static final String ELEMENT_SERVICE = "service";
public static final String ELEMENT_PROVIDE = "provide";
public static final String ELEMENT_REFERENCE = "reference";
public static final String ELEMENT_PROPERTY = "property";
public static final String ATTRIB_INTERFACE = "interface";
public static final String ATTRIB_CARDINALITY = "cardinality";
public static final String ATTRIB_NAME = "name";
public static final String ATTRIB_TYPE = "type";
public static final String ATTRIB_VALUE = "value";
public static final String ATTRIB_TARGET = "target";
private LogService log;
public SCRAnalyzer(LogService log) {
this.log = log;
}
public void analyzeResource(Resource resource, List<Capability> caps, List<Requirement> reqs) throws Exception {
String header = null;
Manifest manifest = resource.getManifest();
if (manifest != null)
header = manifest.getMainAttributes().getValue(ComponentConstants.SERVICE_COMPONENT);
if (header == null)
return;
StringTokenizer tokenizer = new StringTokenizer(header, ",");
Version highest = null;
while (tokenizer.hasMoreTokens()) {
String pattern = tokenizer.nextToken().trim();
List<String> paths = Util.findMatchingPaths(resource, pattern);
if (paths != null)
for (String path : paths) {
Version version = processScrXml(resource, path, caps, reqs);
if (version == null)
continue;
if (highest == null || (version.compareTo(highest) > 0))
highest = version;
}
}
if (highest != null) {
Version lower = new Version(highest.getMajor(), highest.getMinor(), 0);
Version upper = new Version(highest.getMajor() + 1, 0, 0);
Requirement requirement = createRequirement(new VersionRange(true, lower, upper, false));
reqs.add(requirement);
}
}
private Version processScrXml(Resource resource, String path, List<Capability> caps, List<Requirement> reqs)
throws IOException {
Resource childResource = resource.getChild(path);
if (childResource == null) {
if (log != null)
log.log(LogService.LOG_WARNING,
MessageFormat.format(
"Cannot analyse SCR requirement version: resource {0} does not contain path {1} referred from Service-Component header.",
resource.getLocation(), path));
return null;
}
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
try {
SAXParser parser = spf.newSAXParser();
SCRContentHandler handler = new SCRContentHandler(caps, reqs);
parser.parse(childResource.getStream(), handler);
return handler.highest;
} catch (Exception e) {
if (log != null)
log.log(LogService.LOG_ERROR,
MessageFormat.format("Processing error: failed to parse child resource {0} in resource {1}.",
path, resource.getLocation()),
e);
return null;
}
}
private static Requirement createRequirement(VersionRange range) {
Builder builder = new Builder().setNamespace(Namespaces.NS_EXTENDER);
StringBuilder filter = new StringBuilder();
filter.append('(').append(Namespaces.NS_EXTENDER).append('=').append(Namespaces.EXTENDER_SCR).append(')');
filter.insert(0, "(&");
Util.addVersionFilter(filter, range, VersionKey.PackageVersion);
filter.append(')');
builder.addDirective(Namespaces.DIRECTIVE_FILTER, filter.toString())
.addDirective(Namespaces.DIRECTIVE_EFFECTIVE, Namespaces.EFFECTIVE_ACTIVE);
Requirement requirement = builder.buildRequirement();
return requirement;
}
private static class SCRContentHandler extends DefaultHandler {
private List<Capability> caps;
private List<Requirement> reqs;
Version highest = null;
private List<String> provides = null;
private List<Requirement> references = null;
private Map<String,Object> properties = null;
private String currentPropertyName = null;
private ScalarType currentPropertyType = null;
private String currentPropertyValue = null;
private StringBuilder currentPropertyText = null;
public SCRContentHandler(List<Capability> caps, List<Requirement> reqs) {
super();
this.caps = caps;
this.reqs = reqs;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attribs) throws SAXException {
super.startElement(uri, localName, qName, attribs);
String localNameLowerCase = localName.toLowerCase();
if (ELEMENT_COMPONENT.equals(localNameLowerCase)) {
provides = new LinkedList<String>();
properties = new LinkedHashMap<String,Object>();
references = new LinkedList<Requirement>();
if (uri == null || "".equals(uri)) {
setVersion(new Version(1, 0, 0));
} else {
//
// Actually, we do not care that match
// since we just create a dependency on that
// version. So lets parse the version out of the
// URI assuming the URI will look similar in the future,
// which is a realistic expectation. If the syntax
// does not match, too bad.
//
Matcher m = URI_VERSION_P.matcher(uri);
if (m.find()) {
String v = m.group(1);
setVersion(new Version(v));
} else
throw new SAXException("Unknown namespace " + uri);
}
}
else if (ELEMENT_PROVIDE.equals(localNameLowerCase)) {
String objectClass = attribs.getValue(ATTRIB_INTERFACE);
provides.add(objectClass);
}
else if (ELEMENT_PROPERTY.equals(localNameLowerCase)) {
currentPropertyName = attribs.getValue(ATTRIB_NAME);
if (currentPropertyName == null)
throw new SAXException("Missing required attribute '" + ATTRIB_NAME + "'.");
currentPropertyType = typeOf(attribs.getValue(ATTRIB_TYPE));
String value = attribs.getValue(ATTRIB_VALUE);
if (value != null) {
currentPropertyValue = value;
} else {
currentPropertyText = new StringBuilder();
}
}
else if (ELEMENT_REFERENCE.equals(localNameLowerCase)) {
references.add(createServiceRequirement(attribs));
}
}
@Override
public void characters(char[] chars, int start, int length) throws SAXException {
if (currentPropertyName != null && currentPropertyText != null)
currentPropertyText.append(chars, start, length);
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
String localNameLowerCase = localName.toLowerCase();
if (ELEMENT_PROPERTY.equals(localNameLowerCase)) {
if (currentPropertyValue != null) {
Object value = readTyped(currentPropertyType, currentPropertyValue);
properties.put(currentPropertyName, new TypedValue(currentPropertyType, value));
} else if (currentPropertyText != null) {
String[] lines = currentPropertyText.toString().split("\n");
List<Object> values = new ArrayList<Object>(lines.length);
for (int i = 0; i < lines.length; i++) {
String line = lines[i].trim();
if (line.length() > 0) {
Object value = readTyped(currentPropertyType, line);
values.add(value);
}
}
properties.put(currentPropertyName, new TypedValue(currentPropertyType, values));
}
currentPropertyName = null;
currentPropertyType = null;
currentPropertyValue = null;
currentPropertyText = null;
}
if (ELEMENT_COMPONENT.equals(localNameLowerCase)) {
if (provides != null && !provides.isEmpty()) {
Builder builder = new Builder().setNamespace(Namespaces.NS_SERVICE);
builder.addAttribute(Constants.OBJECTCLASS, provides);
for (Entry<String,Object> entry : properties.entrySet()) {
builder.addAttribute(entry.getKey(), entry.getValue());
}
StringBuilder uses = new StringBuilder();
boolean first = true;
for (String objectClass : provides) {
if (!first)
uses.append(',');
first = false;
int dotindex = objectClass.lastIndexOf('.');
if (dotindex < 0)
throw new SAXException("Service interface in default package.");
String pkgName = objectClass.substring(0, dotindex);
uses.append(pkgName);
}
builder.addDirective(Constants.USES_DIRECTIVE, uses.toString());
caps.add(builder.buildCapability());
}
if (references != null && !references.isEmpty()) {
reqs.addAll(references);
}
}
super.endElement(uri, localName, qName);
}
private void setVersion(Version version) {
if (highest == null || (version.compareTo(highest) > 0))
highest = version;
}
private static Requirement createServiceRequirement(Attributes attribs) throws SAXException {
String interfaceClass = attribs.getValue(ATTRIB_INTERFACE);
if (interfaceClass == null || interfaceClass.length() == 0) {
throw new SAXException("Missing required " + ATTRIB_INTERFACE + " attribute");
}
Builder builder = new Builder().setNamespace(Namespaces.NS_SERVICE);
String filter;
// #770 https://github.com/bndtools/bnd/issues/770
//
// We should not include any target properties since this is more a
// runtime
// filter that is often overridden by config admin. Since this is
// handled in
// runtime anyway, we should be able to ignore it easily.
// @formatter:off
// String target = attribs.getValue(ATTRIB_TARGET);
// if (target != null)
// filter = String.format("(&(%s=%s)%s)", Constants.OBJECTCLASS, interfaceClass, target);
// else
// @formatter:on
filter = String.format("(%s=%s)", Constants.OBJECTCLASS, interfaceClass);
builder.addDirective(Namespaces.DIRECTIVE_FILTER, filter);
String cardinality = attribs.getValue(ATTRIB_CARDINALITY);
if (cardinality != null) {
cardinality = cardinality.trim().toLowerCase();
if (cardinality.length() > 0) {
if ('0' == cardinality.charAt(0))
builder.addDirective(Namespaces.DIRECTIVE_RESOLUTION, Namespaces.RESOLUTION_OPTIONAL);
if ('n' == cardinality.charAt(cardinality.length() - 1))
builder.addDirective(Namespaces.DIRECTIVE_CARDINALITY, Namespaces.CARDINALITY_MULTIPLE);
}
}
builder.addDirective(Namespaces.DIRECTIVE_EFFECTIVE, Namespaces.EFFECTIVE_ACTIVE);
return builder.buildRequirement();
}
private static ScalarType typeOf(String name) {
if (name == null)
return ScalarType.String;
if (Long.class.getSimpleName().equals(name) || Integer.class.getSimpleName().equals(name)
|| Short.class.getSimpleName().equals(name) || Byte.class.getSimpleName().equals(name))
return ScalarType.Long;
if (Float.class.getSimpleName().equals(name) || Double.class.getSimpleName().equals(name))
return ScalarType.Double;
return ScalarType.String;
}
private static Object readTyped(ScalarType type, String string) {
switch (type) {
case Long :
return Long.parseLong(string);
case Double :
return Double.parseDouble(string);
default :
return string;
}
}
}
}