forplay.rebind.AutoClientBundleGenerator.java Source code

Java tutorial

Introduction

Here is the source code for forplay.rebind.AutoClientBundleGenerator.java

Source

/**
 * Copyright 2011 The ForPlay Authors
 * 
 * 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 forplay.rebind;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.util.collect.HashMap;
import com.google.gwt.resources.client.ClientBundleWithLookup;
import com.google.gwt.resources.client.DataResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ResourcePrototype;
import com.google.gwt.resources.client.TextResource;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import forplay.core.Asserts;

/**
 * Automatically generate a client bundle from the resources available in the provided interface's
 * package directory and below.
 */
public class AutoClientBundleGenerator extends Generator {

    private static final Map<String, String> EXTENSION_MAP = new HashMap<String, String>();

    private static FileFilter fileFilter = new FileFilter() {
        @Override
        public boolean accept(File file) {
            if (file.isDirectory()) {
                // Skip .svn directories
                if (file.getName().equals(".svn")) {
                    return false;
                }
                // By default descend into all directories
                return true;
            } else {
                // Include only all explicitly mapped extensions
                String extension = getExtension(file.getName());
                return EXTENSION_MAP.containsKey(extension);
            }
        }
    };

    private static final String WEB_INF_CLASSES = "WEB-INF/classes/";

    static {
        EXTENSION_MAP.put(".png", "image/png");
        EXTENSION_MAP.put(".gif", "image/gif");
        EXTENSION_MAP.put(".jpg", "image/jpeg");

        /*
         * Do not include WAV files, since HTML5 audio playback ended events are not yet a part of GWT
         * 2.2.0, which means we won't know when these sounds stop playing.
         * 
         * EXTENSION_MAP.put(".wav", "audio/wav");
         */
        EXTENSION_MAP.put(".mp3", "audio/mp3");

        EXTENSION_MAP.put(".json", "text/json");
    }

    private static String getContentType(TreeLogger logger, File file) {
        String name = file.getName().toLowerCase();
        int pos = name.lastIndexOf('.');
        String extension = pos == -1 ? "" : name.substring(pos);
        String contentType = EXTENSION_MAP.get(extension);
        if (contentType == null) {
            logger.log(TreeLogger.WARN,
                    "No Content Type mapping for files with '" + extension
                            + "' extension. Please add a mapping to the "
                            + AutoClientBundleGenerator.class.getCanonicalName() + " class.");
            contentType = "application/octet-stream";
        }
        return contentType;
    }

    /**
     * Determine whether the provided method name is valid in Java.
     */
    private static boolean isValidMethodName(String methodName) {
        return methodName.matches("^[a-zA-Z_$][a-zA-Z0-9_$]*$"); // TODO: fix regex
    }

    /**
     * Strip off the filename extension, if it's present.
     */
    private static String stripExtension(String filename) {
        return filename.replaceFirst("\\.[^.]+$", "");
    }

    /**
     * Get the filename extension or return an empty string if there's no extension.
     */
    private static String getExtension(String filename) {
        return filename.replaceFirst(".*(\\.[^.]+)$", "$1");
    }

    @Override
    public String generate(TreeLogger logger, GeneratorContext context, String typeName)
            throws UnableToCompleteException {
        TypeOracle typeOracle = context.getTypeOracle();

        JClassType userType;
        try {
            userType = typeOracle.getType(typeName);
        } catch (NotFoundException e) {
            logger.log(TreeLogger.ERROR, "Unable to find metadata for type: " + typeName, e);
            throw new UnableToCompleteException();
        }
        String packageName = userType.getPackage().getName();
        String className = userType.getName();
        className = className.replace('.', '_');

        if (userType.isInterface() == null) {
            logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName() + " is not an interface", null);
            throw new UnableToCompleteException();
        }

        ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(packageName,
                className + "Impl");
        composerFactory.addImplementedInterface(userType.getQualifiedSourceName());

        composerFactory.addImport(ClientBundleWithLookup.class.getName());
        composerFactory.addImport(DataResource.class.getName());
        composerFactory.addImport(GWT.class.getName());
        composerFactory.addImport(ImageResource.class.getName());
        composerFactory.addImport(ResourcePrototype.class.getName());
        composerFactory.addImport(TextResource.class.getName());

        File warDirectory = getWarDirectory(logger);
        Asserts.check(warDirectory.isDirectory());

        File classesDirectory = new File(warDirectory, WEB_INF_CLASSES);
        Asserts.check(classesDirectory.isDirectory());

        File resourcesDirectory = new File(classesDirectory, packageName.replace('.', '/'));
        Asserts.check(resourcesDirectory.isDirectory());

        String baseClassesPath = classesDirectory.getPath();
        logger.log(TreeLogger.DEBUG, "baseClassesPath: " + baseClassesPath);

