com.google.gxp.base.dynamic.StubGxpTemplate.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gxp.base.dynamic.StubGxpTemplate.java

Source

/*
 * Copyright (C) 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.gxp.base.dynamic;

import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.CharStreams;
import com.google.common.io.ByteStreams;
import com.google.gxp.base.GxpTemplate;
import com.google.gxp.compiler.Compiler;
import com.google.gxp.compiler.Configuration;
import com.google.gxp.compiler.InvalidConfigException;
import com.google.gxp.compiler.alerts.AlertPolicy;
import com.google.gxp.compiler.alerts.AlertSet;
import com.google.gxp.compiler.fs.FileRef;
import com.google.gxp.compiler.fs.FileSystem;
import com.google.gxp.compiler.fs.InMemoryFileSystem;
import com.google.gxp.compiler.fs.JavaFileManagerImpl;
import com.google.gxp.compiler.fs.JavaFileRef;
import com.google.gxp.compiler.fs.SystemFileSystem;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Base class for all GxpTemplates that compile their source .gxp at runtime.
 */
public class StubGxpTemplate extends GxpTemplate {

    // system filesystem
    // can be changed for testing
    protected static FileSystem systemFS = SystemFileSystem.INSTANCE;

    public static void setSystemFileSystem(FileSystem systemFS) {
        StubGxpTemplate.systemFS = systemFS;
    }

    public static void setJavaCompiler(JavaCompiler javaCompiler) {
        StubGxpTemplate.javaCompiler = javaCompiler;
    }

    protected static FileRef parseFilename(String filename) {
        return systemFS.parseFilename(filename);
    }

    protected static Set<FileRef> parseFilenames(String... filenames) {
        Set<FileRef> fileRefs = Sets.newHashSet();
        for (String filename : filenames) {
            fileRefs.add(parseFilename(filename));
        }
        return fileRefs;
    }

    // java compiler
    // can be changed for testing
    private static JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();

