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 extends Member> 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 += "\nFrom 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 extends Tag> 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 extends Tag> 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