        Set<File> files = preferMp3(getFiles(resourcesDirectory, fileFilter));
        Set<String> methodNames = new HashSet<String>();

        PrintWriter pw = context.tryCreate(logger, packageName, className + "Impl");
        if (pw != null) {
            SourceWriter sw = composerFactory.createSourceWriter(context, pw);

            // write out jump methods

            sw.println("public ResourcePrototype[] getResources() {");
            sw.indent();
            sw.println("return MyBundle.INSTANCE.getResources();");
            sw.outdent();
            sw.println("}");

            sw.println("public ResourcePrototype getResource(String name) {");
            sw.indent();
            sw.println("return MyBundle.INSTANCE.getResource(name);");
            sw.outdent();
            sw.println("}");

            // write out static ClientBundle interface

            sw.println("static interface MyBundle extends ClientBundleWithLookup {");
            sw.indent();
            sw.println("MyBundle INSTANCE = GWT.create(MyBundle.class);");

            for (File file : files) {
                String filepath = file.getPath();
                String relativePath = filepath.replace(baseClassesPath, "").replace('\\', '/').replaceFirst("^/",
                        "");
                String filename = file.getName();
                String contentType = getContentType(logger, file);
                String methodName = stripExtension(filename);

                if (!isValidMethodName(methodName)) {
                    logger.log(TreeLogger.WARN,
                            "Skipping invalid method name (" + methodName + ") due to: " + relativePath);
                    continue;
                }
                if (!methodNames.add(methodName)) {
                    logger.log(TreeLogger.WARN, "Skipping duplicate method name due to: " + relativePath);
                    continue;
                }

                logger.log(TreeLogger.DEBUG, "Generating method for: " + relativePath);

                Class<? extends ResourcePrototype> returnType = getResourcePrototype(contentType);

                // generate method
                sw.println();

                if (returnType == DataResource.class) {
                    if (contentType.startsWith("audio/")) {
                        // Prevent the use of data URLs, which Flash won't play
                        sw.println("@DataResource.DoNotEmbed");
                    } else {
                        // Specify an explicit MIME type, for use in the data URL
                        sw.println("@DataResource.MimeType(\"" + contentType + "\")");
                    }
                }

                sw.println("@Source(\"" + relativePath + "\")");

                sw.println(returnType.getName() + " " + methodName + "();");
            }

            sw.outdent();
            sw.println("}");

            sw.commit(logger);
        }
        return composerFactory.getCreatedClassName();
    }

    /**
     * Filter file set, preferring *.mp3 files where alternatives exist.
     */
    private HashSet<File> preferMp3(Set<File> files) {
        HashMap<String, File> map = new HashMap<String, File>();
        for (File file : files) {
            String path = stripExtension(file.getPath());
            if (file.getName().endsWith(".mp3") || !map.containsKey(path)) {
                map.put(path, file);
            }
        }
        return new HashSet<File>(map.values());
    }

    /**
     * Recursively get a list of files in the provided directory.
     */
    private HashSet<File> getFiles(File dir, FileFilter filter) {
        Asserts.checkNotNull(dir);
        Asserts.checkNotNull(filter);
        Asserts.checkArgument(dir.isDirectory());

        HashSet<File> fileList = new HashSet<File>();
        File[] files = dir.listFiles(filter);
        for (int i = 0; i < files.length; i++) {
            File f = files[i];
            if (f.isFile()) {
                // f = file
                fileList.add(f);
            } else {
                // f = directory
                if (filter.accept(f)) {
                    fileList.addAll(getFiles(f, filter));
                }
            }
        }
        return fileList;
    }

    private Class<? extends ResourcePrototype> getResourcePrototype(String contentType) {
        Class<? extends ResourcePrototype> returnType;
        if (contentType.startsWith("image/")) {
            returnType = ImageResource.class;
        } else if (contentType.startsWith("text/")) {
            returnType = TextResource.class;
        } else {
            returnType = DataResource.class;
        }
        return returnType;
    }

    /**
     * When invoking the GWT compiler from GPE, the working directory is the Eclipse project
     * directory. However, when launching a GPE project, the working directory is the project 'war'
     * directory. This methods returns the war directory in either case in a fairly naive and
     * non-robust manner.
     */
    private File getWarDirectory(TreeLogger logger) throws UnableToCompleteException {
        File currentDirectory = new File(".");
        try {
            String canonicalPath = currentDirectory.getCanonicalPath();
            logger.log(TreeLogger.INFO, "Current directory in which this generator is executing: " + canonicalPath);
            if (canonicalPath.endsWith("war")) {
                return currentDirectory;
            } else {
                return new File("war");
            }
        } catch (IOException e) {
            logger.log(TreeLogger.ERROR, "Failed to get canonical path", e);
            throw new UnableToCompleteException();
        }
    }

}