net.jangaroo.exml.tools.ExtAsApiGenerator Maven / Gradle / Ivy
package net.jangaroo.exml.tools;
import net.jangaroo.exml.tools.ExtJsApi.ExtClass;
import net.jangaroo.jooc.Jooc;
import net.jangaroo.jooc.backend.ActionScriptCodeGeneratingModelVisitor;
import net.jangaroo.jooc.backend.JsCodeGenerator;
import net.jangaroo.jooc.model.AbstractAnnotatedModel;
import net.jangaroo.jooc.model.AnnotationModel;
import net.jangaroo.jooc.model.AnnotationPropertyModel;
import net.jangaroo.jooc.model.ClassModel;
import net.jangaroo.jooc.model.CompilationUnitModel;
import net.jangaroo.jooc.model.CompilationUnitModelRegistry;
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.NamedModel;
import net.jangaroo.jooc.model.NamespacedModel;
import net.jangaroo.jooc.model.ParamModel;
import net.jangaroo.jooc.model.PropertyModel;
import net.jangaroo.jooc.model.TypedModel;
import net.jangaroo.jooc.mxml.MxmlUtils;
import net.jangaroo.utils.AS3Type;
import net.jangaroo.utils.CompilerUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static net.jangaroo.exml.tools.ExtJsApi.Cfg;
import static net.jangaroo.exml.tools.ExtJsApi.Deprecation;
import static net.jangaroo.exml.tools.ExtJsApi.Event;
import static net.jangaroo.exml.tools.ExtJsApi.Member;
import static net.jangaroo.exml.tools.ExtJsApi.Method;
import static net.jangaroo.exml.tools.ExtJsApi.Param;
import static net.jangaroo.exml.tools.ExtJsApi.Property;
import static net.jangaroo.exml.tools.ExtJsApi.Tag;
import static net.jangaroo.exml.tools.ExtJsApi.Var;
import static net.jangaroo.exml.tools.ExtJsApi.isConst;
import static net.jangaroo.exml.tools.ExtJsApi.isSingleton;
/**
* Generate ActionScript 3 APIs from a jsduck JSON export of the Ext JS 4.x API.
*/
public class ExtAsApiGenerator {
private static final Pattern SINGLETON_CLASS_NAME_PATTERN = Pattern.compile("^S[A-Z]");
private static ExtJsApi extJsApi;
private static Set extClasses;
private static CompilationUnitModelRegistry compilationUnitModelRegistry;
private static Set interfaces;
private static final List NON_COMPILE_TIME_CONSTANT_INITIALIZERS = Arrays.asList("window", "document", "document.body", "new Date()", "this", "`this`", "10||document.body", "caller", "array.length");
private static ExtAsApi referenceApi;
private static boolean generateEventClasses;
private static boolean generateForMxml;
private static Properties jsAsNameMappingProperties = new Properties();
private static Properties jsConfigClassNameMappingProperties = new Properties();
private static Properties eventWordsProperties = new Properties();
private static final String EXT_3_4_EVENT = "ext.IEventObject";
private static String EXT_EVENT;
private static Map> aliasGroupToAliasToClass = new TreeMap>();
public static void main(String[] args) throws IOException {
File srcDir = new File(args[0]);
File outputDir = new File(args[1]);
referenceApi = new ExtAsApi("2.0.14", "2.0.13");
EXT_EVENT = referenceApi.getMappedQName(EXT_3_4_EVENT);
if (EXT_EVENT == null) {
EXT_EVENT = EXT_3_4_EVENT;
}
generateEventClasses = args.length <= 2 || Boolean.valueOf(args[2]);
generateForMxml = args.length <= 3 ? false : Boolean.valueOf(args[3]);
jsAsNameMappingProperties.load(ExtAsApiGenerator.class.getClassLoader().getResourceAsStream("net/jangaroo/exml/tools/js-as-name-mapping.properties"));
jsConfigClassNameMappingProperties.load(ExtAsApiGenerator.class.getClassLoader().getResourceAsStream("net/jangaroo/exml/tools/js-config-name-mapping.properties"));
eventWordsProperties.load(ExtAsApiGenerator.class.getClassLoader().getResourceAsStream("net/jangaroo/exml/tools/event-words.properties"));
File[] files = srcDir.listFiles();
if (files != null) {
compilationUnitModelRegistry = new CompilationUnitModelRegistry();
interfaces = new HashSet();
extJsApi = new ExtJsApi(files);
extClasses = new HashSet(extJsApi.getExtClasses());
removePrivateApiClasses();
Set mixins = extJsApi.getMixins();
for (ExtClass mixin : mixins) {
String mixinName = getActionScriptName(mixin);
if (mixinName != null) {
interfaces.add(mixinName);
}
}
// correct wrong usage of util.Observable as a mixin:
interfaces.remove("ext.util.Observable");
// correct wrong usage of dom.Element as a mixin, not superclass, in dom.CompositeElementLite:
interfaces.remove("ext.dom.Element");
// since every Ext object extends Base, there is no need to generate an interface for that:
interfaces.remove("ext.Base");
interfaces.remove("Object");
interfaces.add("ext.EventObjectImpl");
for (ExtClass extClass : extClasses) {
generateClassModel(extClass);
}
compilationUnitModelRegistry.complementOverrides();
compilationUnitModelRegistry.complementImports();
adaptToReferenceApi();
// again, to make corrections consistent (actually needed?)
// compilationUnitModelRegistry.complementOverrides();
// compilationUnitModelRegistry.complementImports();
annotateBindableConfigProperties();
if (!outputDir.exists()) {
System.err.println("No output directory specified, skipping code generation.");
return;
}
for (CompilationUnitModel compilationUnitModel : compilationUnitModelRegistry.getCompilationUnitModels()) {
generateActionScriptCode(compilationUnitModel, outputDir);
}
}
generateManifest(outputDir);
}
private static void generateManifest(File outputDir) throws FileNotFoundException, UnsupportedEncodingException {
// create manifest.xml component library:
File outputFile = new File(outputDir, "manifest.xml");
System.out.printf("Creating manifest file %s...%n", outputFile.getPath());
PrintStream out = new PrintStream(new FileOutputStream(outputFile), true, net.jangaroo.exml.api.Exmlc.OUTPUT_CHARSET);
out.println("");
out.println("");
for (String aliasGroup : aliasGroupToAliasToClass.keySet()) {
String previousId = "";
for (Map.Entry aliasToClass : aliasGroupToAliasToClass.get(aliasGroup).entrySet()) {
String alias = aliasToClass.getKey();
String classQName = aliasToClass.getValue();
String id = computeId(alias, classQName + previousId);
previousId = id;
if (!"widget".equals(aliasGroup)) {
id = aliasGroup + "_" + id;
}
out.printf(" %n", id, classQName);
}
}
out.println(" ");
out.close();
}
private static String computeId(String alias, String classQName) {
int dotPos = alias.indexOf('.');
if (dotPos != -1) {
// special format used e.g. in "data" aliases: capitalize the dot-prefix and use it as suffix.
return computeId(alias.substring(dotPos + 1), classQName) + capitalize(alias.substring(0, dotPos));
}
// remove all dashes from alias:
alias = alias.replaceAll("-", "");
// match unqualified class name word by word:
String className = CompilerUtils.className(classQName);
String[] words = className.split("(? iterator = ((MethodModel) member).getParams().iterator(),
newIterator = ((MethodModel) newMember).getParams().iterator();
while (iterator.hasNext() && newIterator.hasNext()) {
ParamModel param = iterator.next();
ParamModel newParam = newIterator.next();
changed |= adaptTypeToReferenceApi(referenceCompilationUnitModel, newParam, param);
}
if (iterator.hasNext() || newIterator.hasNext()) {
changed = true;
}
}
if (changed) {
++changedCount;
}
}
}
int sameCount = referenceClassModel.getMembers().size() - changedCount - removedCount;
//System.err.printf("=== Adapted %s: removed: %d, changed: %d, same: %d%n", compilationUnitModel.getQName(), removedCount, changedCount, sameCount);
System.err.printf("%s\t%d\t%d\t%d%n", compilationUnitModel.getQName(), removedCount, changedCount, sameCount);
}
private static boolean adaptTypeToReferenceApi(CompilationUnitModel referenceCompilationUnitModel, TypedModel newMember, TypedModel member) {
String oldType = referenceApi.resolveQualifiedName(referenceCompilationUnitModel, member.getType());
String newType = newMember.getType(); // already fully-qualified!
if (oldType != null && !oldType.equals(newType)) {
//System.err.printf("*** found %s member %s type change: from %s to %s%n", classModel.getName(), member.getName(), oldType, newType);
if (shouldCorrect(oldType, newType)) {
newMember.setType(oldType);
//System.err.printf("!!! corrected type of %s.%s from %s back to %s%n", classModel.getName(), member.getName(), newType, oldType);
return true;
}
}
return false;
}
private static void adaptNamespaceToReferenceApi(MemberModel newMember, MemberModel member) {
String oldNamespace = member.getNamespace();
String newNamespace = newMember.getNamespace();
if (oldNamespace != null && !oldNamespace.equals(newNamespace)) {
newMember.setNamespace(oldNamespace);
}
}
private static boolean shouldCorrect(String oldType, String newType) {
return newType == null ||
"void".equals(newType) ||
"*".equals(newType) ||
"void".equals(oldType) ||
"Object".equals(newType) && "*".equals(oldType) ||
EXT_EVENT.equals(oldType) && newType.contains("Event") ||
"Function".equals(newType) && "Class".equals(oldType);
}
private static MemberModel findMemberModel(CompilationUnitModel compilationUnitModel, MemberModel referenceMemberModel) {
ClassModel classModel = compilationUnitModel.getClassModel();
String referenceMemberName = referenceApi.getMappedMemberName(compilationUnitModel, referenceMemberModel.getName());
MemberModel memberModel = referenceMemberModel instanceof MethodModel
? classModel.getMethod(referenceMemberModel.isStatic(), ((MethodModel) referenceMemberModel).getMethodType(), referenceMemberName)
: classModel.getMember(referenceMemberModel.isStatic(), referenceMemberName);
if (memberModel == null) {
CompilationUnitModel superclass = compilationUnitModelRegistry.getSuperclassCompilationUnit(classModel);
if (superclass != null) {
return findMemberModel(superclass, referenceMemberModel);
}
}
return memberModel;
}
private static void generateClassModel(ExtClass extClass) {
String extClassName = getActionScriptName(extClass);
if (extClassName == null) {
return;
}
CompilationUnitModel extAsClassUnit = createClassModel(convertType(extClass.name));
ClassModel extAsClass = extAsClassUnit.getClassModel();
System.out.printf("Generating AS3 API model %s for %s...%n", extAsClassUnit.getQName(), extClassName);
extAsClass.setAsdoc(toAsDoc(extClass));
addDeprecation(extClass.deprecated, extAsClass);
CompilationUnitModel extAsInterfaceUnit = null;
if (interfaces.contains(extClassName)) {
extAsInterfaceUnit = createClassModel(convertToInterface(extClassName));
System.out.printf("Generating AS3 API model %s for %s...%n", extAsInterfaceUnit.getQName(), extClassName);
ClassModel extAsInterface = (ClassModel)extAsInterfaceUnit.getPrimaryDeclaration();
extAsInterface.setInterface(true);
extAsInterface.setAsdoc(toAsDoc(extClass.doc) + "\n * @see " + extClassName);
if (extClass.extends_ != null) {
String superInterface = convertToInterface(getActionScriptName(extClass.extends_));
if (superInterface != null) {
extAsInterface.addInterface(superInterface);
}
}
}
AnnotationModel nativeAnnotation = createNativeAnnotation(extClass.name);
if (isSingleton(extClass)) {
extAsClass.addAnnotation(createNativeAnnotation(null));
FieldModel singleton = new FieldModel(CompilerUtils.className(extAsClass.getName().substring(1)), extAsClassUnit.getQName());
singleton.setConst(true);
singleton.setValue("new " + extAsClassUnit.getQName());
singleton.addAnnotation(nativeAnnotation);
singleton.setAsdoc(extAsClass.getAsdoc());
CompilationUnitModel singletonUnit = new CompilationUnitModel(extAsClassUnit.getPackage(), singleton);
compilationUnitModelRegistry.register(singletonUnit);
extAsClass.setAsdoc(String.format("%s%nType of singleton %s.
%n@see %s %s",
extAsClass.getAsdoc(),
singleton.getName(),
CompilerUtils.qName(extAsClassUnit.getPackage(), "#" + singleton.getName()),
singletonUnit.getQName()));
} else {
extAsClass.addAnnotation(nativeAnnotation);
}
if (extAsInterfaceUnit != null) {
extAsInterfaceUnit.getClassModel().addAnnotation(new AnnotationModel(Jooc.MIXIN_ANNOTATION_NAME,
new AnnotationPropertyModel(null, CompilerUtils.quote(extClassName))));
}
if (extClass.private_) {
extAsClass.addAnnotation(new AnnotationModel(Jooc.PUBLIC_API_EXCLUSION_ANNOTATION_NAME));
}
extAsClass.setSuperclass(convertType(extClass.extends_));
if (extAsInterfaceUnit != null) {
extAsClass.addInterface(extAsInterfaceUnit.getQName());
}
for (String mixin : extClass.mixins) {
String superInterface = convertToInterface(getActionScriptName(mixin));
if (superInterface != null) {
extAsClass.addInterface(superInterface);
if (extAsInterfaceUnit != null) {
extAsInterfaceUnit.getClassModel().addInterface(superInterface);
}
}
}
if (extAsInterfaceUnit != null) {
addNonStaticMembers(extClass, extAsInterfaceUnit);
} else {
addFields(extAsClass, extJsApi.filterByOwner(false, true, extClass, extClass.members, Property.class));
addMethods(extAsClass, extJsApi.filterByOwner(false, true, extClass, extClass.members, Method.class));
}
addNonStaticMembers(extClass, extAsClassUnit);
// todo: remove #getConfigClassQName and its mapping properties, a constructor needs to be generated if and only if the class or a superclass has config parameters
if (getConfigClassQName(extClass) != null) {
addConfigConstructor(extAsClassUnit);
}
for (Map.Entry> aliasEntry : extClass.aliases.entrySet()) {
String aliasGroup = aliasEntry.getKey();
Map aliasMapping = aliasGroupToAliasToClass.get(aliasGroup);
if (aliasMapping == null) {
aliasMapping = new TreeMap();
aliasGroupToAliasToClass.put(aliasGroup, aliasMapping);
}
for (String alias : aliasEntry.getValue()) {
aliasMapping.put(alias, extAsClassUnit.getQName());
}
}
}
private static void addConfigConstructor(CompilationUnitModel extAsClassUnit) {
ClassModel extAsClass = extAsClassUnit.getClassModel();
MethodModel targetClassConstructor = extAsClass.getConstructor();
if (targetClassConstructor == null) {
targetClassConstructor = extAsClass.createConstructor();
targetClassConstructor.addParam(new ParamModel("config", extAsClassUnit.getQName(), "null", "@inheritDoc"));
} else {
for (ParamModel param : targetClassConstructor.getParams()) {
if ("config".equals(param.getName())) {
param.setType(extAsClass.getName());
param.setOptional(true);
break;
}
}
}
}
private static String getActionScriptName(String extClassName) {
return getActionScriptName(extJsApi.getExtClass(extClassName));
}
private static String getConfigClassQName(ExtClass extClass) {
String extClassName = extClass.name;
String alias = jsConfigClassNameMappingProperties.getProperty(extClassName);
if (alias == null) {
return null;
}
String configClassQName = CompilerUtils.qName("ext.config", alias);
System.err.println("********* derived config class name " + configClassQName + " from Ext class " + extClassName + " with alias " + alias);
return configClassQName;
}
private static String getPreferredAlias(Map.Entry> aliases) {
String alias;
alias = aliases.getValue().get(aliases.getValue().size() - 1);
if (aliases.getValue().size() > 1) {
// for the following exceptions, use the first alias:
if ("box".equals(alias)) {
alias = aliases.getValue().get(0);
}
System.err.println("***### multiple aliases: " + aliases.getValue() + ", choosing " + alias);
}
return alias;
}
private static Map.Entry> getAlias(ExtClass extClass) {
Iterator>> iterator = extClass.aliases.entrySet().iterator();
if (iterator.hasNext()) {
Map.Entry> firstEntry = iterator.next();
if (firstEntry.getValue().size() > 0) {
return firstEntry;
}
}
return null;
}
private static AnnotationModel createNativeAnnotation(String nativeName) {
AnnotationModel nativeAnnotation = new AnnotationModel(Jooc.NATIVE_ANNOTATION_NAME);
if (nativeName != null) {
nativeAnnotation.addProperty(new AnnotationPropertyModel(null, CompilerUtils.quote(nativeName)));
nativeAnnotation.addProperty(new AnnotationPropertyModel(Jooc.NATIVE_ANNOTATION_REQUIRE_PROPERTY, null));
}
return nativeAnnotation;
}
private static CompilationUnitModel createClassModel(String qName) {
CompilationUnitModel oldCompilationUnitModel = compilationUnitModelRegistry.resolveCompilationUnit(qName);
if (oldCompilationUnitModel != null) {
System.err.println("[WARN] Redefining class " + qName);
return oldCompilationUnitModel;
}
CompilationUnitModel compilationUnitModel = new CompilationUnitModel(null, new ClassModel());
compilationUnitModel.setQName(qName);
compilationUnitModelRegistry.register(compilationUnitModel);
return compilationUnitModel;
}
private static void addNonStaticMembers(ExtClass extClass, CompilationUnitModel extAsClassUnit) {
ClassModel extAsClass = extAsClassUnit.getClassModel();
if (!extAsClass.isInterface()) {
addEvents(extAsClass, extAsClassUnit, extJsApi.filterByOwner(false, false, extClass, extClass.members, Event.class));
}
addProperties(extAsClass, extJsApi.filterByOwner(extAsClass.isInterface(), false, extClass, extClass.members, Property.class), false);
addMethods(extAsClass, extJsApi.filterByOwner(extAsClass.isInterface(), false, extClass, extClass.members, Method.class));
addProperties(extAsClass, extJsApi.filterByOwner(extAsClass.isInterface(), false, extClass, extClass.members, Cfg.class), true);
}
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.getCanonicalPath());
extAsClass.visit(new ActionScriptCodeGeneratingModelVisitor(new OutputStreamWriter(new FileOutputStream(outputFile), "UTF-8")));
}
private static void addDeprecation(Deprecation deprecation, AbstractAnnotatedModel model) {
if (deprecation != null) {
final AnnotationModel deprecated = new AnnotationModel("Deprecated");
if (deprecation.text != null && !deprecation.text.matches("\\s*")) {
Matcher replacementMatcher = Pattern.compile(".*").matcher(deprecation.text);
String name;
String value;
if (replacementMatcher.find()) {
name = "replacement";
String reference = replacementMatcher.group(1);
Matcher referenceMatcher = Pattern.compile("(.*)-(.*)-(.*)").matcher(reference);
if (referenceMatcher.matches()) {
value = referenceMatcher.group(3); // TODO: check whether group 1 contains the current class, otherwise mention it!
} else {
value = convertType(reference);
}
} else {
name = "message";
value = deprecation.text.replace("", "").replace("
", "");
}
deprecated.addProperty(new AnnotationPropertyModel(name, CompilerUtils.quote(value, false)));
}
if (deprecation.version != null && !deprecation.version.matches("\\s*")) {
deprecated.addProperty(new AnnotationPropertyModel(
"since",
deprecation.version.startsWith("\"") ? deprecation.version : CompilerUtils.quote(deprecation.version)));
}
model.addAnnotation(deprecated);
}
}
private static void addEvents(ClassModel classModel, CompilationUnitModel compilationUnitModel, List events) {
for (Event event : events) {
String eventName = toCamelCase(event.name);
AnnotationModel annotationModel = new AnnotationModel("Event",
new AnnotationPropertyModel("name", "'on" + eventName + "'"));
String asdoc = toAsDoc(event);
if (generateEventClasses) {
String eventTypeQName = generateEventClass(compilationUnitModel, event);
annotationModel.addProperty(new AnnotationPropertyModel("type", "'" + eventTypeQName + "'"));
asdoc += String.format("%n@eventType %s.%s", eventTypeQName, toConstantName(event.name));
}
annotationModel.setAsdoc(asdoc);
classModel.addAnnotation(annotationModel);
}
}
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 eventClientClass, Event event) {
String eventTypeNamePrefix = eventClientClass.getPrimaryDeclaration().getName();
if (SINGLETON_CLASS_NAME_PATTERN.matcher(eventTypeNamePrefix).find()) {
eventTypeNamePrefix = eventTypeNamePrefix.substring(1);
}
String eventTypeQName = eventClientClass.getPackage() + ".events." + eventTypeNamePrefix;
String eventClientClassQName = eventClientClass.getQName();
for (int i = 0; i < event.params.size(); i++) {
Param param = event.params.get(i);
String asType = convertType(param.type);
if (!"eOpts".equals(param.name) && !"this".equals(param.name) && (i > 0 || !eventClientClassQName.equals(asType))) {
eventTypeQName += "_" + param.name;
}
}
eventTypeQName += "Event";
String eventName = toCamelCase(event.name);
CompilationUnitModel eventType = compilationUnitModelRegistry.resolveCompilationUnit(eventTypeQName);
if (eventType == null) {
eventType = createClassModel(eventTypeQName);
ClassModel extAsClass = eventType.getClassModel();
extAsClass.setSuperclass("net.jangaroo.ext.FlExtEvent");
MethodModel constructorModel = extAsClass.createConstructor();
constructorModel.addParam(new ParamModel("type", "String"));
constructorModel.addParam(new ParamModel("arguments", "Array"));
constructorModel.setBody("super(type, arguments);");
StringBuilder parameterSequence = new StringBuilder();
String separator = "[";
for (Param param : event.params) {
String parameterName = convertName(param.name);
// add parameter to sequence constant:
parameterSequence.append(separator).append(CompilerUtils.quote(parameterName));
separator = ", ";
if (!"eOpts".equals(param.name)) { // eOpts is inherited from FlExtEvent!
// add getter method:
MethodModel property = new MethodModel(MethodType.GET, parameterName, convertType(param.type));
property.setAsdoc(toAsDoc(param));
extAsClass.addMember(property);
}
}
parameterSequence.append("]");
FieldModel parameterSequenceConstant = new FieldModel("__PARAMETER_SEQUENCE__", "Array", parameterSequence.toString());
parameterSequenceConstant.setStatic(true);
parameterSequenceConstant.setConst(true);
extAsClass.addMember(parameterSequenceConstant);
}
FieldModel eventNameConstant = new FieldModel(toConstantName(event.name), "String", CompilerUtils.quote("on" + eventName));
eventNameConstant.setStatic(true);
eventNameConstant.setConst(true);
eventNameConstant.setAsdoc(String.format("\"%s%n@see %s%n@eventType %s", toAsDoc(event), eventClientClass.getQName(), "on" + eventName));
eventType.getClassModel().addMember(eventNameConstant);
return eventTypeQName;
}
private static String toCamelCase(String eventName) {
if (!eventName.toLowerCase().equals(eventName)) {
// already CamelCase:
return eventName;
}
StringBuilder camelCaseName = new StringBuilder();
for (String word : splitIntoWords(eventName)) {
camelCaseName.append(capitalize(word));
}
assert camelCaseName.toString().toLowerCase().equals(eventName);
return camelCaseName.toString();
}
private static String toConstantName(String eventName) {
StringBuilder constantName = new StringBuilder();
for (String word : splitIntoWords(eventName.toLowerCase())) {
constantName.append(word.toUpperCase()).append('_');
}
constantName.setLength(constantName.length() - 1); // cut last '_'
return constantName.toString();
}
private static List splitIntoWords(String mergedWords) {
List words = new ArrayList();
String remaining = mergedWords;
while (!remaining.isEmpty()) {
String candidate = "";
for (Object keyObject : eventWordsProperties.keySet()) {
String key = (String) keyObject;
if (key.length() > candidate.length() && remaining.startsWith(key)) {
candidate = key;
}
}
if (candidate.isEmpty()) {
System.err.printf("No word found in dictionary for %s's suffix '%s'.%n", mergedWords, remaining);
candidate = remaining;
}
words.add(candidate);
remaining = remaining.substring(candidate.length());
} return words;
}
private static void addFields(ClassModel classModel, List fields) {
for (Member member : fields) {
PropertyModel fieldModel = new PropertyModel(convertName(member.name), convertType(member.type));
setVisibility(fieldModel, member);
setStatic(fieldModel, member);
fieldModel.addGetter().setAsdoc(toAsDoc(member));
if (!isConst(member)) {
fieldModel.addSetter().setAsdoc("@private");
}
addDeprecation(member.deprecated, fieldModel);
classModel.addMember(fieldModel);
}
}
private static void addProperties(ClassModel classModel, List properties, boolean isConfig) {
for (Member member : properties) {
if (extJsApi.inheritsDoc(member)) {
// suppress overridden properties with the same JSDoc!
continue;
}
boolean isStatic = extJsApi.isStatic(member);
String name = convertName(member.name);
String type = convertType(member.type);
String asDoc = toAsDoc(member);
if (type == null || "*".equals(type) || "Object".equals(type)) {
// try to deduce a more specific type from the property name:
type = "cls".equals(member.name) ? "String"
: "useBodyElement".equals(member.name) ? "Boolean"
: "items".equals(member.name) || "plugins".equals(member.name) ? "Array"
: type;
}
MemberModel priorMember = classModel.getMember(isStatic, name);
if (priorMember != null) {
String priorMemberType;
if (priorMember.isMethod() && !priorMember.isAccessor()) {
priorMemberType = "Function";
} else {
priorMemberType = priorMember.getType();
}
if (!priorMemberType.equals(type)){
System.err.println("Duplicate member " + member.name + (isConfig ? " (config)" : "")
+ " in class " + classModel.getName()
+ " with deviating type " + type + " instead of " + priorMemberType + ".");
}
if ("Array".equals(type) && priorMemberType.contains("Collection")) {
String newName = (name.endsWith("s") ? name.substring(0, name.length() - 1) : name) + "Collection";
System.out.println("Renaming member " + priorMember.getName() + " to " + newName + " in class " + classModel.getName()
+ " to avoid name clash with config.");
priorMember.setName(newName);
} else if ("Function".equals(priorMemberType)) {
name += "_";
System.out.println("Renaming config " + member.name + " to " + name + " in class " + classModel.getName()
+ " to avoid name clash with method.");
} else {
type = priorMemberType;
asDoc = priorMember.isProperty() ? ((PropertyModel)priorMember).getGetter().getAsdoc() : priorMember.getAsdoc();
System.out.println("Merging member " + priorMember.getName() + " and config " + member.name + " in class " + classModel.getName()
+ " to avoid name clash.");
classModel.removeMember(priorMember);
}
}
PropertyModel propertyModel = new PropertyModel(name, type);
if (generateForMxml && "items".equals(member.name)) {
propertyModel.addAnnotation(new AnnotationModel(MxmlUtils.MXML_DEFAULT_PROPERTY_ANNOTATION));
}
propertyModel.setAsdoc(asDoc);
addDeprecation(member.deprecated, propertyModel);
setVisibility(propertyModel, member);
propertyModel.setStatic(isStatic);
MethodModel getter = propertyModel.addGetter();
AnnotationModel extConfigAnnotation = null;
if (isConfig) {
extConfigAnnotation = new AnnotationModel(Jooc.EXT_CONFIG_ANNOTATION_NAME);
if (!name.equals(member.name)) {
extConfigAnnotation.addProperty(new AnnotationPropertyModel(null, CompilerUtils.quote(member.name)));
}
getter.addAnnotation(extConfigAnnotation);
}
if (!extJsApi.isReadOnly(member)) {
MethodModel setter = propertyModel.addSetter();
if (classModel.isInterface()) {
// do not add @private to ASDoc in interfaces, or IDEA will completely ignore the declaration!
setter.setAsdoc(null);
}
if (extConfigAnnotation != null) {
setter.addAnnotation(extConfigAnnotation);
}
}
classModel.addMember(propertyModel);
}
}
private static void addMethods(ClassModel classModel, List methods) {
for (Method method : methods) {
String methodName = method.name;
if (methodName == null || methodName.length() == 0) {
System.err.printf("methods name missing for method #%d in class %s", methods.indexOf(method) + 1, classModel.getName());
continue;
}
if (classModel.getMember(methodName) == null) {
boolean isConstructor = methodName.equals("constructor");
if (!isConstructor && extJsApi.inheritsDoc(method)) {
// suppress overridden methods with the same JSDoc!
continue;
}
MethodModel methodModel = isConstructor
? new MethodModel(classModel.getName(), null)
: new MethodModel(convertName(methodName), method.return_ == null ? "void" : convertType(method.return_.type));
methodModel.setAsdoc(toAsDoc(method));
if (method.return_ != null) {
methodModel.getReturnModel().setAsdoc(toAsDoc(method.return_));
}
setVisibility(methodModel, method);
setStatic(methodModel, method);
addDeprecation(method.deprecated, methodModel);
for (Param param : method.params) {
String paramName = param.name == null ? "param" + (method.params.indexOf(param) + 1) : convertName(param.name);
ParamModel paramModel = new ParamModel(paramName, convertType(param.type));
paramModel.setAsdoc(toAsDoc(param, param.name));
setDefaultValue(paramModel, param);
paramModel.setRest(param == method.params.get(method.params.size() - 1) && param.type.endsWith("..."));
methodModel.addParam(paramModel);
}
try {
classModel.addMember(methodModel);
} catch (IllegalArgumentException e) {
System.err.println("while adding method " + methodModel + ": " + e);
}
}
}
}
private static void setVisibility(MemberModel memberModel, Member member) {
memberModel.setNamespace(extJsApi.isProtected(member) ? NamespacedModel.PROTECTED : NamespacedModel.PUBLIC);
}
private static void setStatic(MemberModel memberModel, Member member) {
memberModel.setStatic(extJsApi.isStatic(member));
}
private static String toAsDoc(Tag tag) {
return toAsDoc(tag, null);
}
private static String toAsDoc(Tag tag, String paramPrefix) {
StringBuilder asDoc = new StringBuilder(toAsDoc(tag.doc));
if (tag instanceof Var && ((Var)tag).default_ != null) {
asDoc.append("\n@default ").append(((Var)tag).default_);
}
if (tag instanceof Member && ((Member)tag).since != null) {
asDoc.append("\n@since ").append(((Member)tag).since);
}
if (tag.properties != null && !tag.properties.isEmpty()) {
if (paramPrefix != null) {
for (Param property : tag.properties) {
asDoc.append("\n * @param ");
String propertyType = convertType(property.type);
if (propertyType != null && !"*".equals(propertyType)) {
asDoc.append("{").append(propertyType).append("} ");
}
String qualifiedPropertyName = paramPrefix + "." + property.name;
if (property.optional) {
asDoc.append("[").append(qualifiedPropertyName).append("]");
} else {
asDoc.append(qualifiedPropertyName);
}
asDoc.append(" ");
asDoc.append(toAsDoc(property, qualifiedPropertyName));
}
} else {
asDoc.append("\n * ");
for (Param property : tag.properties) {
asDoc.append("\n * - ");
asDoc.append("
").append(property.name).append("
");
String propertyType = convertType(property.type);
if (propertyType != null && !"*".equals(propertyType)) {
asDoc.append(" : ").append(propertyType);
}
if (property.optional) {
asDoc.append(" (optional)");
}
String propertyAsDoc = toAsDoc(property);
if (!propertyAsDoc.trim().isEmpty()) {
asDoc.append("\n * ").append(propertyAsDoc).append("\n * ");
}
asDoc.append(" ");
}
asDoc.append("\n *
");
}
}
String result = asDoc.toString();
if (tag instanceof Param) {
// suppress multiple new lines in nested ASDoc, or IDEA will treat everything following as top-level ASDoc:
result = result.replaceAll("\n+", "\n");
}
return result;
}
private static String toAsDoc(String doc) {
// remove and :
String asDoc = doc.replaceAll("", "");
asDoc = asDoc.trim();
if (asDoc.startsWith("")) {
// remove
...
around first paragraph:
asDoc = asDoc.substring(3);
int endTagPos = asDoc.indexOf("");
if (endTagPos != -1) {
asDoc = asDoc.substring(0, endTagPos) + asDoc.substring(endTagPos + 4);
}
}
if (asDoc.startsWith("{")) {
int closingBracePos = asDoc.indexOf("} ");
if (closingBracePos != -1) {
asDoc = asDoc.substring(closingBracePos + 2);
}
}
// add closing "/" on
elements:
asDoc = asDoc.replaceAll("(
]*[^/])>", "$1/>");
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());
}
if (defaultValue != null && "String".equals(param.type) &&
!(defaultValue.equals("null") || defaultValue.startsWith("'") || defaultValue.startsWith("\""))) {
defaultValue = CompilerUtils.quote(defaultValue);
}
paramModel.setValue(defaultValue);
}
private static String convertName(String name) {
name = replaceSeparatorByCamelCase(name, '-');
return "is".equals(name) ? "matches" :
"class".equals(name) ? "cls" :
"this".equals(name) ? "source" :
"new".equals(name) ? "new_" :
"default".equals(name) ? "default_" :
name;
}
private static String replaceSeparatorByCamelCase(String string, char separator) {
while (true) {
int separatorPos = string.indexOf(separator);
if (separatorPos == -1) {
break;
}
string = string.substring(0, separatorPos) + string.substring(separatorPos + 1, separatorPos + 2).toUpperCase() + string.substring(separatorPos + 2);
}
return string;
}
private static String convertToInterface(String className) {
if (className == null || !interfaces.contains(className)) {
return null;
}
String interfaceName = "I" + CompilerUtils.className(className);
if (interfaceName.endsWith("Impl")) {
interfaceName = interfaceName.substring(0, interfaceName.length() - 4);
}
return CompilerUtils.qName(CompilerUtils.packageName(className), interfaceName);
}
private static String convertType(String extType) {
if (extType == null) {
return null;
}
if ("undefined".equals(extType) || "null".equals(extType)) {
return "void";
}
if ("number".equals(extType) || "boolean".equals(extType) || "string".equals(extType)) {
return capitalize(extType);
}
if ("HTMLElement".equals(extType) || "Event".equals(extType) || "XMLHttpRequest".equals(extType)) {
return "js." + extType;
}
if ("google.maps.Map".equals(extType) || "CSSStyleSheet".equals(extType) || "CSSStyleRule".equals(extType)) {
return "Object"; // no AS3 type yet
}
// enums and ad-hoc enums:
if (extType.startsWith("Ext.enums.") || extType.matches("(['\"].*['\"]/)*['\"].*['\"]")) {
return "String";
}
// array / vararg syntax:
if (extType.endsWith("...") || extType.matches("[a-zA-Z0-9._$<>]+\\[\\]")) {
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?
}
if (JsCodeGenerator.PRIMITIVES.contains(extType)) {
return extType;
}
ExtClass extClass = extJsApi.getExtClass(extType);
if (extClass == null) {
//throw new RuntimeException("Fatal: No Ext class '" + extType + "' found.");
System.err.println("Warning: No Ext class '" + extType + "' found, falling back to Object");
return "Object";
}
String qName = getActionScriptName(extClass);
if (qName == null) {
// try with super class:
return convertType(extClass.extends_);
}
if (isSingleton(extClass)) {
qName = CompilerUtils.qName(CompilerUtils.packageName(qName), "S" + CompilerUtils.className(qName));
}
return qName;
}
// normalize / use alternate class name if it can be found in reference API:
private static void removePrivateApiClasses() {
// collect all non-public classes:
Set privateClasses = new HashSet();
for (ExtClass extClass: extClasses) {
// correct wrong usage of Ext.util.Observable as a mixin:
replaceMixin(extClass, "Ext.util.Observable", "Ext.mixin.Observable");
// simplify "extends Base mixins Mixin-that-extends-Base" to "extends Mixin-that-extends-Base":
replaceMixinByExtends(extClass, "Ext.dom.Element");
// all classes that are mapped explicitly must remain part of the API:
if (getActionScriptName(extClass) != null) {
continue;
}
// Classes to remove from public API:
// explicitly marked private OR
// a built-in type / class OR
// Ext enums - they are just for documentation, so treat them as private API, too.
if (extClass.private_ || JsCodeGenerator.PRIMITIVES.contains(extClass.name) || extClass.name.startsWith("Ext.enums.")) {
privateClasses.add(extClass);
}
}
// all super classes of public classes must be public:
for (ExtClass extClass : extClasses) {
if (!privateClasses.contains(extClass)) {
markPublic(privateClasses, extClass.name);
}
}
extClasses.removeAll(privateClasses);
System.out.println("*****ADD TO JS-AS-NAME-MAPPING:");
for (ExtClass extClass : extClasses) {
if (getActionScriptName(extClass) == null) {
System.out.println(extClass.name + " = " + extClass.name.substring(0, 1).toLowerCase() + extClass.name.substring(1));
}
}
System.out.println("*****END ADD TO JS-AS-NAME-MAPPING");
}
private static CompilationUnitModel getReferenceDeclaration(String jooClassName) {
List referenceDeclarations = getReferenceDeclarations(jooClassName);
return referenceDeclarations.isEmpty() ? null : referenceDeclarations.get(0);
}
private static List getReferenceDeclarations(String jooClassName) {
return referenceApi.getCompilationUnitModels(jooClassName);
}
private static void replaceMixin(ExtClass extClass, String mixinImpl, String mixin) {
// instead of implementing mixinImpl, a class has to implement its interface:
int mixinImplIndex = extClass.mixins.indexOf(mixinImpl);
if (mixinImplIndex != -1) {
extClass.mixins.set(mixinImplIndex, mixin);
}
replaceMixinByExtends(extClass, mixin);
}
private static void replaceMixinByExtends(ExtClass extClass, String mixin) {
// instead of extending Ext.Base and implementing the mixin, it is simpler to extend the mixin
if ("Ext.Base".equals(extClass.extends_) && extClass.mixins.contains(mixin)) {
extClass.mixins.remove(mixin);
extClass.extends_ = mixin;
}
}
private static void markPublic(Set privateClasses, String extClassName) {
ExtClass extClass = extJsApi.getExtClass(extClassName);
//noinspection StatementWithEmptyBody
if (privateClasses.remove(extClass)) {
//System.err.println("*** marked public because it is a super class: " + extClass.name);
}
if (extClass.extends_ != null) {
markPublic(privateClasses, extClass.extends_);
}
for (String mixin : extClass.mixins) {
markPublic(privateClasses, mixin);
}
}
// normalize / use alternate class name if it can be found in reference API:
private static String getActionScriptName(ExtClass extClass) {
String normalizedClassName = jsAsNameMappingProperties.getProperty(extClass.name);
if (normalizedClassName == null) {
// System.err.println(String.format("Ext JS class name %s not mapped to AS.", extClass.name));
// throw new IllegalStateException("unmapped class " + extClass.name);
return null;
}
return normalizedClassName;
}
private static void annotateBindableConfigProperties() {
for (CompilationUnitModel compilationUnitModel : compilationUnitModelRegistry.getCompilationUnitModels()) {
ClassModel classModel = compilationUnitModel.getClassModel();
if (classModel != null) {
annotateBindableConfigProperties(classModel);
}
}
}
private static void annotateBindableConfigProperties(ClassModel classModel) {
List members = classModel.getMembers();
// two-pass to get the order of @see #get() and @see #set() right:
// first, the getters:
for (MemberModel member : members) {
if (member.isGetter()) {
annotateBindableConfigProperty(classModel, (MethodModel) member);
}
}
// then, the setters:
for (MemberModel member : members) {
if (member.isSetter()) {
annotateBindableConfigProperty(classModel, (MethodModel) member);
}
}
}
private static void annotateBindableConfigProperty(ClassModel classModel, MethodModel accessor) {
if (accessor.getAnnotations(Jooc.EXT_CONFIG_ANNOTATION_NAME).isEmpty()) {
return;
}
String prefix = accessor.getMethodType().toString();
String propertyType = getMethodType(accessor, accessor.getMethodType());
if (propertyType == null) {
warnConfigProperty(prefix + " property accessor without type", classModel, accessor);
return;
}
String methodName = prefix + capitalize(accessor.getName());
MethodModel method = compilationUnitModelRegistry.resolveMethod(classModel, null, methodName);
if (method == null) {
warnConfigProperty("no matching " + prefix + "ter method", classModel, accessor);
return;
}
List methodParams = method.getParams();
if (accessor.isSetter() && methodParams.isEmpty()) {
warnConfigProperty(String.format("matching setter method '%s' without parameters. "
+ "Still marking property as [Bindable] - assuming it's compatible at runtime.",
method.getName()), classModel, accessor);
} else {
List moreParams = accessor.isSetter() ? methodParams.subList(1, methodParams.size()) : methodParams;
for (ParamModel param : moreParams) {
if (!param.isOptional()) {
warnConfigProperty(String.format("matching %ster method '%s' has additional non-optional parameter '%s'",
prefix, method.getName(), param.getName()), classModel, accessor);
return;
}
}
String methodType = getMethodType(method, accessor.getMethodType());
if (!propertyType.equals(methodType)) {
boolean probablyCompatible = "*".equals(propertyType) || "*".equals(methodType)
|| "Object".equals(propertyType) || "Object".equals(methodType);
if (!probablyCompatible) {
warnConfigProperty(String.format("type '%s' does not match method '%s' with type '%s'",
propertyType, method.getName(), methodType), classModel, accessor);
return;
}
warnConfigProperty(String.format("type '%s' does not quite match method '%s' with type '%s'. "
+ "Still marking property as [Bindable] - assuming it's compatible at runtime.",
propertyType, method.getName(), methodType), classModel, accessor);
}
}
accessor.addAnnotation(new AnnotationModel(Jooc.BINDABLE_ANNOTATION_NAME));
MethodModel documentedMethod = null;
if (accessor.isSetter()) {
documentedMethod = classModel.getMethod(accessor.isStatic(), MethodType.GET, accessor.getName());
}
if (documentedMethod == null) {
documentedMethod = accessor;
}
String asDoc = documentedMethod.getAsdoc();
documentedMethod.setAsdoc((asDoc == null ? "" : asDoc) + "\n@see #" + methodName + "()");
}
private static String getMethodType(MethodModel method, MethodType methodType) {
if (methodType == MethodType.GET) {
return method.getType();
}
List propertySetterParams = method.getParams();
if (propertySetterParams.isEmpty()) {
return null;
}
return propertySetterParams.get(0).getType();
}
private static void warnConfigProperty(String message, ClassModel classModel, MethodModel propertySetter) {
System.err.format("!!! Config property %s#%s: %s\n", classModel.getName(), propertySetter.getName(), message);
}
}