package net.jangaroo.exml.tools;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.jangaroo.exml.utils.ExmlUtils;
import net.jangaroo.jooc.Jooc;
import net.jangaroo.jooc.backend.ActionScriptCodeGeneratingModelVisitor;
import net.jangaroo.jooc.backend.JsCodeGenerator;
import net.jangaroo.jooc.model.AnnotationModel;
import net.jangaroo.jooc.model.AnnotationPropertyModel;
import net.jangaroo.jooc.model.ClassModel;
import net.jangaroo.jooc.model.CompilationUnitModelRegistry;
import net.jangaroo.jooc.model.CompilationUnitModel;
import net.jangaroo.jooc.model.FieldModel;
import net.jangaroo.jooc.model.MemberModel;
import net.jangaroo.jooc.model.MethodModel;
import net.jangaroo.jooc.model.MethodType;
import net.jangaroo.jooc.model.ParamModel;
import net.jangaroo.jooc.model.PropertyModel;
import net.jangaroo.utils.AS3Type;
import net.jangaroo.utils.CompilerUtils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Generate ActionScript 3 APIs from a jsduck JSON export of the Ext JS 4.x API.
*/
public class ExtAsApiGenerator {
private static Map<String,ExtClass> extClasses;
private static CompilationUnitModelRegistry compilationUnitModelRegistry;
private static Set<String> interfaces;
public static final List<String> NON_COMPILE_TIME_CONSTANT_INITIALIZERS = Arrays.asList("window", "document", "document.body");
public static void main(String[] args) throws IOException {
File srcDir = new File(args[0]);
File outputDir = new File(args[1]);
File[] files = srcDir.listFiles();
if (files != null) {
extClasses = new HashMap<String, ExtClass>();
compilationUnitModelRegistry = new CompilationUnitModelRegistry();
interfaces = new HashSet<String>();
for (File jsonFile : files) {
ExtClass extClass = readExtApiJson(jsonFile);
if (extClass != null) {
extClasses.put(extClass.name, extClass);
for (String alternateClassName : extClass.alternateClassNames) {
extClasses.put(alternateClassName, extClass);
}
for (String mixin : extClass.mixins) {
interfaces.add(mixin);
}
}
}
markTransitiveSupersAsInterfaces(interfaces);
//markTransitiveSupersAsInterfaces(singletons);
for (ExtClass extClass : new HashSet<ExtClass>(extClasses.values())) {
generateClassModel(extClass);
}
compilationUnitModelRegistry.complementOverrides();
compilationUnitModelRegistry.complementImports();
for (CompilationUnitModel compilationUnitModel : compilationUnitModelRegistry.getCompilationUnitModels()) {
generateActionScriptCode(compilationUnitModel, outputDir);
}
}
}
private static void markTransitiveSupersAsInterfaces(Set<String> extClasses) {
Set<String> supers = supers(extClasses);
while (interfaces.addAll(supers)) {
supers = supers(supers);
}
}
private static Set<String> supers(Set<String> extClasses) {
Set<String> result = new HashSet<String>();
for (String extClass : extClasses) {
String superclass = ExtAsApiGenerator.extClasses.get(extClass).extends_;
if (superclass != null) {
result.add(superclass);
}
}
return result;
}
private static ExtClass readExtApiJson(File jsonFile) throws IOException {
System.out.printf("Reading API from %s...\n", jsonFile.getPath());
ExtClass extClass = new ObjectMapper().readValue(jsonFile, ExtClass.class);
if (JsCodeGenerator.PRIMITIVES.contains(extClass.name)) {
System.err.println("ignoring built-in class " + extClass.name);
return null;
}
return extClass;
}
private static void generateClassModel(ExtClass extClass) {
CompilationUnitModel extAsClassUnit = createClassModel(convertType(extClass.name));
ClassModel extAsClass = (ClassModel)extAsClassUnit.getPrimaryDeclaration();
System.out.printf("Generating AS3 API model %s for %s...%n", extAsClassUnit.getQName(), extClass.name);
//configClass.setName(extClass.aliases.get("widget").get(0));
//configClass.setPackage("ext.config");
extAsClass.setAsdoc(toAsDoc(extClass.doc));
CompilationUnitModel extAsInterfaceUnit = null;
if (interfaces.contains(extClass.name)) {
extAsInterfaceUnit = createClassModel(convertToInterface(extClass.name));
System.out.printf("Generating AS3 API model %s for %s...%n", extAsInterfaceUnit.getQName(), extClass.name);
ClassModel extAsInterface = (ClassModel)extAsInterfaceUnit.getPrimaryDeclaration();
extAsInterface.setInterface(true);
extAsInterface.setAsdoc(toAsDoc(extClass.doc));
addInterfaceForSuperclass(extClass, extAsInterface);
}
if (isSingleton(extClass)) {
FieldModel singleton = new FieldModel(CompilerUtils.className(extClass.name), extAsClassUnit.getQName());
singleton.setConst(true);
singleton.setValue("new " + extAsClassUnit.getQName());
singleton.setAsdoc(extAsClass.getAsdoc());
CompilationUnitModel singletonUnit = new CompilationUnitModel(extAsClassUnit.getPackage(), singleton);
compilationUnitModelRegistry.register(singletonUnit);
extAsClass.setAsdoc(String.format("%s\n<p>Type of singleton %s.</p>\n * @see %s %s",
extAsClass.getAsdoc(),
singleton.getName(),
CompilerUtils.qName(extAsClassUnit.getPackage(), "#" + singleton.getName()),
singletonUnit.getQName()));
}
extAsClass.setSuperclass(convertType(extClass.extends_));
if (extAsInterfaceUnit != null) {
extAsClass.addInterface(extAsInterfaceUnit.getQName());
}
for (String mixin : extClass.mixins) {
String superInterface = convertToInterface(mixin);
extAsClass.addInterface(superInterface);
if (extAsInterfaceUnit != null) {
extAsInterfaceUnit.getClassModel().addInterface(superInterface);
}
}
if (extAsInterfaceUnit != null) {
addNonStaticMembers(extClass, extAsInterfaceUnit);
}
if (!extAsClass.isInterface()) {
addFields(extAsClass, filterByOwner(false, extClass, extClass.statics.property));
addMethods(extAsClass, filterByOwner(false, extClass, extClass.statics.method));
}
addNonStaticMembers(extClass, extAsClassUnit);
}
private static void addInterfaceForSuperclass(ExtClass extClass, ClassModel extAsInterface) {
if (extClass.extends_ != null) {
extAsInterface.addInterface(convertToInterface(extClass.extends_));
}
}
private static CompilationUnitModel createClassModel(String qName) {
CompilationUnitModel compilationUnitModel = new CompilationUnitModel(null, new ClassModel());
compilationUnitModel.setQName(qName);
compilationUnitModelRegistry.register(compilationUnitModel);
return compilationUnitModel;
}
private static void addNonStaticMembers(ExtClass extClass, CompilationUnitModel extAsClassUnit) {
addEvents(extAsClassUnit, filterByOwner(false, extClass, extClass.members.event));
ClassModel extAsClass = extAsClassUnit.getClassModel();
addProperties(extAsClass, filterByOwner(extAsClass.isInterface(), extClass, extClass.members.property));
addMethods(extAsClass, filterByOwner(extAsClass.isInterface(), extClass, extClass.members.method));
addProperties(extAsClass, filterByOwner(extAsClass.isInterface(), extClass, extClass.members.cfg));
}
private static void generateActionScriptCode(CompilationUnitModel extAsClass, File outputDir) throws IOException {
File outputFile = CompilerUtils.fileFromQName(extAsClass.getQName(), outputDir, Jooc.AS_SUFFIX);
//noinspection ResultOfMethodCallIgnored
outputFile.getParentFile().mkdirs(); // NOSONAR
System.out.printf("Generating AS3 API for %s into %s...\n", extAsClass.getQName(), outputFile.getPath());
extAsClass.visit(new ActionScriptCodeGeneratingModelVisitor(new FileWriter(outputFile)));
}
private static <T extends Member> List<T> filterByOwner(boolean isInterface, ExtClass owner, List<T> members) {
List<T> result = new ArrayList<T>();
for (T member : members) {
if (member.owner.equals(owner.name) && (!isInterface || isPublicNonStaticMethod(member))) {
result.add(member);
}
}
return result;
}
private static boolean isPublicNonStaticMethod(Member member) {
return member instanceof Method && !member.meta.static_ && !member.meta.private_ && !member.meta.protected_
&& !"constructor".equals(member.name);
}
private static boolean isConst(Member member) {
return member.meta.readonly || (member.name.equals(member.name.toUpperCase()) && member.default_ != null);
}
private static void addEvents(CompilationUnitModel compilationUnitModel, List<Event> events) {
for (Event event : events) {
ClassModel classModel = compilationUnitModel.getClassModel();
String eventTypeQName = generateEventClass(compilationUnitModel, event);
AnnotationModel annotationModel = new AnnotationModel("Event",
new AnnotationPropertyModel("name", "'" + event.name + "'"),
new AnnotationPropertyModel("type", "'" + eventTypeQName + "'"));
annotationModel.setAsdoc(toAsDoc(event.doc) + String.format("\n * @eventType %s.NAME", eventTypeQName));
classModel.addAnnotation(annotationModel);
System.err.println("*** adding event " + event.name + " to class " + classModel.getName());
}
}
public static String capitalize(String name) {
return name == null || name.length() == 0 ? name : Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
private static String generateEventClass(CompilationUnitModel compilationUnitModel, Event event) {
ClassModel classModel = compilationUnitModel.getClassModel();
String eventTypeQName = CompilerUtils.qName(compilationUnitModel.getPackage(),
"events." + classModel.getName() + capitalize(event.name) + "Event");
CompilationUnitModel extAsClassUnit = createClassModel(eventTypeQName);
ClassModel extAsClass = (ClassModel)extAsClassUnit.getPrimaryDeclaration();
extAsClass.setAsdoc(toAsDoc(event.doc) + "\n * @see " + compilationUnitModel.getQName());
FieldModel eventNameConstant = new FieldModel("NAME", "String", CompilerUtils.quote(event.name));
eventNameConstant.setStatic(true);
eventNameConstant.setAsdoc(MessageFormat.format("This constant defines the value of the <code>type</code> property of the event object\nfor a <code>{0}</code> event.\n * @eventType {0}", event.name));
extAsClass.addMember(eventNameConstant);
MethodModel constructorModel = extAsClass.createConstructor();
constructorModel.addParam(new ParamModel("arguments", "Array"));
StringBuilder propertyAssignments = new StringBuilder();
for (int i = 0; i < event.params.size(); i++) {
Param param = event.params.get(i);
// add assignment to constructor body:
if (i > 0) {
propertyAssignments.append("\n ");
}
propertyAssignments.append(String.format("this['%s'] = arguments[%d];", convertName(param.name), i));
// add getter method:
MethodModel property = new MethodModel(MethodType.GET, convertName(param.name), convertType(param.type));
property.setAsdoc(toAsDoc(param.doc));
extAsClass.addMember(property);
}
constructorModel.setBody(propertyAssignments.toString());
return eventTypeQName;
}
private static void addFields(ClassModel classModel, List<? extends Member> fields) {
for (Member member : fields) {
FieldModel fieldModel = new FieldModel(convertName(member.name), convertType(member.type), member.default_);
fieldModel.setAsdoc(toAsDoc(member.doc));
setStatic(fieldModel, member);
fieldModel.setConst(isConst(member));
classModel.addMember(fieldModel);
}
}
private static void addProperties(ClassModel classModel, List<? extends Member> properties) {
for (Member member : properties) {
if (classModel.getMember(member.name) == null) {
PropertyModel propertyModel = new PropertyModel(convertName(member.name), convertType(member.type));
propertyModel.setAsdoc(toAsDoc(member.doc));
setStatic(propertyModel, member);
propertyModel.addGetter();
if (!member.meta.readonly) {
propertyModel.addSetter();
}
classModel.addMember(propertyModel);
}
}
}
private static void addMethods(ClassModel classModel, List<Method> methods) {
for (Method method : methods) {
if (classModel.getMember(method.name) == null) {
boolean isConstructor = method.name.equals("constructor");
MethodModel methodModel = isConstructor
? new MethodModel(classModel.getName(), null)
: new MethodModel(convertName(method.name), convertType(method.return_.type));
methodModel.setAsdoc(toAsDoc(method.doc));
methodModel.getReturnModel().setAsdoc(toAsDoc(method.return_.doc));
setStatic(methodModel, method);
for (Param param : method.params) {
ParamModel paramModel = new ParamModel(convertName(param.name), convertType(param.type));
paramModel.setAsdoc(toAsDoc(param.doc));
setDefaultValue(paramModel, param);
paramModel.setRest(param == method.params.get(method.params.size() - 1) && param.type.endsWith("..."));
methodModel.addParam(paramModel);
}
classModel.addMember(methodModel);
}
}
}
private static void setStatic(MemberModel memberModel, Member member) {
memberModel.setStatic(member.meta.static_ || isStaticSingleton(extClasses.get(member.owner)));
}
private static String toAsDoc(String doc) {
String asDoc = doc.trim();
if (asDoc.startsWith("<p>")) {
// remove <p>...</p> around first paragraph:
int endTagPos = asDoc.indexOf("</p>");
asDoc = asDoc.substring(3, endTagPos) + asDoc.substring(endTagPos + 4);
}
if (asDoc.startsWith("{")) {
int closingBracePos = asDoc.indexOf("} ");
if (closingBracePos != -1) {
asDoc = asDoc.substring(closingBracePos + 2);
}
}
return asDoc;
}
private static void setDefaultValue(ParamModel paramModel, Param param) {
String defaultValue = param.default_;
if (defaultValue != null) {
if (NON_COMPILE_TIME_CONSTANT_INITIALIZERS.contains(defaultValue)) {
paramModel.setAsdoc("(Default " + defaultValue + ") " + paramModel.getAsdoc());
defaultValue = null;
param.optional = true; // only in case it is set inconsistently...
}
}
if (defaultValue == null && param.optional) {
defaultValue = AS3Type.getDefaultValue(paramModel.getType());
}
paramModel.setValue(defaultValue);
}
private static String convertName(String name) {
return "is".equals(name) ? "matches" :
"class".equals(name) ? "cls" :
"this".equals(name) ? "source" :
"new".equals(name) ? "new_" :
name;
}
private static String convertToInterface(String mixin) {
String packageName = CompilerUtils.packageName(mixin).toLowerCase();
String className = "I" + CompilerUtils.className(mixin);
if (packageName.startsWith("ext")) {
packageName = "ext4" + packageName.substring(3);
}
return CompilerUtils.qName(packageName, className);
}
private static String convertType(String extType) {
if (extType == null) {
return null;
}
if ("undefined".equals(extType)) {
return "void";
}
if ("HTMLElement".equals(extType) || "Event".equals(extType)) {
return "js." + extType;
}
if (extType.endsWith("...")) {
return "Array";
}
if (!extType.matches("[a-zA-Z0-9._$<>]+") || "Mixed".equals(extType)) {
return "*"; // TODO: join types? rather use Object? simulate overloading by splitting into several methods?
}
ExtClass extClass = extClasses.get(extType);
if (extClass != null) {
// normalize:
extType = extClass.name;
}
if ("Ext".equals(extType)) {
// special case: move singleton "Ext" into package "ext":
extType = "ext.Ext";
}
String packageName = CompilerUtils.packageName(extType).toLowerCase();
String className = CompilerUtils.className(extType);
if (isSingleton(extClass)) {
className = "S" + className;
}
if (JsCodeGenerator.PRIMITIVES.contains(className)) {
if ("ext".equals(packageName)) {
if (isStaticSingleton(extClass)) {
// for most built-in classes, there is a static ...Util class:
packageName = "ext.util";
className += "Util";
} else {
// all others in package "ext" are prefixed with "Ext":
className = "Ext" + className;
}
} else {
// all in other packages are postfixed with the upper-cased last package segment:
className += ExmlUtils.createComponentClassName(packageName.substring(packageName.lastIndexOf('.') + 1));
}
} else if ("is".equals(className)) {
// special case lower-case "is" class:
className = "Is";
}
if (packageName.startsWith("ext")) {
packageName = "ext4" + packageName.substring(3);
}
return CompilerUtils.qName(packageName, className);
}
private static boolean isSingleton(ExtClass extClass) {
return extClass != null && extClass.singleton && !isStaticSingleton(extClass);
}
private static boolean isStaticSingleton(ExtClass extClass) {
return extClass != null && extClass.singleton && (extClass.extends_ == null || extClass.extends_.length() == 0)
&& extClass.statics.cfg.isEmpty() && extClass.statics.event.isEmpty() && extClass.statics.method.isEmpty()
&& extClass.statics.property.isEmpty() && extClass.statics.css_mixin.isEmpty() && extClass.statics.css_var.isEmpty();
}
@SuppressWarnings("UnusedDeclaration")
@JsonIgnoreProperties({"html_meta", "html_type"})
public static class Tag {
public String tagname;
public String name;
public String doc;
@JsonProperty("private")
public String private_;
public MemberReference inheritdoc;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Tag tag = (Tag)o;
return name.equals(tag.name) && !(tagname != null ? !tagname.equals(tag.tagname) : tag.tagname != null);
}
@Override
public int hashCode() {
int result = tagname != null ? tagname.hashCode() : 0;
result = 31 * result + name.hashCode();
return result;
}
}
@SuppressWarnings("UnusedDeclaration")
public static class ExtClass extends Tag {
@JsonProperty("extends")
public String extends_;
public List<String> mixins;
public List<String> alternateClassNames;
public Map<String,List<String>> aliases;
public boolean singleton;
public List<String> requires;
public List<String> uses;
public String code_type;
public boolean inheritable;
public Meta meta;
public String id;
public Members members;
public Members statics;
public List<Object> files;
public boolean component;
public List<String> superclasses;
public List<String> subclasses;
public List<String> mixedInto;
public List<String> parentMixins;
@JsonProperty("abstract")
public boolean abstract_;
}
@SuppressWarnings("UnusedDeclaration")
public static class Members {
public List<Member> cfg;
public List<Property> property;
public List<Method> method;
public List<Event> event;
public List<Member> css_var;
public List<Member> css_mixin;
}
@JsonIgnoreProperties({"html_type", "html_meta", "properties"})
public abstract static class Var extends Tag {
public String type;
@JsonProperty("default")
public String default_;
}
@SuppressWarnings("UnusedDeclaration")
public static class Member extends Var {
public String owner;
public String shortDoc;
public Meta meta;
public boolean inheritable;
public String id;
public List<String> files;
public boolean accessor;
public boolean evented;
public List<Overrides> overrides;
}
@SuppressWarnings("UnusedDeclaration")
public static class MemberReference extends Tag {
public String cls;
public String member;
public String type;
}
@SuppressWarnings("UnusedDeclaration")
public static class Overrides {
public String name;
public String owner;
public String id;
}
public static class Property extends Member {
@Override
public String toString() {
return meta + "var " + super.toString();
}
}
public static class Param extends Var {
public boolean optional;
}
public static class Method extends Member {
public List<Param> params;
@JsonProperty("return")
public Param return_;
}
@SuppressWarnings("UnusedDeclaration")
public static class Event extends Member {
public List<Param> params;
}
@SuppressWarnings("UnusedDeclaration")
public static class Meta {
@JsonProperty("protected")
public boolean protected_;
@JsonProperty("private")
public boolean private_;
public boolean readonly;
@JsonProperty("static")
public boolean static_;
@JsonProperty("abstract")
public boolean abstract_;
public boolean markdown;
public Map<String,String> deprecated;
public String template;
public List<String> author;
public List<String> docauthor;
public boolean required;
public Map<String,String> removed;
}
}