/**
* 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();
}
}
}