/**
* Copyright 2008 - 2015 The Loon Game Engine 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.
*
* @project loon
* @author cping
* @email:javachenpeng@yahoo.com
* @version 0.5
*/
package loon.html5.gwt.preloader;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import loon.LSystem;
import loon.html5.gwt.preloader.AssetFilter.AssetType;
import loon.utils.Base64Coder;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.ConfigurationProperty;
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.dev.resource.Resource;
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;
public class PreloaderBundleGenerator extends Generator {
private static final Map<String, String> EXTENSION_MAP = new HashMap<String, String>();
static {
EXTENSION_MAP.put(".png", "image/png");
EXTENSION_MAP.put(".gif", "image/gif");
EXTENSION_MAP.put(".jpg", "image/jpeg");
EXTENSION_MAP.put(".mp3", "audio/mp3");
EXTENSION_MAP.put(".json", "text/json");
}
private static FileFilter fileFilter = new FileFilter() {
@Override
public boolean accept(File file) {
if (file.isDirectory()) {
if (file.getName().equals(".svn")
|| file.getName().equals(".tmp")) {
return false;
}
return true;
} else {
String extension = LSystem.getExtension(file.getName());
return EXTENSION_MAP.containsKey(extension);
}
}
};
static class Var {
String name;
String context;
}
@Override
public String generate(TreeLogger logger, GeneratorContext context,
String typeName) throws UnableToCompleteException {
info(logger, "location : " + new File(".").getAbsolutePath());
String assetPath = getAssetPath(context);
info(logger, "set assets path : " + assetPath);
String assetOutputPath = getAssetOutputPath(context);
if (assetOutputPath == null) {
assetOutputPath = "war/";
}
AssetFilter assetFilter = getAssetFilter(context);
ResourcesWrapper source = new ResourcesWrapper(assetPath);
String[] source_list = source.file().list();
int idx = 0;
boolean fix_loc = false;
// 防止定位到奇怪的assets资源目录……
if (source_list != null && source_list.length > 0) {
for (String file : source_list) {
if (file.contains("assets/loon_logo.png")
|| file.contains("assets/loon_pad_ui.png")
|| file.contains("assets/loon_ui.png")
|| file.contains("assets.txt")) {
idx++;
}
if (idx >= 1) {
fix_loc = true;
break;
}
}
}
if (fix_loc) {
source = new ResourcesWrapper("../" + assetPath);
}
if (!source.exists() || source.list().length == 0) {
source = new ResourcesWrapper("../" + assetPath);
if (!source.exists() || source.list().length == 0) {
source = new ResourcesWrapper(getPath(assetPath).substring(
assetPath.indexOf('/') + 1, assetPath.length()));
if (!source.exists() || source.list().length == 0) {
source = new ResourcesWrapper(getPath(assetPath).replace(
assetOutputPath, "").replace("../", ""));
if (!source.exists()) {
source = new ResourcesWrapper(assetPath);
if (!source.exists()) {
throw new RuntimeException(
"assets path '"
+ assetPath
+ "' does not exist. Check your loon.assetpath property in your GWT project's module gwt.xml file");
}
}
}
}
}
if (!source.isDirectory()) {
throw new RuntimeException(
"assets path '"
+ assetPath
+ "' is not a directory. Check your loon.assetpath property in your GWT project's module gwt.xml file");
}
info(logger, "Copying resources from " + assetPath + " to "
+ assetOutputPath);
info(logger, source.file.getAbsolutePath());
ResourcesWrapper target = new ResourcesWrapper("assets/");
info(logger, target.file.getAbsolutePath());
if (!target.file.getAbsolutePath().replace("\\", "/")
.endsWith(assetOutputPath + "assets")) {
target = new ResourcesWrapper(assetOutputPath + "assets/");
}
if (target.exists()) {
if (!target.deleteDirectory()) {
throw new RuntimeException("Couldn't clean target path '"
+ target + "'");
}
}
ArrayList<Asset> assets = new ArrayList<Asset>();
boolean addtojs = false;
ConfigurationProperty property = null;
try {
property = context.getPropertyOracle().getConfigurationProperty(
"loon.addtojs");
if (property != null && property.getValues().size() > 0) {
String parameter = property.getValues().get(0).toLowerCase();
if ("yes".equals(parameter) || "true".equals(parameter)
|| "ok".equals(parameter)) {
addtojs = true;
}
}
} catch (BadPropertyValueException e) {
addtojs = false;
}
if (addtojs) {
// 附带一提,这里不能调用jsni给LocalAssetResources赋值,因为动态加载的关系,gwt不会处理此文件中内容,所以用@方式引用不了gwt中对象,只能传基本对象和一维数组给gwt识别.
// 要想在这个js直接引用gwt中对象,需要一个执行顺序更优先的外部程序执行自己的编译顺序才行,而不能让gwt自行处理,但暂时还没有打算。
StringBuilder dcode = new StringBuilder();
dcode.append("function LocalResources(){\n");
// 先传个LocalAssetResources过来备用,如果自制开发工具的话,此处可以直接做注入
// PS:还有个流氓招数,就是分别穷举函数名,记录混淆前和混淆后的实际函数名以及参数,然后动态传值,不过那样太流氓,不考虑……
dcode.append("this.running = function(res){\n");
dcode.append("var list = new Array();");
ArrayList<String> list = listFile(source.file());
for (String fileName : list) {
ResourcesWrapper fileRes = new ResourcesWrapper(fileName);
info(logger, "<<" + fileRes.path() + ">>");
String extension = fileRes.extension().toLowerCase();
if (LSystem.isText(extension)) {
String path = getPath(fileRes.path());
String resName = getResName(path);
Var var = getVarText(resName, path);
if (var != null) {
dcode.append('\n');
dcode.append(var.context);
dcode.append('\n');
push(dcode, resName, var.name, true);
}
} else if (LSystem.isImage(extension)) {
String path = getPath(fileRes.path());
String resName = getResName(path);
dcode.append('\n');
if (addtojs) {
String result;
try {
result = new String(Base64Coder.encode(fileRes
.readBytes()));
} catch (IOException e) {
result = "";
}
StringBuilder ctx = new StringBuilder();
char[] chars = result.toCharArray();
int size = chars.length;
int count = 0;
for (int i = 0; i < size; i++) {
char ch = chars[i];
if (count == 156) {
ctx.append("\"+\n");
count = 0;
ctx.append("\"");
}
ctx.append(ch);
count++;
}
push(dcode, resName, ctx.toString(), false);
} else {
push(dcode, resName, fileRes.length(), true);
}
} else if (LSystem.isAudio(extension)) {
copyDirectory(source, target, assetFilter, assets);
} else {
String path = getPath(fileRes.path());
String resName = getResName(path);
dcode.append('\n');
if (addtojs) {
String result;
try {
result = new String(Base64Coder.encode(fileRes
.readBytes()));
} catch (IOException e) {
result = "";
}
StringBuilder ctx = new StringBuilder();
char[] chars = result.toCharArray();
int size = chars.length;
int count = 0;
for (int i = 0; i < size; i++) {
char ch = chars[i];
if (count == 128) {
ctx.append("\"+\n");
count = 0;
ctx.append("\"");
}
ctx.append(ch);
count++;
}
push(dcode, resName, ctx.toString(), false);
}
}
}
dcode.append("return list;};};");
ResourcesWrapper res = new ResourcesWrapper(target.path()
+ "/resources.js");
res.writeString(dcode.toString(), false, LSystem.ENCODING);
return createDummyClass(logger, context, typeName);
}
copyDirectory(source, target, assetFilter, assets);
List<String> classpathFiles = getClasspathFiles(context);
for (String classpathFile : classpathFiles) {
info(logger, classpathFile);
if (assetFilter.accept(classpathFile, false)) {
try {
InputStream is = context.getClass().getClassLoader()
.getResourceAsStream(classpathFile);
ResourcesWrapper dest = target.child(classpathFile);
dest.write(is, false);
assets.add(new Asset(dest, assetFilter.getType(dest.path())));
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
HashMap<String, ArrayList<Asset>> bundles = new HashMap<String, ArrayList<Asset>>();
for (Asset asset : assets) {
String bundleName = assetFilter.getBundleName(asset.file.path());
if (bundleName == null) {
bundleName = "assets";
}
ArrayList<Asset> bundleAssets = bundles.get(bundleName);
if (bundleAssets == null) {
bundleAssets = new ArrayList<Asset>();
bundles.put(bundleName, bundleAssets);
}
bundleAssets.add(asset);
}
for (Entry<String, ArrayList<Asset>> bundle : bundles.entrySet()) {
StringBuffer buffer = new StringBuffer();
for (Asset asset : bundle.getValue()) {
String path = asset.file.path().replace('\\', '/')
.replace(assetOutputPath, "")
.replaceFirst("assets/", "");
if (path.startsWith("/"))
path = path.substring(1);
buffer.append(asset.type.code);
buffer.append(":");
buffer.append(path);
buffer.append(":");
buffer.append(asset.file.isDirectory() ? 0 : asset.file
.length());
buffer.append(":");
String mimetype = URLConnection
.guessContentTypeFromName(asset.file.name());
String ext = LSystem.getExtension(asset.file.name())
.toLowerCase();
if (ext.equals("an") || ext.equals("tmx")) {
buffer.append("text/plain");
} else {
buffer.append(mimetype == null ? "application/unknown"
: mimetype);
}
buffer.append("\n");
}
target.child(bundle.getKey() + ".txt").writeString(
buffer.toString(), false);
}
return createDummyClass(logger, context, typeName);
}
private static ArrayList<String> listFile(File file) {
ArrayList<String> ret = new ArrayList<String>();
String[] listFile = file.list();
if (listFile != null) {
for (int i = 0; i < listFile.length; i++) {
File tempfile = new File(file.getPath() + '/' + listFile[i]);
if (tempfile.isDirectory()) {
ArrayList<String> ls = listFile(tempfile);
ret.addAll(ls);
ls.clear();
ls = null;
} else {
ret.add(tempfile.getPath());
}
}
}
return ret;
}
private class Asset {
ResourcesWrapper file;
AssetType type;
public Asset(ResourcesWrapper file, AssetType type) {
this.file = file;
this.type = type;
}
}
private static void push(StringBuilder code, String key, Object value,
boolean allplus) {
if (allplus) {
code.append(String.format("list.push({k:%s,v:%s});", "\"" + key
+ "\"", value));
} else {
if (value instanceof String) {
String result = (String) value;
if (result.startsWith("\"") || result.startsWith("\\\"")) {
code.append(String.format("list.push({k:%s,v:%s});", "\""
+ key + "\"", result));
} else {
code.append(String.format("list.push({k:%s,v:%s});", "\""
+ key + "\"", "\"" + result + "\""));
}
} else {
code.append(String.format("list.push({k:%s,v:%s});", "\"" + key
+ "\"", value));
}
}
}
private void copyFile(ResourcesWrapper source, ResourcesWrapper dest,
AssetFilter filter, ArrayList<Asset> assets) {
if (!filter.accept(dest.path(), false))
return;
try {
assets.add(new Asset(dest, filter.getType(dest.path())));
dest.write(source.read(), false);
} catch (Exception ex) {
throw new RuntimeException("Error copying source file: " + source
+ "\n" //
+ "To destination: " + dest, ex);
}
}
private void copyDirectory(ResourcesWrapper sourceDir,
ResourcesWrapper destDir, AssetFilter filter,
ArrayList<Asset> assets) {
if (!filter.accept(destDir.path(), true))
return;
assets.add(new Asset(destDir, AssetType.Directory));
destDir.mkdirs();
ResourcesWrapper[] files = sourceDir.list();
for (int i = 0, n = files.length; i < n; i++) {
ResourcesWrapper srcFile = files[i];
ResourcesWrapper destFile = destDir.child(srcFile.name());
if (srcFile.isDirectory())
copyDirectory(srcFile, destFile, filter, assets);
else
copyFile(srcFile, destFile, filter, assets);
}
}
private AssetFilter getAssetFilter(GeneratorContext context) {
ConfigurationProperty assetFilterClassProperty = null;
try {
assetFilterClassProperty = context.getPropertyOracle()
.getConfigurationProperty("loon.assetfilterclass");
} catch (BadPropertyValueException e) {
return new DefaultAssetFilter();
}
if (assetFilterClassProperty.getValues().size() == 0) {
return new DefaultAssetFilter();
}
String assetFilterClass = assetFilterClassProperty.getValues().get(0);
if (assetFilterClass == null)
return new DefaultAssetFilter();
try {
return (AssetFilter) Class.forName(assetFilterClass).newInstance();
} catch (Exception e) {
throw new RuntimeException(
"Couldn't instantiate custom AssetFilter '"
+ assetFilterClass
+ "', make sure the class is public and has a public default constructor",
e);
}
}
private String getAssetPath(GeneratorContext context) {
ConfigurationProperty assetPathProperty = null;
try {
assetPathProperty = context.getPropertyOracle()
.getConfigurationProperty("loon.assetpath");
} catch (BadPropertyValueException e) {
throw new RuntimeException(
"No loon.assetpath defined. Add <set-configuration-property name=\"loon.assetpath\" value=\"relative/path/to/assets/\"/> to your GWT projects gwt.xml file");
}
if (assetPathProperty.getValues().size() == 0) {
throw new RuntimeException(
"No loon.assetpath defined. Add <set-configuration-property name=\"loon.assetpath\" value=\"relative/path/to/assets/\"/> to your GWT projects gwt.xml file");
}
String paths = assetPathProperty.getValues().get(0);
if (paths == null) {
throw new RuntimeException(
"No loon.assetpath defined. Add <set-configuration-property name=\"loon.assetpath\" value=\"relative/path/to/assets/\"/> to your GWT projects gwt.xml file");
} else {
String[] tokens = paths.split(",");
for (String token : tokens) {
if (new ResourcesWrapper(token).exists()
|| new ResourcesWrapper("../" + token).exists()
|| new ResourcesWrapper(getPath(token).replace("../",
"")).exists()) {
return token;
}
}
throw new RuntimeException(
"No valid loon.assetpath defined. Fix <set-configuration-property name=\"loon.assetpath\" value=\"relative/path/to/assets/\"/> in your GWT projects gwt.xml file");
}
}
private String getAssetOutputPath(GeneratorContext context) {
ConfigurationProperty assetPathProperty = null;
try {
assetPathProperty = context.getPropertyOracle()
.getConfigurationProperty("loon.assetoutputpath");
} catch (BadPropertyValueException e) {
return null;
}
if (assetPathProperty.getValues().size() == 0) {
return null;
}
String paths = assetPathProperty.getValues().get(0);
if (paths == null) {
return null;
} else {
String[] tokens = paths.split(",");
String path = null;
for (String token : tokens) {
if (new ResourcesWrapper(token).exists()
|| new ResourcesWrapper(token).mkdirs()) {
path = token;
}
}
if (path != null && !path.endsWith("/")) {
path += "/";
}
return path;
}
}
private List<String> getClasspathFiles(GeneratorContext context) {
List<String> classpathFiles = new ArrayList<String>();
try {
ConfigurationProperty prop = context.getPropertyOracle()
.getConfigurationProperty("loon.files.classpath");
for (String value : prop.getValues()) {
classpathFiles.add(value);
}
} catch (BadPropertyValueException e) {
}
return classpathFiles;
}
private String createDummyClass(TreeLogger logger,
GeneratorContext context, String typeName) {
String packageName = "loon.html5.gwt.preloader";
String className = "PreloaderBundleImpl";
PrintWriter printWriter = context.tryCreate(logger, packageName,
className);
if (printWriter == null) {
return packageName + "." + className;
}
ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(
packageName, className);
composer.addImplementedInterface(packageName + ".PreloaderBundle");
composer.addImport(ClientBundleWithLookup.class.getName());
composer.addImport(DataResource.class.getName());
composer.addImport(GWT.class.getName());
composer.addImport(ImageResource.class.getName());
composer.addImport(ResourcePrototype.class.getName());
composer.addImport(TextResource.class.getName());
Set<Resource> resources = preferMp3(getResources(context, packageName,
fileFilter));
Set<String> methodNames = new HashSet<String>();
SourceWriter sourceWriter = composer.createSourceWriter(context,
printWriter);
sourceWriter.println("public ResourcePrototype[] getResources() {");
sourceWriter.indent();
sourceWriter.println("return SiteBundle.INSTANCE.getResources();");
sourceWriter.outdent();
sourceWriter.println("}");
sourceWriter
.println("public ResourcePrototype getResource(String name) {");
sourceWriter.indent();
sourceWriter.println("return SiteBundle.INSTANCE.getResource(name);");
sourceWriter.outdent();
sourceWriter.println("}");
sourceWriter
.println("static interface SiteBundle extends ClientBundleWithLookup {");
sourceWriter.indent();
sourceWriter
.println("SiteBundle INSTANCE = GWT.create(SiteBundle.class);");
for (Resource resource : resources) {
String relativePath = resource.getPath();
String filename = resource.getPath().substring(
resource.getPath().lastIndexOf('/') + 1);
String contentType = getContentType(logger, resource);
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);
sourceWriter.println();
if (returnType == DataResource.class) {
if (contentType.startsWith("audio/")) {
sourceWriter.println("@DataResource.DoNotEmbed");
} else {
sourceWriter.println("@DataResource.MimeType(\""
+ contentType + "\")");
}
}
sourceWriter.println("@Source(\"" + relativePath + "\")");
sourceWriter.println(returnType.getName() + " " + methodName
+ "();");
}
sourceWriter.outdent();
sourceWriter.println("}");
sourceWriter.commit(logger);
return packageName + "." + className;
}
private HashSet<Resource> preferMp3(HashSet<Resource> files) {
HashMap<String, Resource> map = new HashMap<String, Resource>();
for (Resource file : files) {
String path = stripExtension(file.getPath());
if (file.getPath().endsWith(".mp3") || !map.containsKey(path)) {
map.put(path, file);
}
}
return new HashSet<Resource>(map.values());
}
private static boolean isValidMethodName(String methodName) {
return methodName.matches("^[a-zA-Z_$][a-zA-Z0-9_$]*$");
}
private static String stripExtension(String filename) {
return filename.replaceFirst("\\.[^.]+$", "");
}
private HashSet<Resource> getResources(GeneratorContext context,
String packName, FileFilter filter) {
final String pack = packName.replace('.', '/');
HashSet<Resource> resourceList = new HashSet<Resource>();
for (String path : context.getResourcesOracle().getPathNames()) {
if (!path.startsWith(pack)) {
continue;
}
String ext = LSystem.getExtension(path);
if (EXTENSION_MAP.containsKey(ext)) {
resourceList
.add(context.getResourcesOracle().getResource(path));
}
}
return resourceList;
}
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;
}
private void info(TreeLogger logger, String msg) {
logger.log(TreeLogger.INFO, msg);
}
private static String readCodeString(File file) throws Exception {
FileInputStream in = new FileInputStream(file);
BufferedReader reader = new BufferedReader(new InputStreamReader(in,
LSystem.ENCODING));
try {
String text = null;
StringBuffer dcode = new StringBuffer();
for (; (text = reader.readLine()) != null;) {
text = text.trim();
if (text.length() > 0) {
char[] chars = text.toCharArray();
dcode.append("\"");
int size = chars.length;
for (int i = 0; i < size; i++) {
char ch = chars[i];
if (ch == '\\') {
dcode.append('\\');
dcode.append(ch);
} else if (ch == '"') {
dcode.append('\\');
dcode.append(ch);
} else {
dcode.append(ch);
}
}
if (size > 0
&& chars[size - 1] != DefaultAssetFilter.special_symbols) {
dcode.append(DefaultAssetFilter.special_symbols);
}
dcode.append("\"+");
dcode.append('\n');
}
}
text = dcode.toString();
int idx = text.lastIndexOf('+');
return text.substring(0, idx);
} finally {
reader.close();
}
}
private static String getPath(String path) {
int pathLen;
do {
pathLen = path.length();
path = path.replaceAll("[^/]+/\\.\\./", "");
} while (path.length() != pathLen);
return path.replace("\\", "/");
}
private static String getResName(String fileName) {
int idx = fileName.indexOf("assets/");
String path = fileName;
if (idx != -1) {
path = fileName.substring(idx + 7, fileName.length());
} else if ((idx = path.indexOf('/')) != -1) {
path = fileName.substring(idx, fileName.length());
} else {
path = LSystem.getFileName(fileName);
}
return path;
}
private static String getContentType(TreeLogger logger, Resource resource) {
String name = resource.getPath().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 "
+ PreloaderBundleGenerator.class.getCanonicalName()
+ " class.");
contentType = "application/octet-stream";
}
return contentType;
}
private static Var getVarText(String resName, String fileName) {
try {
String varname = "txt_"
+ resName.replace('.', '_').replace('/', '_');
String result = readCodeString(new File(fileName));
Var var = new Var();
var.name = varname;
var.context = ("var " + varname + " = (" + result) + ").replace('"
+ DefaultAssetFilter.special_symbols + "', '\\n');";
return var;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}