forked from Apple-2-Tools/jace
552 lines
21 KiB
Java
552 lines
21 KiB
Java
/*
|
|
* 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<Class> findClasses(String pckgname, Class clazz) {
|
|
Set<Class> 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<Class> findClassesInJar(URL jarLocation, Class clazz) {
|
|
Set<Class> output = new HashSet<>();
|
|
JarFile jarFile = null;
|
|
try {
|
|
JarURLConnection conn = (JarURLConnection) jarLocation.openConnection();
|
|
jarFile = conn.getJarFile();
|
|
Enumeration<JarEntry> 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<Class, Collection<Class>> classCache = new HashMap<>();
|
|
|
|
public static List<Class> findAllSubclasses(Class clazz) {
|
|
if (classCache.containsKey(clazz)) {
|
|
return (List<Class>) classCache.get(clazz);
|
|
}
|
|
TreeMap<String, Class> 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<Class> 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> {
|
|
|
|
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<String> search) {
|
|
if (search == null || search.isEmpty()) {
|
|
return null;
|
|
}
|
|
RankingComparator r = new RankingComparator(match);
|
|
List<String> 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<Class, Map<String, Object>> enumCache = new HashMap<>();
|
|
|
|
public static Object findClosestEnumConstant(String value, Class type) {
|
|
Map<String, Object> 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);
|
|
}
|
|
} |