/* * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * For more information, please refer to <http://unlicense.org/> */ package jxtn.core.fmpp; import fmpp.Engine; import fmpp.dataloaders.XmlDataLoader; import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import javax.xml.parsers.DocumentBuilderFactory; import jxtn.core.axi.collections.LinkLineIterator; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * 以JAR檔類別結構作為資料來源的載入器 * <p> * 以產生JavaFX的建構類別為主要用途,尚不支援一般性使用 * </p> * * @author AqD */ @SuppressWarnings({ "rawtypes", "unchecked" }) public class JarReflectionDataLoader extends XmlDataLoader { // 替換:javafx.scene.control.ListView.javafx.scene.control.ListView$EditEvent // 成為:javafx.scene.control.ListView.EditEvent private final Pattern genericFixPattern = Pattern.compile("(([a-z0-9]+\\.)+[A-Z][A-Za-z0-9]+)\\.\\1\\$"); private final Pattern packageClassPattern = Pattern .compile("(?<package>([a-z0-9]+)(\\.[a-z0-9]+)*)\\.(?<class>[A-Z].*)"); @Override public Object load(Engine engine, List args) throws Exception { try { // 參數 String[] arguments = ((String) args.get(0)).split("\\|"); List<String> jarLocations = Arrays.asList(arguments[0].split(",")).map(String::trim).toArrayList(); List<String> rootPackages = Arrays.asList(arguments[1].split(",")).map(String::trim).toArrayList(); String rootClassName = arguments[2]; // JAR路徑 List<Path> jarLocPaths = jarLocations.map(loc -> { if (loc.startsWith("/") || loc.startsWith("./") || loc.startsWith("../") || (loc.length() >= 2 && loc.charAt(1) == ':')) { return Paths.get(loc); } else { return Paths.get(System.getProperty("java.home"), loc); } }).toArrayList(); // 設定類別載入器 ClassLoader loader = new URLClassLoader(jarLocPaths.map(p -> { try { return p.toUri().toURL(); } catch (Exception e) { throw new RuntimeException(e); } }).toArray(URL.class)); // 載入最上層類別 Matcher rootClassMatcher = this.packageClassPattern.matcher(rootClassName); rootClassMatcher.matches(); Class rootClass = loader.loadClass(rootClassName); // 建立XML文件 Document xmlDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); Element xmlRoot = xmlDoc.createElement("reflection"); xmlRoot.setAttribute("loader", "JarReflectionDataLoader-1.0.0"); xmlDoc.appendChild(xmlRoot); // 處理JAR檔 for (Path jarPath : jarLocPaths) { Path jarName = jarPath.getFileName(); if (jarName == null) { continue; } Element elemJar = xmlDoc.createElement("jar"); elemJar.setAttribute("file", jarName.toString()); System.out.println("jarPath: " + jarPath); // 載入JAR內所有類別 List<String> classNames = this.listClassNames(jarPath); classNames.sort((c1, c2) -> c1.compareTo(c2)); List<Class> classList = classNames .filter(name -> rootPackages.any(pkg -> name.startsWith(pkg.replace(".", "/") + "/"))) .map(name -> this.loadClass(loader, name)) .filter(c -> Modifier.isPublic(c.getModifiers()) && rootClass.isAssignableFrom(c) && !c.isAnnotation() && !c.isArray() && !c.isEnum() && !c.isInterface() && !c.isAnonymousClass() && !c.isLocalClass() && !c.isSynthetic() && (!c.isMemberClass() || Modifier.isStatic(c.getModifiers())) && !c.isAnnotationPresent(Deprecated.class)) .toArrayList(); // 掃描靜態屬性 HashMap<Class, HashMap<String, ArrayList<Method>>> staticPropertyMapRegistry = new HashMap<>(); for (Class klass : classList) { HashMap<String, ArrayList<Method>> tmpMap = Arrays .asList(klass.getDeclaredMethods()) .filter(m -> m.isPublic() && m.isStatic() && !m.isAnnotationPresent(Deprecated.class) && this.isStaticGetterOrSetter(m)) .toArrayListSorted(m -> m.getName()) .toHashMapGrouped(m -> this.getPropertyName(m.getName())); for (String prop : tmpMap.keySet()) { ArrayList<Method> mList = tmpMap.get(prop); if (mList.size() != 2) { continue; } Class type0 = mList.get(0).getParameters()[0].getType(); Class type1 = mList.get(1).getParameters()[0].getType(); if (type0.equals(type1)) { HashMap<String, ArrayList<Method>> pmap = staticPropertyMapRegistry.computeIfAbsent( type0, k -> new HashMap<>()); pmap.put(getShortName(klass) + "_" + prop, mList); } } } // 處理 for (Class klass : classList) { System.out.println(klass.getTypeName()); Element elemClass = xmlDoc.createElement("class"); elemClass.setAttribute("name", getShortName(klass)); elemClass.setAttribute("package", klass.getPackage().getName()); String paramsDeclaration = this.buildGenericDeclaration(klass.getTypeParameters()); String paramsName = this.buildGenericName(klass.getTypeParameters()); elemClass.setAttribute("genericDeclaration", paramsDeclaration); elemClass.setAttribute("genericDeclarationPrepend", paramsDeclaration.length() == 0 ? "" : paramsDeclaration + ", "); elemClass.setAttribute("genericParam", paramsName); elemClass.setAttribute("genericParamPrepend", paramsName.length() == 0 ? "" : paramsName + ", "); elemClass.setAttribute("genericName", getShortName(klass) + (paramsName.length() == 0 ? "" : "<" + paramsName + ">")); elemClass.setAttribute("abstract", Boolean.toString(Modifier.isAbstract(klass.getModifiers()))); // Parent Type supertype = this.findSuperclass(klass); { if (supertype != null) { String sname = supertype.getTypeName(); if (!rootPackages.any(pkg -> sname.startsWith(pkg))) { supertype = null; } } } if (supertype == null) { elemClass.setAttribute("super", ""); } else { if (supertype instanceof ParameterizedType) { String superclassName = ((Class) ((ParameterizedType) supertype).getRawType()) .getCanonicalName(); elemClass.setAttribute("super", superclassName); String supertypeName = this.fixGenericTypeName(supertype.getTypeName()); int index = supertypeName.indexOf("<"); if (index >= 0) { String superParam = supertypeName.substring( supertypeName.indexOf("<") + 1, supertypeName.length() - 1); elemClass.setAttribute("superParam", superParam); elemClass.setAttribute("superParamPrepend", superParam + ", "); } else { elemClass.setAttribute("superParam", ""); elemClass.setAttribute("superParamPrepend", ""); } } else if (supertype instanceof Class) { String superclassName = ((Class) supertype).getCanonicalName(); String supertypeName = this.fixGenericTypeName(supertype.getTypeName()); elemClass.setAttribute("super", superclassName); int index = supertypeName.indexOf("<"); if (index >= 0) { String superParam = supertypeName.substring(index + 1, supertypeName.length() - 1); elemClass.setAttribute("superParam", superParam); elemClass.setAttribute("superParamPrepend", superParam + ", "); } else { elemClass.setAttribute("superParam", ""); elemClass.setAttribute("superParamPrepend", ""); } } } // Members for (TypeVariable tvar : klass.getTypeParameters()) { elemClass.appendChild(this.describeTypeParameter(xmlDoc, tvar)); } for (Constructor constructor : Arrays.asList(klass.getDeclaredConstructors()) .filter(c -> c.isPublic() && !c.isAnnotationPresent(Deprecated.class)) .toArrayListSorted(c -> getConstructorSortKey(c))) { elemClass.appendChild(this.describeConstructor(xmlDoc, constructor)); } for (Field field : Arrays.asList(klass.getDeclaredFields()) .filter(f -> f.isPublic() && !f.isAnnotationPresent(Deprecated.class)) .toArrayListSorted(f -> getFieldSortKey(f))) { elemClass.appendChild(this.describeField(xmlDoc, field)); } for (Method method : Arrays.asList(klass.getDeclaredMethods()) .filter(m -> m.isPublic() && !m.isAnnotationPresent(Deprecated.class)) .toArrayListSorted(m -> getMethodSortKey(m))) { elemClass.appendChild(this.describeMethod(xmlDoc, method)); } // Properties HashMap<String, ArrayList<Method>> propertyMap = Arrays .asList(klass.getDeclaredMethods()) .filter(m -> m.isPublic() && !m.isStatic() && !m.isAnnotationPresent(Deprecated.class) && this.isGetterOrSetter(m)) .toArrayListSorted(m -> getMethodSortKey(m)) .toHashMapGrouped(m -> this.getPropertyName(m.getName())); for (String propName : propertyMap.keySet().toArrayListSorted(n -> n)) { ArrayList<Method> propAccessors = propertyMap.get(propName); List<Method> getterList = propAccessors .filter(m -> m.getName().startsWith("get") || m.getName().startsWith("is")) .toArrayList(); if (getterList.size() > 1) { System.out.println("duplicated property getters: " + propName + "; " + String.join(", ", getterList.map(m -> m.getName() + m.getParameters().length))); } Method getter = getterList.firstOrNull(); List<Method> setterList = propAccessors .filter(m -> m.getName().startsWith("set")) .toArrayListSorted( m -> ((getter == null || getter.getGenericReturnType() .equals(m.getGenericParameterTypes()[0])) ? "0" : "1") + "," + m.getGenericParameterTypes()[0].getTypeName()); if (getterList.size() == 0 && setterList.size() == 0) { System.out.println("empty property: " + propName); continue; } elemClass.appendChild(this.describeProperty(xmlDoc, propName, getter, setterList)); } // Static Properties HashMap<String, ArrayList<Method>> staticPropertyMap = staticPropertyMapRegistry.get(klass); if (staticPropertyMap != null) { for (String propName : staticPropertyMap.keySet().toArrayListSorted(n -> n)) { ArrayList<Method> propAccessors = staticPropertyMap.get(propName); Method getter = propAccessors.get(0); Method setter = propAccessors.get(1); elemClass.appendChild(this.describeStaticProperty(xmlDoc, propName, getter, setter)); } } // elemJar.appendChild(elemClass); } xmlRoot.appendChild(elemJar); } Element srcXml = xmlDoc.createElement("xml"); srcXml.appendChild(xmlDoc.createCDATASection(XML.toText(xmlRoot))); xmlRoot.appendChild(srcXml); return this.load(engine, args, xmlDoc); } catch (Throwable throwable) { throwable.printStackTrace(); throw throwable; } } private Element describeProperty(Document doc, String name, Method getter, Collection<Method> setterList) { Element elem = doc.createElement("property"); elem.setAttribute("name", name); elem.setAttribute("override", "false"); elem.setAttribute("static", "false"); if (getter != null) { elem.setAttribute("genericType", this.fixGenericTypeName( getter.getGenericReturnType().getTypeName())); } else { elem.setAttribute("genericType", this.fixGenericTypeName( setterList.first().getParameters()[0].getParameterizedType().getTypeName())); } if (getter != null) { Element elemGetter = this.describeMethod(doc, getter); elem.appendChild(doc.renameNode(elemGetter, null, "getter")); if (this.checkMethodOverride(getter)) { elem.setAttribute("override", "true"); } } int setterIndex = 0; for (Method setter : setterList) { Element elemSetter = this.describeMethod(doc, setter); elemSetter.setAttribute("index", setterIndex == 0 ? "" : Integer.toString(setterIndex)); String gType = this.fixGenericTypeName(setter.getGenericParameterTypes()[0].getTypeName()); elemSetter.setAttribute("genericType", gType); int ltIndex = gType.indexOf("<"); if (ltIndex >= 0) { elemSetter.setAttribute("genericParam", gType.substring(ltIndex + 1, gType.length() - 1)); elemSetter.setAttribute("rawType", gType.substring(0, ltIndex)); } else { elemSetter.setAttribute("genericParam", ""); elemSetter.setAttribute("rawType", gType); } elem.appendChild(doc.renameNode(elemSetter, null, "setter")); if (this.checkMethodOverride(setter)) { elem.setAttribute("override", "true"); } setterIndex += 1; } String gtype = elem.getAttribute("genericType"); int index = gtype.indexOf("<"); if (index >= 0) { elem.setAttribute("genericParam", gtype.substring(index + 1, gtype.length() - 1)); elem.setAttribute("rawType", gtype.substring(0, index)); } else { elem.setAttribute("genericParam", ""); elem.setAttribute("rawType", gtype); } return elem; } private Element describeStaticProperty(Document doc, String name, Method getter, Method setter) { Element elem = doc.createElement("property"); elem.setAttribute("name", name); elem.setAttribute("override", "false"); elem.setAttribute("static", "true"); elem.setAttribute("staticType", getter.getDeclaringClass().getTypeName()); elem.setAttribute("staticName", this.getPropertyName(getter.getName())); elem.setAttribute("genericType", this.fixGenericTypeName( setter.getParameters()[1].getParameterizedType().getTypeName())); Element elemGetter = this.describeMethod(doc, getter); elem.appendChild(doc.renameNode(elemGetter, null, "getter")); Element elemSetter = this.describeMethod(doc, setter); elemSetter.setAttribute("index", ""); elem.appendChild(doc.renameNode(elemSetter, null, "setter")); String gtype = elem.getAttribute("genericType"); int index = gtype.indexOf("<"); if (index >= 0) { elem.setAttribute("genericParam", gtype.substring(index + 1, gtype.length() - 1)); elem.setAttribute("rawType", gtype.substring(0, index)); elemSetter.setAttribute("genericType", gtype); elemSetter.setAttribute("genericParam", gtype.substring(index + 1, gtype.length() - 1)); elemSetter.setAttribute("rawType", gtype.substring(0, index)); } else { elem.setAttribute("genericParam", ""); elem.setAttribute("rawType", gtype); elemSetter.setAttribute("genericType", gtype); elemSetter.setAttribute("genericParam", ""); elemSetter.setAttribute("rawType", gtype); } return elem; } private Element describeField(Document doc, Field field) { Element elem = doc.createElement("field"); elem.setAttribute("name", field.getName()); elem.setAttribute("type", field.getType().getCanonicalName()); elem.setAttribute("final", Boolean.toString(field.isFinal())); elem.setAttribute("public", Boolean.toString(field.isPublic())); elem.setAttribute("static", Boolean.toString(field.isStatic())); elem.setAttribute("access", field.getAccessLevel()); return elem; } private Element describeConstructor(Document doc, Constructor constructor) { Element elem = doc.createElement("constructor"); elem.setAttribute("name", constructor.getName()); elem.setAttribute("parameterCount", Integer.toString(constructor.getParameterCount())); elem.setAttribute("public", Boolean.toString(constructor.isPublic())); elem.setAttribute("static", Boolean.toString(constructor.isStatic())); elem.setAttribute("access", constructor.getAccessLevel()); elem.setAttribute("parametersDeclaration", this.describeParametersDeclaration(constructor.getParameters())); elem.setAttribute("parametersCall", this.describeParametersCall(constructor.getParameters())); Arrays.asList(constructor.getParameters()).forEach(p -> elem.appendChild(this.describeParameter(doc, p))); return elem; } private Element describeMethod(Document doc, Method method) { Element elem = doc.createElement("method"); elem.setAttribute("name", method.getName()); elem.setAttribute("returnType", this.fixGenericTypeName(method.getGenericReturnType().getTypeName())); elem.setAttribute("parameterCount", Integer.toString(method.getParameterCount())); elem.setAttribute("public", Boolean.toString(method.isPublic())); elem.setAttribute("static", Boolean.toString(method.isStatic())); elem.setAttribute("abstract", Boolean.toString(method.isAbstract())); elem.setAttribute("final", Boolean.toString(method.isFinal())); elem.setAttribute("access", method.getAccessLevel()); elem.setAttribute("parametersDeclaration", this.describeParametersDeclaration(method.getParameters())); elem.setAttribute("parametersCall", this.describeParametersCall(method.getParameters())); elem.setAttribute("override", Boolean.toString(this.checkMethodOverride(method))); Arrays.asList(method.getParameters()).forEach(p -> elem.appendChild(this.describeParameter(doc, p))); return elem; } private Element describeTypeParameter(Document doc, TypeVariable typeParameter) { Element elem = doc.createElement("typeParameter"); elem.setAttribute("name", typeParameter.getName()); return elem; } private Element describeParameter(Document doc, Parameter parameter) { Element elem = doc.createElement("parameter"); elem.setAttribute("name", parameter.getName()); elem.setAttribute("type", parameter.getType().getCanonicalName()); elem.setAttribute("genericType", this.fixGenericTypeName(parameter.getParameterizedType().getTypeName())); return elem; } private String describeParametersCall(Parameter[] parameter) { return String.join(", ", Arrays.asList(parameter).map(p -> p.getName())); } private String describeParametersDeclaration(Parameter[] parameter) { return String.join(", ", Arrays.asList(parameter) .map(p -> this.fixGenericTypeName(p.getParameterizedType().getTypeName()) + " " + p.getName())); } private Type findSuperclass(Class klass) { if (klass.getSuperclass() == null || klass.getSuperclass() == Object.class) { return null; } Type supertype = klass.getGenericSuperclass(); if (supertype instanceof ParameterizedType) { ParameterizedType superparam = (ParameterizedType) supertype; Type superraw = superparam.getRawType(); if (superraw instanceof Class) { Class superclass = (Class) superraw; if (!Modifier.isPublic(superclass.getModifiers())) { return this.findSuperclass(superclass); } } } if (supertype instanceof Class) { Class superclass = (Class) supertype; if (!Modifier.isPublic(superclass.getModifiers())) { System.out.println("super..."); return this.findSuperclass(superclass); } } return supertype; } private String fixGenericTypeName(String genericTypeName) { if (genericTypeName.contains("$")) { Matcher m = this.genericFixPattern.matcher(genericTypeName); if (m.find()) { return m.replaceAll("$1.").replace('$', '.'); } return genericTypeName.replace('$', '.'); } return genericTypeName; } private Class loadClass(ClassLoader loader, String name) { try { return loader.loadClass(name.replace('/', '.')); } catch (Throwable e) { System.err.println("failed to load " + name); throw new RuntimeException("failed to load " + name, e); } } private List<String> listClassNames(Path jarPath) throws IOException { List<String> classNames = new ArrayList<String>(); try (FileInputStream file = new FileInputStream(jarPath.toFile()); ZipInputStream zip = new ZipInputStream(file)) { for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { if (entry.isDirectory()) { continue; } String name = entry.getName(); if (!name.endsWith(".class")) { continue; } int m1 = name.indexOf('$'); int m2 = name.lastIndexOf('$'); if (m1 != -1) { if (m1 != m2) { continue; } char c = name.charAt(m1 + 1); if (c >= '0' && c <= '9') { continue; } } classNames.add(name.substring(0, name.length() - ".class".length())); } } return classNames; } private String buildGenericDeclaration(TypeVariable[] typeParameters) { if (typeParameters.length == 0) { return ""; } else { return String.join( ", ", Arrays.asList(typeParameters).map(p -> p.getName() + (p.getBounds().length == 0 ? "" : " extends " + String.join("&", Arrays.asList(p.getBounds()).map(b -> b.getTypeName()))))); } } protected String buildGenericName(TypeVariable[] typeParameters) { if (typeParameters.length == 0) { return ""; } else { return String.join(", ", Arrays.asList(typeParameters).map(p -> p.getName())); } } /* private String insertPostfix(String genericTypeName, String postfix) { if (genericTypeName == null) return null; if (genericTypeName.length() == 0) return genericTypeName; int index = genericTypeName.indexOf('<'); if (index >= 0) return genericTypeName.substring(0, index) + postfix + genericTypeName.substring(index); else return genericTypeName + postfix; } */ private boolean isGetterOrSetter(Method method) { String name = method.getName(); if (name.length() > 2 && name.startsWith("is") && method.getParameterCount() == 0) { return true; } if (name.length() > 3 && name.startsWith("get") && method.getParameterCount() == 0) { return true; } if (name.length() > 3 && name.startsWith("set") && method.getParameterCount() == 1) { return true; } return false; } private boolean isStaticGetterOrSetter(Method method) { String name = method.getName(); if (name.length() > 2 && name.startsWith("is") && method.getParameterCount() == 1) { return true; } if (name.length() > 3 && name.startsWith("get") && method.getParameterCount() == 1) { return true; } if (name.length() > 3 && name.startsWith("set") && method.getParameterCount() == 2) { return true; } return false; } private String getPropertyName(String accessorName) { if (accessorName.startsWith("is")) { return accessorName.substring(2).toUncapitalized(); } else { return accessorName.substring(3).toUncapitalized(); } } private boolean checkMethodOverride(Method method) { if (method.getDeclaringClass() == Object.class) { return false; } // if (method.getName().equals("getSelectedItems")) { System.out.println(method.getDeclaringClass().getTypeName() + "found getSelectedItems"); Iterator<Class> it = new LinkLineIterator<>( method.getDeclaringClass().getSuperclass(), c -> c.getSuperclass()); while (it.hasNext()) { Class klass = it.next(); if (!Modifier.isPublic(klass.getModifiers())) { continue; } Arrays.asList(klass.getDeclaredMethods()) .filter(m -> m.isPublic() && !m.isStatic() && !m.isAbstract() && m.getName().equals(method.getName())) .forEach(System.out::println); } } // Iterator<Class> iterator = new LinkLineIterator<>( method.getDeclaringClass().getSuperclass(), c -> c.getSuperclass()); while (iterator.hasNext()) { Class klass = iterator.next(); if (!Modifier.isPublic(klass.getModifiers())) { continue; } if (Arrays.asList(klass.getDeclaredMethods()) .filter(m -> m.isPublic() && !m.isStatic() && !m.isAbstract()) .any(m -> m.getName().equals(method.getName()))) { return true; } } return false; } private static String getConstructorSortKey(Constructor constructor) { return String.format("%03d: %s", constructor.getParameterCount(), String.join(",", Arrays.asList(constructor.getGenericParameterTypes()).map(t -> t.getTypeName()))); } private static String getFieldSortKey(Field field) { return field.getName(); } private static String getMethodSortKey(Method method) { return String.format("%s %s %s", method.getName(), method.toGenericString(), String.join(",", Arrays.asList(method.getGenericParameterTypes()).map(t -> t.getTypeName()))); } private static String getShortName(Class<?> klass) { String name = klass.getName(); int index = name.lastIndexOf('.'); if (index != -1) { name = name.substring(index + 1); } return name.replace('$', '.'); } }