package aQute.bnd.metatype;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Icon;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.osgi.service.metatype.annotations.Option;
import aQute.bnd.annotation.xml.XMLAttribute;
import aQute.bnd.metatype.MetatypeAnnotations.Options;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Annotation;
import aQute.bnd.osgi.ClassDataCollector;
import aQute.bnd.osgi.Clazz;
import aQute.bnd.osgi.Clazz.FieldDef;
import aQute.bnd.osgi.Clazz.MethodDef;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.xmlattribute.XMLAttributeFinder;
class OCDReader {
final Analyzer analyzer;
private final Clazz clazz;
final EnumSet<Options> options;
private final Set<TypeRef> analyzed = new HashSet<TypeRef>();;
private final OCDDef ocd;
final XMLAttributeFinder finder;
private OCDReader(Analyzer analyzer, Clazz clazz, EnumSet<Options> options, XMLAttributeFinder finder,
MetatypeVersion minVersion) {
this.analyzer = analyzer;
this.clazz = clazz;
this.options = options;
this.finder = finder;
this.ocd = new OCDDef(finder, minVersion);
}
static OCDDef getOCDDef(Clazz c, Analyzer analyzer, EnumSet<Options> options, XMLAttributeFinder finder, MetatypeVersion minVersion)
throws Exception {
OCDReader r = new OCDReader(analyzer, c, options, finder, minVersion);
return r.getDef();
}
private OCDDef getDef() throws Exception {
clazz.parseClassFileWithCollector(new OCDDataCollector(ocd));
if (ocd.id == null) {
return null;
}
parseExtends(clazz);
return ocd;
}
private void parseExtends(Clazz clazz) {
TypeRef[] inherits = clazz.getInterfaces();
if (inherits != null) {
for (TypeRef typeRef : inherits) {
if (!typeRef.isJava() && analyzed.add(typeRef)) {
try {
Clazz inherit = analyzer.findClass(typeRef);
if (inherit != null) {
inherit.parseClassFileWithCollector(new OCDDataCollector(ocd));
parseExtends(inherit);
} else {
analyzer.error("Could not obtain super class %s of class %s", typeRef.getFQN(),
clazz.getClassName().getFQN());
}
} catch (Exception e) {
analyzer.exception(e, "Could not obtain super class %s of class %s; exception %s",
typeRef.getFQN(), clazz.getClassName().getFQN(), e);
}
}
}
}
}
// TODO what about Queue|Stack|Deque?
static final Pattern GENERIC = Pattern.compile("((" + Collection.class.getName() + "|" + Set.class.getName() + "|"
+ List.class.getName() + "|" + Iterable.class.getName() + ")|(.*))<(L.+;)>");
// Determine if we can identify that this class is a concrete subtype of
// collection with a no-arg constructor
// So far this implementation doesn't try very hard. It only looks to see if
// the class directly implements a known collection interface.
static final Pattern COLLECTION = Pattern
.compile("(" + Collection.class.getName() + "|" + Set.class.getName() + "|" + List.class.getName() + "|"
+ Queue.class.getName() + "|" + Stack.class.getName() + "|" + Deque.class.getName() + ")");
static final Pattern IDENTIFIERTOPROPERTY = Pattern.compile("(__)|(_)|(\\$_\\$)|(\\$\\$)|(\\$)");
private final class OCDDataCollector extends ClassDataCollector {
private final OCDDef ocd;
private final Map<MethodDef,ADDef> methods = new LinkedHashMap<MethodDef,ADDef>();
private Clazz clazz;
private TypeRef name;
private int hasNoDefault = 0;
private boolean hasValue = false;
private FieldDef prefixField = null;
private ADDef current;
OCDDataCollector(OCDDef ocd) {
this.ocd = ocd;
}
@Override
public boolean classStart(Clazz clazz) {
this.clazz = clazz;
this.name = clazz.getClassName();
return true;
}
@Override
public void field(FieldDef defined) {
if (defined.isStatic() && defined.getName().equals("PREFIX_")) {
prefixField = defined;
}
}
@Override
public void method(MethodDef defined) {
if (defined.isStatic()) {
current = null;
return;
}
current = new ADDef(finder);
methods.put(defined, current);
if (clazz.isAnnotation()) {
if (defined.getName().equals("value")) {
hasValue = true;
} else {
hasNoDefault++;
}
}
}
@Override
public void annotationDefault(MethodDef defined, Object value) {
if (!defined.getName().equals("value")) {
hasNoDefault--;
}
}
@Override
public void annotation(Annotation annotation) throws Exception {
try {
java.lang.annotation.Annotation a = annotation.getAnnotation();
if (a instanceof ObjectClassDefinition)
doOCD((ObjectClassDefinition) a, annotation);
else if (a instanceof AttributeDefinition) {
current.ad = (AttributeDefinition) a;
current.a = annotation;
} else {
XMLAttribute xmlAttr = finder.getXMLAttribute(annotation);
if (xmlAttr != null) {
doXmlAttribute(annotation, xmlAttr);
}
}
} catch (Exception e) {
analyzer.exception(e, "During generation of a component on class %s, exception %s", clazz, e);
}
}
@Override
public void memberEnd() {
current = null;
}
@Override
public void classEnd() throws Exception {
current = null;
if (ocd.id == null) {
return; // no ObjectClassDefinition annotation found
}
String prefix = null;
if (prefixField != null) {
Object c = prefixField.getConstant();
if (prefixField.isFinal() && (prefixField.getType() == analyzer.getTypeRef("java/lang/String"))
&& (c instanceof String)) {
prefix = (String) c;
ocd.updateVersion(MetatypeVersion.VERSION_1_4);
} else {
analyzer.warning(
"Field PREFIX_ in %s is not a static final String field with a compile-time constant value: %s",
name.getFQN(), c);
}
}
String singleElementAnnotation = null;
if (hasValue && (hasNoDefault == 0)) {
StringBuilder sb = new StringBuilder(name.getShorterName());
boolean lastLowerCase = false;
for (int i = 0; i < sb.length(); i++) {
char c = sb.charAt(i);
if (Character.isUpperCase(c)) {
sb.setCharAt(i, Character.toLowerCase(c));
if (lastLowerCase) {
sb.insert(i++, '.');
}
lastLowerCase = false;
} else {
lastLowerCase = Character.isLowerCase(c);
}
}
singleElementAnnotation = sb.toString();
ocd.updateVersion(MetatypeVersion.VERSION_1_4);
}
for (Map.Entry<MethodDef,ADDef> entry : methods.entrySet()) {
MethodDef defined = entry.getKey();
if (defined.isConstructor()) {
analyzer.error("Constructor %s for %s.%s found; only interfaces and annotations allowed for OCDs",
defined.getName(), clazz.getClassName().getFQN(), defined.getName());
}
if (defined.getPrototype().length > 0) {
analyzer.error(
"Element %s for %s.%s has parameters; only no-parameter elements in an OCD interface allowed",
defined.getName(), clazz.getClassName().getFQN(), defined.getName());
continue;
}
ADDef ad = entry.getValue();
ocd.attributes.add(ad);
String key = defined.getName();
if ((singleElementAnnotation != null) && key.equals("value")) {
key = singleElementAnnotation;
} else {
key = identifierToPropertyName(key);
}
if (prefix != null) {
key = prefix + key;
}
ad.id = key;
ad.name = space(defined.getName());
String rtype = defined.getGenericReturnType();
if (rtype.endsWith("[]")) {
ad.cardinality = Integer.MAX_VALUE;
rtype = rtype.substring(0, rtype.length() - 2);
}
Matcher m = GENERIC.matcher(rtype);
if (m.matches()) {
boolean knownCollection = m.group(2) != null;
boolean collection = knownCollection || identifiableCollection(m.group(3), false, true);
if (collection) {
if (ad.cardinality != 0)
analyzer.error(
"AD for %s.%s uses an array of collections in return type (%s), Metatype allows either Vector or array",
clazz.getClassName().getFQN(), defined.getName(), defined.getType().getFQN());
rtype = Clazz.objectDescriptorToFQN(m.group(4));
ad.cardinality = Integer.MIN_VALUE;
}
}
if (rtype.indexOf('<') > 0) {
rtype = rtype.substring(0, rtype.indexOf('<'));
}
ad.type = getType(rtype);
ad.required = true;
TypeRef typeRef = analyzer.getTypeRefFromFQN(rtype);
try {
Clazz c = analyzer.findClass(typeRef);
if (c != null && c.isEnum()) {
parseOptionValues(c, ad.options);
}
} catch (Exception e) {
analyzer.exception(e, "AD for %s.%s Can not parse option values from type (%s), %s",
clazz.getClassName().getFQN(), defined.getName(), defined.getType().getFQN(), e);
}
if (ad.ad != null) {
doAD(ad);
}
if (ad.defaults == null && clazz.isAnnotation() && defined.getConstant() != null) {
// defaults from annotation default
Object value = defined.getConstant();
boolean isClass = false;
TypeRef type = defined.getType().getClassRef();
if (!type.isPrimitive()) {
if (Class.class.getName().equals(type.getFQN())) {
isClass = true;
} else {
try {
Clazz r = analyzer.findClass(type);
if (r.isAnnotation()) {
analyzer.warning("Nested annotation type found in field %s, %s", defined.getName(),
type.getFQN());
return;
}
} catch (Exception e) {
analyzer.exception(e,
"Exception looking at annotation type default for element with descriptor %s, type %s",
defined, type);
}
}
}
if (value != null) {
if (value.getClass().isArray()) {
// add element individually
ad.defaults = new String[Array.getLength(value)];
for (int i = 0; i < Array.getLength(value); i++) {
Object element = Array.get(value, i);
ad.defaults[i] = valueToProperty(element, isClass);
}
} else {
ad.defaults = new String[] {
valueToProperty(value, isClass)
};
}
}
}
}
}
private void doOCD(ObjectClassDefinition o, Annotation annotation) {
if (ocd.id == null) {
if (clazz.isInterface()) {
ocd.id = o.id() == null ? name.getFQN() : o.id();
ocd.name = o.name() == null ? space(ocd.id) : o.name();
ocd.description = o.description() == null ? "" : o.description();
ocd.localization = o.localization() == null ? "OSGI-INF/l10n/" + name.getFQN() : o.localization();
if (annotation.get("pid") != null) {
String[] pids = o.pid();
designates(name.getFQN(), pids, false);
}
if (annotation.get("factoryPid") != null) {
String[] pids = o.factoryPid();
designates(name.getFQN(), pids, true);
}
if (annotation.get("icon") != null) {
Icon[] icons = o.icon();
for (Icon icon : icons) {
ocd.icons.add(new IconDef(icon.resource(), icon.size()));
}
}
} else {
analyzer.error("ObjectClassDefinition applied to non-interface, non-annotation class %s", clazz);
}
}
}
private void doAD(ADDef adDef) throws Exception {
AttributeDefinition ad = adDef.ad;
Annotation a = adDef.a;
if (ad.name() != null) {
adDef.name = ad.name();
}
adDef.description = a.get("description");
if (a.get("type") != null) {
adDef.type = ad.type();
}
if (a.get("cardinality") != null) {
adDef.cardinality = ad.cardinality();
}
adDef.max = ad.max();
adDef.min = ad.min();
if (a.get("defaultValue") != null) {
adDef.defaults = ad.defaultValue();
}
if (a.get("required") != null) {
adDef.required = ad.required();
}
if (a.get("options") != null) {
adDef.options.clear();
for (Object o : (Object[]) a.get("options")) {
Option opt = ((Annotation) o).getAnnotation();
adDef.options.add(new OptionDef(opt.label(), opt.value()));
}
}
}
private void doXmlAttribute(Annotation annotation, XMLAttribute xmlAttr) {
if (current == null) {
if (clazz.isInterface()) {
ocd.addExtensionAttribute(xmlAttr, annotation);
}
} else {
current.addExtensionAttribute(xmlAttr, annotation);
}
}
private boolean identifiableCollection(String type, boolean intface, boolean topLevel) {
try {
Clazz clazz = analyzer.findClass(analyzer.getTypeRefFromFQN(type));
if (clazz != null && (!topLevel || !clazz.isAbstract())
&& ((intface && clazz.isInterface()) ^ clazz.hasPublicNoArgsConstructor())) {
TypeRef[] intfs = clazz.getInterfaces();
if (intfs != null) {
for (TypeRef intf : intfs) {
if (COLLECTION.matcher(intf.getFQN()).matches()
|| identifiableCollection(intf.getFQN(), true, false)) {
return true;
}
}
}
TypeRef ext = clazz.getSuper();
return ext != null && identifiableCollection(ext.getFQN(), false, false);
}
} catch (Exception e) {
return false;
}
return false;
}
private String valueToProperty(Object value, boolean isClass) {
if (isClass)
return ((TypeRef) value).getFQN();
return value.toString();
}
private void parseOptionValues(Clazz c, final List<OptionDef> options) throws Exception {
c.parseClassFileWithCollector(new ClassDataCollector() {
@Override
public void field(Clazz.FieldDef def) {
if (def.isEnum()) {
OptionDef o = new OptionDef(def.getName(), def.getName());
options.add(o);
}
}
});
}
private AttributeType getType(String rtype) {
if (rtype.endsWith("[]")) {
analyzer.error("Can only handle array of depth one field , nested type %s", rtype);
return null;
}
if ("boolean".equals(rtype) || Boolean.class.getName().equals(rtype))
return AttributeType.BOOLEAN;
else if ("byte".equals(rtype) || Byte.class.getName().equals(rtype))
return AttributeType.BYTE;
else if ("char".equals(rtype) || Character.class.getName().equals(rtype))
return AttributeType.CHARACTER;
else if ("short".equals(rtype) || Short.class.getName().equals(rtype))
return AttributeType.SHORT;
else if ("int".equals(rtype) || Integer.class.getName().equals(rtype))
return AttributeType.INTEGER;
else if ("long".equals(rtype) || Long.class.getName().equals(rtype))
return AttributeType.LONG;
else if ("float".equals(rtype) || Float.class.getName().equals(rtype))
return AttributeType.FLOAT;
else if ("double".equals(rtype) || Double.class.getName().equals(rtype))
return AttributeType.DOUBLE;
else if (String.class.getName().equals(rtype) || Class.class.getName().equals(rtype) || acceptableType(rtype))
return AttributeType.STRING;
else {
return null;
}
}
private boolean acceptableType(String rtype) {
TypeRef ref = analyzer.getTypeRefFromFQN(rtype);
try {
Clazz returnType = analyzer.findClass(ref);
if (returnType.isEnum()) {
return true;
}
// TODO check this is true for interfaces and annotations
if (!returnType.isAbstract() || (returnType.isInterface() && options.contains(Options.nested))) {
return true;
}
if (!returnType.isInterface()) {
analyzer.error("Abstract classes not allowed as interface method return values: %s", rtype);
} else {
analyzer.error("Nested metatype only allowed with option: nested type %s", rtype);
}
return false;
} catch (Exception e) {
analyzer.exception(e, "could not examine class for return type %s, exception message: %s", rtype, e);
return false;
}
}
private String identifierToPropertyName(String name) {
Matcher m = IDENTIFIERTOPROPERTY.matcher(name);
StringBuffer b = new StringBuffer();
while (m.find()) {
String replacement;// null;
if (m.group(1) != null) // __ to _
replacement = "_";
else if (m.group(2) != null) // _ to .
replacement = ".";
else if (m.group(3) != null) { // $_$ to -
replacement = "-";
ocd.updateVersion(MetatypeVersion.VERSION_1_4);
}
else if (m.group(4) != null) // $$ to $
replacement = "\\$";
else // $ removed.
replacement = "";
m.appendReplacement(b, replacement);
}
m.appendTail(b);
return b.toString();
}
private String space(String name) {
return Clazz.unCamel(name);
}
private void designates(String name, String[] pids, boolean factory) {
for (String pid : pids) {
if ("$".equals(pid)) {
pid = name;
}
ocd.designates.add(new DesignateDef(ocd.id, pid, factory, finder));
}
}
}
}