/* * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA */ package jace.core; import jace.Emulator; import java.awt.BorderLayout; import java.awt.EventQueue; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.JarURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JProgressBar; /** * This is a set of helper functions which do not belong anywhere else. Functions vary from introspection, discovery, and string/pattern matching. * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ public class Utility { //--------------- Introspection utilities private static Set findClasses(String pckgname, Class clazz) { Set output = new HashSet<>(); // Code from JWhich // ====== // Translate the package name into an absolute path String name = pckgname; if (!name.startsWith("/")) { name = "/" + name; } name = name.replace('.', '/'); // Get a File object for the package URL url = Utility.class.getResource(name); if (url == null || url.getFile().contains("jre/lib")) { return output; } if (url.getProtocol().equalsIgnoreCase("jar")) { return findClassesInJar(url, clazz); } File directory = new File(url.getFile()); // New code // ====== if (directory.exists()) { // Get the list of the files contained in the package for (String filename : directory.list()) { char firstLetter = filename.charAt(0); if (firstLetter < 'A' || (firstLetter > 'Z' && firstLetter < 'a') || firstLetter > 'z') { continue; } // we are only interested in .class files if (filename.endsWith(".class")) { // removes the .class extension String classname = filename.substring(0, filename.length() - 6); try { // Try to create an instance of the object String className = pckgname + "." + classname; // System.out.println("Class: " + className); Class c = Class.forName(className); if (clazz.isAssignableFrom(c)) { output.add(c); } } catch (ClassNotFoundException cnfex) { System.err.println(cnfex); } } else { // System.out.println("Skipping non class: " + filename); } } } return output; } private static Set findClassesInJar(URL jarLocation, Class clazz) { Set output = new HashSet<>(); JarFile jarFile = null; try { JarURLConnection conn = (JarURLConnection) jarLocation.openConnection(); jarFile = conn.getJarFile(); Enumeration entries = jarFile.entries(); String last = ""; while (entries.hasMoreElements()) { JarEntry jarEntry = entries.nextElement(); if (jarEntry.getName().equals(last)) { return output; } last = jarEntry.getName(); if (jarEntry.getName().endsWith(".class")) { String className = jarEntry.getName(); className = className.substring(0, className.length() - 6); className = className.replaceAll("/", "\\."); if (className.startsWith("com.sun")) { continue; } if (className.startsWith("java")) { continue; } if (className.startsWith("javax")) { continue; } if (className.startsWith("com.oracle")) { continue; } // removes the .class extension try { // Try to create an instance of the object // System.out.println("Class: " + className); Class c = Class.forName(className); if (clazz.isAssignableFrom(c)) { output.add(c); } } catch (ClassNotFoundException cnfex) { System.err.println(cnfex); } catch (Throwable cnfex) { // System.err.println(cnfex); } } else { // System.out.println("Skipping non class: " + jarEntry.getName()); } } } catch (IOException ex) { Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); } finally { try { if (jarFile != null) { jarFile.close(); } } catch (IOException ex) { Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); } } return output; } private static final Map> classCache = new HashMap<>(); public static List findAllSubclasses(Class clazz) { if (classCache.containsKey(clazz)) { return (List) classCache.get(clazz); } TreeMap allClasses = new TreeMap<>(); for (Package p : Package.getPackages()) { if (p.getName().startsWith("java") || p.getName().startsWith("com.sun") || p.getName().startsWith("com.oracle")) { continue; } findClasses(p.getName(), clazz).stream().filter((c) -> !(Modifier.isAbstract(c.getModifiers()))).forEach((c) -> { allClasses.put(c.getSimpleName(), c); }); } List values = new ArrayList(allClasses.values()); classCache.put(clazz, values); return values; } //------------------------------ String comparators /** * Rank two strings similarity in terms of distance The lower the number, * the more similar these strings are to each other See: * http://en.wikipedia.org/wiki/Levenshtein_distance#Computing_Levenshtein_distance * * @param s * @param t * @return Distance (higher is better) */ public static int levenshteinDistance(String s, String t) { if (s == null || t == null || s.length() == 0 || t.length() == 0) { return -1; } s = s.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", ""); t = t.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", ""); int m = s.length(); int n = t.length(); int[][] dist = new int[m + 1][n + 1]; for (int i = 1; i <= m; i++) { dist[i][0] = i; } for (int i = 1; i <= n; i++) { dist[0][i] = i; } for (int j = 1; j <= n; j++) { for (int i = 1; i <= m; i++) { if (s.charAt(i - 1) == t.charAt(j - 1)) { dist[i][j] = dist[i - 1][j - 1]; } else { int del = dist[i - 1][j] + 1; int insert = dist[i][j - 1] + 1; int sub = dist[i - 1][j - 1] + 1; dist[i][j] = Math.min(Math.min(del, insert), sub); } } } return Math.max(m, n) - dist[m][n]; } /** * Compare strings based on a tally of similar patterns found, using a fixed * search window The resulting score is heavily penalized if the strings * differ greatly in length This is not as efficient as levenshtein, so it's * only used as a tie-breaker. * * @param c1 * @param c2 * @param width Search window size * @return Overall similarity score (higher is beter) */ public static double rankMatch(String c1, String c2, int width) { double score = 0; String s1 = c1.toLowerCase(); String s2 = c2.toLowerCase(); for (int i = 0; i < s1.length() + 1 - width; i++) { String m = s1.substring(i, i + width); int j = 0; while ((j = s2.indexOf(m, j)) > -1) { score += width; j++; } } double l1 = s1.length(); double l2 = s2.length(); // If the two strings are equivilent in length, the score is higher // If the two strings are different in length, the score is adjusted lower depending on how large the difference is // This is offset just a hair for tuning purposes double adjustment = (Math.min(l1, l2) / Math.max(l1, l2)) + 0.1; return score * adjustment * adjustment; } public static String join(Collection c, String d) { String result = ""; boolean isFirst = true; for (Object o : c) { result += (isFirst ? "" : d) + o.toString(); isFirst = false; } return result; } public static ImageIcon loadIcon(String filename) { URL imageUrl = Utility.class.getClassLoader().getResource("jace/data/" + filename); ImageIcon i = new ImageIcon(imageUrl); return i; } public static void runModalProcess(String title, final Runnable runnable) { final JDialog frame = new JDialog(Emulator.getFrame()); final JProgressBar progressBar = new JProgressBar(); progressBar.setIndeterminate(true); final JPanel contentPane = new JPanel(); contentPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); contentPane.setLayout(new BorderLayout()); contentPane.add(new JLabel(title), BorderLayout.NORTH); contentPane.add(progressBar, BorderLayout.CENTER); frame.setContentPane(contentPane); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); new Thread(() -> { runnable.run(); frame.setVisible(false); frame.dispose(); }).start(); } public static class RankingComparator implements Comparator { String match; public RankingComparator(String match) { // Adding a space helps respect word boundaries as part of the match // In the case of very close matches this is another tie-breaker // Especially for very small search terms this.match = match + " "; } @Override public int compare(String o1, String o2) { double s1 = levenshteinDistance(match, o1); double s2 = levenshteinDistance(match, o2); if (s2 == s1) { s1 = rankMatch(o1, match, 3) + rankMatch(o1, match, 2); s2 = rankMatch(o2, match, 3) + rankMatch(o2, match, 2); if (s2 == s1) { return (o1.compareTo(o2)); } else { // Normalize result to -1, 0 or 1 so there is no rounding issues! return (int) Math.signum(s2 - s1); } } else { return (int) (s2 - s1); } } } /** * Given a desired search string and a search space of recognized * selections, identify the best match in the list * * @param match String to search for * @param search Space of all valid results * @return Best match found, or null if there was nothing close to a match * found. */ public static String findBestMatch(String match, Collection search) { if (search == null || search.isEmpty()) { return null; } RankingComparator r = new RankingComparator(match); List candidates = new ArrayList<>(search); Collections.sort(candidates, r); // for (String c : candidates) { // double m2 = rankMatch(c, match, 2); // double m3 = rankMatch(c, match, 3); // double m4 = rankMatch(c, match, 4); // double l = levenshteinDistance(match, c); // System.out.println(match + "->" + c + ":" + l + " -- "+ m2 + "," + m3 + "," + "(" + (m2 + m3) + ")"); // } // double score = rankMatch(match, candidates.get(0), 2); double score = levenshteinDistance(match, candidates.get(0)); if (score > 1) { return candidates.get(0); } return null; } public static void printStackTrace() { System.out.println("CURRENT STACK TRACE:"); for (StackTraceElement s : Thread.currentThread().getStackTrace()) { System.out.println(s.getClassName() + "." + s.getMethodName() + " (line " + s.getLineNumber() + ") " + (s.isNativeMethod() ? "NATIVE" : "")); } System.out.println("END OF STACK TRACE"); } public static int parseHexInt(Object s) { if (s == null) { return -1; } if (s instanceof Integer) { return (Integer) s; } String val = String.valueOf(s).trim(); int base = 10; if (val.startsWith("$")) { base = 16; val = val.contains(" ") ? val.substring(1, val.indexOf(' ')) : val.substring(1); } else if (val.startsWith("0x")) { base = 16; val = val.contains(" ") ? val.substring(2, val.indexOf(' ')) : val.substring(2); } try { return Integer.parseInt(val, base); } catch (NumberFormatException ex) { gripe("This isn't a valid number: " + val + ". If you put a $ in front of that then I'll know you meant it to be a hex number."); throw ex; } } public static void gripe(final String message) { EventQueue.invokeLater(() -> { JOptionPane.showMessageDialog(Emulator.getFrame(), message, "Error", JOptionPane.ERROR_MESSAGE); }); } public static Object findChild(Object object, String fieldName) { if (object instanceof Map) { Map map = (Map) object; for (Object key : map.keySet()) { if (key.toString().equalsIgnoreCase(fieldName)) { return map.get(key); } } return null; } try { Field f = object.getClass().getField(fieldName); return f.get(object); } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) { for (Method m : object.getClass().getMethods()) { if (m.getName().equalsIgnoreCase("get" + fieldName) && m.getParameterTypes().length == 0) { try { return m.invoke(object, new Object[0]); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex1) { } } } } return null; } public static Object setChild(Object object, String fieldName, String value, boolean hex) { if (object instanceof Map) { Map map = (Map) object; for (Object key : map.entrySet()) { if (key.toString().equalsIgnoreCase(fieldName)) { map.put(key, value); return null; } } return null; } Field f; try { f = object.getClass().getField(fieldName); } catch (NoSuchFieldException ex) { System.out.println("Object type " + object.getClass().getName() + " has no field named " + fieldName); Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); return null; } catch (SecurityException ex) { Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); return null; } Object useValue = deserializeString(value, f.getType(), hex); try { f.set(object, useValue); return useValue; } catch (IllegalArgumentException | IllegalAccessException ex) { for (Method m : object.getClass().getMethods()) { if (m.getName().equalsIgnoreCase("set" + fieldName) && m.getParameterTypes().length == 0) { try { m.invoke(object, useValue); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex1) { } } } } return useValue; } static Map> enumCache = new HashMap<>(); public static Object findClosestEnumConstant(String value, Class type) { Map enumConstants = enumCache.get(type); if (enumConstants == null) { Object[] constants = type.getEnumConstants(); enumConstants = new HashMap<>(); for (Object o : constants) { enumConstants.put(o.toString(), o); } enumCache.put(type, enumConstants); } String key = findBestMatch(value, enumConstants.keySet()); if (key == null) { return null; } return enumConstants.get(key); } public static Object deserializeString(String value, Class type, boolean hex) { int radix = hex ? 16 : 10; if (type.equals(Integer.TYPE) || type == Integer.class) { value = value.replaceAll(hex ? "[^0-9\\-A-Fa-f]" : "[^0-9\\-]", ""); try { return Integer.parseInt(value, radix); } catch (NumberFormatException ex) { return null; } } else if (type.equals(Short.TYPE) || type == Short.class) { value = value.replaceAll(hex ? "[^0-9\\-\\.A-Fa-f]" : "[^0-9\\-\\.]", ""); try { return Short.parseShort(value, radix); } catch (NumberFormatException ex) { return null; } } else if (type.equals(Long.TYPE) || type == Long.class) { value = value.replaceAll(hex ? "[^0-9\\-\\.A-Fa-f]" : "[^0-9\\-\\.]", ""); try { return Long.parseLong(value, radix); } catch (NumberFormatException ex) { return null; } } else if (type.equals(Byte.TYPE) || type == Byte.class) { try { value = value.replaceAll(hex ? "[^0-9\\-A-Fa-f]" : "[^0-9\\-]", ""); return Byte.parseByte(value, radix); } catch (NumberFormatException ex) { return null; } } else if (type.equals(Boolean.TYPE) || type == Boolean.class) { return Boolean.valueOf(value); } else if (type == File.class) { return new File(String.valueOf(value)); } else if (type.isEnum()) { value = value.replaceAll("[\\.\\s\\-]", ""); return findClosestEnumConstant(value, type); } return null; } public static Object getProperty(Object object, String path) { String[] paths = path.split("\\."); for (String path1 : paths) { object = findChild(object, path1); if (object == null) { return null; } } return object; } public static Object setProperty(Object object, String path, String value, boolean hex) { String[] paths = path.split("\\."); for (int i = 0; i < paths.length - 1; i++) { object = findChild(object, paths[i]); if (object == null) { return null; } } return setChild(object, paths[paths.length - 1], value, hex); } }