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

net.jangaroo.exml.tools.ExtJsApi Maven / Gradle / Ivy

package net.jangaroo.exml.tools;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Load API model from a jsduck JSON export of the Ext JS API.
 */
public class ExtJsApi {

  private static final Pattern CONSTANT_NAME_PATTERN = Pattern.compile("[A-Z][A-Z0-9_]*");
  private Set extClasses;
  private Map extClassByName;
  private Set mixins;
  private String version;

  private static Doxi readExtApiJson(File jsonFile) throws IOException {
    System.out.printf("Reading API from %s...\n", jsonFile.getPath());
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
    return objectMapper.readValue(jsonFile, Doxi.class);
  }

  public  List filterByOwner(boolean isInterface, boolean isStatic, ExtClass owner, String membersTypeName, Class memberType) {
    return filterByOwner(false, isInterface, isStatic, owner, membersTypeName, memberType);
  }

  public  List filterByOwner(boolean isMixin, boolean isInterface, boolean isStatic, ExtClass owner, String membersTypeName, Class memberType) {
    List result = new ArrayList();
    Optional membersWithType = owner.items.stream().filter(members -> membersTypeName.equals(members.$type)).findFirst();
    List members = membersWithType.isPresent() ? membersWithType.get().items : Collections.emptyList();
    for (Member member : members) {
      if (memberType.isInstance(member) &&
              member.static_ == isStatic &&
              !"private".equals(member.access) &&
              (!isInterface || isPublicNonStaticMethodOrPropertyOrCfg(member)) &&
              (!isMixin || !isPublicNonStaticMethodOrPropertyOrCfg(member))) {
        result.add(memberType.cast(member));
      }
    }
    return result;
  }

  public  T resolve(String reference, String thisClassName, Class memberType) {
    String[] parts = reference.split("[#-]");
    String className = parts[0].isEmpty() ? thisClassName : parts[0];
    ExtClass owner = getExtClass(className);
    if (parts.length == 1) {
      return memberType.cast(owner);
    }
    String name = parts[parts.length - 1];
    return resolve(owner, name, memberType);
  }

  private  T resolve(ExtClass owner, String name, Class memberType) {
    if (owner != null) {
      for (Members members : owner.items) {
        for (Member member : members.items) {
          if (name.equals(member.name) && memberType.isInstance(member)) {
            return memberType.cast(member);
          }
        }
      }
      if (owner.extends_ != null) {
        T result = resolve(getExtClass(owner.extends_), name, memberType);
        if (result != null) {
          return result;
        }
        for (String mixin : owner.mixins) {
          result = resolve(getExtClass(mixin), name, memberType);
          if (result != null) {
            return result;
          }
        }
      }
    }
    return null;
  }

  public boolean isStatic(Member member) {
    return member.static_ || isConstantName(member.name);
  }

  private static boolean isConstantName(String name) {
    return CONSTANT_NAME_PATTERN.matcher(name).matches();
  }

  public boolean isReadOnly(Member member) {
    return member.readonly || isConstantName(member.name);
  }

  public boolean isProtected(Member member) {
    return "protected".equals(member.access);
  }

  public static boolean isPublicNonStaticMethodOrPropertyOrCfg(Member member) {
    return (member instanceof Method || member instanceof Property)
            && !member.static_ && !"private".equals(member.access) && !"protected".equals(member.access)
            && !"constructor".equals(member.name);
  }

  public static boolean isConst(Member member) {
    return member.readonly || (member.name.equals(member.name.toUpperCase()) && member.value != null);
  }

