package com.habitsoft.kiyaa.rebind;
import static org.apache.commons.lang.StringUtils.isEmpty;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import nu.xom.Attribute;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Elements;
import nu.xom.Node;
import nu.xom.Nodes;
import nu.xom.ParentNode;
import nu.xom.Text;
import nu.xom.XPathContext;
import org.apache.commons.lang.StringUtils;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
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.JEnumType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.core.ext.typeinfo.TypeOracleException;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Widget;
import com.habitsoft.kiyaa.rebind.typeinfo.ExpressionInfo;
import com.habitsoft.kiyaa.rebind.typeinfo.GeneratedClassInfo;
import com.habitsoft.kiyaa.rebind.typeinfo.GeneratedInnerClassInfo;
import com.habitsoft.kiyaa.rebind.typeinfo.GeneratorMethodInfo;
import com.habitsoft.kiyaa.rebind.typeinfo.GeneratorTypeInfo;
import com.habitsoft.kiyaa.rebind.typeinfo.JClassTypeWrapper;
import com.habitsoft.kiyaa.rebind.typeinfo.JTypeWrapper;
import com.habitsoft.kiyaa.rebind.typeinfo.PrimitiveTypeInfo;
import com.habitsoft.kiyaa.rebind.typeinfo.RuntimeClassWrapper;
import com.habitsoft.kiyaa.util.Name;
import com.habitsoft.kiyaa.views.GeneratedHTMLView.ActionMethod;
import com.habitsoft.kiyaa.views.GeneratedHTMLView.TemplatePath;
import com.habitsoft.kiyaa.views.ModelView;
import com.habitsoft.kiyaa.views.View;
import com.habitsoft.kiyaa.views.ViewFactory;
import com.habitsoft.xhtml.dtds.FailingEntityResolver;
import com.habitsoft.xhtml.dtds.XhtmlEntityResolver;
import com.sun.facelets.util.Classpath;
/**
* This generator creates a View implementation from an XML/XHTML template.
*
* The template contains a mix of XHTML tags and special tags for the construction of subviews,
* which come from a tag library.
*
* The format/architecture is based loosely on facelets/JSF, but since we're generating widgets and
* not text, some things are changed.
*
* Any HTML element can be "converted" into a special tag by specifying kc="ns:tag" which means the
* template parser treats the whole tag as if it were an <ns:tag ... > element.
*
* Tags which are not part of HTML are converted into a View or Widget subclass,
* and the attributes of that tag are used to initialize the new view object.
*
* The view provides attributes in the form of setters;
*
* - If a setter takes an Action object, the tag attribute value may be
* one or more assignments or method calls seperated by a semicolon. The
* generated code constructs an Action object which executes those
* statements. The first statement to fail terminates the action sequence;
* asynchronous operations ARE supported. It may also be an expression
* for an Action to copy to that place.
* - If a setter takes a Value object, or a type which isn't automatically
* converted from a string by the generated (a non-primitive type), then
* the XML attribute value should be an EL-style expression, either
* "${...}" or "#{...}". The expression is evaluated during the template's
* LOAD phase and the view's setter is called with the result. If "#{...}"
* is used, then the view's getter is called during the SAVE phase, and the
* setter of the given expression is called with that result.
* - Other values are converted to the target type on a best-effort basis; for
* example an integer, float, or boolean value may be parsed.
*
* Some tag attributes are processed specially:
*
* - The special attribute "binding" allows you to store a reference to the
* view object into the given EL expression. The setter for the expression
* is called after constructing the view object from the tag.
* - The special attributes "onclick" and "onchange" will automatically create
* a ClickListener or a ChangeListener on view objects that implements
* SourcesClickEvents or SourcesChangeEvents. The attribute value follows
* the syntax of an Action, as above.
* - The special attributes "class" and "style" will automatically call
* the addStyleName() or DOM.setElementAttribute() to apply the given
* CSS class or style string to the created view object.
*
* When a tag is converted and it contains elements inside it, the following
* rules apply:
*
* - If the inner tags match methods like setX or addX on the view, those
* setters/adders are called once for each element. The parameters to
* the setter or added are calculated as follows:
* - Attribute names are matched to parameter names and are
* reordered to match the parameter list
* - The special attribute name "class" can be used to match a
* method parameter "styleName" because "class" is a java
* reserved word.
* - The attributes are processed and converted according to the
* same rules as calling setters on widgets and views (see above).
* - If all parameters are matched to an attribute but one, and the
* element has a body (either text or child elements), the body
* may be used to fill that parameter as follows:
* - If the parameter is a View, Widget, or ViewFactory, the body is
* treated as a sub-template
* - If the missing parameter is a string, and the method takes just
* one parameter or one parameter plus an AsyncCallback, then the
* text contents of the element are assumed to be the value of that
* parameter
* - If the View or Widget being constructed has a setView() or setViewFactory(),
* the child elements are converted to a sub-template and passed as a view
* instance or factory.
*
* The Expression Language has the following features:
*
* - "#{...}" is evaluated during load to set the target attribute, and
* during save the attribute is copied back into the same location by
* changing getters to setters.
* - "%{...}" is a constant read-only expression, evaluated just once
* during initialization
* - "${...}" refers to a read-only expression, which is evaluated during
* load, after the base class load() if any.
* - "@{...}" is an "early load" read-only expression, which is evaluated
* during load, before the base class load() if any. If there is no
* base class load this is equivalent to "${...}"
* - An identifier refers to a property of the template object, accessed by
* capitalizing the identifier and prepending "get", "is", or "set" according
* to standard bean accessor rules. Any view with an id="x" attribute is also
* available using the assigned id as an identifier.
* - Dotted expressions can be used to fetch sub-properties; e.g. "a.b.c"
* translates to "this.getA().getB().getC()". The last property may
* be asynchronous, but not the intermediate ones (until support for that is
* added, I suppose).
* - Boolean expressions can be constructed using &&, ||, !=, ==, and !.
* - It also supports the aliases "and", "or", and "not" for &&, ||, and !.
* - Primitive int values can be made using numbers, like: 123
* - Primitive long values can be made using the L suffix, like: 1234L
* - Primitive double values can be made using a decimal number, like: 12.34
* - Literal strings can be entered using double quotes, like: "hello"
* - Various automatic conversions will be attempted
*
* Phases:
*
* - Construction: on creation, all the view objects are constructed and
* any properties of the views that don't take an expression are
* set.
* - Load: when it is time to display the data from the database, and after
* executing any action, load() is called, which reads all the expression
* values and stores them into properties on the views.
* - Save: before any action is performed, all the #{...} expressions are
* set to the matching property from the view object.
*
* Included tags:
*
* <k:view/> All the tags outside this are ignored; this is useful to take pieces of a complete
* HTML document and make a control out of it without making the page an invalid HTML document.
*
* <k:list value="#{...}"/> The children of the list are repeated once for each item in the
* collection
*
* <k:when test="${...}"/> If the condition is false, the contents are removed
*
* <k:when test="${! ...}"/> If the condition is true, the contents are removed
*
* UI pre-defined tags:
*
* <ui:button onclick="${...}"/>
*
* Plus new tags can be defined by creating a tag library file which specifies a mapping from a tag
* to a View class. The class is constructed and configured at runtime. See the included
* META-INF folder for examples.
*
*/
public class GeneratedHTMLViewGenerator extends BaseGenerator {
public static class ActionInfo {
final String action;
final boolean object;
final boolean async;
final boolean saveBefore;
final boolean loadAfter;
final String targetView;
final String origExpr;
final int timeout;
private ActionInfo(String origExpr, String action, boolean object, boolean async, String targetView, boolean saveBefore, boolean loadAfter, int timeout) {
this.origExpr = origExpr;
this.action = action;
this.object = object;
this.async = async;
this.saveBefore = saveBefore;
this.loadAfter = loadAfter;
this.targetView = targetView;
this.timeout = timeout;
}
/**
* One or more statements that perform the required action.
*
* When async == true this will return success or failure to
* a callback parameter named "callback".
*
* When async == false this will not invoke any callback.
*/
public String getAction() {
return action;
}
public boolean isAsync() {
return async;
}
public boolean issaveBefore() {
return saveBefore;
}
public boolean isloadAfter() {
return loadAfter;
}
public int getTimeout() {
return timeout;
}
/**
* @param callbackExpr AsyncCallback to return errors or success to
* @param callbackOptional True if the callback expression doesn't have to be called (i.e. group.<Void>member() or AsyncCallbackFactory.<Void>defaultNewInstance() calls)
* @return one or more statements separated by a semicolon to execute this action.
*/
public String toString(String callbackExpr, boolean callbackOptional) {
if(object || async || saveBefore) {
if(saveBefore || loadAfter) {
return "ViewAction.performOnView("+(action==null?"null":toActionCtor())+", "+targetView+", "+saveBefore+", "+loadAfter+(timeout>0?", "+timeout:"")+", "+callbackExpr+");";
} else if(action == null) {
return "";
} else {
return toActionCtor()+".perform("+callbackExpr+");";
}
} else {
String syncAction=action;
if(syncAction == null) syncAction = "";
else syncAction = "try { "+syncAction+" } catch(Throwable t) { "+callbackExpr+".onFailure(t); return; }";
if(loadAfter) {
// Not async, and no load before
return syncAction+targetView+".load("+callbackExpr+");";
} else if(callbackOptional) {
// Not async, no load before, no save after
return syncAction;
} else {
// Not async, no load before, no save after, still have to call the callback, though.
return syncAction+callbackExpr+".onSuccess(null);";
}
}
}
public String toViewAction() {
if(saveBefore || loadAfter)
return "new ViewAction("+toActionCtor()+", "+targetView+", "+saveBefore+", "+loadAfter+(timeout>0?", "+timeout:"")+")";
else
return toActionCtor();
}
private String toActionCtor() {
String actionObj = object ? action
: async ? "new Action(\""+escape(origExpr)+"\") { public void perform(AsyncCallback callback) { "+(timeout>0?"if(callback instanceof AsyncCallbackExtensions) ((AsyncCallbackExtensions)callback).resetTimeout("+timeout+"); ":"")+action+" }}"
: "new Action(\""+escape(origExpr)+"\") { public void perform(AsyncCallback callback) { try { "+action+" callback.onSuccess(null); } catch(Throwable t) { callback.onFailure(t); }}}";
return actionObj;
}
}
protected static String escapeMultiline(String input) {
return escape(input).replace("\\n", "\\n\"+\n\"");
}
public static interface TagHandlerApi {
}
public static interface TagHandler {
}
public static class SimpleTagHandler implements TagHandler {
final String contentAttribute;
final String viewClassName;
final Map<String,String> defaults;
public SimpleTagHandler(String viewClassName, String contentAttribute, Map<String, String> defaults) {
super();
this.viewClassName = viewClassName;
this.contentAttribute = contentAttribute;
this.defaults = defaults;
}
public GeneratorTypeInfo getViewClass(TypeOracle types) throws NotFoundException {
return JTypeWrapper.wrap(types.getType(getViewClassName()));
}
public String getContentAttribute() {
return contentAttribute;
}
public String getViewClassName() {
return viewClassName;
}
public Map<String, String> getDefaults() {
return defaults;
}
}
public static class TagLibrary {
final HashMap<String,TagHandler> tags = new HashMap<String,TagHandler>();
void addSimpleTagHandler(String tag, String viewClassName, String contentAttribute, Map<String, String> defaults) {
tags.put(tag, new SimpleTagHandler(viewClassName, contentAttribute, defaults));
}
public TagHandler getHandler(String tag) {
return tags.get(tag);
}
}
/**
* Provide access to a non-async value in transition in order
* to perform some kind of transformation on it.
* @author dobes
*
*/
public static class OperatorInfo {
public String onGetExpr(String expr) throws UnableToCompleteException {
return expr;
}
public String onSetExpr(String expr) throws UnableToCompleteException {
return expr;
}
}
// static class Prof {
// static String msg;
// static long startTime;
// static long elapsed() { return (new Date().getTime() - startTime); }
// static void start(String msg) {
// if(msg == null) {
// long t = elapsed();
// if(t > 100) {
// System.out.println(elapsed()+"ms ...");
// }
// }
// stop();
// Prof.msg = msg;
// }
// static void stop() {
// if(msg != null) {
// System.out.println(elapsed()+"ms - "+msg);
// msg = null;
// }
// Prof.startTime = new Date().getTime();
// }
// }
public static class GeneratorInstance extends BaseGenerator.GeneratorInstance {
public static final String KIYAA_CORE_TAGS_NAMESPACE = "http://habitsoft.com/kiyaa/core";
public static final String KIYAA_VIEW_TAGS_NAMESPACE = "http://habitsoft.com/kiyaa/ui";
public static final String SUBVIEW_CLASS_NAME_ATTRIBUTE = "subviewClassName";
public static final String PARENT_VIEW_FIELD_NAME = "_pv";
public static final String ROOT_VIEW_FIELD_NAME = "_root";
static final String XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
static boolean tagLibrariesLoaded=false;
static long lastTagLibraryLoad = 0;
static HashMap<String, TagLibrary> tagLibraries = new HashMap<String, TagLibrary>();
static HashMap<String, String> namespaces = new HashMap<String, String>();
protected ClassGenerator rootClassGenerator;
protected Element rootElement;
protected int subviewNumber;
public static class SubviewToGenerate {
public String name;
public Element element;
public GeneratedClassInfo parentViewClass;
public SubviewToGenerate(String name, Element element, GeneratedClassInfo parentViewClass) {
super();
this.name = name;
this.element = element;
this.parentViewClass = parentViewClass;
}
}
protected LinkedList<SubviewToGenerate> subviewsToGenerate = new LinkedList<SubviewToGenerate>();
@Override
public void init() throws UnableToCompleteException {
super.init();
// Caching these classes seem to occasionally create some weirdness; we need to
// re-load the tag libraries each time there is a new compile operation, or we
// should change it so we can "refresh" them without reparsing the xml files.
if(!tagLibrariesLoaded) {
loadTagLibraries();
tagLibrariesLoaded = true;
lastTagLibraryLoad = System.currentTimeMillis();
} else {
//refreshTagLibraryClasses();
}
rootClassGenerator = new ClassGenerator();
String templatePath = getSimpleClassName(baseType, ".") + ".xhtml";
final TemplatePath annotation = baseType.getAnnotation(TemplatePath.class);
if(annotation != null) {
templatePath = annotation.value();
//System.out.println("Found TemplatePath annotation on "+baseType+" with value "+templatePath);
}
rootElement = loadAndParseTemplate(templatePath);
if (rootElement.getAttribute("with-model") != null) {
final String modelViewClassName = ModelView.class.getName();
this.composerFactory.addImplementedInterface(modelViewClassName);
}
}
protected Element loadAndParseTemplate(String templatePath) throws Error,
UnableToCompleteException {
XMLReader reader;
try {
reader = XMLReaderFactory.createXMLReader();
} catch (SAXException caught1) {
throw new Error(caught1);
}
// Load XHTML declaration from the jar file, otherwise fail if someone wants to load a DTD
reader.setEntityResolver(new XhtmlEntityResolver(new FailingEntityResolver()));
nu.xom.Builder b = new nu.xom.Builder(reader);
Document d;
java.io.File f;
JClassType topLevelClass = baseType;
while(topLevelClass.getEnclosingType() != null) topLevelClass = topLevelClass.getEnclosingType();
try {
Class<?> clazzInstance = Class.forName(topLevelClass.getQualifiedSourceName());
URL resource = clazzInstance.getResource(templatePath);
if(resource == null) {
logger.log(TreeLogger.ERROR, "No template found at "+templatePath);
throw new UnableToCompleteException();
}
f = new File(resource.toURI());
} catch (ClassNotFoundException caught1) {
logger.log(TreeLogger.ERROR, "Couldn't find class "+topLevelClass+" in order to determine path to template file.", caught1);
throw new UnableToCompleteException();
} catch (URISyntaxException caught) {
logger.log(TreeLogger.ERROR, "Invalid template path: "+templatePath, caught);
throw new UnableToCompleteException();
}
//java.io.File f = new File(new File(baseType.getCompilationUnit().getLocation()).getParentFile(), templatePath);
try {
logger.log(TreeLogger.TRACE, "Looking for template as a file with path " + f.getPath(), null);
if (!f.exists()) {
logger.log(TreeLogger.WARN, "Looking for template as a file with path " + f.getPath()
+ " failed; looking for template as a resource with path " + templatePath, null);
final InputStream resourceAsStream = getClass().getResourceAsStream(templatePath);
try {
d = b.build(resourceAsStream);
} finally {
resourceAsStream.close();
}
} else {
d = b.build(f);
}
} catch (Exception caught) {
logger.log(TreeLogger.ERROR, "Failed to load template '" + templatePath + ": "+caught.toString(), null);
throw new UnableToCompleteException();
}
return getComponentRootElement(d);
}
protected void loadTagLibraries() throws UnableToCompleteException {
URL[] taglibs;
try {
taglibs = Classpath.search("META-INF/", ".kiyaa-taglib.xml");
} catch (IOException caught) {
logger.log(TreeLogger.ERROR, null, caught);
throw new UnableToCompleteException();
}
for (int i = 0; i < taglibs.length; i++) {
URL url = taglibs[i];
nu.xom.Builder b = new nu.xom.Builder(false);
try {
Document d = b.build(url.openStream());
Element root = d.getRootElement();
if (!root.getLocalName().equals("kiyaa-taglib")) {
continue;
}
String namespace = root.getFirstChildElement("namespace").getValue();
String packagePrefix = "";
try {
packagePrefix = root.getFirstChildElement("package").getValue() + ".";
} catch (NullPointerException npe) {
}
if(tagLibraries.get(namespace) != null) {
//logger.log(TreeLogger.WARN, "Namespace "+namespace+" defined in multiple taglib files, including "+url, null);
continue;
}
TagLibrary library = new TagLibrary();
Elements tags = root.getChildElements("tag");
for (int j = 0; j < tags.size(); j++) {
Element tag = tags.get(j);
String tagClass = packagePrefix + tag.getFirstChildElement("tag-class").getValue();
String tagName = tag.getFirstChildElement("tag-name").getValue();
final Element contentAttributeElement = tag.getFirstChildElement("content-attribute");
String contentAttribute = contentAttributeElement==null?null:contentAttributeElement.getValue();
Map<String,String> defaults = new TreeMap<String,String>();
Elements defaultElements = tag.getChildElements("default");
for(int k=0; k < defaultElements.size(); k++) {
Element defaultElt = defaultElements.get(k);
String property = defaultElt.getAttributeValue("for");
String value = defaultElt.getValue();
defaults.put(property, value);
}
library.addSimpleTagHandler(tagName, tagClass, contentAttribute, defaults);
}
tagLibraries.put(namespace, library);
} catch (Throwable caught) {
logger.log(TreeLogger.WARN, "Failed to parse taglib at " + url, caught);
}
}
}
@Override
protected void addImports() {
composerFactory.addImport("com.google.gwt.core.client.*");
composerFactory.addImport("com.google.gwt.user.client.*");
composerFactory.addImport("com.google.gwt.user.client.rpc.AsyncCallback");
/*
composerFactory.addImport("com.google.gwt.user.client.ui.Widget");
composerFactory.addImport("com.google.gwt.user.client.ui.ClickListener");
composerFactory.addImport("com.google.gwt.user.client.ui.ChangeListener");
composerFactory.addImport("com.google.gwt.user.client.ui.FocusListener");
composerFactory.addImport("com.google.gwt.user.client.ui.FlowPanel");
*/
composerFactory.addImport("com.google.gwt.user.client.ui.*");
composerFactory.addImport("com.habitsoft.kiyaa.widgets.*");
composerFactory.addImport("com.habitsoft.kiyaa.metamodel.*");
composerFactory.addImport("com.habitsoft.kiyaa.views.*");
composerFactory.addImport("com.habitsoft.kiyaa.util.*");
}
private Element getComponentRootElement(Document d) {
Element rootElement = d.getRootElement();
XPathContext context = new XPathContext("k", KIYAA_CORE_TAGS_NAMESPACE);
Nodes components = d.query("//k:view", context);
if (components.size() > 0) {
if (components.size() > 1) {
logger.log(TreeLogger.WARN, "Found more than one component; only the first will be used", null);
}
rootElement = (Element) components.get(0);
}
return rootElement;
}
@Override
protected void generateClassBody() throws UnableToCompleteException {
GeneratedClassInfo rootViewClass = new GeneratedClassInfo(implName, JClassTypeWrapper.wrap(baseType));
rootClassGenerator.generateClassBody(rootElement, rootViewClass, null, rootViewClass);
// Now generate the subview classes create as part of creating those views
generateSubviewClasses(rootViewClass);
}
private void generateSubviewClasses(GeneratedClassInfo rootViewClass) throws UnableToCompleteException {
while(!subviewsToGenerate.isEmpty()) {
SubviewToGenerate sv = subviewsToGenerate.removeFirst();
Element elem = sv.element;
String subviewClassName = sv.name;
pushLogger("Inside subview element "+elem.getQualifiedName()+" class name "+subviewClassName);
try {
boolean isModelView = elem.getAttribute("with-model") != null;
sw.println("protected static class " + subviewClassName
+ " implements "+(isModelView?"ModelView":"View")+" {");
sw.indent();
GeneratedClassInfo genClass = new GeneratedInnerClassInfo(subviewClassName, rootViewClass, commonTypes.object, true);
if(isModelView)
genClass.addImplementedInterface(getType(ModelView.class.getName()));
new ClassGenerator().generateClassBody(elem, genClass, sv.parentViewClass, rootViewClass);
sw.outdent();
sw.println("}");
} finally {
popLogger();
}
}
}
class ClassGenerator {
protected HashMap<String, Element> insertedViews = new HashMap<String, Element>();
protected HashMap<String, String> insertedText = new HashMap<String, String>();
protected HashMap<String, String> values = new HashMap<String, String>();
protected HashMap<String,ActionInfo> actions = new HashMap<String, ActionInfo>();
protected ArrayList<String> memberDecls = new ArrayList<String>();
protected ArrayList<String> calculations = new ArrayList<String>();
protected ArrayList<String> asyncProxies = new ArrayList<String>();
protected ArrayList<String> earlyLoads = new ArrayList<String>();
protected ArrayList<String> earlyAsyncLoads = new ArrayList<String>();
protected ArrayList<String> loads = new ArrayList<String>();
protected ArrayList<String> asyncLoads = new ArrayList<String>();
protected ArrayList<String> subviewLoads = new ArrayList<String>();
protected ArrayList<String> saves = new ArrayList<String>();
protected ArrayList<String> clearFields = new ArrayList<String>();
protected LinkedHashSet<String> fieldNames = new LinkedHashSet<String>();
protected GeneratorTypeInfo myModelClass;
protected String myModelVarName;
protected final ArrayList<SubviewInfo> subviews = new ArrayList<SubviewInfo>();
protected boolean hasHtml=false;
protected boolean useInnerHTML=false;
protected Element myRootElement;
protected GeneratedClassInfo myClass;
protected GeneratedClassInfo parentViewClass;
protected GeneratedClassInfo rootViewClass;
private void generateClassBody(Element rootElement, GeneratedClassInfo myClass, GeneratedClassInfo parentViewClass, GeneratedClassInfo rootViewClass)
throws UnableToCompleteException {
this.myRootElement = rootElement;
this.myClass = myClass;
this.parentViewClass = parentViewClass;
this.rootViewClass = rootViewClass;
String withModel = rootElement.getAttributeValue("with-model");
if (withModel != null) {
String[] pieces = withModel.split("\\s+");
String modelTypeName;
if (pieces.length == 1) {
modelTypeName = "java.lang.Object";
myModelVarName = pieces[0];
} else {
modelTypeName = pieces[0];
myModelVarName = pieces[1];
}
this.myModelClass = getType(modelTypeName);
generateField(myModelVarName, myModelClass);
} else if (myClass.implementsInterface(commonTypes.modelView)) {
logger.log(TreeLogger.WARN, "Generated views that implement ModelView should define"
+ " an attribute with-model='Type modelName'"
+ " on their root element ("+rootElement.getQualifiedName()+"), or implement"
+ " getModel/setModel in the base class (" + myClass.getName() + ")", null);
}
String withVars = rootElement.getAttributeValue("with-vars");
if(withVars == null) withVars = rootElement.getAttributeValue("with-var");
if(withVars != null) {
String[] vars = withVars.split("\\s*[,;]\\s*");
for (int i = 0; i < vars.length; i++) {
String var = vars[i];
String[] pieces = var.split("\\s+");
String varTypeName;
String fieldName;
if (pieces.length == 1) {
varTypeName = "java.lang.Object";
fieldName = pieces[0];
} else {
varTypeName = pieces[0];
fieldName = pieces[1];
}
JType fieldType;
try {
fieldType = types.parse(varTypeName);
} catch(NotFoundException nfe) {
fieldType = JPrimitiveType.valueOf(varTypeName);
if(fieldType == null) {
logger.log(TreeLogger.ERROR, "Can't find any type matching "+varTypeName, null);
throw new UnableToCompleteException();
}
} catch (TypeOracleException caught) {
logger.log(TreeLogger.ERROR, "Can't find any type matching "+varTypeName, null);
throw new UnableToCompleteException();
}
generateField(fieldName, JTypeWrapper.wrap(fieldType));
}
}
if(parentViewClass != null) {
generateField(PARENT_VIEW_FIELD_NAME, parentViewClass);
generateField(ROOT_VIEW_FIELD_NAME, rootViewClass);
}
if (myModelClass != null && !myModelVarName.equals("model")) {
myClass.addField(myModelVarName, myModelClass);
sw.println("public Object getModel() {");
sw.indentln("return " + myModelVarName + ";");
sw.println("}");
myClass.addGetter("model", RuntimeClassWrapper.OBJECT, "getModel", false);
}
sw.println("public void validate(AsyncCallback callback) {");
sw.indentln("callback.onFailure(new Error(\"Not implemented\"));");
sw.println("}");
myClass.addMethod("validate", PrimitiveTypeInfo.VOID, commonTypes.asyncCallback);
// Make clearFields available as an action
myClass.addMethod("clearFields", PrimitiveTypeInfo.VOID);
// Two results of this operation:
// the template string
// the code to insert widgets into it
sw.println("public void addFields() {");
sw.indent();
sw.println("try {");
sw.indent();
parseTree(rootElement);
for (SubviewInfo subviewInfo : subviews) {
generateSubview(subviewInfo);
}
sw.println("} catch(Throwable t) {");
sw.indent();
sw.println("String whichField;");
sw.print("if(!didInit) whichField = \"before didInit\";");
for (String field : fieldNames) {
sw.println("else if("+field+" == null) whichField = \"before "+field+"\";");
}
sw.println("else whichField = \"after "+(fieldNames.isEmpty()?"didInit":"last field")+"\";");
sw.println("throw new Error(\""+myClass.getName()+".addFields() threw an exception \"+whichField+\": \"+t, t);");
sw.outdent();
sw.println("}");
sw.outdent();
sw.println("}");
myClass.addMethod("addFields", PrimitiveTypeInfo.VOID);
generatePanel();
generateRemoveFields();
if(hasHtml)
generateTemplate(rootElement);
generateConstructor(rootElement);
generateMemberDecls();
generateLoad();
generateSave();
generateClearFields();
for (Iterator<String> i = asyncProxies.iterator(); i.hasNext();) {
String line = i.next();
sw.println(line);
}
for (Iterator<String> i = calculations.iterator(); i.hasNext();) {
String line = i.next();
sw.println(line);
}
if (myModelClass != null)
generateSetModel();
}
private void generatePanel() throws UnableToCompleteException {
String rootView = getRootView(false);
if(hasHtml) {
sw.println("protected ComplexHTMLPanel panel = new ComplexHTMLPanel();");
sw.println("protected void addWidget(String id, Widget widget) {");
sw.indentln("panel.replace("+rootView+".maybeEnsureDebugId(id, widget), id);");
sw.println("}");
} else {
sw.println("protected FlowPanel panel = new FlowPanel();");
sw.println("protected void addWidget(String id, Widget widget) {");
sw.indentln("panel.add("+rootView+".maybeEnsureDebugId(id, widget));");
sw.println("}");
}
if(parentViewClass == null) {
sw.println("protected <T extends View> T maybeEnsureDebugId(String id, T view) { maybeEnsureDebugId(id, view.getViewWidget()); return view; }");
sw.println("protected Widget maybeEnsureDebugId(String id, Widget widget) {");
sw.indent();
sw.println("try {");
sw.indent();
sw.println("String panelId = panel.getElement().getId();");
sw.println("if(panelId != null && panelId.startsWith(UIObject.DEBUG_ID_PREFIX))");
sw.indentln("widget.ensureDebugId(panelId.substring(UIObject.DEBUG_ID_PREFIX.length())+'-'+id);");
sw.outdent();
sw.println("} catch(Throwable t) {com.allen_sauer.gwt.log.client.Log.warn(\"ensureDebugId failed on \"+widget+\": \"+t);}");
sw.println("return widget;");
sw.outdent();
sw.println("}");
}
sw.println("protected void addView(String id, View view) {");
sw.indentln("addWidget(id, view.getViewWidget());");
sw.println("}");
sw.println("public Widget getViewWidget() {");
sw.indentln("return panel;");
sw.println("}");
}
protected void generateField(String fieldName, GeneratorTypeInfo fieldType) {
myClass.addField(fieldName, fieldType);
memberDecls.add(fieldType.getParameterizedQualifiedSourceName() + " " + fieldName + ";");
fieldNames.add(fieldName);
}
protected void generateConstructor(Element rootElement) throws UnableToCompleteException {
// Try to add what fields we can, although the model may be
// null, we might have other fields.
// In fact, this form might operate perfectly well on a null
// model.
String ctorArgs = "";
if(parentViewClass != null) {
ctorArgs = parentViewClass.getName()+" parentView, "+rootViewClass.getName()+" rootView";
}
sw.println("public " + myClass.getSimpleSourceName() + "("+ctorArgs+") {");
sw.indent();
if(parentViewClass != null) {
sw.println("this."+PARENT_VIEW_FIELD_NAME+" = parentView;");
sw.println("this."+ROOT_VIEW_FIELD_NAME+" = rootView;");
}
Attribute styleClass = rootElement.getAttribute("class");
if(styleClass != null) {
generateSetClass(getType(Widget.class.getName()), "panel", styleClass.getValue());
rootElement.removeAttribute(styleClass);
}
Attribute styleDefn = rootElement.getAttribute("style");
if(styleDefn != null) {
generateSetStyle(getType(Widget.class.getName()), "panel", "style", styleDefn.getValue());
rootElement.removeAttribute(styleDefn);
}
sw.outdent();
sw.println("}");
sw.println("boolean didInit=false;");
sw.println(myClass.getName()+" init() {");
sw.indent();
sw.println("if(didInit) return this;");
sw.println("didInit = true;");
if(hasHtml) {
if(useInnerHTML) {
sw.println("panel.setTemplate(TEMPLATE);");
} else {
sw.println("panel.setDomTemplate(generateDomTree());");
}
}
sw.println("addFields();");
// if(!usesModel && subviewClass) {
// sw.println("setModel(null, null);");
// }
if (parentViewClass == null)
generateAttributes(rootElement, JTypeWrapper.wrap(baseType), "this");
sw.println("return this;");
sw.outdent();
sw.println("}");
}
int elementCount=0;
private void generateTemplate(Element rootElement) {
if(useInnerHTML) {
StringBuffer templ = new StringBuffer();
for (int i = 0; i < rootElement.getChildCount(); i++) {
templ.append(rootElement.getChild(i).toXML());
}
sw.println("static final String TEMPLATE = \"" + escapeMultiline(templ.toString().trim()) + "\";");
} else {
sw.println("static private Element generateDomTree() {");
sw.indent();
String rootEltVar = writeElement(rootElement, null);
sw.println("return "+rootEltVar+";");
sw.outdent();
sw.println("}");
}
}
private String writeElement(Node child, String parentEltVar) {
if(child instanceof Element) {
Element e = (Element)child;
String eltVar = "e"+elementCount;
elementCount++;
String nodeName = e.getLocalName();
if(!e.getNamespaceURI().equals(XHTML_NAMESPACE))
nodeName = "DIV";
if("DIV".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createDiv();");
else if("DIV".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createSpan();");
else if("TABLE".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createTable();");
else if("TBODY".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createTBody();");
else if("THEAD".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createTHead();");
else if("TFOOT".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createTFoot();");
else if("TH".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createTH();");
else if("TD".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createTD();");
else if("TR".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createTR();");
else if("LABEL".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createLabel();");
else if("FIELDSET".equalsIgnoreCase(nodeName))
sw.println("Element "+eltVar+" = DOM.createFieldSet();");
else
sw.println("Element "+eltVar+" = DOM.createElement(\""+nodeName+"\");");
if(e.getNamespaceURI().equals(XHTML_NAMESPACE)) {
for(int i=0; i < e.getAttributeCount(); i++) {
Attribute a = e.getAttribute(i);
// TODO Attribute substitutions
String value = a.getValue();
if(value.matches("[$#%@]\\{.*\\}$"))
value = "";
if(a.getLocalName().equalsIgnoreCase("class")) {
sw.println("DOM.setElementProperty("+eltVar+", \"className\", \""+backslashEscape(value)+"\");");
} else {
sw.println("DOM.setElementAttribute("+eltVar+", \""+a.getLocalName()+"\", \""+backslashEscape(value)+"\");");
}
}
}
for(int i=0; i < e.getChildCount(); i++) {
sw.indent();
String childVar = writeElement(e.getChild(i), eltVar);
if(childVar != null)
sw.println("DOM.appendChild("+eltVar+", "+childVar+");");
sw.outdent();
}
return eltVar;
} else if(child instanceof Text) {
Text e = (Text)child;
Element parentElement = (Element)child.getParent();
String text = e.getValue();
if(!text.trim().isEmpty()) {
if(parentElement != null && parentElement.getChildCount() == 1 && parentEltVar != null) {
final String value = child.getValue();
if(!value.matches("[$#%@]\\{.*\\}$"))
sw.println("DOM.setInnerText("+parentEltVar+", \""+backslashEscape(value)+"\");");
return null;
} else {
//logger.log(TreeLogger.WARN, "Wrapping text into a SPAN .... "+text, null);
// TODO GWT doesn't include a createTextNode() so we have to put text into a span sometimes. However,
// that sucks because it can screw up the rendering of the page :(
String eltVar = "t"+elementCount;
elementCount++;
sw.println("Element "+eltVar+" = DOM.createElement(\"span\");");
sw.println("DOM.setInnerText("+eltVar+", \""+backslashEscape(text)+"\");");
return eltVar;
}
}
}
return null;
}
private void generateMemberDecls() {
for (String line : memberDecls) {
sw.println(line.replaceAll("<[^>]*>", "")); // strip generics
}
}
private void generateRemoveFields() {
sw.println("public void removeFields() {");
sw.indent();
sw.println("// TODO remove all fields");
sw.println("// didInit = false;");
sw.outdent();
sw.println("}");
}
private void generateLoad() throws UnableToCompleteException {
String name = "load";
final boolean nothingToLoad = loads.isEmpty() && subviewLoads.isEmpty() && asyncLoads.isEmpty() && earlyLoads.isEmpty() && earlyAsyncLoads.isEmpty();
boolean baseClassHasSyncLoad = myClass.getSuperclass().hasMethodMatching("load", false, PrimitiveTypeInfo.VOID);
boolean baseClassHasAsyncLoad = myClass.getSuperclass().hasMethodMatching("load", false, PrimitiveTypeInfo.VOID, commonTypes.asyncCallback);
final boolean baseClassLoads = baseClassHasSyncLoad || baseClassHasAsyncLoad;
if(baseClassHasAsyncLoad) {
if(nothingToLoad)
return;
sw.println("public void load(AsyncCallback<Void> callback) {");
sw.indent();
sw.println("try { init(); } catch(Throwable t) { callback.onFailure(t); return; }");
sw.println("callback = new AsyncCallbackDirectProxy<Void>(callback) { public void onSuccess(Void result) { loadImpl(takeCallback()); } };");
for(String load : earlyLoads) {
sw.println(load);
}
if(earlyAsyncLoads.isEmpty()) {
sw.println("super.load(callback);");
} else {
sw.println("AsyncCallbackGroup group = new AsyncCallbackGroup()");
for(String load : earlyAsyncLoads) {
sw.println(load);
}
sw.println("super.load(group.<Void>member());");
sw.println("group.ready(callback);");
}
sw.outdent();
sw.println("}");
name = "loadImpl";
}
sw.println("public void "+name+"(final AsyncCallback callback) {");
sw.indent();
if(!baseClassLoads)
sw.println("try { init(); } catch(Throwable t) { callback.onFailure(t); return; }");
if(baseClassHasSyncLoad)
sw.println("try { init(); super.load(); } catch(Throwable t) { callback.onFailure(t); return; }");
if(nothingToLoad) {
sw.println("callback.onSuccess(null);");
} else {
sw.println("try {");
sw.indent();
sw.println("final AsyncCallbackGroup group = new AsyncCallbackGroup(\""+myClass.getName()+".load()\");");
if(!baseClassHasAsyncLoad) {
for (String load : earlyAsyncLoads) {
sw.println(load);
}
for(String load : earlyLoads) {
sw.println(load);
}
}
for (String load : loads) {
sw.println(load);
}
for (String load : asyncLoads) {
sw.println(load);
}
if((asyncLoads.isEmpty() && (baseClassLoads || earlyAsyncLoads.isEmpty())) || subviewLoads.isEmpty()) {
for (String load : subviewLoads) {
sw.println(load);
}
sw.println("group.ready(callback);");
} else {
sw.println("group.ready(new AsyncCallbackDirectProxy<Void>(callback) {");
sw.indent();
sw.println("public void onSuccess(Void result) {");
sw.indent();
sw.println("final AsyncCallbackGroup group = new AsyncCallbackGroup(\""+myClass.getName()+".load() (subviews)\");");
sw.println("try {");
sw.indent();
for (String load : subviewLoads) {
sw.println(load);
}
sw.println("group.ready(callback);");
sw.outdent();
sw.println("} catch(Throwable t) {");
sw.indentln("callback.onFailure(t);");
sw.println("}");
sw.outdent();
sw.println("}");
sw.outdent();
sw.println("});");
}
sw.outdent();
sw.println("} catch(Throwable t) {");
sw.indentln("callback.onFailure(t);");
sw.println("}");
}
sw.outdent();
sw.println("}");
}
private void generateSave() throws UnableToCompleteException {
String name;
boolean baseClassHasSave=myClass.getSuperclass().hasMethodMatching("save", false, PrimitiveTypeInfo.VOID, commonTypes.asyncCallback);
boolean nothingToSave = saves.isEmpty();
if(baseClassHasSave) {
if(nothingToSave)
return;
name="saveImpl";
sw.println("public void save(AsyncCallback<Void> callback) {");
sw.indent();
sw.println("super.save(new AsyncCallbackDirectProxy<Void>(callback, \""+myClass.getName()+".save()\") { public void onSuccess(Void result) { "+name+"(takeCallback()); } });");
sw.outdent();
sw.println("}");
} else {
name = "save";
}
sw.println("public void "+name+"(final AsyncCallback<Void> callback) {");
sw.indent();
if(nothingToSave) {
sw.println("callback.onSuccess(null);");
} else {
sw.println("if(!didInit) return;");
sw.println("try {");
sw.indent();
sw.println("AsyncCallbackGroup group = new AsyncCallbackGroup(\""+myClass.getName()+".save()\");");
for (String save : saves) {
sw.println(save);
}
sw.println("group.ready(callback);");
sw.outdent();
sw.println("} catch(Throwable t) {");
sw.indentln("callback.onFailure(t);");
sw.println("}");
}
sw.outdent();
sw.println("}");
}
private void generateClearFields() throws UnableToCompleteException {
sw.println("public void clearFields() {");
sw.indent();
sw.println("if(!didInit) return;");
boolean baseClassHasClearFields = myClass.getSuperclass().hasMethodMatching("clearFields", false, PrimitiveTypeInfo.VOID);
if(baseClassHasClearFields) {
sw.println("super.clearFields();");
}
for (String clearField : clearFields) {
sw.println(clearField);
}
sw.outdent();
sw.println("}");
}
private void generateSetModel() throws UnableToCompleteException {
sw.println("public void setModel(final Object model, AsyncCallback callback) {");
sw.indent();
sw.println("try {");
sw.indent();
sw.println("init();");
sw.println("if(model == null) { callback.onFailure(new NullPointerException()); return; }");
sw.println("this." + myModelVarName + " = (" + myModelClass.getName() + ") model;");
// sw.println("AsyncCallbackGroup group = new AsyncCallbackGroup();");
// for (String call : setModels) {
// sw.println(call);
// }
// sw.println("group.ready(callback);");
sw.println("load(callback);");
sw.outdent();
sw.println("} catch(Throwable t) {");
sw.indentln("callback.onFailure(t);");
sw.println("}");
sw.outdent();
sw.println("}");
}
protected void parseTree(Element rootElement) throws UnableToCompleteException {
for (int i = 0; i < rootElement.getNamespaceDeclarationCount(); i++) {
String prefix = rootElement.getNamespacePrefix(i);
String uri = rootElement.getNamespaceURI(prefix);
namespaces.put(prefix, uri);
}
pushLogger("Inside tag "+rootElement.getQualifiedName());
try {
for (int i = 0; i < rootElement.getChildCount(); i++) {
Node childNode = rootElement.getChild(i);
if(childNode instanceof Text) {
if(!childNode.getValue().trim().isEmpty())
hasHtml = true;
handleTextSubstitution((Text)childNode);
continue;
}
if (!(childNode instanceof Element)) {
hasHtml = true;
continue;
}
Element elem = (Element) childNode;
String[] namespaceAndTag = getNamespaceAndTag(elem);
String namespace = namespaceAndTag[0];
String tag = namespaceAndTag[1];
if (namespace.equals(XHTML_NAMESPACE)) {
hasHtml = true;
if(useInnerHTML) {
if (!"br".equals(tag) && !"hr".equals(tag) && !"input".equals(tag) && !"button".equals(tag)) {
if (elem.getChildCount() == 0) {
elem.appendChild("");
}
}
}
parseTree(elem);
continue;
}
if(KIYAA_CORE_TAGS_NAMESPACE.equalsIgnoreCase(namespace) && "insert".equals(tag)) {
String templatePath = elem.getAttributeValue("templatePath");
Element newElem = (Element) loadAndParseTemplate(templatePath).copy();
rootElement.replaceChild(elem, newElem);
parseTree(newElem);
continue;
}
GeneratorTypeInfo tagClass = getTagClass(elem);
Element viewElem = new Element(XHTML_NAMESPACE.equals(elem.getNamespaceURI())?elem.getLocalName():"div", XHTML_NAMESPACE);
String id = identifier(elem.getAttributeValue("id"));
if (id == null)
id = "view" + insertedViews.size();
else {
myClass.addField(id, tagClass);
}
viewElem.addAttribute(new Attribute("id", id));
viewElem.appendChild("");
// Need open/close tag, innerHTML doesn't support XML
rootElement.replaceChild(elem, viewElem);
insertedViews.put(id, elem);
if (tagClass != null) {
subviews.add(new SubviewInfo(elem, id, namespace, tag, tagClass));
}
}
} finally {
popLogger();
}
}
protected String[] getNamespaceAndTag(Element elem) {
String[] namespaceAndTag;
String ns = elem.getNamespaceURI();
String ln = elem.getLocalName();
String kc = elem.getAttributeValue("kc");
if (kc != null) {
String[] split = kc.split(":", 2);
if (split.length == 1) {
ln = kc;
} else {
ns = namespaces.get(split[0]);
ln = split[1];
}
}
namespaceAndTag = new String[] {ns,ln};
return namespaceAndTag;
}
protected GeneratorTypeInfo getTagClass(Element elem) throws UnableToCompleteException {
String[] namespaceAndTag = getNamespaceAndTag(elem);
String namespace = namespaceAndTag[0];
String tag = namespaceAndTag[1];
GeneratorTypeInfo tagClass = null;
if (tag.equals("custom") && namespace.equals(KIYAA_VIEW_TAGS_NAMESPACE)) {
String viewClassName = elem.getAttributeValue("viewClass");
if (viewClassName != null) {
try {
tagClass = getType(viewClassName);
} catch(UnableToCompleteException e) {
logger.log(TreeLogger.ERROR, "Couldn't find custom view class: " + viewClassName,
null);
throw e;
}
} else {
logger.log(TreeLogger.ERROR, "custom tag must specify viewClass=, in: " + elem.toXML(),
null);
throw new UnableToCompleteException();
}
} else {
TagLibrary tagLibrary = tagLibraries.get(namespace);
if (tagLibrary != null) {
TagHandler th = tagLibrary.getHandler(tag);
if(th == null) {
logger.log(TreeLogger.ERROR, "No tag '" + tag + "' found in tag library "
+ namespace, null);
throw new UnableToCompleteException();
} else /* if(th instanceof SimpleTagHandler) */ {
final SimpleTagHandler simpleTagHandler = ((SimpleTagHandler)th);
try {
tagClass = simpleTagHandler.getViewClass(types);
} catch (NotFoundException caught) {
logger.log(TreeLogger.ERROR, "No class found for tag '" + tag + "' found in tag library "
+ namespace, caught);
throw new UnableToCompleteException();
}
String contentAttribute = simpleTagHandler.getContentAttribute();
if(contentAttribute != null && elem.getAttribute(contentAttribute) == null && elem.getValue().trim().length()>0) {
elem.addAttribute(new Attribute(contentAttribute, elem.getValue()));
}
// Apply default attributes
for(Map.Entry<String,String> e : simpleTagHandler.getDefaults().entrySet()) {
if(elem.getAttribute(e.getKey()) == null)
elem.addAttribute(new Attribute(e.getKey(), e.getValue()));
}
}
} else {
logger.log(TreeLogger.WARN, "Namespace \"" + namespace
+ "\" not recognized, and not the XHTML namespace (" + XHTML_NAMESPACE
+ ") while looking for class for tag "+tag, null);
}
}
return tagClass;
}
private void handleTextSubstitution(Text childNode) throws UnableToCompleteException {
String text = collapseWhitespace(childNode.getValue());
try {
pushLogger("Processing text: \""+text+"\"");
childNode.setValue(text);
Matcher matcher = Pattern.compile("[$#%@]\\{((\\\\\\}|[^}])*)\\}").matcher(text);
int textMarker = 0;
boolean areConstant=true;
boolean areEarly=true;
StringBuffer stringBuildExpr = new StringBuffer();
while(matcher.find()) {
if(stringBuildExpr.length() > 0) stringBuildExpr.append(" + ");
if(matcher.start() > 0)
stringBuildExpr.append('"').append(backslashEscape(text.substring(textMarker, matcher.start()))).append("\" + ");
String path = matcher.group(1);
char typeChar = matcher.group().charAt(0);
if(typeChar != '%') areConstant = false;
if(typeChar != '@' && typeChar != '%') areEarly = false; // constant is as good as early :-)
stringBuildExpr.append(path);
textMarker = matcher.end();
}
ParentNode parent = childNode.getParent();
if(stringBuildExpr.length() > 0) {
String id;
// TODO If the parent element is the root of the view, this doesn't work right
if(parent != myRootElement && parent.getChildCount() == 1) {
Element parentElement = ((Element)parent);
id = parentElement.getAttributeValue("id");
if(id == null) {
id = "interpolation"+insertedText.size();
parentElement.addAttribute(new Attribute("id", id));
insertedText.put(id, id);
}
} else {
id = "interpolation"+insertedText.size();
insertedText.put(id, id);
Element element = new Element("span", XHTML_NAMESPACE);
element.addAttribute(new Attribute("id", id));
element.appendChild("$$$");
parent.replaceChild(childNode, element);
}
if(textMarker < text.length()) {
stringBuildExpr.append(" + \"").append(backslashEscape(text.substring(textMarker))).append('"');
}
String setterName = "set" + capitalize(id);
memberDecls.add("public final void "+ setterName + "(String newValue) { panel.setText(\"" + id + "\", newValue); }");
myClass.addMethod(setterName, PrimitiveTypeInfo.VOID, RuntimeClassWrapper.STRING);
//System.out.println("Using string build expression: "+stringBuildExpr+" for "+text);
ExpressionInfo expr = findAccessors(stringBuildExpr.toString(), true, true);
if(expr == null) {
logger.log(TreeLogger.ERROR, "Unable to resolve expression: "+stringBuildExpr, null);
throw new UnableToCompleteException();
}
//System.out.println("Got getter: "+expr.getter);
ExpressionInfo textExpr = new ExpressionInfo(expr.getOriginalExpr(), id, setterName, RuntimeClassWrapper.STRING);
if((expr.isConstant() || areConstant) && expr.hasSynchronousGetter()) {
sw.println(textExpr.copyStatement(expr));
} else if(areEarly) {
if(expr.hasSynchronousGetter())
earlyLoads.add(textExpr.copyStatement(expr));
else
earlyAsyncLoads.add(textExpr.asyncCopyStatement(expr, "group.<Void>member()", true));
} else {
asyncLoads.add(textExpr.asyncCopyStatement(expr, "group.<Void>member()", true));
}
}
} finally {
popLogger();
}
}
private String collapseWhitespace(String value) {
return value.replaceAll("([\\s\n])\\s+", " ");
}
public class SubviewInfo {
public Element elem;
public String id;
public String namespace;
public String tag;
public GeneratorTypeInfo subviewClass;
public SubviewInfo(Element elem, String id, String namespace, String tag, GeneratorTypeInfo subviewClass) {
this.elem = elem;
this.id = id;
this.namespace = namespace;
this.tag = tag;
this.subviewClass = subviewClass;
}
}
protected void generateSubview(SubviewInfo sv) throws UnableToCompleteException {
// Some other kind of view
// just construct it and call the setters/getters
pushLogger("Generate subview of "+myClass+": "+sv.elem);
try {
generateField(sv.id, sv.subviewClass);
if (sv.subviewClass.isAbstract()) {
sw.println(sv.id + " = (" + sv.subviewClass.getParameterizedQualifiedSourceName() + ") GWT.create("
+ sv.subviewClass.getName() + ".class);");
} else {
boolean takesTag = sv.subviewClass.implementsInterface(JClassTypeWrapper.wrap(types.findType("com.habitsoft.kiyaa.views.TakesElementName")));
sw.println(sv.id + " = new " + sv.subviewClass.getName() + (takesTag?"(\""+escape(sv.elem.getLocalName())+"\", \""+escape(sv.elem.getNamespaceURI())+"\");":"();"));
}
generateContents(sv.elem, sv.subviewClass, sv.id);
generateAttributes(sv.elem, sv.subviewClass, sv.id);
generateSubviewCommon(sv.elem, sv.id, sv.id, "this", sv.subviewClass, false);
} finally {
popLogger();
}
}
/**
* Generate code to pass along any load(), save(), and clearFields() calls
* to subviews that support those operations.
*/
private void generateSubviewCommon(Element elem, String id, String viewExpr, String modelExpr,
GeneratorTypeInfo viewClass, boolean readOnly) throws UnableToCompleteException {
boolean isWidget = viewClass.isSubclassOf(commonTypes.widget);
String addMethod = isWidget?"addWidget":"addView";
sw.println(addMethod+"(\"" + id + "\", " + viewExpr + ");");
boolean isView = viewClass.implementsInterface(commonTypes.view);
boolean hasLoad = isView || viewClass.hasMethodMatching("load", true, PrimitiveTypeInfo.VOID, commonTypes.asyncCallback);
if (hasLoad) {
subviewLoads.add(viewExpr + ".load(group.<Void>member());");
}
if (!readOnly) {
boolean hasSave = isView || viewClass.hasMethodMatching("save", true, PrimitiveTypeInfo.VOID, commonTypes.asyncCallback);
if (hasSave)
saves.add(viewExpr + ".save(group.<Void>member());");
}
boolean hasClearFields = isView || viewClass.hasMethodMatching("clearFields", true, PrimitiveTypeInfo.VOID);
if(hasClearFields) {
clearFields.add(viewExpr + ".clearFields();");
}
}
protected void generateAttributes(Element elem, GeneratorTypeInfo type, String name)
throws UnableToCompleteException {
for (int j = 0; j < elem.getAttributeCount(); j++) {
final Attribute attr = elem.getAttribute(j);
final String key = attr.getLocalName();
// These keys are taken care of in an earlier step
if ("viewClass".equals(key) || "id".equals(key) || "with-model".equals(key) || "with-vars".equals(key) || "with-var".equals(key) || "kc".equals(key))
continue;
// final String prefix = attr.getNamespacePrefix();
final String value = attr.getValue();
generateAttribute(type, name, key, value);
// Don't let "class" and "style" propagate to the subview since they'll be
// applied to THIS view
if("class".equals(key) || "style".equals(key)) {
elem.removeAttribute(attr);
j--;
}
}
}
private void generateAttribute(GeneratorTypeInfo type, String name, final String key, final String value)
throws UnableToCompleteException {
LocalTreeLogger.pushLogger(logger.branch(TreeLogger.INFO, "Attribute "+key+"='"+value+"' in element "+name+" which is a "+type));
try {
ExpressionInfo baseExpr = new ExpressionInfo(name, name, type, false);
ExpressionInfo attributeAccessors = findAccessors(baseExpr, key, true, false);
// Automatically propagate some properties to the getViewWidget()
if(attributeAccessors == null && key.matches("visible|width|height|title") && type.implementsInterface(commonTypes.view))
attributeAccessors = findAccessors(baseExpr, "viewWidget."+key, true, false);
if(key.equals("class")) {
if(type.implementsInterface(commonTypes.view))
attributeAccessors = findAccessors(baseExpr, "viewWidget.styleName", false, false);
else if(type.isSubclassOf(commonTypes.uiObject))
attributeAccessors = findAccessors(baseExpr, "styleName", false, false);
else
logger.log(TreeLogger.WARN, "Found attribute 'class' on something that isn't a View or Widget (a "+type+")");
}
boolean readOnly = false;
boolean constant = false;
boolean earlyLoad = false;
boolean isExpr=false;
String path = null;
ExpressionInfo pathAccessors = null;
if (((readOnly = (value.startsWith("${")
|| (earlyLoad = value.startsWith("@{"))
|| (constant = value.startsWith("%{"))))
|| value.startsWith("#{")) && value.endsWith("}")) {
path = value.substring(2, value.length() - 1).trim();
pathAccessors = findAccessors(path, false, true);
isExpr=true;
}
String valueExpr;
ActionInfo action;
if ("class".equals(key) && pathAccessors == null) {
generateSetClass(type, name, value);
} else if ("style".equals(key)) {
generateSetStyle(type, name, key, value);
} else if ("binding".equals(key)) {
generateBinding(type, name, value);
} else if (attributeAccessors == null) {
if ("onclick".equals(key)) {
generateOnClickHandler(type, name, value, pathAccessors);
} else if ("onchange".equals(key)) {
generateOnChangeListener(type, name, value, pathAccessors);
} else if ("onfocus".equals(key)) {
generateOnFocusListener(type, name, value, pathAccessors);
} else if ("onblur".equals(key)) {
generateOnBlurListener(type, name, value, pathAccessors);
} else if ("onPressEnter".equalsIgnoreCase(key)) {
generateKeyPressHandler(type, name, value, "KEY_ENTER");
} else if ("onPressSpace".equalsIgnoreCase(key)) {
generateKeyPressHandler(type, name, value, "' '");
} else if ("onPressEscape".equalsIgnoreCase(key)) {
generateKeyPressHandler(type, name, value, "KEY_ESCAPE");
} else if ("onKeyPress".equalsIgnoreCase(key)) {
generateKeyPressHandler(type, name, value, null);
} else {
logger.log(TreeLogger.ERROR, "Unable to find a property '" + key + "' in " + type + "; value is '" + value + "'", null);
throw new UnableToCompleteException();
}
} else if (!attributeAccessors.hasSetter()) {
logger.log(TreeLogger.ERROR, "Unable to find a setter for attribute '" + key + "' in "
+ type + "; value is '" + value + "', getter is "+attributeAccessors+" asyncGetter is "+attributeAccessors.getAsyncGetter(), null);
throw new UnableToCompleteException();
} else {
final GeneratorTypeInfo attributeType = attributeAccessors.getType();
if (attributeType.equals(commonTypes.action)
&& (pathAccessors == null || !pathAccessors.hasGetter())
&& (action = getAction(value, false)) != null) {
if(attributeAccessors.hasSynchronousSetter() == false) {
logger.log(TreeLogger.ERROR, "Async setters do not support for Actions yet.", null);
throw new UnableToCompleteException();
}
sw.println(attributeAccessors.callSetter(action.toViewAction()).toString());
} else if (path != null // If this is a ${...} or #{...}
&& commonTypes.value.equals(attributeType) // and the target attribute accepts a Value object
) {
valueExpr = getFieldValue(path);
if(valueExpr == null) {
logger.log(TreeLogger.ERROR, "Failed to evaluate expression to construct Value object for "+key+"="+path+" on "+type, null);
throw new UnableToCompleteException();
}
if(attributeAccessors.hasSynchronousSetter() == false) {
if(attributeAccessors.hasAsynchronousSetter())
logger.log(TreeLogger.ERROR, "Async setters not supported for Value yet; found use of async setter "+attributeAccessors.getAsyncSetter()+" for attribute "+key+" on "+type+" to store value "+path, null);
else
logger.log(TreeLogger.ERROR, "No setter found for attribute "+key+" on "+type, null);
throw new UnableToCompleteException();
}
sw.println(attributeAccessors.callSetter(valueExpr).toString());
} else if (pathAccessors != null && pathAccessors.hasGetter()) {
generateAttributeLoadSave(type, attributeAccessors, pathAccessors, readOnly, constant, earlyLoad);
} else if (path != null && (valueExpr = getFieldValue(path)) != null) {
logger.log(TreeLogger.WARN, "Using a Value "+valueExpr+"to read "+path+" for attribute "+key+"="+path);
ExpressionInfo valueAccessors = findAccessors(new ExpressionInfo(path, valueExpr, commonTypes.value, true), "value", true, false);
generateAttributeLoadSave(type, attributeAccessors, valueAccessors, readOnly, constant, earlyLoad);
} else if(isExpr) {
logger.log(TreeLogger.WARN, "Couldn't figure out how to set attribute "+key+" on "+type+"; couldn't find a getter for "+value, null);
} else if (attributeType.equals(getType("java.lang.String"))) {
ExpressionInfo valueAccessors = new ExpressionInfo(path, "\"" + backslashEscape(value) + "\"", getType("java.lang.String"), true);
generateAttributeLoadSave(type, attributeAccessors, valueAccessors, true, true, true);
} else if (attributeType.isEnum()) {
if(attributeType.getEnumMembers().contains(value)) {
ExpressionInfo valueAccessors = new ExpressionInfo(path, attributeType.getParameterizedQualifiedSourceName()+"."+value, attributeType, true);
generateAttributeLoadSave(type, attributeAccessors, valueAccessors, true, true, true);
} else {
logger.log(TreeLogger.ERROR, "Enum constant '" + value + "' not found in enum "+attributeType.getParameterizedQualifiedSourceName()+" for attribute "+key,
null);
throw new UnableToCompleteException();
}
} else if (attributeType.equals(getType("java.lang.Boolean"))) {
if (!"true".equals(value) && !"false".equals(value)) {
logger.log(TreeLogger.ERROR, "Boolean attribute '" + key + "' should be true or false; got '"+value+"'",
null);
throw new UnableToCompleteException();
}
ExpressionInfo valueAccessors = new ExpressionInfo(path, "Boolean." + value.toUpperCase(), attributeType, true);
generateAttributeLoadSave(type, attributeAccessors, valueAccessors, true, true, true);
} else if (attributeType.getName().equals("boolean")) {
if (!"true".equals(value) && !"false".equals(value)) {
logger.log(TreeLogger.ERROR, "Boolean attribute '" + key + "' should be true or false; got '"+value+"'",
null);
throw new UnableToCompleteException();
}
ExpressionInfo valueAccessors = new ExpressionInfo(path, value, attributeType, true);
generateAttributeLoadSave(type, attributeAccessors, valueAccessors, true, true, true);
} else if (attributeType.getName().equals("char")) {
ExpressionInfo valueAccessors = new ExpressionInfo(path, "'"+backslashEscape(value)+"'", attributeType, true);
generateAttributeLoadSave(type, attributeAccessors, valueAccessors, true, true, true);
} else if (attributeType.isPrimitive()) {
ExpressionInfo valueAccessors = new ExpressionInfo(path, value, attributeType, true);
generateAttributeLoadSave(type, attributeAccessors, valueAccessors, true, true, true);
} else if (attributeType.equals(getType("java.lang.Class"))) {
try {
ExpressionInfo valueAccessors = new ExpressionInfo(path, types.getType(value).getQualifiedSourceName()
+ ".class", attributeType, true);
generateAttributeLoadSave(type, attributeAccessors, valueAccessors, true, true, true);
} catch (NotFoundException caught) {
logger.log(TreeLogger.ERROR, "Unable to find class '" + value + "' for class attribute '"
+ key + "'", null);
throw new UnableToCompleteException();
}
} else {
if(path != null) {
logger.log(TreeLogger.WARN, "Couldn't figure out how to set attribute "+key+" on "+type+"; couldn't find a getter for "+value, null);
} else {
logger.log(TreeLogger.WARN, "Couldn't figure out how to set attribute "+key+" on "+type+" with value "+value+" (did you forget to use ${...}?)", null);
}
}
}
} finally {
LocalTreeLogger.popLogger();
}
}
private String generateSetStyle(GeneratorTypeInfo type, String name, final String key, final String value)
throws UnableToCompleteException {
// Assuming that we either have a View or a Widget ...
String widgetExpr;
if (type.implementsInterface(commonTypes.view)) {
widgetExpr = name + ".getViewWidget()";
} else if (type.isSubclassOf(commonTypes.widget)) {
widgetExpr = name;
} else {
logger.log(TreeLogger.ERROR, "Don't know how to set the style of a " + type, null);
throw new UnableToCompleteException();
}
sw.println("DOM.setElementAttribute(" + widgetExpr + ".getElement(), \"" + escape(key)
+ "\", \"" + escape(value) + "\");");
return widgetExpr;
}
private String generateSetClass(GeneratorTypeInfo type, String name, final String value)
throws UnableToCompleteException {
String widgetExpr;
if (type.implementsInterface(commonTypes.view)) {
widgetExpr = name + ".getViewWidget()";
} else if (type.isSubclassOf(commonTypes.widget)) {
widgetExpr = name;
} else {
logger.log(TreeLogger.ERROR, "Don't know how to set the style of a " + type, null);
throw new UnableToCompleteException();
}
String[] styleNames = value.split("\\s+");
for (int i = 0; i < styleNames.length; i++) {
String string = styleNames[i];
sw.println(widgetExpr + "."+(i==0?"set":"add")+"StyleName(\"" + escape(string) + "\");");
}
return widgetExpr;
}
private void generateBinding(GeneratorTypeInfo type, String name, String value)
throws UnableToCompleteException {
if(value.startsWith("${") || value.startsWith("#{") || value.startsWith("%{") || value.startsWith("@{")) value = value.substring(2);
if(value.endsWith("}")) value = value.substring(0, value.length()-1);
ExpressionInfo accessors = findAccessors(value, false, false);
if (accessors != null && accessors.hasSetter()) {
sw.println(accessors.copyStatement(new ExpressionInfo(name, name, type, false)));
} else {
logger.log(TreeLogger.WARN, "Unable to find a "+(accessors == null?"property":"setter method")+" for binding expression: " + value, null);
}
}
private ActionInfo generateOnChangeListener(GeneratorTypeInfo type, String name, final String value,
ExpressionInfo pathAccessors) throws UnableToCompleteException {
if(!type.implementsInterface(commonTypes.sourcesChangeEvents)) {
logger.log(TreeLogger.ERROR, "onchange attribute must be on a View/Widget" +
" that implements SourcesChangeEvents.", null);
throw new UnableToCompleteException();
}
ActionInfo actionExpr = getAction(value, true);
if(actionExpr == null) {
logger.log(TreeLogger.ERROR, "Unable to find action for "+value+" for an onchange handler", null);
throw new UnableToCompleteException();
}
attachWidgetEventListener(name, actionExpr, "ChangeListener", "onChange(Widget sender)", null, value);
return actionExpr;
}
private ActionInfo generateOnFocusListener(GeneratorTypeInfo type, String name, final String value,
ExpressionInfo pathAccessors) throws UnableToCompleteException {
if(!type.implementsInterface(commonTypes.sourcesFocusEvents)) {
logger.log(TreeLogger.ERROR, "onfocus attribute must be on a View/Widget" +
" that implements SourcesFocusEvents.", null);
throw new UnableToCompleteException();
}
ActionInfo actionExpr = getAction(value, true);
if(actionExpr == null) {
logger.log(TreeLogger.ERROR, "Unable to find action for "+value+" for an onfocus handler", null);
throw new UnableToCompleteException();
}
sw.println(name + ".add" + "FocusListener" + "(new " + "FocusListener" + "() {");
sw.indent();
sw.println("public void " + "onFocus(Widget sender)" + " {");
sw.indent();
sw.println(actionExpr.toString("AsyncCallbackFactory.<Void>defaultNewInstance()", true));
sw.outdent();
sw.println("}");
sw.println("public void " + "onLostFocus(Widget sender)" + " { }");
sw.outdent();
sw.println("});");
return actionExpr;
}
private ActionInfo generateOnBlurListener(GeneratorTypeInfo type, String name, final String value,
ExpressionInfo pathAccessors) throws UnableToCompleteException {
if(!type.implementsInterface(commonTypes.sourcesFocusEvents)) {
logger.log(TreeLogger.ERROR, "onblur attribute must be on a View/Widget" +
" that implements SourcesFocusEvents.", null);
throw new UnableToCompleteException();
}
ActionInfo actionExpr = getAction(value, true);
if(actionExpr == null) {
logger.log(TreeLogger.ERROR, "Unable to find action for "+value+" for an onblur handler", null);
throw new UnableToCompleteException();
}
sw.println(name + ".add" + "FocusListener" + "(new " + "FocusListener" + "() {");
sw.indent();
sw.println("public void " + "onLostFocus(Widget sender)" + " {");
sw.indent();
sw.println(actionExpr.toString("AsyncCallbackFactory.<Void>defaultNewInstance()", true));
sw.outdent();
sw.println("}");
sw.println("public void " + "onFocus(Widget sender)" + " { }");
sw.outdent();
sw.println("});");
return actionExpr;
}
private ActionInfo generateOnClickHandler(GeneratorTypeInfo type, String name, final String value,
ExpressionInfo pathAccessors) throws UnableToCompleteException {
if(!type.implementsInterface(commonTypes.sourcesClickEvents)) {
logger.log(TreeLogger.ERROR, "onclick attribute must be on a View/Widget" +
" that implements SourceClickEvents.", null);
throw new UnableToCompleteException();
}
ActionInfo actionExpr = getAction(value, true);
if(actionExpr == null) {
logger.log(TreeLogger.ERROR, "Unable to find action for "+value
+" for an onclick handler", null);
throw new UnableToCompleteException();
}
attachWidgetEventListener(name, actionExpr, "ClickListener", "onClick(Widget sender)", null, value);
return actionExpr;
}
private ActionInfo generateKeyPressHandler(GeneratorTypeInfo type, String name, final String value, String keyName)
throws UnableToCompleteException {
if(!type.implementsInterface(commonTypes.sourcesClickEvents)) {
logger.log(TreeLogger.ERROR, name+" attribute must be on a View/Widget" +
" that implements SourcesKeyEvents.", null);
throw new UnableToCompleteException();
}
ActionInfo actionExpr = getAction(value, true);
if(actionExpr == null) {
logger.log(TreeLogger.ERROR, "Unable to find action for "+value
+" for a onPressXXX handler", null);
throw new UnableToCompleteException();
}
final String condition = keyName==null?null:"keyCode == "+keyName+" && (modifiers & ~MODIFIER_SHIFT) == 0";
attachWidgetEventListener(name, actionExpr, "KeyboardListenerAdapter", "onKeyPress(Widget sender, char keyCode, int modifiers)", condition, value);
return actionExpr;
}
protected void generateAttributeLoadSave(GeneratorTypeInfo type, ExpressionInfo attributeAccessors, ExpressionInfo pathAccessors,
boolean readOnly, boolean constant, boolean earlyLoad)
throws UnableToCompleteException {
LocalTreeLogger.pushLogger(logger.branch(TreeLogger.INFO, "Attribute load/save for "+attributeAccessors.setterString()+(readOnly?" = ":" <=> ")+pathAccessors));
try {
String loadExpr = attributeAccessors.asyncCopyStatement(pathAccessors, "group.<Void>member()", true);
// Put the value into the widget on load()
//if(attributeAccessors.getter != null && attributeAccessors.getType().equals(getType(String.class.getName())) && pathAccessors.getter != null) {
// It turns out that calling setText() and setValue to the same value is a high-cost operation
// loadExpr = "if(!"+attributeAccessors.getter+".equals("+pathAccessors.conversionExpr(attributeAccessors.getType())+")) { "+loadExpr+" }";
//}
// Constant values stored to a non-async setter are set during initialization
boolean constantLoad = (constant || pathAccessors.isConstant())
&& pathAccessors.hasSynchronousGetter()
&& attributeAccessors.hasSynchronousSetter();
boolean asyncLoad = pathAccessors.hasAsynchronousGetter() || attributeAccessors.hasAsynchronousSetter();
if(constantLoad)
sw.println(loadExpr);
else if(earlyLoad) {
if(asyncLoad)
earlyAsyncLoads.add(loadExpr);
else
earlyLoads.add(loadExpr);
} else if(asyncLoad)
asyncLoads.add(loadExpr);
else
loads.add(loadExpr);
if (!(readOnly || constant)) {
if (!attributeAccessors.hasGetter()) {
logger.log(TreeLogger.ERROR, "Missing matching getter for attribute '"+attributeAccessors.getSetter()+"' on "+type+"; use ${} to set the value only. Value is "+pathAccessors, null);
throw new UnableToCompleteException();
} else if(!pathAccessors.hasSetter()) {
logger.log(TreeLogger.ERROR, "Missing matching setter for '" + pathAccessors + "' for attribute '"+attributeAccessors.getSetter()+"' on "+type+"; use ${} to set the value only.", null);
throw new UnableToCompleteException();
}
// If it's a two-way affair, copy the value back on save()
String saveExpr = pathAccessors.asyncCopyStatement(attributeAccessors, "group.<Void>member()", true);
saves.add(saveExpr);
}
} finally {
LocalTreeLogger.popLogger();
}
}
private void attachWidgetEventListener(String name, ActionInfo action, String listenerClass,
String listenerMethod, String condition, String actionStr) {
String adder = "add"+listenerClass;
if(adder.endsWith("Adapter")) adder = adder.substring(0, adder.length()-7);
sw.println(name + "." + adder + "(new " + listenerClass + "() {");
sw.indent();
sw.println("public void " + listenerMethod + " {");
sw.indent();
if(condition != null) {
sw.println("if("+condition+")");
sw.indent();
}
sw.println(action.toString("AsyncCallbackFactory.<Void>defaultNewInstance()", true));
if(condition != null)
sw.outdent();
sw.outdent();
sw.println("}");
sw.outdent();
sw.println("});");
}
private void generateContents(Element elem, GeneratorTypeInfo type, String name)
throws UnableToCompleteException {
// Assuming for now this is an existing GWT defined class; not the one we're generating and not a primitive type
final JClassType classType = ((JClassTypeWrapper)type).getClassType();
// Now, if there are nested tags, decide what to do with them
// We do this before the attributes, because we want to call
// setViewFactory() before setting the other
// attributes. Many of the classes started their life with
// ViewFactory as a parameter to the ctor and
// don't behave well if they don't have a child view yet.
Elements childElems = elem.getChildElements();
if (elem.getChildCount() > 0) {
boolean foundSetter=false;
if(childElems.size() > 0) {
// TODO Currently this is order-dependent for overloads :-(
JMethod[] methods = classType.getOverridableMethods();
ArrayList<Element> missingSetter=new ArrayList<Element>();
ArrayList<ArrayList<JMethod>> missingSetterCandidates = new ArrayList<ArrayList<JMethod>>();
for(int i=0; i < childElems.size(); i++) {
Element childElem = childElems.get(i);
String capChildElementName = capitalize(childElem.getLocalName());
String setMethodName = "set"+capChildElementName;
String addMethodName = "add"+capChildElementName;
boolean elemProcessed = false;
ArrayList<JMethod> foundSetters = new ArrayList<JMethod>(methods.length);
for (int j = 0; j < methods.length; j++) {
JMethod method = methods[j];
//System.out.println("Looking for "+setMethod+" or "+addMethod+" for "+elem+" found "+method);
Name methodNameAnnotation = method.getAnnotation(Name.class);
if(methodNameAnnotation != null) {
if(!methodNameAnnotation.value().equalsIgnoreCase(childElem.getLocalName()))
continue;
} else if(!method.getName().equalsIgnoreCase(setMethodName) &&
!method.getName().equalsIgnoreCase(addMethodName)) {
continue;
}
foundSetters.add(method);
foundSetter = true;
final JParameter[] parameters = method.getParameters();
String[] paramStrings = new String[parameters.length];
boolean generatedSubview = false;
HashSet<String> usedAttributes = new HashSet<String>();
boolean failedParameterMatch = false;
for (int k = 0; k < parameters.length; k++) {
JParameter parameter = parameters[k];
String parameterName = getParameterName(parameter);
String attribute = parameterName;
String value = childElem.getAttributeValue(attribute);
if("styleName".equals(parameterName) && value == null) {
attribute = "class";
value = childElem.getAttributeValue(attribute);
}
if(value == null) {
String parameterTypeName = parameter.getType().getQualifiedSourceName();
final boolean isView = parameterTypeName.equals(View.class.getName());
final boolean isViewFactory = parameterTypeName.equals(ViewFactory.class.getName());
final boolean isWidget = parameterTypeName.equals(Widget.class.getName());
final boolean isString = parameterTypeName.equals(String.class.getName());
final boolean asyncSetter = k == 0 && parameters.length == 2 && AsyncCallback.class.getName().equals(parameters[1].getType().getQualifiedSourceName());
final boolean setter = parameters.length == 1;
if((isView || isWidget || isViewFactory) && childElem.getChildCount() > 0 && !generatedSubview) {
generatedSubview = true;
} else if((isString || asyncSetter || setter) && childElem.getValue().trim().length() > 0 && !generatedSubview) {
// Okay, we'll take it, I guess
} else {
logger.log(TreeLogger.TRACE, "Couldn't match an attribute for parameter "+parameterName+" from "+method+" for tag "+childElem.toXML(), null);
failedParameterMatch = true;
}
} else {
usedAttributes.add(attribute);
}
}
if(failedParameterMatch) // At least one parameter did not match
continue;
if(usedAttributes.size() < childElem.getAttributeCount()) {
logger.log(TreeLogger.TRACE, "Not all attributes used from "+childElem.toXML()+" to call "+method+":", null);
for (int k = 0; k < childElem.getAttributeCount(); k++) {
if(!usedAttributes.contains(childElem.getAttribute(k).getLocalName())) {
logger.log(TreeLogger.TRACE, " Attribute "+childElem.getAttribute(k).getLocalName()+" was not used.", null);
}
}
continue; // Not all attributes used, so this isn't the one they wanted
}
for (int k = 0; k < parameters.length; k++) {
JParameter parameter = parameters[k];
String parameterName = getParameterName(parameter);
String value = childElem.getAttributeValue(parameterName);
if("styleName".equals(parameterName) && value == null) value = childElem.getAttributeValue("class");
String paramString = calculateParameterValueExpressionForView(elem, childElem,
method, parameters, k, parameter, parameterName, value);
paramStrings[k] = paramString;
}
sw.println(name+"."+method.getName()+"("+joinWithCommas(0, paramStrings)+");");
elemProcessed = true;
break;
}
if(!elemProcessed) {
//System.out.println("NOT PROCESSED: " + elem);
missingSetter.add(childElem);
missingSetterCandidates.add(foundSetters);
}
}
if(foundSetter && !missingSetter.isEmpty()) {
int i=0;
for(Element childElem : missingSetter) {
logger.log(TreeLogger.WARN, "Ignored "+childElem.toXML()+"; couldn't find any matching setter.", null);
for(JMethod candidate : missingSetterCandidates.get(i++)) {
logger.log(TreeLogger.WARN, "Candidate setter method: "+candidate);
}
}
}
}
// Support:
// setView() - set a view directly
// setViewFactory() - set a view factory (used by lists)
if(!foundSetter) {
boolean factory = type.hasMethodMatching("setViewFactory", true, null, commonTypes.viewFactory);
boolean widget = !factory && type.hasMethodMatching("setWidget", true, null, commonTypes.widget);
String fieldName = "sv"+memberDecls.size();
String id = elem.getAttributeValue("id");
if(id == null) {
id = fieldName;
}
String createViewExpr = generateCreateSubview(elem, factory, false, id);
final boolean modelView = elem.getAttribute("with-model") != null;
generateField(fieldName, factory?commonTypes.viewFactory:
//widget?commonTypes.widget:
modelView?commonTypes.modelView:
commonTypes.view);
sw.println(fieldName + " = " + createViewExpr + ";");
if (factory) {
sw.println(name + ".setViewFactory("+fieldName+");");
} else if(widget) {
sw.println(name + ".setWidget("+fieldName+".getViewWidget());");
subviewLoads.add(fieldName + ".load(group.<Void>member());");
saves.add(fieldName + ".save(group.<Void>member());");
clearFields.add(fieldName + ".clearFields();");
} else if (type.hasMethodMatching("setView", true, null, commonTypes.view)) {
sw.println(name + ".setView("+fieldName+");");
} else if(childElems.size() > 0){
logger.log(TreeLogger.WARN,
"Discarding child elements of "
+ elem
+ " because the view class doesn't have setView(), addView(), or setViewFactory(), and the child element tags don't match any setX() or addX() on "+type,
null);
}
}
}
}
/**
* Return an expression to create a view for the given tag. This might be a generated
* subview (ComplexHTMLPanel), a widget, or a view.
*
* By default this returns an expression of type View, pass factory or widget == true (but not both)
* to get a ViewFactory or a Widget instead.
*
* @param elem
* @param factory If true, return an expression of type ViewFactory
* @param widget If true, return an expression of type Widget
* @param id ID of the element, used for debug IDs
* @return
* @throws UnableToCompleteException
*/
private String generateCreateSubview(Element elem, boolean factory, boolean widget, String id) throws UnableToCompleteException {
assert !(factory && widget);
// TODO In order to resurrect this feature, I need to get the simple views to support load/save
// boolean hasText=false;
// boolean hasElements=false;
// boolean isSimple;
// if(factory || elem.getAttribute("with-model") != null || elem.getAttribute("with-var") != null || elem.getAttribute("with-vars") != null) {
// isSimple = false;
// } else {
// isSimple = true;
// for(int i=0; i < elem.getChildCount(); i++) {
// Node elemChild = elem.getChild(i);
// if(elemChild instanceof Text) {
// if(((Text)elemChild).getValue().trim().length() > 0) {
// if(hasElements || hasText) isSimple = false;
// else hasText = true;
// }
// } else if(elemChild instanceof Element) {
// String[] namespaceAndTag = getNamespaceAndTag((Element)elemChild);
// if(namespaceAndTag[0].equals(XHTML_NAMESPACE)) {
// // If it has HTML tags in it, it's a complicated one
// isSimple = false;
// } else {
// if(hasElements || hasText) isSimple = false;
// else hasElements = true;
// }
// } else {
// isSimple = false;
// }
// }
// }
String createViewExpr;
// if(isSimple) {
// if(hasText) {
// String text = elem.getValue();
// String widgetCtor = "new HTML(\""+escapeMultiline(text)+"\")";
// if(widget) return widgetCtor;
// createViewExpr = "new WidgetWrapperView("+widgetCtor+")";
// } else {
// Element childElem = elem.getChildElements().get(0);
// // Has to have one or the other or we wouldn't get here
// JClassType tagClass = getTagClass(childElem);
// if(tagClass.isAbstract()) {
// createViewExpr = "("+tagClass.getQualifiedSourceName()+")GWT.create("+tagClass.getQualifiedSourceName()+".class)";
// } else {
// createViewExpr = "new "+tagClass.getQualifiedSourceName()+"()";
// }
// if(!implementsInterface(tagClass, getType(View.class.getName()))) {
// if(widget)
// return createViewExpr;
//
// // Assume it's a widget if it isn't a view, since tags must be either a widget or a View
// createViewExpr = "new WidgetWrapperView("+createViewExpr+")";
// }
// }
// } else {
String className = enqueueSubviewClass(elem);
String thisViewExpr = myClass.getParameterizedQualifiedSourceName()+".this";
String rootViewExpr = parentViewClass != null ? ROOT_VIEW_FIELD_NAME : thisViewExpr; // If we have no parent, we are the root view
createViewExpr = getRootView(factory) + ".maybeEnsureDebugId(\"" + id + "\", new " + className + "(" + thisViewExpr + ", " + rootViewExpr + ")).init()";
// }
if(factory)
createViewExpr = "new ViewFactory() { public View createView() { return "+createViewExpr+"; } }";
else if(widget)
createViewExpr += ".getViewWidget()";
return createViewExpr;
}
private String calculateParameterValueExpressionForView(Element elem, Element childElem, JMethod method,
final JParameter[] parameters, int k, JParameter parameter, String parameterName, String value)
throws UnableToCompleteException {
String paramString;
if(value == null) {
String parameterTypeName = parameter.getType().getQualifiedSourceName();
final boolean isView = parameterTypeName.equals(View.class.getName());
final boolean isViewFactory = parameterTypeName.equals((ViewFactory.class.getName()));
final boolean isWidget = parameterTypeName.equals((Widget.class.getName()));
final boolean isString = parameterTypeName.equals((String.class.getName()));
final boolean asyncSetter = k == 0 && parameters.length == 2 && AsyncCallback.class.getName().equals(parameters[1].getType().getQualifiedSourceName());
final boolean setter = parameters.length == 1;
if(isView || isWidget || isViewFactory) {
String withModel = elem.getAttributeValue("with-model");
if(withModel != null)
childElem.addAttribute(new Attribute("with-model", withModel));
paramString = generateCreateSubview(childElem, isViewFactory, isWidget, parameterName);
} else if((isString || asyncSetter || setter) && childElem.getValue().trim().length() > 0) {
paramString = '"'+escape(childElem.getValue().trim())+'"';
} else {
logger.log(TreeLogger.ERROR, "Missing attribute "+parameterName+" for parameter "+(k+1)+" to method "+method, null);
throw new UnableToCompleteException();
}
} else {
boolean readOnly = false;
String path = null;
ExpressionInfo pathAccessors = null;
if (((readOnly = (value.startsWith("${") || value.startsWith("@{") || value.startsWith("%{"))) || value.startsWith("#{")) && value.endsWith("}")) {
path = value.substring(2, value.length() - 1).trim();
pathAccessors = findAccessors(path, false, true);
if(pathAccessors == null) {
logger.log(TreeLogger.ERROR, "Failed to evaluate parameter "+parameterName+" = "+value+" for method "+method, null);
throw new UnableToCompleteException();
}
}
if(pathAccessors != null && !readOnly) {
logger.log(TreeLogger.ERROR, "Using #{...} for a setter with multiple parameters is not supported, use ${...}; setter is "+method+" expression is "+value, null);
throw new UnableToCompleteException();
}
String valueExpr;
ActionInfo action;
if (parameter.getType().isClass() != null
&& parameter.getType().isClass().isAssignableTo(commonTypes.action.getJClassType())
&& (pathAccessors == null || !pathAccessors.hasGetter())
&& (action = getAction(value, true)) != null) {
paramString = action.toViewAction();
} else if (path != null
&& commonTypes.value.getJClassType().getErasedType().equals(parameter.getType().isClassOrInterface().getErasedType())
&& (valueExpr = getFieldValue(path)) != null) {
paramString = valueExpr;
} else if (pathAccessors != null) {
if(!pathAccessors.hasSynchronousGetter()) {
logger.log(TreeLogger.ERROR, "Async/write-only values when calling a setter with multiple parameters is not supported currently.", null);
throw new UnableToCompleteException();
}
paramString = pathAccessors.conversionExpr(JTypeWrapper.wrap(parameter.getType()));
if(paramString == null) {
logger.log(TreeLogger.ERROR, "Cannot convert "+pathAccessors.getType()+" '"+path+"' to "+parameter.getType()+" for call to "+method, null);
throw new UnableToCompleteException();
}
// If we get here, there's no "${...}" or "#{...}" so it's a constant value / string or an action
} else if(parameter.getType().getQualifiedSourceName().equals("java.lang.String")) {
if(value.startsWith("${") || value.startsWith("%{") || value.startsWith("#{") || value.startsWith("@{"))
System.out.println("Warning: expression "+value+" treated as string...");
paramString = '"'+escape(value)+'"';
} else {
logger.log(TreeLogger.ERROR, "Cannot convert '"+value+"' to "+parameter.getType()+" for call to "+method, null);
throw new UnableToCompleteException();
}
}
return paramString;
}
/**
* Add a subview to the list of subview that need to be generated after this class.
*/
protected String enqueueSubviewClass(Element childElem) {
String className = subviewClassName(childElem.getLocalName());
subviewsToGenerate.add(new SubviewToGenerate(className, childElem, myClass));
return className;
}
protected String subviewClassName(String localName) {
subviewNumber++;
StringBuffer sb = new StringBuffer();
char[] chars = localName.toCharArray();
boolean capNext = true;
for(char ch : chars) {
if(ch == '_' || ch == '-') {
capNext = true;
continue;
}
if(Character.isJavaIdentifierPart(ch)) {
if(capNext) { ch = Character.toUpperCase(ch); capNext = false; }
sb.append(ch);
}
}
sb.append(subviewNumber);
String className = sb.toString();
return className;
}
/**
* Return an expression which is a Value that accesses the given field.
*
* A Value is an object that can be given to a class to dynamically load
* or set a value without knowing much about it - a kind of wrapper for
* a variable.
*/
protected String getFieldValue(String path) throws UnableToCompleteException {
LocalTreeLogger.pushLogger(logger.branch(TreeLogger.INFO, "Trying to create a Value object for path '"+path+"'"));
try {
String existingValue = values.get(path);
if (existingValue != null) {
return existingValue;
}
ExpressionInfo accessors = findAccessors(path, true, true);
if (accessors != null) {
if(commonTypes.value.equals(accessors.getType())) {
if(!accessors.hasSynchronousGetter()) {
logger.log(TreeLogger.ERROR, "Can't handle an async Value getter yet; for "+path, null);
throw new UnableToCompleteException();
}
return accessors.getterExpr();
}
String valueName = "value" + values.size();
values.put(path, valueName);
generateField(valueName, commonTypes.value);
sw.println(valueName + " = new Value() { ");
if(accessors.hasSynchronousGetter()) {
sw.indentln("public void getValue(AsyncCallback callback) { callback.onSuccess(" + accessors.getterExpr() + "); } ");
} else if(accessors.hasAsynchronousGetter()) {
sw.indentln("public void getValue(AsyncCallback callback) {\n\t\t\t\t"+accessors.callAsyncGetter("callback")+";\n\t\t\t\t} ");
} else {
sw.indentln("public void getValue(AsyncCallback callback) { callback.onFailure(null); } ");
}
if (accessors.hasSynchronousSetter()) {
sw.indentln("public void setValue(Object value, AsyncCallback callback) { try {\n\t\t\t\t" +
accessors.callSetter(ExpressionInfo.converter("value", RuntimeClassWrapper.OBJECT, accessors.getType())) +
"\n\t\t\t\tcallback.onSuccess(null); } catch(Throwable caught) { callback.onFailure(caught); } }");
} else if(accessors.hasAsynchronousSetter()) {
sw.indentln("public void setValue(Object value, AsyncCallback callback) { " +
accessors.callAsyncSetter(ExpressionInfo.converter("value", RuntimeClassWrapper.OBJECT, accessors.getType()), "callback")
+"}");
} else {
sw.indentln("public void setValue(Object value, AsyncCallback callback) { callback.onFailure(null); }");
}
sw.println("};");
return valueName;
}
/*
* ExpressionInfo metadata = getFieldMetadata(path); if(metadata != null) { String
* valueName = "value"+values.size(); values.put(path, valueName);
* addDeclaration(getType(Value.class.getName()), valueName); sw.println(valueName+" =
* "+metadata.getter+".getMetadata().getFieldByPath(\""+metadata.setter+"\").bindToModel(new
* Value() {"); sw.indentln("public void getValue(AsyncCallback callback) {
* callback.onSuccess("+metadata.getter+"); }"); sw.indentln("public void
* setValue(Object value, AsyncCallback callback) { throw new Error(); }");
* sw.println("});"); return valueName; }
*/
return null;
} finally {
LocalTreeLogger.popLogger();
}
}
protected String getRootView(boolean innerClass) throws UnableToCompleteException {
if(parentViewClass == null) {
if(innerClass) return myClass.getSimpleSourceName()+".this";
return "this";
} else {
return ROOT_VIEW_FIELD_NAME;
}
}
ActionInfo getAction(String expr, boolean innerClass) throws UnableToCompleteException {
final Matcher matcher = Pattern.compile("^on\\s+([^:]+):\\s*(.*)$").matcher(expr);
final String targetView;
if(matcher.matches()) {
final ExpressionInfo targetViewExpr = findAccessors(matcher.group(1), innerClass, false);
if(targetViewExpr == null || !targetViewExpr.hasSynchronousGetter()) {
logger.log(TreeLogger.ERROR, "Unable to resolve target view expression '"+matcher.group(1)+"' for action '"+expr+"'", null);
throw new UnableToCompleteException();
}
targetView = targetViewExpr.getGetter();
expr = matcher.group(2);
} else {
targetView = getRootView(innerClass);
}
return getAction(expr, targetView, true, true);
}
/**
* Multiple actions can be seperated using semicolons; the actions will run in sequence
* until they are all complete, or one fails in which case execution stops and the
* failure is returned to the default async callback (if any).
*
* Actions which are an expression ${...} or #{...} or %{...} or @{..} are trated as expressions leading to
* an Action object somewhere, which is executed.
*
* Actions starting with ';' won't save before running; actions ending with ';' won't load after
* running. This can be used to avoid full save/load cycles when save/load is slow or unnecessary.
*/
protected ActionInfo getAction(String path, String targetView, boolean saveBefore, boolean loadAfter) throws UnableToCompleteException {
if((path.startsWith("${") || path.startsWith("#{") || path.startsWith("%{") || path.startsWith("@{")) && path.endsWith("}")) {
path = path.substring(2, path.length()-1);
ExpressionInfo expr = findAccessors(path, true, false);
if(expr != null && expr.hasSynchronousGetter())
return new ActionInfo(path, expr.getGetter(), true, true, targetView, saveBefore, loadAfter, 0);
logger.log(TreeLogger.ERROR, "Unable to resolve action variable at path "+path, null);
throw new UnableToCompleteException();
}
if(path.startsWith(";")) {
saveBefore = false;
path = path.substring(1).trim();
}
if(path.endsWith(";")) {
loadAfter = false;
path = path.substring(0, path.length()-1).trim();
}
String actionKey = path+","+saveBefore+","+targetView+","+loadAfter;
ActionInfo existing = actions.get(actionKey);
if (existing != null)
return existing;
String[] actionSeries = path.split("\\s*;\\s*");
if(actionSeries.length > 1) {
ArrayList<String> actionList = new ArrayList<String>();
int timeout=0;
for (int i = 0; i < actionSeries.length; i++) {
ActionInfo action = getAction(actionSeries[i], targetView, false, false);
timeout += action.getTimeout();
if(action != null && action.getAction() != null)
actionList.add(action.toActionCtor());
}
String ctor = "new ActionSeries("+StringUtils.join(actionList, ",\n\t\t\t")+")";
return new ActionInfo(path, ctor, true, true, targetView, saveBefore, loadAfter, timeout);
}
if("".equals(path) || "null".equals(path)) {
//sw.println("final Action " + actionName + " = new ViewAction(null, "+rootView+", "+saveBefore+", "+loadAfter+");");
return new ActionInfo(path, null, true, true, targetView, saveBefore, loadAfter, 0);
}
String[] args;
String preargs = path;
int argstart;
if (path.endsWith(")") && (argstart = smartIndexOf(path, '(')) != -1) {
args = path.substring(argstart + 1, path.length() - 1).split("\\s*,\\s*");
// Check for an empty parameter list
if(args.length == 1 && args[0].length() == 0)
args = new String[0];
preargs = path.substring(0, argstart);
} else {
args = new String[0];
}
int assignmentIndex = smartIndexOf(preargs, '=');
if(assignmentIndex != -1) {
String left = path.substring(0, assignmentIndex).trim();
String right = path.substring(assignmentIndex+1).trim();
ExpressionInfo lvalue = findAccessors(left, true, true);
if(lvalue == null || (lvalue.hasSynchronousSetter() == false && lvalue.hasAsynchronousSetter() == false)) {
logger.log(TreeLogger.ERROR, "Can't find any setter for the left side of "+path, null);
throw new UnableToCompleteException();
}
ExpressionInfo rvalue = findAccessors(right, true, true);
if(rvalue == null || !rvalue.hasGetter()) {
logger.log(TreeLogger.ERROR, "Can't find any getter for the right side of "+path, null);
throw new UnableToCompleteException();
}
if(lvalue.hasAsynchronousSetter() == false && rvalue.hasAsynchronousGetter() == false) {
return new ActionInfo(path, lvalue.copyStatement(rvalue), false, false, targetView, saveBefore, loadAfter, 0);
} else {
return new ActionInfo(path, lvalue.asyncCopyStatement(rvalue, "callback", false), false, true, targetView, saveBefore, loadAfter, 0);
}
} else {
int objectPathEnd = preargs.lastIndexOf('.');
String objectPath;
String methodName;
String getter;
final GeneratorTypeInfo objectType;
boolean searchingThis = (objectPathEnd == -1);
if (searchingThis) {
objectPath = getter = "this";
objectType = myClass;
methodName = preargs;
} else {
objectPath = preargs.substring(0, objectPathEnd);
methodName = preargs.substring(objectPathEnd+1);
ExpressionInfo accessors = findAccessors(objectPath, true, false);
if (accessors == null || !accessors.hasSynchronousGetter() || accessors.getType() == null) {
logger.log(TreeLogger.ERROR, "Can't find any object for " + objectPath + " for action "+ path, null);
return null;
}
getter = accessors.getterExpr();
objectType = accessors.getType();
if (objectType.isPrimitive()) {
logger.log(TreeLogger.ERROR, "Can't call a method on a primitive "
+ accessors.getType() + " for expression " + path, null);
throw new UnableToCompleteException();
}
}
boolean asyncMethod = false;
GeneratorMethodInfo actionMethod = null;
GeneratorTypeInfo searchType = objectType;
if(searchType == null) {
logger.log(TreeLogger.ERROR, "Can't call a method on a " + objectType + " for expression " + path, null);
throw new UnableToCompleteException();
}
final String asyncCallbackClassName = AsyncCallback.class.getName();
for(;;) {
// Look for a synchronous action method with the right number of parameters
actionMethod = searchType.findMethodMatching(methodName, true, PrimitiveTypeInfo.VOID, new GeneratorTypeInfo[args.length]);
if(actionMethod != null) {
asyncMethod = false;
break;
}
// Look for the method with one extra parameter which is the async callback
GeneratorTypeInfo[] asyncParamTypes = new GeneratorTypeInfo[args.length+1];
asyncParamTypes[args.length] = commonTypes.asyncCallback;
actionMethod = searchType.findMethodMatching(methodName, true, PrimitiveTypeInfo.VOID, asyncParamTypes );
if(actionMethod != null) {
asyncMethod = true;
break;
}
if(searchingThis && searchType instanceof GeneratedInnerClassInfo) {
searchType = searchType.getFieldType(PARENT_VIEW_FIELD_NAME, true);
if(searchType == null)
break;
getter = getter+"."+PARENT_VIEW_FIELD_NAME;
//System.out.println("Looking up into "+searchType+" getter "+getter+" for "+methodName);
} else break;
}
if (actionMethod == null) {
logger.log(TreeLogger.WARN, "getAction(): Unable to find a method with the right number of arguments ("
+ args.length + " [ + AsyncCallback]) with name '" + methodName + "' on " + objectType + " in " + myClass
+ " for expression " + path +" searchingThis = "+searchingThis, null);
return null;
}
for (int i = 0; i < args.length; i++) {
String arg = args[i].trim();
ExpressionInfo argAccessors = findAccessors(arg, true, false);
if (argAccessors == null) {
logger.log(TreeLogger.ERROR, "Couldn't evaluate '" + arg + "' as argument to '" + path + "'", null);
throw new UnableToCompleteException();
}
args[i] = argAccessors.conversionExpr(actionMethod.getParameterTypes()[i]);
}
String methodCall;
if (getter.startsWith("this.")) {
methodCall = getter.substring(5) + "." + methodName;
} else if (getter.equals("this")) {
methodCall = methodName;
} else {
methodCall = getter + "." + methodName;
}
final ActionMethod annotation = actionMethod.getAnnotation(ActionMethod.class);
int timeout=0;
if(annotation != null) {
saveBefore = annotation.saveBefore();
loadAfter = annotation.loadAfter();
timeout = annotation.timeout();
}
if (asyncMethod) {
return new ActionInfo(path, methodCall + "(" + joinWithCommas(0, args) + (args.length > 0 ? ", " : "")
+ "callback);", false, true, targetView, saveBefore, loadAfter, timeout);
} else {
return new ActionInfo(path, methodCall + "(" + joinWithCommas(0, args) + ");", false, false, targetView, saveBefore, loadAfter, timeout);
}
}
}
/**
* Find getter and setter for the given expression (path) relative to the given base expression (base).
*
* @param base Starting point, used as the prefix for the returned expression info
* @param path Path relative to that starting point
* @param matchAsync If true, allow an asynchronous getter and/or setter to be returned
* @param staticAccess If true, use static methods instead if instance methods
* @return A new ExpressionInfo representing whatever getter and setter could be found
* @throws UnableToCompleteException If it fails to figure out the expression
*/
protected ExpressionInfo findAccessors(ExpressionInfo base, final String path, final boolean matchAsync, boolean staticAccess) throws UnableToCompleteException {
LocalTreeLogger.pushLogger(logger.branch(TreeLogger.INFO, "findAccessors('"+base+"', path='"+path+"', matchAsync="+matchAsync+", staticAccess="+staticAccess+")"));
try {
GeneratorTypeInfo inType = base.getType();
if(inType.isPrimitive()) {
logger.log(TreeLogger.ERROR, "Can't find any member inside of non-class type "+base.getType()+" with path "+path);
throw new UnableToCompleteException();
}
String expr = base.getGetter();
//System.out.println("findAccessors("+inType+", '"+expr+"', '"+path+"', "+matchAsync+")");
// Split "path" into two parts - the part before the first dot and the "rest" of the expression
String[] splitPath = smartSplit(path, '.', 2);
String name = splitPath[0];
if (name.length() == 0) {
return null;
}
String getter;
boolean asyncGetter = false;
String setter = null;
boolean asyncSetter = false;
GeneratorTypeInfo type;
/*if(name.endsWith("]")) {
int openBraceIdx = smartIndexOf(name, '[');
if(openBraceIdx == -1) {
logger.log(TreeLogger.ERROR, "Can't find opening [ for ] in "+name, null);
throw new UnableToCompleteException();
}
// TODO array indexing
throw new UnableToCompleteException();
} else */
boolean endsWithParen = name.endsWith(")");
boolean methodInvokation = endsWithParen // Has (), assume method call
|| inType.hasMethodMatching(name, true, null) // no parens, but expression has the same name as a zero-arg method
|| (matchAsync && inType.hasMethodMatching(name, true, null, commonTypes.asyncCallback)); // no parens, but expression has the same name as an async method and async is OK
boolean lastOrOnlyPartOfTheExpression = splitPath.length == 1;
if(methodInvokation) {
String getterMethodName;
ExpressionInfo[] args;
GeneratorTypeInfo[] argTypes;
if(endsWithParen) {
int openIdx = smartIndexOf(name, '(');
if(openIdx == -1) {
logger.log(TreeLogger.ERROR, "Can't find opening ( for ) in "+name, null);
throw new UnableToCompleteException();
}
String[] argExprs = smartSplit(name.substring(openIdx + 1, name.length() - 1), ',', 100);
//System.out.println("Splitting '"+name.substring(openIdx + 1, name.length() - 1)+" around ',' gives "+args.length+" args: "+joinWithCommas(0, args));
// Check for an empty parameter list
if(argExprs.length == 1 && argExprs[0].length() == 0)
argExprs = new String[0];
getterMethodName = identifier(name.substring(0, openIdx));
args = new ExpressionInfo[argExprs.length];
argTypes = new GeneratorTypeInfo[argExprs.length];
for(int i=0; i < argExprs.length; i++) {
String arg = argExprs[i].trim();
ExpressionInfo argAccessors = findAccessors(arg, true, false);
if (argAccessors == null) {
logger.log(TreeLogger.ERROR, "Couldn't evaluate '" + arg + "' as argument to '" + name + "'", null);
throw new UnableToCompleteException();
}
args[i] = argAccessors;
argTypes[i] = argAccessors.getType();
}
} else {
getterMethodName = identifier(name);
args = new ExpressionInfo[0];
argTypes = new GeneratorTypeInfo[0];
}
GeneratorTypeInfo objectType = inType;
boolean searchingThis = objectType.equals(myClass);
boolean asyncMethod = false;
GeneratorMethodInfo getterMethod = null;
for(;;) {
GeneratorTypeInfo[] syncArgTypesWildcard = new GeneratorTypeInfo[argTypes.length];
GeneratorTypeInfo[] asyncArgTypesWildcard = new GeneratorTypeInfo[argTypes.length+1];
asyncArgTypesWildcard[argTypes.length] = commonTypes.asyncCallback;
GeneratorTypeInfo[] asyncArgTypes = Arrays.copyOf(argTypes, argTypes.length+1);
asyncArgTypes[argTypes.length] = commonTypes.asyncCallback;
HashSet<String> candidates = new HashSet<String>();
candidates.add(getterMethodName);
String capGetterMethodName = capitalize(getterMethodName);
candidates.add("get"+capGetterMethodName);
candidates.add("is"+capGetterMethodName);
for(String candidate : candidates) {
getterMethod = objectType.findMethodMatching(candidate, true, null, argTypes);
if(getterMethod == null)
getterMethod = objectType.findMethodMatching(candidate, true, null, syncArgTypesWildcard);
if(getterMethod != null) {
asyncMethod = false;
type = getterMethod.getReturnType();
break;
}
if(matchAsync) {
getterMethod = objectType.findMethodMatching(candidate, true, null, asyncArgTypes);
if(getterMethod == null)
getterMethod = objectType.findMethodMatching(candidate, true, null, asyncArgTypesWildcard);
if(getterMethod != null) {
asyncMethod = true;
type = getterMethod.getAsyncReturnType();
if(type == null) type = commonTypes.object;
break;
}
}
}
if(searchingThis && getterMethod == null && objectType instanceof GeneratedInnerClassInfo) {
objectType = objectType.getFieldType(PARENT_VIEW_FIELD_NAME, true);
if(objectType == null)
break;
expr = expr+"."+PARENT_VIEW_FIELD_NAME;
//System.out.println("Ascending to "+expr+" "+objectType);
} else break;
}
if (getterMethod == null) {
logger.log(TreeLogger.ERROR, "findAccessors(): Unable to find a "+(staticAccess?"static":"instance")+" method with the right number of arguments ("
+ args.length + (matchAsync?" [ + optional AsyncCallback]":"")+") with name '" + getterMethodName + "' in " + inType
+ " for expression '" + path + "'", null);
throw new UnableToCompleteException();
}
StringBuffer getterBuf = new StringBuffer();
getterBuf.append(expr).append('.').append(getterMethod.getName()).append('(');
for (int i = 0; i < args.length; i++) {
if(i > 0) getterBuf.append(", ");
getterBuf.append(args[i].conversionExpr(getterMethod.getParameterTypes()[i]));
}
if(asyncMethod) {
if(args.length > 0) getterBuf.append(","); // trailing comma for async methods so we can append the callback parameter when we call the method
} else getterBuf.append(')');
getter = getterBuf.toString();
asyncGetter = asyncMethod;
type = asyncMethod?getterMethod.getAsyncReturnType():getterMethod.getReturnType();
// Find the matching setter method (if any).
String setterMethodName = getterMethod.getName().replaceFirst("^(is|get)", "set");
GeneratorTypeInfo[] setterArgTypes = Arrays.copyOf(argTypes, argTypes.length+1);
setterArgTypes[argTypes.length] = type;
GeneratorMethodInfo setterMethod = objectType.findMethodMatching(setterMethodName, true, null, setterArgTypes);
// If searching for something matching the types we got doesn't work, try it a wildcard for the type
if(setterMethod == null) {
setterArgTypes = new GeneratorTypeInfo[argTypes.length+1];
setterArgTypes[argTypes.length] = type;
setterMethod = objectType.findMethodMatching(setterMethodName, true, null, setterArgTypes);
}
if(setterMethod == null) {
setterArgTypes = Arrays.copyOf(argTypes, argTypes.length+2);
setterArgTypes[argTypes.length] = type;
setterArgTypes[argTypes.length+1] = commonTypes.asyncCallback;
setterMethod = objectType.findMethodMatching(setterMethodName, true, PrimitiveTypeInfo.VOID, setterArgTypes);
if(setterMethod == null) {
setterArgTypes = new GeneratorTypeInfo[argTypes.length+2];
setterArgTypes[argTypes.length] = type;
setterArgTypes[argTypes.length+1] = commonTypes.asyncCallback;
setterMethod = objectType.findMethodMatching(setterMethodName, true, PrimitiveTypeInfo.VOID, setterArgTypes);
}
if(setterMethod != null)
asyncSetter = true;
} else {
asyncSetter = false;
}
if(setterMethod != null) {
StringBuffer setterBuf = new StringBuffer();
setterBuf.append(expr).append('.').append(setterMethod.getName()).append('(');
for (int i = 0; i < args.length; i++) {
if(i > 0) setterBuf.append(", ");
String convertedArg = args[i].conversionExpr(setterMethod.getParameterTypes()[i]);
if(convertedArg.isEmpty()) {
throw new IllegalStateException("Got empty result back from "+args[i]+" converted to "+setterMethod.getParameterTypes()[i]);
}
setterBuf.append(convertedArg);
}
if(args.length > 0)
setterBuf.append(","); // trailing comma for setters so we can append the value parameter and possibly the async callback
setter = setterBuf.toString();
}
} else {
// No array or function specifier, so look for a normal property or field
String baseExpr = (expr.equals("this") ? "" : expr + ".");
name = identifier(name);
String getterName = "get" + capitalize(name);
String setterName = "set" + capitalize(name);
GeneratorMethodInfo getterMethod = inType.findMethodMatching(getterName, true, null);
if(getterMethod == null && matchAsync) { // Check for async version, if allowed in this context
getterMethod = inType.findMethodMatching(getterName, true, PrimitiveTypeInfo.VOID, commonTypes.asyncCallback);
asyncGetter = getterMethod != null;
}
if (getterMethod == null) {
getterName = "is" + capitalize(name);
getterMethod = inType.findMethodMatching(getterName, true, null);
if(getterMethod == null && matchAsync) { // Check for async version, if allowed in this context
getterMethod = inType.findMethodMatching(getterName, true, PrimitiveTypeInfo.VOID, commonTypes.asyncCallback);
asyncGetter = getterMethod != null;
}
}
if (getterMethod != null) {
getter = baseExpr + getterName + (asyncGetter?"":"()"); // No trailing brackets for an async call
if(asyncGetter) {
type = getterMethod.getAsyncReturnType();
if(type == null) type = commonTypes.object;
} else {
type = getterMethod.getReturnType();
}
} else {
asyncGetter = false;
// Try direct field access
type = inType.getFieldType(name, baseExpr.startsWith("this."));
if(type != null) {
getter = baseExpr + name;
setter = baseExpr + name + "=";
asyncSetter = false;
asyncGetter = false;
} else {
getter = null;
}
}
if(setter == null) {
GeneratorMethodInfo setterMethod;
// Only look for the setter if this is the last (or only) part of the chain. i.e. for an expression
// a.b.c we would only look for a setter for c, not a or b.
if(lastOrOnlyPartOfTheExpression) {
setterMethod = inType.findMethodMatching(setterName, true, null, (GeneratorTypeInfo)null);
if(setterMethod == null) {
setterMethod = inType.findMethodMatching(setterName, true, PrimitiveTypeInfo.VOID, (GeneratorTypeInfo)null, commonTypes.asyncCallback);
asyncSetter = setterMethod != null;
}
if(setterMethod != null) {
//System.out.println("Found setter "+setterMethod);
setter = baseExpr + setterName + "(";
type = setterMethod.getParameterTypes()[0];
}
}
}
}
//System.out.println("Looking for "+name+" in "+inType+", found "+getter+" and "+setter);
if(getter != null && splitPath.length == 2) {
if(type.isArray()) {
if("length".equals(splitPath[1])) {
return new ExpressionInfo(path, getter+"."+splitPath[1], PrimitiveTypeInfo.INT, false);
} else {
logger.log(TreeLogger.ERROR, "Attempting to access a property of array that I don't recognize: "+getter+"."+splitPath[1], null);
throw new UnableToCompleteException();
}
} else if(type.isPrimitive()) {
logger.log(TreeLogger.ERROR, "Attempting to access a property of a primitive type: "+getter+" of type "+type+" async = "+asyncGetter, null);
throw new UnableToCompleteException();
}
if(asyncGetter == false) {
// Easy... just get them to create a new getter based on this one
return findAccessors(new ExpressionInfo(path, getter, type, isConstants(type)), splitPath[1], matchAsync, staticAccess);
} else {
// Oops, we're getting a property of an async property, time for some magic
// The trick: generate a new method that does the first async operation and
// then returns the result of the getter of the proceeding attributes.
ExpressionInfo subexpr = findAccessors(new ExpressionInfo(path, "base", type, false), splitPath[1], matchAsync, staticAccess);
if(subexpr == null) {
logger.log(TreeLogger.ERROR, "Failed to find property '"+splitPath[1]+"' of type '"+type+"' of expression '"+getter+"'", null);
throw new UnableToCompleteException();
}
String getterName = "getAsync"+asyncProxies.size();
if(subexpr.hasGetter()) {
if(subexpr.hasSynchronousGetter()) {
// Synchronous sub-expression, how merciful!
asyncProxies.add(" public void "+getterName+"(AsyncCallback<Object> callback) {\n"+
" "+ExpressionInfo.callAsyncGetter(getter, "new AsyncCallbackDirectProxy<Object>(callback, \""+path+"\") {\n"+
" public void onSuccess(Object result) {\n"+
" "+type.getParameterizedQualifiedSourceName()+" base = ("+type.getParameterizedQualifiedSourceName()+") result;\n"+
" returnSuccess("+subexpr.getterExpr()+");\n"+
" }\n"+
" }")+";\n"+
" }\n");
} else if(subexpr.hasAsyncGetter()) {
asyncProxies.add(" public void "+getterName+"(AsyncCallback<Object> callback) {\n"+
" "+ExpressionInfo.callAsyncGetter(getter, "new AsyncCallbackDirectProxy<Object>(callback, \""+path+"\") {\n"+
" public void onSuccess(Object result) {"+
" "+type.getParameterizedQualifiedSourceName()+" base = ("+type.getParameterizedQualifiedSourceName()+") result;\n"+
" "+subexpr.callAsyncGetter("callback")+";\n"+
" }\n"+
" }")+";\n"+
" }\n");
}
} else getterName = null;
String setterName = "setAync"+asyncProxies.size();
if(subexpr.hasSetter()) {
if(subexpr.hasSynchronousSetter()) {
// Synchronous sub-expression, how merciful!
asyncProxies.add(" public void "+setterName+"(final "+subexpr.getType().getParameterizedQualifiedSourceName()+" value, AsyncCallback<Void> callback) {\n"+
" "+ExpressionInfo.callAsyncGetter(getter, "new AsyncCallbackDirectProxy<Void>(callback, \""+path+"\") {\n"+
" public void onSuccess(Void result) {\n"+
" "+type.getParameterizedQualifiedSourceName()+" base = ("+type.getParameterizedQualifiedSourceName()+") result;\n"+
" "+subexpr.callSetter("value")+"\n"+
" returnSuccess(null);\n"+
" }\n"+
" }")+";\n"+
" }");
} else if(subexpr.hasAsynchronousSetter()) {
asyncProxies.add(" public void "+setterName+"(final "+subexpr.getType().getParameterizedQualifiedSourceName()+" value, AsyncCallback<Void> callback) {\n"+
" "+ExpressionInfo.callAsyncGetter(getter, "new AsyncCallbackDirectProxy<Void>(callback, \""+path+"\") {\n"+
" public void onSuccess(Void result) {\n"+
" "+type.getParameterizedQualifiedSourceName()+" base = ("+type.getParameterizedQualifiedSourceName()+") result;\n"+
" "+subexpr.callAsyncSetter("value", "callback")+"\n"+
" }\n"+
" }")+";\n"+
" }");
}
} else setterName = null;
return new ExpressionInfo(path, getterName, setterName, subexpr.getType(), true, true, false);
}
} else if(setter != null && lastOrOnlyPartOfTheExpression) {
return new ExpressionInfo(path, getter, setter, type, asyncGetter, asyncSetter, false);
}
/*
JClassType superclass = inType.getSuperclass();
if (superclass != null && !ReflectedClassInfo.OBJECT.equals(superclass)) {
ExpressionInfo inherited = findAccessors(superclass, expr, path, matchAsync);
if(inherited != null) {
if(getter == null) {
if(inherited.getter != null) { getter = inherited.getter; asyncGetter = false; }
else if(inherited.hasAsynchronousGetter()) { getter = inherited.asyncGetter; asyncGetter = true; }
}
if(setter == null) {
if(inherited.hasSynchronousSetter()) { setter = inherited.setter; asyncSetter = false; }
else if(inherited.hasAsynchronousSetter()) { setter = inherited.asyncSetter; asyncSetter = true; }
}
if(type == null) type = inherited.getType();
}
}
*/
if(type != null) {
if((getter != null) && (setter == null) && !asyncGetter
&& (isConstants(inType))) {
//logger.log(TreeLogger.INFO, "Considering value to be constant since it is part of a Constants or DictionaryConstants: "+getter);
return new ExpressionInfo(path, getter, type, base.isConstant());
} else {
return new ExpressionInfo(path, getter, setter, type, asyncGetter, asyncSetter, false);
}
}
//System.out.println("Failed to find property "+name+" on "+inType);
return null;
} finally {
LocalTreeLogger.popLogger();
}
}
/**
* A wrapper for string.split() that doesn't split inside matching ()'s or []'s.
*
* maxlen refers to the maximum length of the resulting array.
*/
private String[] smartSplit(String s, char seperator, int maxlen) {
ArrayList<String> result = new ArrayList<String>();
int lastCut=0;
maxlen--;
while(result.size() < maxlen) {
int i = smartIndexOf(s, seperator, lastCut);
if(i == -1) {
break;
}
result.add(s.substring(lastCut, i));
lastCut = i+1;
}
result.add(s.substring(lastCut));
return result.toArray(new String[result.size()]);
}
/**
* Version of indexOf() that ignores results inside brackets or quotes
*/
private int smartIndexOf(String s, String term, int start) {
int depth=0;
int quote=0;
int i=start;
for(;;) {
if(depth == 0 && s.substring(i).startsWith(term)) {
return i;
} else if(quote != 0) {
if(s.charAt(i) == quote && s.charAt(i-1) != '\\') {
quote = 0;
depth--;
i++;
}
} else if(s.charAt(i) == '"' || s.charAt(i) == '\'') {
quote = s.charAt(i);
depth++;
} else if(s.charAt(i) == '(' || s.charAt(i) == '[') {
depth++; i++;
} else if(s.charAt(i) == ')' || s.charAt(i) == ']') {
depth--; i++;
} else if(s.substring(i).startsWith(term)) {
// Found the term, but it's inside brackets or quotes or something, so skip it
i += term.length();
}
int nextPos;
if(quote != 0) {
nextPos = s.indexOf(quote, i+1);
// if nextPos == -1, it's a mismatched quote
} else {
nextPos = s.indexOf('(', i);
nextPos = minAboveZero(nextPos, s.indexOf('"', i));
nextPos = minAboveZero(nextPos, s.indexOf('\'', i));
nextPos = minAboveZero(nextPos, s.indexOf(')', i));
nextPos = minAboveZero(nextPos, s.indexOf(']', i));
nextPos = minAboveZero(nextPos, s.indexOf('[', i));
nextPos = minAboveZero(nextPos, s.indexOf(term, i));
}
if(nextPos == -1)
return -1;
i = nextPos;
}
}
private int smartIndexOf(String s, String term) {
return smartIndexOf(s, term, 0);
}
private int minAboveZero(int a, int b) {
if(b < 0) return a;
if(a < 0) return b;
return Math.min(a, b);
}
/**
* Version of indexOf() that ignores results inside brackets.
*/
int smartIndexOf(String s, char term, int start) {
if(s.length()==0)
return -1;
int depth=0;
int quote=0;
int i=start;
for( ;; ) {
if(depth == 0 && s.charAt(i) == term) {
return i;
} else if(quote != 0) {
if(s.charAt(i) == quote && s.charAt(i-1) != '\\') {
quote = 0;
depth--;
i++;
}
} else if(s.charAt(i) == '"' || s.charAt(i) == '\'') {
quote = s.charAt(i);
depth++;
} else if(s.charAt(i) == '(' || s.charAt(i) == '[') {
depth++; i++;
} else if(s.charAt(i) == ')' || s.charAt(i) == ']') {
depth--; i++;
}
int nextPos;
if(quote != 0) {
nextPos = s.indexOf(quote, i+1);
// if nextPos == -1, it's a mismatched quote
} else {
nextPos = s.indexOf('(', i);
nextPos = minAboveZero(nextPos, s.indexOf('"', i));
nextPos = minAboveZero(nextPos, s.indexOf('\'', i));
nextPos = minAboveZero(nextPos, s.indexOf(')', i));
nextPos = minAboveZero(nextPos, s.indexOf(']', i));
nextPos = minAboveZero(nextPos, s.indexOf('[', i));
if(depth == 0)
nextPos = minAboveZero(nextPos, s.indexOf(term, i));
}
//System.out.println("Next pos in <<"+s+">> is "+nextPos+" <<"+(nextPos==-1?"":s.substring(nextPos))+">> i="+i+" <<"+s.substring(i)+">> term='"+term+"' depth="+depth);
if(nextPos == -1)
return -1;
i = nextPos;
}
}
int smartIndexOf(String s, char term) {
return smartIndexOf(s, term, 0);
}
/**
* Find a getter/setter pair for an object path.
*
* The getter is a complete expression; the setter typically ends at the method name or
* an assignment operator and should have its parameter wrapped in parentheses.
*
* This only works for getters/setters that can be found using static type information.
*
* This returns null for the setter if attempts to set the value should be ignored.
* @param innerType
* If the expression is going into a nested anonymous class, pass true
* @param matchAsync If true, include asynchronous methods in the search; otherwise do not include them
*
* @throws UnableToCompleteException
*/
protected ExpressionInfo findAccessors(String path, boolean innerType, boolean matchAsync) throws UnableToCompleteException {
//System.out.println("findAccessors("+path+")");
int operIdx;
if(((operIdx = smartIndexOf(path, "&&")) != -1
|| (operIdx = smartIndexOf(path, " and ")) != -1
|| (operIdx = smartIndexOf(path, "||")) != -1
|| (operIdx = smartIndexOf(path, " or ")) != -1
|| (operIdx = smartIndexOf(path, ">=")) != -1
|| (operIdx = smartIndexOf(path, "<=")) != -1
|| (operIdx = smartIndexOf(path, ">")) != -1
|| (operIdx = smartIndexOf(path, "<")) != -1
|| (operIdx = smartIndexOf(path, "==")) != -1
|| (operIdx = smartIndexOf(path, "!=")) != -1
|| (operIdx = smartIndexOf(path, "+")) != -1
|| (operIdx = smartIndexOf(path, "-")) != -1
|| (operIdx = smartIndexOf(path, "/")) != -1
|| (operIdx = smartIndexOf(path, "*")) != -1)) {
return handleBinaryOperator(path, innerType, matchAsync, operIdx);
} else if(path.startsWith("!") || path.startsWith("not ")) {
String remainder = path.startsWith("!")?path.substring(1).trim():path.substring(4).trim();
//System.out.println("Boolean NOT");
final ExpressionInfo accessors = findAccessors(remainder, innerType, matchAsync);
if(accessors != null) {
return new ExpressionInfo(accessors, new OperatorInfo() {
@Override
public String onGetExpr(String expr) throws UnableToCompleteException {
return "!(" + ExpressionInfo.converter(expr, accessors.getType(), PrimitiveTypeInfo.BOOLEAN) + ')';
}
@Override
public String onSetExpr(String expr) throws UnableToCompleteException {
return ExpressionInfo.converter("!(" + expr + ')', PrimitiveTypeInfo.BOOLEAN, accessors.getType());
}
});
} else {
return null;
}
} else if(path.startsWith("-") || path.startsWith("~")) {
String remainder = path.substring(1).trim();
final String oper = path.substring(0,1);
ExpressionInfo accessors = findAccessors(remainder, innerType, matchAsync);
if(accessors != null) {
return new ExpressionInfo(accessors, new OperatorInfo() {
@Override
public String onGetExpr(String expr) {
return oper + '(' + expr + ')';
}
@Override
public String onSetExpr(String expr) {
return oper + '(' + expr + ')';
}
});
} else {
return null;
}
} else if(path.length() > 0 && (Character.isDigit(path.charAt(0)) || (path.length() >= 2 && path.charAt(0) == '-' && Character.isDigit(path.charAt(1))))) {
if(smartIndexOf(path, '.') != -1) {
// floating-point
return new ExpressionInfo(path, path, JPrimitiveType.DOUBLE, true);
} else if(path.endsWith("L")) {
return new ExpressionInfo(path, path, JPrimitiveType.LONG, true);
} else {
return new ExpressionInfo(path, path, JPrimitiveType.INT, true);
}
} else if(path.equals("true") || path.equals("false")) {
return new ExpressionInfo(path, path, JPrimitiveType.BOOLEAN, true);
} else if(path.equals("null")) {
return new ExpressionInfo(path, path, RuntimeClassWrapper.OBJECT, true);
} else if(path.startsWith("\"") && path.endsWith("\"")) {
return new ExpressionInfo(path, path, getType("java.lang.String"), true);
}
String thisExpr = innerType ? myClass.getSimpleSourceName() + ".this" : "this";
GeneratorTypeInfo classToSearch = myClass;
if (path.equals("this")) {
while(classToSearch instanceof GeneratedInnerClassInfo) {
classToSearch = ((GeneratedInnerClassInfo)classToSearch).getFieldType(PARENT_VIEW_FIELD_NAME, true);
if(classToSearch == null)
break;
thisExpr = thisExpr+"."+PARENT_VIEW_FIELD_NAME;
}
// When they use the expression "this", be sure to use the superclass of the generated class;
// the generated class won't behave well with isAssignableFrom() and isAssignableTo() because
// it's a fake object we created and isn't "known" by the type oracle.
return new ExpressionInfo(path, thisExpr, null, classToSearch, false, false, false);
}
// like books.service.AccountType.ACCOUNTS_RECEIVABLE or abc.def.Foo.bar
Matcher staticReference = Pattern.compile("([a-z0-9_]+(?:\\.[a-z0-9_]+)+(?:\\.[A-Z][A-Za-z0-9_]+)+)\\.([A-Za-z0-9_]+.*)").matcher(path);
if(staticReference.matches()) {
String className = staticReference.group(1);
String property = staticReference.group(2);
//System.out.println("Static reference: "+className+" property "+property);
try {
JClassType staticType = types.getType(className);
JField field = staticType.getField(property);
JEnumType enum1 = staticType.isEnum();
if(field != null && field.isStatic()) {
return new ExpressionInfo(path, path, field.getType(), field.isFinal() || enum1!=null);
}
if(enum1 != null && property.equals("values()")) {
return new ExpressionInfo(path, path, types.getArrayType(enum1), true);
}
return findAccessors(new ExpressionInfo(path, className, staticType, true), property, matchAsync, true);
} catch (NotFoundException e) {
}
}
for (;;) {
ExpressionInfo accessors = findAccessors(new ExpressionInfo(path, thisExpr, classToSearch, true), path, matchAsync, false);
if (accessors != null) {
return accessors;
} else if(classToSearch instanceof GeneratedInnerClassInfo){
classToSearch = classToSearch.getFieldType(PARENT_VIEW_FIELD_NAME, true);
if (classToSearch == null) {
return null;
}
if(parentViewClass != null) {
thisExpr = thisExpr+"."+PARENT_VIEW_FIELD_NAME;
} else {
thisExpr = classToSearch.getSimpleSourceName() + ".this";
}
} else {
return null;
}
}
}
private ExpressionInfo handleBinaryOperator(String path, boolean innerType, boolean matchAsync, int operIdx)
throws UnableToCompleteException {
final String leftPath = path.substring(0, operIdx).trim();
ExpressionInfo left = findAccessors(leftPath, innerType, matchAsync);
int operLen;
String oper;
boolean arithmeticOper=false;
if(path.startsWith(" or ", operIdx)) {
operLen = 4;
oper = "||";
} else if(path.startsWith(" and ", operIdx)) {
operLen = 5;
oper = "&&";
} else if("+-*/".indexOf(path.charAt(operIdx)) != -1) {
operLen = 1;
oper = path.substring(operIdx, operIdx+1);
arithmeticOper = true;
} else {
operLen = 2;
oper = path.substring(operIdx, operIdx+operLen);
}
final String rightPath = path.substring(operIdx+operLen).trim();
ExpressionInfo right = findAccessors(rightPath, innerType, matchAsync);
if(left == null || !left.hasGetter()) {
logger.log(TreeLogger.ERROR, "Can't evaluate "+leftPath+" in "+path+" match async = "+matchAsync, null);
throw new UnableToCompleteException();
}
if(right == null || !right.hasGetter()) {
logger.log(TreeLogger.ERROR, "Can't evaluate "+rightPath+" in "+path+" match async = "+matchAsync, null);
throw new UnableToCompleteException();
}
GeneratorTypeInfo targetOperandType;
GeneratorTypeInfo resultType;
GeneratorTypeInfo java_lang_String = RuntimeClassWrapper.STRING;
if ("&&".equals(oper) || "||".equals(oper)){
targetOperandType = PrimitiveTypeInfo.BOOLEAN;
resultType = PrimitiveTypeInfo.BOOLEAN;
} else if("+".equals(oper) && (right.getType().getParameterizedQualifiedSourceName().equals("java.lang.String") || left.getType().getParameterizedQualifiedSourceName().equals("java.lang.String"))) {
targetOperandType = java_lang_String;
resultType = java_lang_String;
} else {
targetOperandType = left.getType();
if(arithmeticOper)
resultType = left.getType();
else // all other operations are boolean
resultType = PrimitiveTypeInfo.BOOLEAN;
}
// Is it an asynchronous calculation?
if(left.hasAsynchronousGetter() || right.hasAsynchronousGetter()) {
return handleAsyncBinaryOperator(left, oper, right, targetOperandType, resultType);
}
String convertRight = right.conversionExpr(targetOperandType);
if(convertRight == null) {
logger.log(TreeLogger.ERROR, "Can't convert "+right.getType()+" "+right+" to "+targetOperandType, null);
throw new UnableToCompleteException();
}
String convertLeft = left.conversionExpr(targetOperandType);
if(convertLeft == null) {
logger.log(TreeLogger.ERROR, "Can't convert "+left+" to "+targetOperandType, null);
throw new UnableToCompleteException();
}
return new ExpressionInfo(path, "(" + convertLeft + oper + convertRight + ")", resultType, left.isConstant() && right.isConstant());
}
private ExpressionInfo handleAsyncBinaryOperator(ExpressionInfo left, String oper, ExpressionInfo right,
GeneratorTypeInfo targetOperandType, GeneratorTypeInfo resultType) throws UnableToCompleteException {
String getterName = "getCalculation"+calculations.size();
String leftTypeName = left.getType().getParameterizedQualifiedSourceName();
String classLeftTypeName = getBoxedClassName(left.getType());
String rightTypeName = right.getType().getParameterizedQualifiedSourceName();
String classRightTypeName = getBoxedClassName(right.getType());
final String resultClassTypeName = getBoxedClassName(resultType);
String convertRight = ExpressionInfo.converter("right", right.getType(), targetOperandType);
if(convertRight == null) {
logger.log(TreeLogger.ERROR, "Can't convert "+right.getType()+" "+right+" to "+targetOperandType, null);
throw new UnableToCompleteException();
}
String convertLeft = ExpressionInfo.converter("left", left.getType(), targetOperandType);
if(convertLeft == null) {
logger.log(TreeLogger.ERROR, "Can't convert "+left.getType()+" "+left+" to "+targetOperandType, null);
throw new UnableToCompleteException();
}
if(left.hasAsynchronousGetter()) {
if(right.hasAsynchronousGetter()) {
calculations.add(" public void "+getterName+"(AsyncCallback<"+resultClassTypeName+"> callback) {\n"+
" "+left.callAsyncGetter("new AsyncCallbackProxy<"+classLeftTypeName+","+resultClassTypeName+">(callback) {\n"+
" public void onSuccess(final "+classLeftTypeName+" left) {\n"+
" "+right.callAsyncGetter("new AsyncCallbackProxy<"+classRightTypeName+","+resultClassTypeName+">(callback) {\n"+
" public void onSuccess(final "+classRightTypeName+" right) {\n"+
" returnSuccess("+convertLeft+oper+convertRight+");\n"+
" }\n"+
" }")+";\n"+
" }\n"+
" }")+";\n"+
" }\n");
} else {
calculations.add(" public void "+getterName+"(AsyncCallback<"+resultClassTypeName+"> callback) {\n"+
" "+left.callAsyncGetter("new AsyncCallbackProxy<"+classLeftTypeName+","+resultClassTypeName+">(callback) {\n"+
" public void onSuccess(final "+classLeftTypeName+" left) {\n"+
" final "+rightTypeName+" right = "+right.getterExpr()+";\n"+
" returnSuccess("+convertLeft+oper+convertRight+");\n"+
" }\n"+
" }")+";\n"+
" }\n");
}
} else {
// right.asyncGetter must != null
calculations.add(" public void "+getterName+"(AsyncCallback<"+resultClassTypeName+"> callback) {\n"+
" final "+leftTypeName+" left = "+left.getterExpr()+";\n"+
" "+right.callAsyncGetter("new AsyncCallbackProxy<"+classRightTypeName+","+resultClassTypeName+">(callback) {\n"+
" public void onSuccess(final "+classRightTypeName+" right) {\n"+
" returnSuccess("+convertLeft+oper+convertRight+");\n"+
" }\n"+
" }")+";\n"+
" }\n");
}
return new ExpressionInfo(left+oper+right, getterName, null, resultType, true, false, false);
}
private String getBoxedClassName(final JType type) {
return type.isPrimitive()!=null?type.isPrimitive().getQualifiedBoxedSourceName():type.getQualifiedSourceName();
}
private String getBoxedClassName(final GeneratorTypeInfo type) {
if(type instanceof JTypeWrapper)
return getBoxedClassName(((JTypeWrapper)type).getJType());
if(type instanceof PrimitiveTypeInfo)
return ((PrimitiveTypeInfo)type).getBoxedTypeName();
return type.getParameterizedQualifiedSourceName();
}
}
public String identifier(String id) {
if(id == null) return null;
id = id.trim().replaceAll("[^A-Za-z0-9_]+", "_");
if(Character.isUpperCase(id.charAt(0)))
id = Character.toLowerCase(id.charAt(0))+id.substring(1);
return id;
}
public String replaceGroupMember(String expr, String newCallback) {
return expr.replaceFirst("group.(?:<[^>]*>)?member\\((?:\"[^\"]*\")?[^)]*\\)", newCallback);
}
public Object backslashEscape(String substring) {
return substring.replaceAll("([\'\"\\\\])", "\\\\$1").replace("\n", "\\n").replace("\r", "\\r");
}
protected boolean isConstants(GeneratorTypeInfo inType) throws UnableToCompleteException {
return inType.implementsInterface(commonTypes.constants)
|| inType.implementsInterface(commonTypes.dictionaryConstants);
}
}
@Override
protected GeneratorInstance createGeneratorInstance() {
return new GeneratorInstance();
}
}