    /**
     * Reconstruct an {@code AlertPolicy} that has been serialized to a byte array.
     */
    protected static AlertPolicy createAlertPolicy(byte[] bytes) {
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStream(bais);
            return (AlertPolicy) ois.readObject();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    protected static FileRef compileGxp(InMemoryFileSystem outFs, Set<FileRef> srcGxps, Set<FileRef> srcSchemas,
            Set<FileRef> srcPaths, String javaBase, long compilationVersion, AlertPolicy alertPolicy)
            throws GxpCompilationException {

        String javaFile = javaBase + compilationVersion + ".java";

        Configuration configuration = new RuntimeConfiguration(systemFS, outFs, srcGxps, srcSchemas, srcPaths,
                javaFile, compilationVersion, alertPolicy);
        try {
            // Perform GXP Compilation
            AlertSet alertSet = new Compiler(configuration).call();

            // check for gxp compilation errors
            if (alertSet.hasErrors(alertPolicy)) {
                throw new GxpCompilationException.Gxp(alertPolicy, alertSet);
            }
        } catch (InvalidConfigException e) {
            throw new GxpCompilationException.Throw(e);
        }

        return outFs.parseFilename(javaFile);
    }

    protected static Map<String, Method> compileJava(InMemoryFileSystem outFs, String classBase,
            final long compilationVersion) {
        // compile java
        DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>();

        JavaFileManager javaFileManager = new JavaFileManagerImpl(
                javaCompiler.getStandardFileManager(diagnosticCollector, Locale.US, Charsets.US_ASCII), outFs);
        String className = classBase + compilationVersion;

        try {
            JavaFileObject compilationUnit = javaFileManager.getJavaFileForInput(StandardLocation.SOURCE_PATH,
                    className, JavaFileObject.Kind.SOURCE);

            Iterable<JavaFileObject> compilationUnits = ImmutableList.of(compilationUnit);

            javaCompiler.getTask(null, javaFileManager, diagnosticCollector, null, null, compilationUnits).call();

            List<Diagnostic<? extends JavaFileObject>> diagnostics = filterErrors(
                    diagnosticCollector.getDiagnostics());

            if (!diagnostics.isEmpty()) {
                throw new GxpCompilationException.Java(diagnostics);
            }

            List<byte[]> classFiles = Lists.newArrayList();
            for (FileRef fileRef : outFs.getManifest()) {
                if (fileRef.getKind().equals(JavaFileObject.Kind.CLASS)) {
                    String outputClassName = javaFileManager.inferBinaryName(StandardLocation.CLASS_OUTPUT,
                            new JavaFileRef(fileRef));
                    if (outputClassName.equals(className) || outputClassName.startsWith(className + "$")) {
                        classFiles.add(ByteStreams.toByteArray(fileRef.openInputStream()));
                    }
                }
            }

            // A single java compile can generate many .class files due to inner classes, and it
            // is difficult to know what order to load them in to avoid NoClassDefFoundErrors,
            // so what we do is go through the whole list attempting to load them all, keeping
            // track of which ones file with NoClassDefFoundError.  Then we loop and try again.
            // This should eventually work no matter what order the files come in.
            //
            // We have an additional check to make sure that at least one file is loaded each
            // time through the loop to prevent infinite looping.
            //
            // I'm not entirely happy with this scheme, but it's the best I can come up with
            // for now.
            int oldCount, newCount;
            do {
                oldCount = classFiles.size();
                classFiles = defineClasses(classFiles);
                newCount = classFiles.size();
            } while (newCount != 0 && newCount != oldCount);

            // get the main class generated durring this compile
            Class<?> c = Class.forName(className);

            // get methods
            return getMethodMap(c);
        } catch (GxpCompilationException e) {
            throw e;
        } catch (Throwable e) {
            throw new GxpCompilationException.Throw(e);
        }
    }

    protected static Map<String, Method> getMethodMap(Class<?> c) {
        Map<String, Method> map = Maps.newHashMap();
        for (Method method : c.getMethods()) {
            map.put(method.getName(), method);
        }
        return map;
    }

    private static List<byte[]> defineClasses(List<byte[]> classFiles) throws Throwable {
        List<byte[]> failures = Lists.newArrayList();
        for (byte[] classFile : classFiles) {
            try {
                defineClass(classFile);
            } catch (NoClassDefFoundError e) {
                failures.add(classFile);
            }
        }
        return failures;
    }

    // TODO(harryh): Consider strategies for detecting and generating an error
    //               when, at runtime, two parameters of the same type but different
    //               names are swapped. Name mangling of the method name with a
    //               parameters name list might be a good way to do this.

    protected static Object exec(Map<String, Method> methods, String function, Object[] args) throws Throwable {
        try {
            return methods.get(function).invoke(null, args);
        } catch (InvocationTargetException e) {
            throw e.getCause();
        } catch (IllegalArgumentException e) {
            throw new GxpCompilationException.GxpParamChange(e);
        } catch (Exception e) {
            throw new GxpCompilationException.Throw(e);
        }
    }

    protected static <T> T execNoExceptions(Map<String, Method> methods, String function, Object[] args) {
        try {
            @SuppressWarnings("unchecked")
            T result = (T) exec(methods, function, args);
            return result;
        } catch (Error e) {
            throw e;
        } catch (RuntimeException e) {
            throw e;
        } catch (Throwable t) {
            throw new GxpCompilationException.Throw(t);
        }
    }

    /**
     * @return a filtered list of {@code Diagnostic}s that only contains
     * errors.
     */
    private static <T> List<Diagnostic<? extends T>> filterErrors(List<Diagnostic<? extends T>> diagnostics) {
        List<Diagnostic<? extends T>> newList = Lists.newArrayList();
        for (Diagnostic<? extends T> diagnostic : diagnostics) {
            if (diagnostic.getKind().equals(Diagnostic.Kind.ERROR)) {
                newList.add(diagnostic);
            }
        }
        return Collections.unmodifiableList(newList);
    }

    private static final Method DEFINE_CLASS = AccessController.doPrivileged(new PrivilegedAction<Method>() {
        public Method run() {
            try {
                Class<ClassLoader> loader = ClassLoader.class;
                Method m = loader.getDeclaredMethod("defineClass", new Class[] { String.class, byte[].class,
                        Integer.TYPE, Integer.TYPE, ProtectionDomain.class });
                m.setAccessible(true);
                return m;
            } catch (NoSuchMethodException e) {
                throw new RuntimeException();
            }
        }
    });

    private static final ProtectionDomain PROTECTION_DOMAIN = StubGxpTemplate.class.getProtectionDomain();

    /**
     * Define a class using the SystemClassLoader so that the class has access to
     * package private items in its java package.
     */
    private static Class<?> defineClass(byte[] classFile) throws Throwable {
        Object[] args = new Object[] { null, classFile, new Integer(0), new Integer(classFile.length),
                PROTECTION_DOMAIN };
        try {
            return (Class<?>) DEFINE_CLASS.invoke(ClassLoader.getSystemClassLoader(), args);
        } catch (InvocationTargetException e) {
            throw e.getCause();
        }
    }

    /**
     * The pattern for a line directive; 1->file 2->line 3->col.
     */
    private final static Pattern LINE_DIRECTIVE = Pattern.compile("^.* // (.*): L(\\d*), C(\\d*)$");

    /**
     * Examine each element of the stack trace that belongs to this throwable
     * looking for a filename that matches the source file for this template.
     * If we find a match, rewrite the filename and line number. The line number
     * is based on line # comments in the source file.
     */
    protected static void rewriteStackTraceElements(Throwable throwable, FileRef sourceFile) {
        try {
            if (sourceFile != null) {
                String sourceFileName = sourceFile.getName().substring(1).replace('/', '.');
                StackTraceElement[] stackTrace = throwable.getStackTrace();
                for (int i = 0; i < stackTrace.length; i++) {
                    if (sourceFileName.equals(stackTrace[i].getFileName())) {
                        // get source name
                        String[] parts = sourceFileName.split("[\\.\\$]");
                        String sourceName = parts[parts.length - 3] + ".gxp";

                        // get source line
                        String line = CharStreams.readLines(sourceFile.openReader(Charsets.UTF_8))
                                .get(stackTrace[i].getLineNumber() - 1);
                        Matcher m = LINE_DIRECTIVE.matcher(line);
                        int sourceLine = m.find() ? Integer.valueOf(m.group(2)) : -1;

                        // fix class name
                        String className = stackTrace[i].getClassName().split("\\$")[0];

                        stackTrace[i] = new StackTraceElement(className, stackTrace[i].getMethodName(), sourceName,
                                sourceLine);
                        throwable.setStackTrace(stackTrace);
                        return;
                    }
                }
            }
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}