All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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%n

Type 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy