All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.jaxx.compiler.tools.jaxxcapture.JAXXCapture Maven / Gradle / Ivy

The newest version!
/*
 * #%L
 * JAXX :: Compiler
 * %%
 * Copyright (C) 2008 - 2024 Code Lutin, Ultreia.io
 * %%
 * This program 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 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * #L%
 */

package org.nuiton.jaxx.compiler.tools.jaxxcapture;

import org.apache.commons.lang3.StringUtils;
import org.nuiton.jaxx.compiler.ClassMap;
import org.nuiton.jaxx.compiler.CompiledObject;
import org.nuiton.jaxx.compiler.JAXXCompiler;
import org.nuiton.jaxx.compiler.reflect.ClassDescriptor;
import org.nuiton.jaxx.compiler.reflect.ClassDescriptorHelper;
import org.nuiton.jaxx.compiler.tools.jaxxcapture.handlers.JTabbedPaneHandler;
import org.nuiton.jaxx.compiler.tools.jaxxcapture.handlers.ObjectHandler;
import org.nuiton.jaxx.compiler.tools.jaxxcapture.handlers.TableHandler;
import org.nuiton.jaxx.compiler.types.TypeManager;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JTabbedPane;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.MouseEvent;
import java.beans.XMLEncoder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

public class JAXXCapture {

    private static final ClassMap objectHandlers = new ClassMap<>();

    static {
        //TODO make a serviceLoader mecanism to allow inter-module loading
        objectHandlers.put(ClassDescriptorHelper.getClassDescriptor(Object.class), new ObjectHandler());
        objectHandlers.put(ClassDescriptorHelper.getClassDescriptor(JTabbedPane.class), new JTabbedPaneHandler());
        try {
            objectHandlers.put(ClassDescriptorHelper.getClassDescriptor("org.nuiton.jaxx.runtime.swing.Table"), new TableHandler());
        } catch (ClassNotFoundException e) {
            System.err.println(e);
        }
    }

    private final Map sourceObjects = new HashMap<>();

    private final Map capturedObjects = new HashMap<>();

    private ClassLoader classLoader;

    private int count;

    private static class CaptureEventQueue extends EventQueue {

        private final ClassLoader classLoader;

        private CaptureEventQueue(ClassLoader classLoader) {
            this.classLoader = classLoader;
        }

