package org.tessell.generators.views;
import static joist.sourcegen.Argument.arg;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import joist.sourcegen.Argument;
import joist.sourcegen.GClass;
import joist.sourcegen.GMethod;
import joist.util.Join;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.tessell.generators.Cleanup;
import org.tessell.generators.GenUtils;
import org.xml.sax.SAXException;
/** Takes a {@code ui.xml} source and generates a {@code IsXxx, GwtXxx, StubXxx} trio of view classes. */
public class ViewGenerator {
private final String packageName;
private final List<UiXmlFile> uiXmlFiles = new ArrayList<UiXmlFile>();
private final UiXmlCache cache;
final File input;
final File output;
final Cleanup cleanup;
final Config config = new Config();
final SAXParser parser;
public ViewGenerator(final File inputDirectory, final String packageName, final File outputDirectory, final Cleanup cleanup) {
input = inputDirectory.getAbsoluteFile();
output = outputDirectory.getAbsoluteFile();
cache = UiXmlCache.loadOrCreate(output);
this.packageName = packageName;
this.cleanup = cleanup;
parser = makeNewParser();
}
public void generate() throws Exception {
for (final File uiXml : findUiXmlFiles()) {
if (uiXml.getName().contains("-nogen.")) {
continue;
}
uiXmlFiles.add(new UiXmlFile(this, uiXml));
}
boolean viewgenChanged = !config.getViewgenTimestamp().equals(cache.getViewgenTimestamp());
for (final UiXmlFile uiXml : uiXmlFiles) {
if (uiXml.hasChanged() || !cache.has(uiXml) || viewgenChanged) {
uiXml.generate();
cache.update(uiXml);
}
cleanup.markOkay(uiXml.isView);
cleanup.markOkay(uiXml.gwtView);
cleanup.markOkay(uiXml.stubView);
cleanup.markOkay(uiXml.uiXmlCopy);
for (final UiStyleDeclaration style : uiXml.getPossiblyCachedStyles(cache)) {
cleanup.markTypeOkay(style.type);
cleanup.markTypeOkay(style.getStubClassName());
}
}
generateAppViews();
generateAppViewsProvider();
generateGwtViews();
generateStubViews();
cache.save(output, config.getViewgenTimestamp());
}
private void generateAppViews() {
final GClass appViews = new GClass(packageName + ".AppViews");
for (final UiXmlFile uiXml : uiXmlFiles) {
GMethod m = appViews.getMethod("new" + uiXml.baseName).returnType(uiXml.isView.getFullName()).setStatic();
m.body.line("return provider.new{}();", uiXml.baseName);
}
appViews.getField("provider").setStatic().type("AppViewsProvider");
GMethod m = appViews.getMethod("setProvider", arg("AppViewsProvider", "provider")).setStatic();
m.body.line("AppViews.provider = provider;");
markAndSaveIfChanged(appViews);
}
private void generateAppViewsProvider() {
final GClass appViewsProvider = new GClass(packageName + ".AppViewsProvider").setInterface();
for (final UiXmlFile uiXml : uiXmlFiles) {
appViewsProvider.getMethod("new" + uiXml.baseName).returnType(uiXml.isView.getFullName());
}
markAndSaveIfChanged(appViewsProvider);
}
private void generateGwtViews() {
final GClass gwtViews = new GClass(packageName + ".GwtViewsProvider").implementsInterface("AppViewsProvider");
final GMethod cstr = gwtViews.getConstructor();
// ui:withs in separate files could use the same type but different
// variable names, so we resolve based on the type only
for (String type : getUniqueWithTypes()) {
final String name = simpleName(type);
gwtViews.getField(name).type(type).setFinal();
cstr.argument(type, name);
cstr.body.line("this.{} = {};", name, name);
}
for (final UiXmlFile uiXml : uiXmlFiles) {
final GMethod m = gwtViews.getMethod("new" + uiXml.baseName).returnType(uiXml.isView.getFullName());
final List<String> withFieldNames = new ArrayList<String>();
for (final UiWithDeclaration with : uiXml.getPossiblyCachedWiths(cache)) {
withFieldNames.add("this." + simpleName(with.type));
}
m.addAnnotation("@Override");
m.body.line("return new {}({});", uiXml.gwtView.getFullName(), Join.commaSpace(withFieldNames));
}
markAndSaveIfChanged(gwtViews);
}
private void generateStubViews() {
final GClass stubViews = new GClass(packageName + ".StubViewsProvider").implementsInterface("AppViewsProvider");
// aggregate stub dependencies across all the files
List<Argument> cstrArgs = new ArrayList<Argument>();
List<String> cstrNames = new ArrayList<String>();
for (String type : getUniqueStubDependencies()) {
final String name = simpleName(type);
stubViews.getField(name).type(type).setFinal();
cstrArgs.add(arg(type, name));
cstrNames.add(name);
}
// ui:withs in separate files could use the same type but different
// variable names, so we resolve based on the type only
for (String type : getUniqueWithTypes()) {
final String name = simpleName(type);
if (!cstrNames.contains(name)) {
stubViews.getField(name).type(type).setFinal();
cstrArgs.add(arg(type, name));
cstrNames.add(name);
}
}
stubViews.getConstructor(cstrArgs).assignFields();
for (final UiXmlFile uiXml : uiXmlFiles) {
final GMethod m = stubViews.getMethod("new" + uiXml.baseName).returnType(uiXml.stubView.getFullName());
m.addAnnotation("@Override");
// look for stub dependencies, sort by simple name
Set<String> stubArgs = new TreeSet<String>();
for (String type : uiXml.getPossiblyCachedStubDependencies(cache)) {
stubArgs.add(simpleName(type));
}
for (UiWithDeclaration uiWith : uiXml.getPossiblyCachedWiths(cache)) {
stubArgs.add(simpleName(uiWith.type));
}
m.body.line("return new {}({});", uiXml.stubView.getFullName(), Join.commaSpace(stubArgs));
}
final GMethod i = stubViews.getMethod("install", cstrArgs).setStatic();
i.body.line("AppViews.setProvider(new StubViewsProvider({}));", Join.commaSpace(cstrNames));
markAndSaveIfChanged(stubViews);
}
/** @return the unique types required by {@code ui:with}s across all views */
private Set<String> getUniqueWithTypes() {
final Set<String> all = new TreeSet<String>();
for (final UiXmlFile uiXml : uiXmlFiles) {
for (final UiWithDeclaration field : uiXml.getPossiblyCachedWiths(cache)) {
all.add(field.type);
}
}
return all;
}
/** @return the unique stub dependencies across all views */
private Set<String> getUniqueStubDependencies() {
final Set<String> all = new TreeSet<String>();
for (final UiXmlFile uiXml : uiXmlFiles) {
all.addAll(uiXml.getPossiblyCachedStubDependencies(cache));
}
return all;
}
void markAndSaveIfChanged(final GClass gclass) {
GenUtils.saveIfChanged(output, gclass);
cleanup.markOkay(gclass);
}
/** If the ui.xml files have had their time stamp changed, we always save the new ones. */
void markAndSave(final GClass gclass) {
cleanup.markOkay(gclass);
try {
FileUtils.writeStringToFile(new File(output, gclass.getFileName()), gclass.toCode());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Collection<File> findUiXmlFiles() {
return FileUtils.listFiles(input, new String[] { "ui.xml" }, true);
}
private static SAXParser makeNewParser() {
final SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(true);
try {
// don't hit the net for DTDs and crap
// http://stackoverflow.com/questions/243728/how-to-disable-dtd-at-runtime-in-javas-xpath
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
return factory.newSAXParser();
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
} catch (SAXException e) {
throw new RuntimeException(e);
}
}
protected static String simpleName(String fullName) {
return StringUtils.uncapitalize(StringUtils.substringAfterLast(fullName, "."));
}
}