package org.tessell.generators.resources; import static joist.sourcegen.Argument.arg; import static org.apache.commons.lang.StringUtils.substringBeforeLast; import java.io.File; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import joist.sourcegen.GClass; import joist.sourcegen.GField; import joist.sourcegen.GMethod; import joist.util.Inflector; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.tessell.generators.Cleanup; import org.tessell.generators.GenUtils; import org.tessell.generators.css.CssGenerator; import org.tessell.generators.css.CssStubGenerator; import org.tessell.gwt.resources.client.StubDataResource; import org.tessell.gwt.resources.client.StubImageResource; import org.tessell.gwt.resources.client.StubTextResource; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource.NotStrict; import com.google.gwt.resources.client.DataResource; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.resources.client.TextResource; /** A utility class for generating resource interfaces from files in directory. */ public class ResourcesGenerator { // ignore literal('url(...)') with a negative look behind private static final Pattern urlPattern = Pattern.compile("(?<!literal\\(')url\\(([^\\)]+)\\)"); private static final Pattern plusKeywordPattern = Pattern.compile("\\-X-([a-z-]+): *([^;]+);"); private final File inputDirectory; private final Cleanup cleanup; private final String packageName; private final File outputDirectory; private final GClass appResources; private final GClass stubResources; private final GClass appResourcesUtil; private final GMethod injectAll; public ResourcesGenerator(final File inputDirectory, Cleanup cleanup, final String packageName, final File outputDirectory) { this.inputDirectory = inputDirectory; this.cleanup = cleanup; this.packageName = packageName; this.outputDirectory = outputDirectory; appResources = new GClass(packageName + ".AppResources"); stubResources = new GClass(packageName + ".StubAppResources"); appResourcesUtil = new GClass(packageName + ".AppResourcesUtil"); injectAll = appResourcesUtil.getMethod("injectAll", arg("AppResources", "r")).setStatic(); } public void run() throws Exception { appResources.setInterface().baseClass(ClientBundle.class); stubResources.implementsInterface(appResources.getFullName()); for (final File file : getFilesInInputDirectory()) { if (file.getName().endsWith(".css")) { addCss(file); } else if (file.getName().endsWith(".png") || file.getName().endsWith(".gif") || file.getName().endsWith(".jpg") || file.getName().endsWith(".bmp") || file.getName().endsWith("htc")) { addImage(file); } else if (file.getName().endsWith(".html") || file.getName().endsWith("js")) { addText(file); } } cleanup.markOkay(appResources); cleanup.markOkay(stubResources); cleanup.markOkay(appResourcesUtil); GenUtils.saveIfChanged(outputDirectory, appResources); GenUtils.saveIfChanged(outputDirectory, stubResources); GenUtils.saveIfChanged(outputDirectory, appResourcesUtil); } private void addCss(final File cssFile) throws Exception { final String methodName = GenUtils.toMethodName(cssFile.getName().replace(".css", "").replace(".notstrict", "")); final String newInterfaceName = packageName + "." + suffixIfNeeded(Inflector.capitalize(methodName), "Style"); final String stubName = packageName + ".Stub" + suffixIfNeeded(Inflector.capitalize(methodName), "Style"); // Copy the file and do any url(...) => @url replacement File cssFileCopy = fileInOutputDirectory(inputDirectory, outputDirectory, cssFile, ".css", ".gen.css"); String cssContent = FileUtils.readFileToString(cssFile); cssContent = doUrlSubstitution(cssContent); cssContent = doMozWebkitSubstitution(cssContent); GenUtils.saveIfChanged(cssFileCopy, cssContent); cleanup.markOkay(cssFileCopy); final GMethod m = appResources.getMethod(methodName).returnType(newInterfaceName); m.addAnnotation("@Source(\"" + getRelativePath(cssFileCopy) + "\")"); boolean notStrict = cssFile.getName().endsWith(".notstrict.css"); if (notStrict) { m.addAnnotation("@NotStrict"); appResources.addImports(NotStrict.class.getName().replace("$", ".")); } // stub final GField sf = stubResources.getField(methodName).type(stubName).setFinal(); sf.initialValue("new {}()", stubName).autoImportInitialValue(); stubResources.getMethod(methodName).returnType(stubName).body.line("return {};", methodName); new CssGenerator(cssFile, cleanup, newInterfaceName, outputDirectory).run(); new CssStubGenerator(cssFile, cleanup, newInterfaceName, outputDirectory).run(); // inject all injectAll.body.line("r.{}().ensureInjected();", methodName); } private String suffixIfNeeded(String name, String suffix) { return name.endsWith(suffix) ? name : name + suffix; } /** @return a file in the same package as {@code file} but in the output (generated) directory */ public static File fileInOutputDirectory(File inputDirectory, File outputDirectory, File file, String extMatch, String extReplace) { return new File(// file.getAbsolutePath()// .replace(inputDirectory.getAbsolutePath(), outputDirectory.getAbsolutePath()) .replace(extMatch, extReplace)); } /** Finds {@code url(...)} in the css and replaces it with GWT @url declarations. */ private static String doUrlSubstitution(String cssContent) throws Exception { // keep track of urls we've already defined so we don't redefine them Set<String> defined = new HashSet<String>(); // buffer for @url definitions, to be prepended at the end StringBuffer defs = new StringBuffer(); // buffer to hold the transformed css, to be appended at the end StringBuffer sb = new StringBuffer(); Matcher m = urlPattern.matcher(cssContent); while (m.find()) { String path = m.group(1); String methodName = GenUtils.toMethodName(substringBeforeLast(new File(path).getName(), ".")); String aliasName = "_" + methodName + "Url"; String resourceName = methodName + "Data"; // for the DataResource if (defined.add(aliasName)) { defs.append("@url " + aliasName + " " + resourceName + ";\n"); } m.appendReplacement(sb, aliasName); } m.appendTail(sb); return defs.toString() + sb.toString(); } public static String doMozWebkitSubstitution(String cssContent) { StringBuffer sb = new StringBuffer(); Matcher m = plusKeywordPattern.matcher(cssContent); while (m.find()) { String property = m.group(1); String value = m.group(2); String newValue = ""; for (String prefix : new String[] { "-moz-", "-webkit-", "-o-", "-ms-", "" }) { newValue = newValue + prefix + property + ": " + value + "; "; } m.appendReplacement(sb, newValue.trim()); } m.appendTail(sb); return sb.toString(); } public void addImage(final File imageFile) throws Exception { final String methodName = GenUtils.toMethodName(StringUtils.substringBeforeLast(imageFile.getName(), ".")); { // ImageResource appResources.getMethod(methodName) // .returnType(ImageResource.class) .addAnnotation("@Source(\"{}\")", getRelativePath(imageFile)); // stub stubResources.getField(methodName) // .type(ImageResource.class) .setFinal() .initialValue("new StubImageResource(\"{}\", \"{}\")", methodName, imageFile.getName()); stubResources.getMethod(methodName).returnType(ImageResource.class).body.line("return {};", methodName); } { // DataResource appResources.getMethod(methodName + "Data") // .returnType(DataResource.class) .addAnnotation("@Source(\"{}\")", getRelativePath(imageFile)); // stub stubResources.getField(methodName + "Data") // .type(DataResource.class) .setFinal() .initialValue("new StubDataResource(\"{}\", \"{}\")", methodName, imageFile.getName()); stubResources.getMethod(methodName + "Data").returnType(DataResource.class).body.line("return {};", methodName + "Data"); } stubResources.addImports(StubImageResource.class, StubDataResource.class); } public void addText(final File textFile) throws Exception { final String methodName = GenUtils.toMethodName(StringUtils.substringBeforeLast(textFile.getName(), ".")); final GMethod m = appResources.getMethod(methodName).returnType(TextResource.class); m.addAnnotation("@Source(\"{}\")", getRelativePath(textFile)); // stub final GField sf = stubResources.getField(methodName).type(TextResource.class).setFinal(); sf.initialValue("new StubTextResource(\"{}\", \"{}\")", methodName, textFile.getName()); stubResources.getMethod(methodName).returnType(TextResource.class).body.line("return {};", methodName); stubResources.addImports(StubTextResource.class); } private String getRelativePath(File file) { String path = absolutePath(file); String inputDirectoryPath = absolutePath(inputDirectory); String relativePath; if (path.contains(inputDirectoryPath)) { relativePath = path.replace(inputDirectoryPath + "/" + packageName.replace(".", "/") + "/", ""); } else { String outputDirectoryPath = absolutePath(outputDirectory); relativePath = path.replace(outputDirectoryPath + "/" + packageName.replace(".", "/") + "/", ""); } return relativePath; } private String absolutePath(File file) { return slashSanitise(file.getAbsolutePath()); } private String slashSanitise(String value) { return value.replaceAll("\\\\", "/"); } private Collection<File> getFilesInInputDirectory() { String[] exts = new String[] { "css", "png", "gif", "jpg", "html", "js", "htc" }; File packageDirectory = new File(inputDirectory, packageName.replace(".", File.separator)); return FileUtils.listFiles(packageDirectory, exts, true); } }