  // normalize / use alternate class name if it can be found in reference API:
  public ExtJsApi(String version, File srcFile) throws IOException {
    this.version = version;
    extClassByName = new HashMap();
    extClasses = new LinkedHashSet();

    Doxi doxi = readExtApiJson(srcFile);

    Set overrides = new LinkedHashSet<>();
    for (Tag global : doxi.global.items) {
      if (global instanceof ExtClass) {
        ExtClass extClass = (ExtClass) global;
        extClasses.add(extClass);
        if (extClass.override != null) {
          overrides.add(extClass);
          if (extClass.name.equals(extClass.override) && extClassByName.containsKey(extClass.name)) {
            continue; // do not let an override hide the original class!
          }
        }
        extClassByName.put(extClass.name, extClass);
        if (extClass.alternateClassNames != null) {
          for (String alternateClassName : extClass.alternateClassNames) {
            extClassByName.put(alternateClassName, extClass);
          }
        }
      }
    }
    // apply overrides:
    for (ExtClass override : overrides) {
      ExtClass overriddenClass = extClassByName.get(override.override);
      if (overriddenClass == override) {
        // ignore self-overrides!
        continue;
      }
      if (overriddenClass == null) {
        System.err.println("Overridden class not found: " + override.name + " wants to override " + override.override);
      } else {
        overriddenClass.text += "\n

From override " + override.name + ":

\n" + override.text; for (Members members : override.items) { // find members.type in overriddenClass: List overriddenMembers = findOrCreateMembers(overriddenClass, members.$type); overriddenMembers.addAll(members.items); // TODO: maybe members are actually replaced, not complemented? Currently not. } } } // mark classes as mixins: Set collectMixins = new HashSet(); for (Tag global : doxi.global.items) { if (global instanceof ExtClass) { ExtClass extClass = (ExtClass) global; for (String mixin : extClass.mixins) { final ExtClass mixinClass = getExtClass(mixin); if (mixinClass != null) { collectMixins.add(mixinClass); } } } } markTransitiveSupersAsMixins(collectMixins); mixins = Collections.unmodifiableSet(collectMixins); } public String getVersion() { return version; } private List findOrCreateMembers(ExtClass extClass, String memberType) { for (Members members : extClass.items) { if (memberType.equals(members.$type)) { return (List) members.items; } } Members members = new Members(); members.$type = memberType; extClass.items.add(members); return (List) members.items; } private void markTransitiveSupersAsMixins(Set extClasses) { Set supers = supers(extClasses); while (!supers.isEmpty()) { extClasses.addAll(supers); supers = supers(supers); } } private Set computeTransitiveSupersAndMixins(ExtClass extClass) { Set result = new HashSet(); addTransitiveSupersAndMixins(result, extClass); return result; } private boolean addTransitiveSupersAndMixins(Set supersAndMixins, ExtClass extClass) { if (extClass != null && supersAndMixins.add(extClass)) { addTransitiveSupersAndMixins(supersAndMixins, getSuperClass(extClass)); for (String mixin : extClass.mixins) { addTransitiveSupersAndMixins(supersAndMixins, getExtClass(mixin)); } return true; } return false; } private Set supers(Set extClasses) { Set result = new HashSet(); for (ExtClass extClass : extClasses) { ExtClass superclass = getSuperClass(extClass); if (superclass != null) { result.add(superclass); } } return result; } public Set getExtClasses() { return extClasses; } public ExtClass getExtClass(String name) { return extClassByName.get(name); } public ExtClass getSuperClass(ExtClass extClass) { return getExtClass(extClass.extends_); } public Set getMixins() { return mixins; } public static boolean isSingleton(ExtClass extClass) { return extClass != null && extClass.singleton; } @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="$type", visible = true) @JsonSubTypes({ @JsonSubTypes.Type(value = ExtClass.class, name = "class"), @JsonSubTypes.Type(value = Enum.class, name = "enum"), @JsonSubTypes.Type(value = Member.class, name = "member"), }) public static class Tag { public String $type; public String name; public String since; public boolean deprecated; public String deprecatedMessage; public String deprecatedVersion; public String text = ""; public String inheritdoc; public String localdoc; public String access; public Object src; @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) && !($type != null ? !$type.equals(tag.$type) : tag.$type != null); } @Override public int hashCode() { int result = $type != null ? $type.hashCode() : 0; result = 31 * result + name.hashCode(); return result; } @Override public String toString() { return (access != null ? access + " " : "") + $type + " " + name; } } @JsonTypeName("doxi") @JsonIgnoreProperties({"files", "version"}) private static class Doxi extends Tag { public Namespace global; } @JsonTypeName("namespace") private static class Namespace extends Tag { public List items; } @JsonIgnoreProperties({"extended", "extenders", "package", "mixed", "mixers", "requires", "uses", "abstract", "Classic", "CT_Location", "disable"}) public static class ExtClass extends Tag { @JsonProperty("extends") @JsonDeserialize(using = FixExtendsDeserializer.class) public String extends_; @JsonDeserialize(using = CommaSeparatedStringsDeserializer.class) public List mixins = Collections.emptyList(); public String override; @JsonDeserialize(using = CommaSeparatedStringsDeserializer.class) public List alternateClassNames; public String alias; public boolean singleton; public List items = Collections.emptyList(); } @JsonSubTypes({ @JsonSubTypes.Type(value = Members.class, name = "methods"), @JsonSubTypes.Type(value = Members.class, name = "static-methods"), @JsonSubTypes.Type(value = Members.class, name = "properties"), @JsonSubTypes.Type(value = Members.class, name = "static-properties"), @JsonSubTypes.Type(value = Members.class, name = "configs"), @JsonSubTypes.Type(value = Members.class, name = "vars"), @JsonSubTypes.Type(value = Members.class, name = "events"), @JsonSubTypes.Type(value = Members.class, name = "sass-mixins"), }) private static class Members extends Tag { private List items; public void setItems(List items) { // "configs" and "properties" both use "property" items, but documentation references require the correct member // type, so fix this: if ("configs".equals($type)) { for (Member item : items) { item.$type = "cfg"; } } // Merge entries with the same name: this.items = new ArrayList<>(items.size()); Map itemByName = new HashMap<>(); for (Member member : items) { String key = member.static_ + "-" + member.name; Member oldMember = itemByName.get(key); if (oldMember != null) { System.out.println("merging " + oldMember + " and " + member); if (oldMember.text.isEmpty()) { oldMember.text = member.text; } if (countNonPrivateItems(oldMember.items) < countNonPrivateItems(member.items)) { oldMember.items = member.items; } // TODO: what else to merge? continue; } itemByName.put(key, member); this.items.add(member); } this.items = items; } private long countNonPrivateItems(List items) { return items.stream().filter(item -> !"private".equals(item.access)).count(); } } @JsonSubTypes({ @JsonSubTypes.Type(value = Param.class, name = "param"), @JsonSubTypes.Type(value = Return.class, name = "return"), @JsonSubTypes.Type(value = Method.class, name = "method"), @JsonSubTypes.Type(value = Property.class, name = "property"), @JsonSubTypes.Type(value = Event.class, name = "event"), }) public abstract static class Var extends Tag { public String type = ""; public String value; public List items = Collections.emptyList(); } @JsonSubTypes({ @JsonSubTypes.Type(value = Method.class, name = "method"), @JsonSubTypes.Type(value = Property.class, name = "property"), @JsonSubTypes.Type(value = Event.class, name = "event"), }) public static class Member extends Var { @JsonProperty("static") public boolean static_; public boolean inheritable; public Object accessor; // TRUE or "w" public boolean evented; public boolean readonly; public boolean hide; public boolean ignore; public boolean undocumented; public boolean locale; } @JsonIgnoreProperties({"aliasPrefix", "items"}) public static class Enum extends Tag { } @JsonIgnoreProperties({"removedMessage", "removedVersion"}) public static class Property extends Member { public boolean required; public boolean optional; public boolean controllable; public boolean dynamic; } public static class Param extends Var { public boolean optional; @JsonProperty("optional") public void setOptional(boolean value) { optional = value; } @JsonProperty("Optional") public void setUpperCaseOptional(boolean value) { optional = value; } } public static class Return extends Var { } @JsonIgnoreProperties({"disable", "fires", "removedMessage", "removedVersion"}) public static class Method extends Member { public boolean template; public boolean constructor; public boolean chainable; @JsonProperty("abstract") public boolean abstract_; @Override public String toString() { return super.toString() + "(" + items.size() + ")"; } } public static class Event extends Member { public List items = Collections.emptyList(); public boolean preventable; } private static class CommaSeparatedStringsDeserializer extends JsonDeserializer> { @Override public List deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { String string = jsonParser.readValueAs(String.class); return new ArrayList<>(Arrays.asList(string.split(","))); } } private static class FixExtendsDeserializer extends JsonDeserializer { @Override public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { String value = jsonParser.readValueAs(String.class); return Arrays.stream(value.split(",")) .filter(className -> !"Object".equals(className)) .findFirst() .orElse("Object"); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy