org.apache.wave.pst.PstFileDescriptor.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wave.pst.PstFileDescriptor.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.wave.pst;

import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.io.CharStreams;
import com.google.common.io.Files;
import com.google.protobuf.Descriptors.FileDescriptor;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A loader for a {@link FileDescriptor}, accepting and handling a proto file
 * specified as:
 * <ul>
 * <li>A path to a .proto file.</li>
 * <li>A path to a .java (protoc-)compiled proto spec.</li>
 * <li>A path to a .class (javac-) compiled proto spec.</li>
 * <li>A proto spec that is already on the classpath.</li>
 * </ul>
 *
 * @author kalman@google.com (Benjamin Kalman)
 */
public final class PstFileDescriptor {

    private final FileDescriptor descriptor;

    /**
     * Loads the {@link FileDescriptor} from a path. The path may be a class name
     * (e.g. foo.Bar), path to a class (e.g. bin/foo/Bar.class), or a path to a
     * Java source file (e.g. src/foo/Bar.java).
     *
     * In each case it is the caller's responsibility to ensure that the classpath
     * of the Java runtime is correct.
     *
     * @param path the path to load the proto description from
     * @param intermediateJavaDir path to save the intermediate protoc-
     *        generated Java file (if any)
     * @param protoPath any additional path to pass to the protoc compiler
     */
    public static FileDescriptor load(String path, File intermediateJavaDir, File protoPath) {
        return new PstFileDescriptor(path, intermediateJavaDir, protoPath).get();
    }

    private PstFileDescriptor(String path, File intermediateJavaDir, File protoPath) {
        Class<?> clazz = null;
        if (path.endsWith(".class")) {
            clazz = fromPathToClass(path);
        } else if (path.endsWith(".java")) {
            clazz = fromPathToJava(path, intermediateJavaDir);
        } else if (path.endsWith(".proto")) {
            clazz = fromPathToProto(path, intermediateJavaDir, protoPath);
        } else {
            clazz = fromClassName(path);
        }

        if (clazz == null) {
            descriptor = null;
        } else {
            descriptor = asFileDescriptor(clazz);
        }
    }

    private FileDescriptor get() {
        return descriptor;
    }