        @Override
        public void dispatchEvent(AWTEvent event) {
            if (event.getID() == MouseEvent.MOUSE_PRESSED && ((MouseEvent) event).isControlDown()) {
                Component target = ((MouseEvent) event).getComponent();
                if (!(target instanceof Window)) {
                    target = SwingUtilities.getWindowAncestor(target);
                }
                if (target instanceof JFrame) {
                    target = ((JFrame) target).getContentPane();
                } else if (target instanceof JDialog) {
                    target = ((JDialog) target).getContentPane();
                }
                if (target instanceof JWindow) {
                    target = ((JWindow) target).getContentPane();
                }
                if (target != null) {
                    Thread.currentThread().setContextClassLoader(classLoader);
                    JAXXCapture capture = new JAXXCapture(classLoader);
                    capture.applyNames(target);
                    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                    XMLEncoder encoder = new XMLEncoder(buffer);
                    encoder.writeObject(target);
                    encoder.close();
                    try {
                        System.err.println(new String(buffer.toByteArray()));
                        System.out.println(capture.convertToJAXX(new ByteArrayInputStream(buffer.toByteArray())));
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            super.dispatchEvent(event);
        }
    }

    private JAXXCapture(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    public ClassLoader getClassLoader() {
        return classLoader;
    }

    public Map getCapturedObjects() {
        return capturedObjects;
    }

    private void applyNames(Component target) {
        String name = target.getName();
        if (name == null || sourceObjects.containsKey(name) || !CompiledObject.isValidID(name)) {
            do {
                name = "Object" + ++count;
            } while (sourceObjects.containsKey(name));
        }
        target.setName(name);
        assert !sourceObjects.containsKey(name) : "ID " + name + " is already registered";
        sourceObjects.put(name, target);

        if (target instanceof Container) {
            Container container = (Container) target;
            for (int i = 0; i < container.getComponentCount(); i++) {
                applyNames(container.getComponent(i));
            }
        }
    }

    public static String getText(Element tag) { // NOT a safe general-purpose implementation!
        return ((Text) tag.getChildNodes().item(0)).getData();
    }

    private String getArgumentsCode(ContextNode[] arguments) {
        StringBuilder result = new StringBuilder();
        result.append('(');
        for (int i = 0; i < arguments.length; i++) {
            if (i != 0) {
                result.append(", ");
            }
            result.append(getJavaCode(arguments[i]));
        }
        result.append(')');
        return result.toString();
    }

    public String getJavaCode(ContextNode node) {
        StringBuilder result = new StringBuilder();
        if (node instanceof PropertyNode) {
            ContextNode[] arguments = node.getArguments();
            result.append(arguments.length == 0 ? "get" : "set");
            result.append(StringUtils.capitalize(((PropertyNode) node).getProperty()));
            result.append(getArgumentsCode(arguments));
        } else if (node instanceof MethodNode) {
            result.append((((MethodNode) node).getMethodName()));
            result.append(getArgumentsCode(node.getArguments()));
        } else if (node instanceof CapturedObject) {
            CapturedObject object = (CapturedObject) node;
            if (object.isInlineable()) {
                result.append("new ");
                result.append(object.getClassName());
                result.append(getArgumentsCode(node.getArguments()));
            } else {
                String id = object.getProperty("id");
                assert id != null;
                result.append(id);
            }
        } else if (node instanceof ValueNode) {
            result.append(TypeManager.getJavaCode(((ValueNode) node).getValue()));
        } else if (node instanceof LiteralNode) {
            result.append(((LiteralNode) node).getJavaCode());
        } else {
            throw new IllegalArgumentException("unrecognized node type: " + node);
        }
        return result.toString();
    }

    // returns the best matching method for the specified argument types
    private static Method getMethod(Class target, String methodName, Class[] arguments) {
        try {
            // use the package-private class java.beans.ReflectionUtils to resolve the method.  This isn't 100% safe, but it's better than
            // having to rewrite the resolution myself.
            Class reflectionUtils = Class.forName("java.beans.ReflectionUtils");
            Method getMethod = reflectionUtils.getDeclaredMethod("getMethod", Class.class, String.class, Class[].class);
            getMethod.setAccessible(true);
            return (Method) getMethod.invoke(null, target, methodName, arguments);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // returns the best matching constructor for the specified argument types
    private static Constructor getConstructor(Class target, Class[] arguments) {
        try {
            // use the package-private class java.beans.ReflectionUtils to resolve the constructor.  This isn't 100% safe, but it's better than
            // having to rewrite the resolution myself.
            Class reflectionUtils = Class.forName("java.beans.ReflectionUtils");
            Method getConstructor = reflectionUtils.getDeclaredMethod("getConstructor", Class.class, Class[].class);
            getConstructor.setAccessible(true);
            return (Constructor) getConstructor.invoke(null, target, arguments);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Object createInstance(CapturedObject object) {
        try {
            ContextNode[] argumentNodes = object.getArguments();
            Object[] arguments = new Object[argumentNodes.length];
            Class[] argumentTypes = new Class[argumentNodes.length];
            for (int j = 0; j < argumentNodes.length; j++) {
                if (argumentNodes[j] instanceof ValueNode) {
                    arguments[j] = ((ValueNode) argumentNodes[j]).getValue();
                    argumentTypes[j] = arguments[j] != null ? arguments[j].getClass() : null;
                } else if (argumentNodes[j] instanceof CapturedObject) {
                    arguments[j] = createInstance((CapturedObject) argumentNodes[j]);
                    argumentTypes[j] = arguments[j] != null ? arguments[j].getClass() : null;
                }
            }
            Constructor constructor = getConstructor(Class.forName(object.getClassName(), true, classLoader), argumentTypes);
            return constructor.newInstance(arguments);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public String getJavaCode(Stack/**/ context) {
        CapturedObject contextCapturedObject = (CapturedObject) context.get(0);
        StringBuilder result = new StringBuilder();
        int start = 1;
        for (int i = context.size() - 1; i > 1; i--) {
            if (context.get(i) instanceof CapturedObject) {
                start = i;
                contextCapturedObject = (CapturedObject) context.get(i);
                break;
            }
        }
        Object contextObject = sourceObjects.get(contextCapturedObject.getProperty("id"));
        Class contextClass = contextObject != null ? contextObject.getClass() : null;

        for (int i = start; i < context.size(); i++) {
            ContextNode node = (ContextNode) context.get(i);
            if (contextObject != null && (node instanceof MethodNode || node instanceof PropertyNode)) {
                // need to follow the call chain so we can insert typecasts as necessary
                try {
                    String methodName;
                    ContextNode[] argumentNodes = node.getArguments();
                    if (node instanceof MethodNode) {
                        methodName = ((MethodNode) node).getMethodName();
                    } else {
                        methodName = (argumentNodes.length == 0 ? "get" : "set") + StringUtils.capitalize(((PropertyNode) node).getProperty());
                    }
                    Object[] arguments = new Object[argumentNodes.length];
                    Class[] argumentTypes = new Class[argumentNodes.length];
                    for (int j = 0; j < argumentNodes.length; j++) {
                        if (argumentNodes[j] instanceof ValueNode) {
                            arguments[j] = ((ValueNode) argumentNodes[j]).getValue();
                            argumentTypes[j] = arguments[j] != null ? arguments[j].getClass() : null;
                        } else if (argumentNodes[j] instanceof CapturedObject) {
                            arguments[j] = createInstance((CapturedObject) argumentNodes[j]);
                            argumentTypes[j] = arguments[j].getClass();
                        } else if (argumentNodes[j] instanceof LiteralNode) {
                            arguments[j] = ((LiteralNode) argumentNodes[j]).getValue();
                            argumentTypes[j] = arguments[j].getClass();
                        } else {
                            throw new IllegalArgumentException("unsupported argument type: " + argumentNodes[j]);
                        }
                    }

                    Method method = getMethod(contextClass, methodName, argumentTypes);
                    if (method == null) {
                        // could not find method in contextClass, must be defined in a subclass -- insert a typecast
                        result.insert(0, "((" + getOutputName(contextObject.getClass()) + ") ");
                        result.append(')');
                        method = getMethod(contextObject.getClass(), methodName, argumentTypes);
                    }
                    if (method == null) {
                        throw new RuntimeException("could not find method " + methodName + Arrays.asList(argumentTypes) + " in " + contextObject.getClass() + " (context: " + context + ")");
                    }
                    contextObject = method.invoke(contextObject, arguments);
                    contextClass = method.getReturnType();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }

            if (i > start) {
                result.append('.');
            }

            result.append(getJavaCode(node));
        }
        return result + ";";
    }

    private String getOutputName(Class c) {
        return c.getName();
    }

    public CapturedObject processObject(Element objectTag, Stack context) {
        String className = objectTag.getAttribute("class");
        ObjectHandler handler;
        if (className.length() > 0) {
            try {
                ClassDescriptor descriptor = ClassDescriptorHelper.getClassDescriptor(className, classLoader);
                handler = (ObjectHandler) objectHandlers.get(descriptor);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        } else {
            handler = (ObjectHandler) objectHandlers.get(ClassDescriptorHelper.getClassDescriptor(Object.class));
        }

        return handler.processObject(objectTag, context, this);
    }

    private synchronized String convertToJAXX(InputStream beansXML) throws IOException {
        try {
            Document document = JAXXCompiler.parseDocument(beansXML);
            Element rootElement = document.getDocumentElement();
            NodeList nodes = rootElement.getChildNodes();
            Stack context = new Stack<>();
            for (int i = 0; i < nodes.getLength(); i++) {
                Node child = nodes.item(i);
                if (child.getNodeType() == Node.ELEMENT_NODE) {
                    Element element = (Element) child;
                    if (!element.getTagName().equals("object")) {
                        throw new Error("expected tag 'object', found '" + element.getTagName() + "'");
                    }
                    CapturedObject root = processObject(element, context);
                    for (CapturedObject object : capturedObjects.values()) { // add all orphan objects to the root, so any non-inlineable ones have their XML created
                        if (object.getParent() == null && object != root) {
                            root.addChild(object, null);
                        }
                    }
                    return root.getXML(this);
                }
            }
            return null;
        } catch (SAXException e) {
            throw new RuntimeException(e);
        } finally {
            reset();
        }
    }

    private void reset() {
        sourceObjects.clear();
        capturedObjects.clear();
        count = 0;
    }

    public static void main(String[] arg) throws Exception {
        File file = new File(arg[0]);
        JarFile jarFile = new JarFile(file);
        ClassLoader classLoader = new URLClassLoader(new URL[]{file.toURI().toURL()});
        Thread.currentThread().setContextClassLoader(classLoader);
        EventQueue systemQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
        systemQueue.push(new CaptureEventQueue(classLoader));
        Manifest mf = jarFile.getManifest();
        String mainClassName = mf.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
        Class mainClass = Class.forName(mainClassName, true, classLoader);
        Method main = mainClass.getMethod("main", String[].class);
        main.invoke(null, new Object[]{new String[0]});
    }
}