diff --git a/pom.xml b/pom.xml index 6d5072d..c4cf3eb 100644 --- a/pom.xml +++ b/pom.xml @@ -73,11 +73,6 @@ scala-library 2.12.6 - - xmlunit - xmlunit - 1.6 - edu.illinois.cs testrunner-running @@ -98,6 +93,16 @@ junit-platform-launcher 1.1.0 + + com.thoughtworks.xstream + xstream + 1.4.19 + + + org.mockito + mockito-core + 3.11.2 + diff --git a/testrunner-running/MANIFEST.MF b/testrunner-running/MANIFEST.MF new file mode 100644 index 0000000..135fece --- /dev/null +++ b/testrunner-running/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Premain-Class: edu.illinois.cs.statecapture.agent.MainAgent diff --git a/testrunner-running/pom.xml b/testrunner-running/pom.xml index 3c92d87..1ee6e05 100644 --- a/testrunner-running/pom.xml +++ b/testrunner-running/pom.xml @@ -37,14 +37,18 @@ org.scala-lang scala-library - - xmlunit - xmlunit - org.junit.platform junit-platform-launcher + + com.thoughtworks.xstream + xstream + + + org.mockito + mockito-core + @@ -120,6 +124,16 @@ + + maven-jar-plugin + 2.4 + + + false + MANIFEST.MF + + + diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/statecapture/IStateCapture.java b/testrunner-running/src/main/scala/edu/illinois/cs/statecapture/IStateCapture.java new file mode 100644 index 0000000..3897ff2 --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/statecapture/IStateCapture.java @@ -0,0 +1,6 @@ +package edu.illinois.cs.statecapture; + +public interface IStateCapture { + + public void capture(); +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/statecapture/StateCapture.java b/testrunner-running/src/main/scala/edu/illinois/cs/statecapture/StateCapture.java new file mode 100644 index 0000000..f45ef8e --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/statecapture/StateCapture.java @@ -0,0 +1,489 @@ +package edu.illinois.cs.statecapture; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.reflection.FieldDictionary; +import com.thoughtworks.xstream.converters.reflection.FieldKey; +import com.thoughtworks.xstream.converters.reflection.FieldKeySorter; +import com.thoughtworks.xstream.core.JVM; +import com.thoughtworks.xstream.io.xml.DomDriver; +import com.thoughtworks.xstream.mapper.Mapper; +import com.thoughtworks.xstream.security.AnyTypePermission; + +import edu.illinois.cs.statecapture.agent.MainAgent; +import edu.illinois.cs.testrunner.configuration.Configuration; +import edu.illinois.cs.xstream.CustomElementIgnoringMapper; +import edu.illinois.cs.xstream.EnumMapConverter; +import edu.illinois.cs.xstream.LambdaConverter; +import edu.illinois.cs.xstream.LookAndFeelConverter; +import edu.illinois.cs.xstream.MapConverter; +import edu.illinois.cs.xstream.ReflectionConverter; +import edu.illinois.cs.xstream.SerializableConverter; +import edu.illinois.cs.xstream.TreeMapConverter; +import edu.illinois.cs.xstream.UnmarshalChain; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; + +import java.lang.Object; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.mockito.Mockito; + +import static com.thoughtworks.xstream.XStream.PRIORITY_LOW; +import static com.thoughtworks.xstream.XStream.PRIORITY_NORMAL; +import static com.thoughtworks.xstream.XStream.PRIORITY_VERY_LOW; + +public class StateCapture implements IStateCapture { + + protected final String testName; + private boolean dirty; + + // The State, field name of static root to object pointed to + private static final LinkedHashMap nameToInstance = new LinkedHashMap(); + + //for reflection and deserialization + private String xmlDir; + private String rootFile; + private String reflectionFile; + + public StateCapture(String testName) { + this.testName = testName; + setup(); + } + + static Set readFileContentsAsSet(String path) { + File file = new File(path); + Set keys = new HashSet<>(); + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + String line; + while ((line = br.readLine()) != null) { + keys.add(line); + } + } catch (FileNotFoundException fnfe) { + fnfe.printStackTrace(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + return keys; + } + + private void setup() { + xmlDir = Configuration.config().getProperty("statecapture.xmlDir"); + rootFile = Configuration.config().getProperty("statecapture.rootFile"); + reflectionFile = Configuration.config().getProperty("statecapture.reflectionFile"); + } + + public void load(String fieldName) throws IOException { + if (xmlDir.isEmpty() || reflectionFile.isEmpty()) { + System.out.println("WARNING: The subxml folder or reflection file are not provided, thus it will not do loading."); + return; + } + try { + String path0 = xmlDir + File.separator + fieldName + ".xml"; + String state0 = readFile(path0); + String className = fieldName.substring(0, fieldName.lastIndexOf(".")); + String subFieldName = fieldName.substring(fieldName.lastIndexOf(".") + 1, fieldName.length()); + + Object obj; + XStream xstream = getXStreamInstance(); + + try { + Class c = Class.forName(className); + Field[] fieldList = c.getDeclaredFields(); + for (int i = 0; i < fieldList.length; i++) { + if (!fieldList[i].getName().equals(subFieldName)) { + continue; + } + try { + fieldList[i].setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(fieldList[i], fieldList[i].getModifiers() & ~Modifier.FINAL); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + String outputPrivateError = fieldName + " reflectionError: " + e + "\n"; + Files.write(Paths.get(reflectionFile), outputPrivateError.getBytes(), + StandardOpenOption.APPEND); + } + try { + obj = fieldList[i].get(null); + boolean threadLocal = false; + if (obj instanceof ThreadLocal) { + threadLocal = true; + } + try { + // directly invoke reset when dealing with a Mockito mock object + if (Mockito.mockingDetails(obj).isMock()) { + Method m = Mockito.class.getDeclaredMethod("reset", Object[].class); + m.invoke(null, new Object[]{new Object[]{obj}}); + } else { + UnmarshalChain.reset(); + UnmarshalChain.initializeChain(fieldList[i].getDeclaringClass().getName(), fieldList[i].getName()); + obj = xstream.fromXML(state0); + } + } catch (NoSuchMethodError nsme) { // In case the Mockito reset does not apply, still try to load from XML + UnmarshalChain.reset(); + UnmarshalChain.initializeChain(fieldList[i].getDeclaringClass().getName(), fieldList[i].getName()); + obj = xstream.fromXML(state0); + } + if (obj == null) { + System.out.println("Unable to construct the relevant object during load"); + return; + } + if (AccessibleObject.class.isAssignableFrom(obj.getClass())) { + ((AccessibleObject) obj).setAccessible(true); + } + // the purpose is to serialize/deserialize the object wrapped within a ThreadLocal. + if (threadLocal) { + Object tmp = fieldList[i].get(null); + ((ThreadLocal)tmp).set(obj); + obj = tmp; + } + + if (!Mockito.mockingDetails(obj).isMock()) { + FieldUtils.writeField(fieldList[i], (Object) null, obj, true); + } + + String output = fieldName + " set\n"; + Files.write(Paths.get(reflectionFile), output.getBytes(), + StandardOpenOption.APPEND); + } catch (IllegalArgumentException | IllegalAccessException | NoSuchMethodException + | SecurityException | InvocationTargetException e) { + e.printStackTrace(); + String outputNormalError = fieldName + " reflectionError: " + e + "\n"; + Files.write(Paths.get(reflectionFile), outputNormalError.getBytes(), + StandardOpenOption.APPEND); + } + } + } catch (ClassNotFoundException | SecurityException e) { + e.printStackTrace(); + String output = fieldName + " deserializeError: " + e + "\n"; + Files.write(Paths.get(reflectionFile), output.getBytes(), + StandardOpenOption.APPEND); + } + } catch (IOException e) { + e.printStackTrace(); + String output = fieldName + " deserializeError: " + e + "\n"; + Files.write(Paths.get(reflectionFile), output.getBytes(), + StandardOpenOption.APPEND); + } + } + + @Override + public void capture() { + try { + if (xmlDir.isEmpty() || rootFile.isEmpty()) { + System.out.println("WARNING: The xml directory or root file are not provided, thus it will not do capturing."); + return; + } + String eagerload = Configuration.config().getProperty("statecapture.eagerload", ""); + if (!eagerload.equals("true")) { + capture_real(); + } else { + capture_class(); + } + } + catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Adds the current serialized reachable state to the currentTestStates list + * and the current roots to the currentRoots list. + * @throws IOException + */ + private void capture_real() throws IOException { + PrintWriter writer; + + createXmlDir(); + + Set allFieldName = new HashSet(); + Class[] loadedClasses = MainAgent.getInstrumentation().getAllLoadedClasses(); + File eagerLoadFile = new File(Configuration.config().getProperty("statecapture.eagerloadfile", "")); + if (eagerLoadFile.exists()) { + List loadedClassesList = new ArrayList(Arrays.asList(loadedClasses)); + Set eagerLoadedClasses = readFileContentsAsSet(eagerLoadFile.toPath().toString()); + for (String clz : eagerLoadedClasses) { + try { + Class tmp = Class.forName(clz); + loadedClassesList.add(tmp); + } catch (ClassNotFoundException cnfe) { + cnfe.printStackTrace(); + continue; + } catch (NoClassDefFoundError ncdfe) { + ncdfe.printStackTrace(); + continue; + } + } + Class[] arrayClasses = new Class[loadedClassesList.size()]; + loadedClassesList.toArray(arrayClasses); + loadedClasses = arrayClasses; + } + + for (Class c : loadedClasses) { + // Ignore classes in standard java to get top-level + // TODO(gyori): make this read from file or config option + String clz = c.getName(); + if (!shouldCaptureClass(clz)) { + continue; + } + + Set allFields = new HashSet(); + try { + Field[] declaredFields = c.getDeclaredFields(); + Field[] fields = c.getFields(); + allFields.addAll(Arrays.asList(declaredFields)); + allFields.addAll(Arrays.asList(fields)); + } catch (NoClassDefFoundError e) { + e.printStackTrace(); + continue; + } + + for (Field f : allFields) { + String fieldName = getFieldFQN(f); + + // if a field is final and has a primitive type there's no point to capture it. + if (Modifier.isStatic(f.getModifiers()) + && !(Modifier.isFinal(f.getModifiers()) && f.getType().isPrimitive())) { + try { + allFieldName.add(fieldName); + f.setAccessible(true); + + Object instance; + try { + instance = f.get(null); + } catch (NoClassDefFoundError ncdfe) { + instance = null; + ncdfe.printStackTrace(); + } + // If it is actually of ThreadLocal type, we want the contents inside + if (instance instanceof ThreadLocal) { + instance = ((ThreadLocal)instance).get(); + } + dirty = false; + String obj4field = serializeObj(instance); + if (!dirty) { + nameToInstance.put(fieldName, instance); + writer = new PrintWriter(xmlDir + File.separator + fieldName + ".xml", "UTF-8"); + writer.println(obj4field); + writer.close(); + } + } catch (OutOfMemoryError ofme) { + ofme.printStackTrace(); + continue; + } catch (NoClassDefFoundError ncdfe) { + ncdfe.printStackTrace(); + continue; + } catch (IllegalAccessException iae) { + iae.printStackTrace(); + continue; + } + } + } + } + + String allFieldsFile = Configuration.config().getProperty("statecapture.allFieldsFile"); + if (allFieldsFile.isEmpty()) { + System.out.println("WARNING: The allFieldsFile file are not provided, thus it can not create a writer to " + + "write all fields to this field."); + return; + } + writer = new PrintWriter(allFieldsFile, "UTF-8"); + for (String ff : allFieldName) { + writer.println(ff); + } + writer.close(); + + writer = new PrintWriter(rootFile, "UTF-8"); + for (String key : nameToInstance.keySet()) { + writer.println(key); + } + writer.close(); + } + + /** + * Adds the current loadable classes to the current class list in eagerLoadingFields.txt file in the phase 2tmp. + * @throws IOException + */ + private void capture_class() throws IOException { + String eagerLoadFileName = Configuration.config().getProperty("statecapture.eagerloadfile", ""); + if (eagerLoadFileName.isEmpty()) { + System.out.println("Need to provide name of file to write what classes are loaded"); + return; + } + // get all loadable classes + Class[] loadedClasses = MainAgent.getInstrumentation().getAllLoadedClasses(); + Set classes = new HashSet<>(); + for (Class c : loadedClasses) { + if (shouldCaptureClass(c.getName())) { + classes.add(c.getName()); + } + } + + File eagerLoadFile = new File(eagerLoadFileName); + PrintWriter writer = new PrintWriter(eagerLoadFile, "UTF-8"); + for (String str : classes) { + writer.println(str); + } + writer.close(); + } + + private boolean shouldCaptureClass(String clz) { + if ((clz.contains("java.") && !clz.startsWith("java.lang.System")) + || (clz.contains("javax.") && !clz.startsWith("javax.cache.Caching")) + || clz.contains("javafx.") + || clz.contains("jdk.") + || clz.contains("scala.") + || clz.contains("sun.") + || clz.contains("edu.illinois.cs") + || clz.contains("org.custommonkey.xmlunit") + || clz.contains("org.junit") + || clz.contains("statecapture.com.")) { + return false; + } + return true; + } + + private void createXmlDir() { + File theDir = new File(xmlDir); + if (!theDir.exists()) { + theDir.mkdirs(); + } + } + + /** + * Takes in a string and removes problematic characters. + * + * @param in the input string to be filtered + * @return the input string with the unparsable characters removed + */ + private static String sanitizeXmlChars(String in) { + in = in.replaceAll("&#", "&#"); + StringBuilder out = new StringBuilder(); + char current; + + if (in == null || ("".equals(in))) + return ""; + for (int i = 0; i < in.length(); i++) { + current = in.charAt(i); + if ((current == 0x9) || + (current == 0xA) || + (current == 0xD) || + ((current >= 0x20) && (current <= 0xD7FF)) || + ((current >= 0xE000) && (current <= 0xFFFD)) || + ((current >= 0x10000) && (current <= 0x10FFFF))) + out.append(current); + } + return out.toString(); + } + + /** + * This is the method that calls XStream to serialize the object into a string. + * + * @param ob the object that need to be serialized + * @return string representing the serialized input object + */ + private String serializeObj(Object ob) { + XStream xstream = getXStreamInstance(); + String s = ""; + + try { + s = xstream.toXML(ob); + s = sanitizeXmlChars(s); + } catch (Exception e) { + // In case serialization fails, mark the StateCapture for this test + // as dirty, meaning it should be ignored + dirty = true; + // throw e; + } + return s; + } + + private static class AlphabeticalFieldkeySorter implements FieldKeySorter { + @Override + public Map sort(Class type, Map keyedByFieldKey) { + final Map map = new TreeMap<>(new Comparator() { + + @Override + public int compare(final FieldKey fieldKey1, final FieldKey fieldKey2) { + return fieldKey1.getFieldName().compareTo(fieldKey2.getFieldName()); + } + }); + map.putAll(keyedByFieldKey); + return map; + } + } + + private XStream getXStreamInstance() { + XStream xstream = new XStream(JVM.newReflectionProvider(new FieldDictionary( + new AlphabeticalFieldkeySorter())),new DomDriver()); + Mapper mapper = xstream.getMapper(); + mapper = new CustomElementIgnoringMapper(mapper); + XStream newXStream = new XStream(JVM.newReflectionProvider(new FieldDictionary( + new AlphabeticalFieldkeySorter())),new DomDriver(),xstream.getClassLoader(),mapper); + // Set fields to be omitted during serialization + + xstream = newXStream; + xstream.setMode(XStream.XPATH_ABSOLUTE_REFERENCES); + xstream.addPermission(AnyTypePermission.ANY); + xstream.omitField(Thread.class, "contextClassLoader"); + xstream.omitField(java.security.ProtectionDomain.class, "classloader"); + xstream.omitField(java.security.ProtectionDomain.class, "codesource"); + xstream.omitField(ClassLoader.class, "defaultDomain"); + xstream.omitField(ClassLoader.class, "classes"); + + xstream.omitField(java.lang.ref.SoftReference.class, "timestamp"); + xstream.omitField(java.lang.ref.SoftReference.class, "referent"); + xstream.omitField(java.lang.ref.Reference.class, "referent"); + + // Register all our custom converters that override the defaults, similar to how XStream registers its default converters + xstream.registerConverter(new MapConverter(xstream.getMapper()), PRIORITY_NORMAL + 1); + xstream.registerConverter(new TreeMapConverter(xstream.getMapper()), PRIORITY_NORMAL + 1); + xstream.registerConverter(new EnumMapConverter(xstream.getMapper()), PRIORITY_NORMAL + 1); + + xstream.registerConverter(new ReflectionConverter(xstream.getMapper(), xstream.getReflectionProvider()), PRIORITY_VERY_LOW + 1); + xstream.registerConverter(new SerializableConverter(xstream.getMapper(), xstream.getReflectionProvider(), xstream.getClassLoaderReference()), PRIORITY_LOW + 1); + xstream.registerConverter(new LambdaConverter(xstream.getMapper(), xstream.getReflectionProvider(), xstream.getClassLoaderReference()), PRIORITY_NORMAL + 1); + + if (JVM.isSwingAvailable()) { + xstream.registerConverter(new LookAndFeelConverter(xstream.getMapper(), xstream.getReflectionProvider()), PRIORITY_NORMAL + 1); + } + + return xstream; + } + + protected String getFieldFQN(Field f) { + String clz = f.getDeclaringClass().getName(); + String fld = f.getName(); + return clz + "." + fld; + } + + private String readFile(String path) throws IOException { + File file = new File(path); + return FileUtils.readFileToString(file, "UTF-8"); + } +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/statecapture/agent/MainAgent.java b/testrunner-running/src/main/scala/edu/illinois/cs/statecapture/agent/MainAgent.java new file mode 100644 index 0000000..e27edbd --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/statecapture/agent/MainAgent.java @@ -0,0 +1,13 @@ +package edu.illinois.cs.statecapture.agent; + +import java.lang.instrument.Instrumentation; + +public class MainAgent { + private static Instrumentation inst; + + public static Instrumentation getInstrumentation() { return inst; } + + public static void premain(String agentArgs, Instrumentation inst) { + MainAgent.inst = inst; + } +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/testrunner/execution/TestListener.java b/testrunner-running/src/main/scala/edu/illinois/cs/testrunner/execution/TestListener.java index 1e5b6b3..d2e94b2 100644 --- a/testrunner-running/src/main/scala/edu/illinois/cs/testrunner/execution/TestListener.java +++ b/testrunner-running/src/main/scala/edu/illinois/cs/testrunner/execution/TestListener.java @@ -1,5 +1,7 @@ package edu.illinois.cs.testrunner.execution; +import edu.illinois.cs.statecapture.StateCapture; +import edu.illinois.cs.testrunner.configuration.Configuration; import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunListener; @@ -35,7 +37,16 @@ public void testIgnored(Description description) throws Exception { @Override public void testStarted(Description description) throws Exception { - times.put(JUnitTestRunner.fullName(description), System.nanoTime()); + String fullTestName = JUnitTestRunner.fullName(description); + times.put(fullTestName, System.nanoTime()); + + String phase = Configuration.config().getProperty("statecapture.phase", ""); + if (Configuration.config().getProperty("statecapture.testname").equals(fullTestName)) { + if (phase.equals("capture_before")) { + StateCapture sc = new StateCapture(fullTestName); + sc.capture(); + } + } } @Override @@ -58,5 +69,23 @@ public void testFinished(Description description) throws Exception { } else { System.out.println("Test finished but did not start: " + fullTestName); } + + String phase = Configuration.config().getProperty("statecapture.phase", ""); + if (Configuration.config().getProperty("statecapture.testname").equals(fullTestName)) { + if (phase.equals("capture_after")) { + StateCapture sc = new StateCapture(fullTestName); + sc.capture(); + } + else if (phase.equals("load")) { + // load one field each time + String fieldName = Configuration.config().getProperty("statecapture.fieldName", ""); + + if (!fieldName.isEmpty()) { + StateCapture sc = new StateCapture(fullTestName); + String diffField = fieldName.split(",")[0]; + sc.load(diffField); + } + } + } } } diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/CustomElementIgnoringMapper.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/CustomElementIgnoringMapper.java new file mode 100644 index 0000000..3d19662 --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/CustomElementIgnoringMapper.java @@ -0,0 +1,65 @@ +package edu.illinois.cs.xstream; + +import com.thoughtworks.xstream.core.util.FastField; +import com.thoughtworks.xstream.mapper.ElementIgnoringMapper; +import com.thoughtworks.xstream.mapper.Mapper; + +import java.util.HashSet; +import java.util.Set; + +public class CustomElementIgnoringMapper extends ElementIgnoringMapper { + public CustomElementIgnoringMapper(Mapper mapper) { + super(mapper); + } + + private static Set ignoreList; + + private static Set getIgnoreList() { + if (ignoreList == null) { + ignoreList = new HashSet<>(); + + ignoreList.add("java.security.CodeSource"); + ignoreList.add("sun.nio.cs.UTF_8$Decoder"); + ignoreList.add("java.nio.charset.CharsetEncoder"); + ignoreList.add("java.nio.charset.CharsetDecoder"); + ignoreList.add("sun.nio.cs.StreamEncoder"); + ignoreList.add("java.util.zip.ZipCoder"); + ignoreList.add("com.sun.crypto.provider.SunJCE"); + ignoreList.add("java.lang.ClassLoader"); + ignoreList.add("java.security.SecureClassLoader"); + ignoreList.add("java.security.Provider"); + ignoreList.add("javax.security.auth.Subject"); + } + return ignoreList; + } + + { + getIgnoreList(); + } + @Override + public boolean shouldSerializeMember(final Class definedIn, final String fieldName) { + if (fieldsToOmit.contains(customKey(definedIn, fieldName))) { + return false; + } else if (definedIn == Object.class && isIgnoredElement(fieldName)) { + return false; + } + try { + // Hack to ignore field of type CodeSource + for(String ignoreItem : ignoreList) { + if (definedIn.getDeclaredField(fieldName).getType().equals(Class.forName(ignoreItem))) { + return false; + } + } + if (Class.forName("java.lang.ClassLoader").isAssignableFrom(definedIn.getDeclaredField(fieldName).getType())) { + return false; + } + } catch (Exception exception) { + // ignore + } + return super.shouldSerializeMember(definedIn, fieldName); + } + + private FastField customKey(final Class type, final String name) { + return new FastField(type, name); + } +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/EnumMapConverter.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/EnumMapConverter.java new file mode 100644 index 0000000..2c53ba4 --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/EnumMapConverter.java @@ -0,0 +1,29 @@ +package edu.illinois.cs.xstream; + +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.mapper.Mapper; + +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +public class EnumMapConverter extends com.thoughtworks.xstream.converters.enums.EnumMapConverter { + + public EnumMapConverter(Mapper mapper) { + super(mapper); + } + + @Override + protected void putCurrentEntryIntoMap(final HierarchicalStreamReader reader, final UnmarshallingContext context, + final Map map, final Map target) { + try { + MapConverterHelper.putCurrentEntryIntoMap(reader, context, map, target, this); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/LambdaConverter.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/LambdaConverter.java new file mode 100644 index 0000000..c91127e --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/LambdaConverter.java @@ -0,0 +1,22 @@ +package edu.illinois.cs.xstream; + +import java.lang.reflect.Field; + +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; +import com.thoughtworks.xstream.core.ClassLoaderReference; +import com.thoughtworks.xstream.mapper.Mapper; + + +public class LambdaConverter extends com.thoughtworks.xstream.converters.reflection.LambdaConverter { + + public LambdaConverter(final Mapper mapper, final ReflectionProvider reflectionProvider, final ClassLoaderReference classLoaderReference) { + super(mapper, reflectionProvider, classLoaderReference); + } + + @Override + protected Object unmarshallField(final UnmarshallingContext context, final Object result, final Class type, + final Field field) { + return ReflectionConverterHelper.unmarshallField(context, result, type, field, mapper); + } +} \ No newline at end of file diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/LookAndFeelConverter.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/LookAndFeelConverter.java new file mode 100644 index 0000000..bd4113b --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/LookAndFeelConverter.java @@ -0,0 +1,21 @@ +package edu.illinois.cs.xstream; + +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; +import com.thoughtworks.xstream.core.ClassLoaderReference; +import com.thoughtworks.xstream.mapper.Mapper; + +import java.lang.reflect.Field; + +public class LookAndFeelConverter extends com.thoughtworks.xstream.converters.extended.LookAndFeelConverter { + + public LookAndFeelConverter(final Mapper mapper, final ReflectionProvider reflectionProvider) { + super(mapper, reflectionProvider); + } + + @Override + protected Object unmarshallField(final UnmarshallingContext context, final Object result, final Class type, + final Field field) { + return ReflectionConverterHelper.unmarshallField(context, result, type, field, mapper); + } +} \ No newline at end of file diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/MapConverter.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/MapConverter.java new file mode 100644 index 0000000..b631e8e --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/MapConverter.java @@ -0,0 +1,62 @@ +package edu.illinois.cs.xstream; + +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import com.thoughtworks.xstream.mapper.Mapper; + +import java.lang.reflect.InvocationTargetException; +import java.util.Iterator; +import java.util.Map; + +public class MapConverter extends com.thoughtworks.xstream.converters.collections.MapConverter { + + public MapConverter(Mapper mapper) { + super(mapper); + } + + // Logic mostly copied from writeItem + protected void writeItemWithName(String name, Object item, MarshallingContext context, HierarchicalStreamWriter writer) { + if (item == null) { + writeNullItem(context, writer); + } else { + String clazz = mapper().serializedClass(item.getClass()); + ExtendedHierarchicalStreamWriterHelper.startNode(writer, name, item.getClass()); + writer.addAttribute("class", clazz); // Map the class as an attribute to the node + writeBareItem(item, context, writer); + writer.endNode(); + } + } + + @Override + public void marshal(final Object source, final HierarchicalStreamWriter writer, final MarshallingContext context) { + Map map = (Map) source; + String entryName = mapper().serializedClass(Map.Entry.class); + for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) { + Map.Entry entry = (Map.Entry) iterator.next(); + ExtendedHierarchicalStreamWriterHelper.startNode(writer, entryName, entry.getClass()); + + // Give consistent name to elements (their types will be encoded as attribute) + writeItemWithName("key", entry.getKey(), context, writer); + writeItemWithName("value", entry.getValue(), context, writer); + + writer.endNode(); + } + } + + @Override + protected void putCurrentEntryIntoMap(final HierarchicalStreamReader reader, final UnmarshallingContext context, + final Map map, final Map target) { + try { + MapConverterHelper.putCurrentEntryIntoMap(reader, context, map, target, this); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/MapConverterHelper.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/MapConverterHelper.java new file mode 100644 index 0000000..9da86ac --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/MapConverterHelper.java @@ -0,0 +1,63 @@ +package edu.illinois.cs.xstream; + +import com.thoughtworks.xstream.converters.ConversionException; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.collections.MapConverter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import com.thoughtworks.xstream.mapper.Mapper; +import edu.illinois.cs.xstream.UnmarshalChain; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.Map; + +public class MapConverterHelper { + + private static Method findMethod(String methodName, Class clz) { + if (clz == null) { + return null; // Should never happen, must have found this method + } + for (Method meth : clz.getDeclaredMethods()) { + if (meth.getName().equals(methodName)) { + return meth; + } + } + return findMethod(methodName, clz.getSuperclass()); + } + + protected static void putCurrentEntryIntoMap(final HierarchicalStreamReader reader, final UnmarshallingContext context, + final Map map, final Map target, MapConverter converter) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method readCompleteItem = findMethod("readCompleteItem", converter.getClass()); + readCompleteItem.setAccessible(true); + final Object key = readCompleteItem.invoke(converter, reader, context, map); + UnmarshalChain.pushNode(UnmarshalChain.makeUnmarshalMapEntryNode(key)); // Try getting the key and putting it in the chain to map to the value + Object value = null; + String nodeName = reader.getNodeName(); + try { + value = readCompleteItem.invoke(converter, reader, context, map); + } catch (ConversionException ce) { + // If there is a problem getting the value from this map entry, use the value in the current heap + try { + value = UnmarshalChain.getCurrObject(); + } catch (UnmarshalChain.MapEntryMissingException e) { // If map entry is simply missing in current heap, then return, don't update map + return; + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + throw new ConversionException(e); + } + } finally { + // Make sure level moves up to the proper location after this kind of exception + while (!reader.getNodeName().equals(nodeName)) { + reader.moveUp(); + } + UnmarshalChain.popNode(); + } + @SuppressWarnings("unchecked") + final Map targetMap = (Map)target; + targetMap.put(key, value); + } +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/ReflectionConverter.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/ReflectionConverter.java new file mode 100644 index 0000000..dd34c6d --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/ReflectionConverter.java @@ -0,0 +1,22 @@ +package edu.illinois.cs.xstream; + +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; +import com.thoughtworks.xstream.mapper.Mapper; + +import java.lang.reflect.Field; + +public class ReflectionConverter extends com.thoughtworks.xstream.converters.reflection.ReflectionConverter { + + public ReflectionConverter(final Mapper mapper, final ReflectionProvider reflectionProvider) { + super(mapper, reflectionProvider); + } + + @Override + protected Object unmarshallField(final UnmarshallingContext context, final Object result, final Class type, + final Field field) { + return ReflectionConverterHelper.unmarshallField(context, result, type, field, mapper); + } + + +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/ReflectionConverterHelper.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/ReflectionConverterHelper.java new file mode 100644 index 0000000..1358e15 --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/ReflectionConverterHelper.java @@ -0,0 +1,30 @@ +package edu.illinois.cs.xstream; + +import com.thoughtworks.xstream.converters.ConversionException; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.mapper.Mapper; + +import java.lang.reflect.Field; + +public abstract class ReflectionConverterHelper { + + public static Object unmarshallField(final UnmarshallingContext context, final Object result, final Class type, + final Field field, final Mapper mapper) { + // Assume properties define the root node and should be initialized as such if not yet + UnmarshalChain.pushNode(UnmarshalChain.makeUnmarshalFieldNode(field.getDeclaringClass().getName(), field.getName())); + try { + return context.convertAnother(result, type, mapper.getLocalConverter(field.getDeclaringClass(), field + .getName())); + } catch (ConversionException ce) { + ce.printStackTrace(); + try { + return UnmarshalChain.getCurrObject(); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + throw new ConversionException(e); + } + } finally { + UnmarshalChain.popNode(); + } + } +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/SerializableConverter.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/SerializableConverter.java new file mode 100644 index 0000000..d4fdcbf --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/SerializableConverter.java @@ -0,0 +1,22 @@ +package edu.illinois.cs.xstream; + +import java.lang.reflect.Field; + +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; +import com.thoughtworks.xstream.core.ClassLoaderReference; +import com.thoughtworks.xstream.mapper.Mapper; + + +public class SerializableConverter extends com.thoughtworks.xstream.converters.reflection.SerializableConverter { + + public SerializableConverter(final Mapper mapper, final ReflectionProvider reflectionProvider, final ClassLoaderReference classLoaderReference) { + super(mapper, reflectionProvider, classLoaderReference); + } + + @Override + protected Object unmarshallField(final UnmarshallingContext context, final Object result, final Class type, + final Field field) { + return ReflectionConverterHelper.unmarshallField(context, result, type, field, mapper); + } +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/TreeMapConverter.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/TreeMapConverter.java new file mode 100644 index 0000000..e52b156 --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/TreeMapConverter.java @@ -0,0 +1,29 @@ +package edu.illinois.cs.xstream; + +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.mapper.Mapper; + +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +public class TreeMapConverter extends com.thoughtworks.xstream.converters.collections.TreeMapConverter { + + public TreeMapConverter(Mapper mapper) { + super(mapper); + } + + @Override + protected void putCurrentEntryIntoMap(final HierarchicalStreamReader reader, final UnmarshallingContext context, + final Map map, final Map target) { + try { + MapConverterHelper.putCurrentEntryIntoMap(reader, context, map, target, this); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } +} diff --git a/testrunner-running/src/main/scala/edu/illinois/cs/xstream/UnmarshalChain.java b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/UnmarshalChain.java new file mode 100644 index 0000000..1bc5c66 --- /dev/null +++ b/testrunner-running/src/main/scala/edu/illinois/cs/xstream/UnmarshalChain.java @@ -0,0 +1,151 @@ +package edu.illinois.cs.xstream; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.LinkedList; +import java.util.Map; + +public class UnmarshalChain { + + private static LinkedList chain = new LinkedList<>(); + + public static boolean isInitialized() { + return !chain.isEmpty(); + } + + public static void reset() { + chain = new LinkedList<>(); + } + + // Initialize with the static root + public static void initializeChain(String className, String fieldName) { + chain = new LinkedList<>(); + chain.add(new UnmarshalStaticFieldNode(className, fieldName)); + } + + // Getting some string representation of the currently saved chain + public static String getChainToString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < chain.size() - 1; i++) { + sb.append(chain.get(i).toString()); + sb.append(" -> "); + } + sb.append(chain.getLast().toString()); + return sb.toString(); + } + + public static void pushNode(UnmarshalNode node) { + chain.add(node); + } + + // Only ever remove last node on "stack" + public static void popNode() { + chain.removeLast(); + } + + public static UnmarshalNode makeUnmarshalMapEntryNode(Object key) { + return new UnmarshalMapEntryNode(key); + } + + public static UnmarshalNode makeUnmarshalFieldNode(String className, String fieldName) { + return new UnmarshalFieldNode(className, fieldName); + } + + // Gets current object represented by the collected chain from the XML unmarshalling process + public static Object getCurrObject() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + Object curr = null; + for (UnmarshalNode node : chain) { + // Static field + if (node instanceof UnmarshalStaticFieldNode) { + UnmarshalStaticFieldNode staticFieldNode = (UnmarshalStaticFieldNode)node; + Class clz = Class.forName(staticFieldNode.className); + Field field = clz.getDeclaredField(staticFieldNode.fieldName); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.setAccessible(true); + if (!Modifier.isStatic(field.getModifiers())) { // Is not static, so problem!!! + throw new NoSuchFieldException("Somehow root is not static: " + staticFieldNode.className + ":" + staticFieldNode.fieldName); + } + curr = field.get(null); + // Instance field + } else if (node instanceof UnmarshalFieldNode) { + UnmarshalFieldNode fieldNode = (UnmarshalFieldNode)node; + Class clz = Class.forName(fieldNode.className); + Field field = clz.getDeclaredField(fieldNode.fieldName); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.setAccessible(true); + if (Modifier.isStatic(field.getModifiers())) { // Is static, so problem!!! + throw new NoSuchFieldException("Somehow current field is static: " + fieldNode.className + ":" + fieldNode.fieldName); + } + curr = field.get(curr); + // Map entry with key (assume current is a map) + } else if (node instanceof UnmarshalMapEntryNode) { + UnmarshalMapEntryNode mapEntryNode = (UnmarshalMapEntryNode)node; + try { + Map map = (Map)curr; + if (!map.containsKey(mapEntryNode.key)) { + throw new UnmarshalChain.MapEntryMissingException(); + } + curr = map.get(mapEntryNode.key); + } catch (Throwable t) { + throw t; + } + } + } + + return curr; + } + + public static class MapEntryMissingException extends RuntimeException { + } +} + +abstract class UnmarshalNode { +} + +class UnmarshalStaticFieldNode extends UnmarshalNode { + String className; + String fieldName; + + UnmarshalStaticFieldNode(String className, String fieldName) { + this.className = className; + this.fieldName = fieldName; + } + + @Override + public String toString() { + return "[" + this.className + "::" + this.fieldName + "]"; + } +} + +class UnmarshalFieldNode extends UnmarshalNode { + String className; + String fieldName; + + UnmarshalFieldNode(String className, String fieldName) { + this.className = className; + this.fieldName = fieldName; + } + + @Override + public String toString() { + return "[" + this.className + "::" + this.fieldName + "]"; + } +} + +class UnmarshalMapEntryNode extends UnmarshalNode { + Object key; + + UnmarshalMapEntryNode(Object key) { + this.key = key; + } + + @Override + public String toString() { + return "{key=" + this.key + "}"; + } +} +