    private Class<?> fromClassName(String className) {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

    private Class<?> fromPathToClass(String pathToClass) {
        String currentBaseDir = new File(pathToClass).isAbsolute() ? "" : ".";
        String currentPath = pathToClass;
        Class<?> clazz = null;
        while (clazz == null) {
            clazz = loadClassAtPath(currentBaseDir, currentPath);
            if (clazz == null) {
                int indexOfSep = currentPath.indexOf(File.separatorChar);
                if (indexOfSep == -1) {
                    break;
                } else {
                    currentBaseDir += File.separator + currentPath.substring(0, indexOfSep);
                    currentPath = currentPath.substring(indexOfSep + 1);
                }
            }
        }
        return clazz;
    }

    private Class<?> loadClassAtPath(String baseDir, String path) {
        try {
            ClassLoader classLoader = new URLClassLoader(new URL[] { new File(baseDir).toURI().toURL() });
            return classLoader.loadClass(getBinaryName(path));
        } catch (Throwable t) {
            return null;
        }
    }

    private String getBinaryName(String path) {
        return path.replace(File.separatorChar, '.').substring(0, path.length() - ".class".length());
    }

    private Class<?> fromPathToJava(String pathToJava, File intermediateJavaDir) {
        try {
            File dir = intermediateJavaDir;
            String[] javacCommand = new String[] { "javac", pathToJava, "-d", dir.getAbsolutePath(), "-verbose",
                    "-cp", determineClasspath(pathToJava) + ":" + determineSystemClasspath() };
            Process javac = Runtime.getRuntime().exec(javacCommand);
            consumeStdOut(javac);
            List<String> stdErr = readLines(javac.getErrorStream());
            int exitCode = javac.waitFor();
            if (exitCode != 0) {
                // Couldn't compile the file.
                System.err.printf("ERROR: running \"%s\" failed (%s):", Joiner.on(' ').join(javacCommand),
                        exitCode);
                for (String line : stdErr) {
                    System.err.println(line);
                }
                return null;
            } else {
                // Compiled the file!  Now to determine where javac put it.
                Pattern pattern;
                if (Integer.parseInt(System.getProperty("java.version").split("\\.")[1]) <= 6) {
                    // JDK 6 or lower
                    pattern = Pattern.compile("\\[wrote ([^\\]]*)\\]");
                } else {
                    // JDK 7 or higher
                    pattern = Pattern.compile("\\[wrote RegularFileObject\\[([^\\]]*)\\]\\]");
                }
                String pathToClass = null;
                for (String line : stdErr) {
                    Matcher lineMatcher = pattern.matcher(line);
                    if (lineMatcher.matches()) {
                        pathToClass = lineMatcher.group(1);
                        // NOTE: don't break, as the correct path is the last one matched.
                    }
                }
                if (pathToClass != null) {
                    return fromPathToClass(pathToClass);
                } else {
                    System.err.println("WARNING: couldn't find javac output from javac " + pathToJava);
                    return null;
                }
            }
        } catch (Exception e) {
            System.err.println("WARNING: exception while processing " + pathToJava + ": " + e.getMessage());
            return null;
        }
    }

    /**
     * Fires off a background thread to consume anything written to a process'
     * standard output. Without running this, a process that outputs too much data
     * will block.
     */
    private void consumeStdOut(Process p) {
        final InputStream o = p.getInputStream();
        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    while (o.read() != -1) {
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        };
        t.setDaemon(true);
        t.start();
    }

    private String determineClasspath(String pathToJava) {
        // Try to determine the classpath component of a path by looking at the
        // path components.
        StringBuilder classpath = new StringBuilder();
        if (new File(pathToJava).isAbsolute()) {
            classpath.append(File.separator);
        }

        // This is just silly, but it will get by for now.
        for (String component : pathToJava.split(File.separator)) {
            if (component.equals("org") || component.equals("com") || component.equals("au")) {
                return classpath.toString();
            } else {
                classpath.append(component + File.separator);
            }
        }

        System.err.println("WARNING: couldn't determine classpath for " + pathToJava);
        return ".";
    }

    private String determineSystemClasspath() {
        StringBuilder s = new StringBuilder();
        boolean needsColon = false;
        for (URL url : ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs()) {
            if (needsColon) {
                s.append(':');
            }
            s.append(url.getPath());
            needsColon = true;
        }
        return s.toString();
    }

    private Class<?> fromPathToProto(String pathToProto, File intermediateJavaDir, File protoPath) {
        try {
            intermediateJavaDir.mkdirs();
            File proto = new File(pathToProto);
            String[] protocCommand = new String[] { "protoc", tryGetRelativePath(proto), "-I" + protoPath.getPath(),
                    "--java_out", intermediateJavaDir.getAbsolutePath() };
            Process protoc = Runtime.getRuntime().exec(protocCommand);
            // TODO(ben): configure timeout?
            killProcessAfter(10, TimeUnit.SECONDS, protoc);
            int exitCode = protoc.waitFor();
            if (exitCode != 0) {
                // Couldn't compile the file.
                System.err.printf("ERROR: running \"%s\" failed (%s):", Joiner.on(' ').join(protocCommand),
                        exitCode);
                for (String line : readLines(protoc.getErrorStream())) {
                    System.err.println(line);
                }
                return null;
            } else {
                final String javaFileName = capitalize(stripSuffix(".proto", proto.getName())) + ".java";
                String maybeJavaFilePath = find(intermediateJavaDir, new Predicate<File>() {
                    @Override
                    public boolean apply(File f) {
                        return f.getName().equals(javaFileName);
                    }
                });
                if (maybeJavaFilePath == null) {
                    System.err.println("ERROR: couldn't find result of protoc in " + intermediateJavaDir);
                    return null;
                }
                return fromPathToJava(maybeJavaFilePath, intermediateJavaDir);
            }
        } catch (Exception e) {
            System.err.println("WARNING: exception while processing " + pathToProto + ": " + e.getMessage());
            e.printStackTrace();
            return null;
        }
    }

    private String find(File dir, Predicate<File> predicate) {
        for (File file : dir.listFiles()) {
            if (file.isDirectory()) {
                String path = find(file, predicate);
                if (path != null) {
                    return path;
                }
            }
            if (predicate.apply(file)) {
                return file.getAbsolutePath();
            }
        }
        return null;
    }

    private String tryGetRelativePath(File file) {
        String pwd = System.getProperty("user.dir");
        return stripPrefix(pwd + File.separator, file.getAbsolutePath());
    }

    private String stripPrefix(String prefix, String s) {
        return s.startsWith(prefix) ? s.substring(prefix.length()) : s;
    }

    private String stripSuffix(String suffix, String s) {
        return s.endsWith(suffix) ? s.substring(0, s.length() - suffix.length()) : s;
    }

    private String capitalize(String s) {
        return Character.toUpperCase(s.charAt(0)) + s.substring(1);
    }

    private List<String> readLines(InputStream is) {
        try {
            return CharStreams.readLines(new InputStreamReader(is));
        } catch (IOException e) {
            e.printStackTrace();
            // TODO(kalman): this is a bit hacky, deal with it properly.
            return Collections.singletonList("(Error, couldn't read lines from the input stream. "
                    + "Try running the command external to PST to view the output.)");
        }
    }

    private FileDescriptor asFileDescriptor(Class<?> clazz) {
        try {
            Method method = clazz.getMethod("getDescriptor");
            return (FileDescriptor) method.invoke(null);
        } catch (Exception e) {
            return null;
        }
    }

    private void killProcessAfter(final long delay, final TimeUnit unit, final Process process) {
        Thread processKiller = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(unit.toMillis(delay));
                    process.destroy();
                } catch (InterruptedException e) {
                }
            }
        };
        processKiller.setDaemon(true);
        processKiller.start();
    }
}