001: /*
002: * Copyright 2006 Ethan Nicholas. All rights reserved.
003: * Use is subject to license terms.
004: */
005: package jaxx.compiler;
006:
007: import java.beans.Introspector;
008: import java.io.*;
009: import java.util.*;
010: import java.util.List;
011:
012: import jaxx.*;
013: import jaxx.parser.*;
014: import jaxx.reflect.*;
015: import jaxx.tags.*;
016: import jaxx.types.*;
017:
018: /** Represents a Java expression which fires a <code>PropertyChangeEvent</code> when it can be
019: * determined that its value may have changed. Events are fired on a "best effort" basis, and events
020: * may either be fired too often (the value has not actually changed) or not often enough (the value
021: * changed but no event was fired).
022: */
023: public class DataSource {
024: private class NULL {
025: }; // type attached to "null" constants in parsed expressions
026:
027: private String id;
028:
029: /** The Java source code for the expression. */
030: private String source;
031:
032: /** The current <code>JAXXCompiler</code>. */
033: private JAXXCompiler compiler;
034:
035: /** List of symbols which this data source expression depends on. */
036: private List/*<String>*/dependencySymbols = new ArrayList/*<String>*/();
037:
038: private StringBuffer addListenerCode = new StringBuffer();
039: private StringBuffer removeListenerCode = new StringBuffer();
040: private boolean compiled;
041:
042: /** Creates a new data source. After creating a <code>DataSource</code>, use {@link #compile}
043: * to cause it to function at runtime.
044: *
045: *@param id the DataSource's id
046: *@param source the Java source code for the data source expression
047: *@param compiler the current <code>JAXXCompiler</code>
048: */
049: public DataSource(String id, String source, JAXXCompiler compiler) {
050: this .id = id;
051: this .source = source;
052: this .compiler = compiler;
053: }
054:
055: public String getId() {
056: return id;
057: }
058:
059: /** Compiles the data source expression and listener. This method calls methods in <code>JAXXCompiler</code>
060: * to add the Java code that performs the data source setup. Adding listeners to <code>DataSource</code> is
061: * slightly more complicated than with ordinary classes, because <code>DataSource</code> only exists at compile
062: * time. You must pass in a Java expression which evaluates to a <code>PropertyChangeListener</code>; this
063: * expression will be compiled and evaluated at runtime to yield the <code>DataSource's</code> listener.
064: *
065: *@param propertyChangeListenerCode Java code snippet which evaluates to a <code>PropertyChangeListener</code>
066: *@return <code>true</code> if the expression has dependencies, <code>false</code> otherwise
067: *@throws CompilerException if a compilation error occurs
068: */
069: public boolean compile(String propertyChangeListenerCode)
070: throws CompilerException {
071: if (compiled)
072: throw new IllegalStateException(this
073: + " has already been compiled");
074: String id = compiler.getAutoId(ClassDescriptorLoader
075: .getClassDescriptor(getClass()));
076: JavaParser p = new JavaParser(new StringReader(source + ";"));
077: while (!p.Line()) {
078: SimpleNode node = p.popNode();
079: scanNode(node, id);
080: }
081:
082: if (dependencySymbols.size() > 0)
083: compiler.bodyCode.append("private PropertyChangeListener "
084: + id + " = " + propertyChangeListenerCode + ";\n");
085:
086: compileListeners();
087: compiled = true;
088:
089: return dependencySymbols.size() > 0;
090: }
091:
092: /** Returns a list of symbols on which this data source depends. */
093: public Collection/*<String>*/getDependencies() {
094: return Collections.unmodifiableList(dependencySymbols);
095: }
096:
097: /** Examines a node to identify any dependencies it contains. */
098: private void scanNode(SimpleNode node, String listenerId)
099: throws CompilerException {
100: switch (node.getId()) {
101: case JavaParserTreeConstants.JJTMETHODDECLARATION:
102: break;
103: case JavaParserTreeConstants.JJTFIELDDECLARATION:
104: break;
105:
106: default:
107: int count = node.jjtGetNumChildren();
108: for (int i = 0; i < count; i++)
109: scanNode(node.getChild(i), listenerId);
110: determineNodeType(node, listenerId);
111: }
112: }
113:
114: private ClassDescriptor determineLiteralType(SimpleNode node) {
115: assert node.getId() == JavaParserTreeConstants.JJTLITERAL;
116: if (node.jjtGetNumChildren() == 1) {
117: int id = node.getChild(0).getId();
118: if (id == JavaParserTreeConstants.JJTBOOLEANLITERAL)
119: return ClassDescriptorLoader
120: .getClassDescriptor(boolean.class);
121: else if (id == JavaParserTreeConstants.JJTNULLLITERAL)
122: return ClassDescriptorLoader
123: .getClassDescriptor(NULL.class);
124: else
125: throw new RuntimeException(
126: "Expected BooleanLiteral or NullLiteral, found "
127: + JavaParserTreeConstants.jjtNodeName[id]);
128: } else {
129: int id = node.firstToken.kind;
130: switch (id) {
131: case JavaParserConstants.INTEGER_LITERAL:
132: if (node.firstToken.image.toLowerCase().endsWith("l"))
133: return ClassDescriptorLoader
134: .getClassDescriptor(long.class);
135: else
136: return ClassDescriptorLoader
137: .getClassDescriptor(int.class);
138: case JavaParserConstants.CHARACTER_LITERAL:
139: return ClassDescriptorLoader
140: .getClassDescriptor(char.class);
141: case JavaParserConstants.FLOATING_POINT_LITERAL:
142: if (node.firstToken.image.toLowerCase().endsWith("f"))
143: return ClassDescriptorLoader
144: .getClassDescriptor(float.class);
145: else
146: return ClassDescriptorLoader
147: .getClassDescriptor(double.class);
148: case JavaParserConstants.STRING_LITERAL:
149: return ClassDescriptorLoader
150: .getClassDescriptor(String.class);
151: default:
152: throw new RuntimeException(
153: "Expected literal token, found "
154: + JavaParserConstants.tokenImage[id]);
155: }
156: }
157: }
158:
159: /** Scans through a compound symbol (foo.bar.baz) to identify and track all trackable pieces of it.
160: * Returns the type of the symbol (or null if it could not be determined).
161: */
162: private ClassDescriptor scanCompoundSymbol(String symbol,
163: ClassDescriptor contextClass, boolean isMethod,
164: String listenerId) {
165: String[] tokens = symbol.split("\\s*\\.\\s*");
166: StringBuffer currentSymbol = new StringBuffer();
167: StringBuffer tokensSeenSoFar = new StringBuffer();
168: boolean accepted = true; // if this ends up false, it means we weren't able to figure out
169: // which object the method is being invoked on
170: boolean recognizeClassNames = true;
171: for (int j = 0; j < tokens.length - (isMethod ? 1 : 0); j++) {
172: accepted = false;
173:
174: if (tokensSeenSoFar.length() > 0)
175: tokensSeenSoFar.append('.');
176: tokensSeenSoFar.append(tokens[j]);
177: if (currentSymbol.length() > 0)
178: currentSymbol.append('.');
179: currentSymbol.append(tokens[j]);
180:
181: if (currentSymbol.indexOf(".") == -1) {
182: String memberName = currentSymbol.toString();
183: CompiledObject object = compiler
184: .getCompiledObject(memberName);
185: if (object != null) {
186: contextClass = object.getObjectClass();
187: currentSymbol.setLength(0);
188: accepted = true;
189: recognizeClassNames = false;
190: } else {
191: try {
192: FieldDescriptor field = contextClass
193: .getFieldDescriptor(memberName);
194: trackMemberIfPossible(tokensSeenSoFar
195: .toString(), contextClass, field
196: .getName(), false, listenerId);
197: contextClass = field.getType();
198: currentSymbol.setLength(0);
199: accepted = true;
200: recognizeClassNames = false;
201: } catch (NoSuchFieldException e) {
202: if (j == 0
203: || j == 1
204: && tokens[0].equals(compiler
205: .getRootObject().getId())) { // still in root context
206: FieldDescriptor[] newFields = compiler
207: .getScriptFields();
208: for (int k = 0; k < newFields.length; k++) {
209: if (newFields[k].getName().equals(
210: memberName)) {
211: addListener(
212: tokensSeenSoFar.toString(),
213: null,
214: "addPropertyChangeListener(\""
215: + memberName
216: + "\", "
217: + listenerId
218: + ");"
219: + JAXXCompiler
220: .getLineSeparator(),
221: "removePropertyChangeListener(\""
222: + memberName
223: + "\", "
224: + listenerId
225: + ");"
226: + JAXXCompiler
227: .getLineSeparator());
228: contextClass = newFields[k]
229: .getType();
230: assert contextClass != null : "script field '"
231: + memberName
232: + "' is defined, but has type null";
233: currentSymbol.setLength(0);
234: accepted = true;
235: recognizeClassNames = false;
236: break;
237: }
238: }
239: }
240: }
241: }
242: }
243: if (currentSymbol.length() > 0 && recognizeClassNames) {
244: contextClass = TagManager.resolveClass(currentSymbol
245: .toString(), compiler);
246: if (contextClass != null) {
247: currentSymbol.setLength(0);
248: accepted = true;
249: recognizeClassNames = false;
250: // TODO: for now we don't handle statics
251: return null;
252: }
253: }
254: if (!accepted)
255: return null;
256: }
257:
258: return contextClass;
259: }
260:
261: /** Adds type information to nodes where possible, and as a side effect adds event listeners to nodes which
262: * can be tracked.
263: */
264: private ClassDescriptor determineExpressionType(
265: SimpleNode expression, String listenerId) {
266: assert expression.getId() == JavaParserTreeConstants.JJTPRIMARYEXPRESSION;
267: SimpleNode prefix = expression.getChild(0);
268: if (prefix.jjtGetNumChildren() == 1) {
269: int type = prefix.getChild(0).getId();
270: if (type == JavaParserTreeConstants.JJTLITERAL
271: || type == JavaParserTreeConstants.JJTEXPRESSION)
272: prefix.setJavaType(prefix.getChild(0).getJavaType());
273: else if (type == JavaParserTreeConstants.JJTNAME
274: && expression.jjtGetNumChildren() == 1) // name with no arguments after it
275: prefix.setJavaType(scanCompoundSymbol(prefix.getText()
276: .trim(), compiler.getRootObject()
277: .getObjectClass(), false, listenerId));
278: }
279:
280: if (expression.jjtGetNumChildren() == 1) {
281: return prefix.getJavaType();
282: }
283:
284: ClassDescriptor contextClass = prefix.getJavaType();
285: if (contextClass == null)
286: contextClass = compiler.getRootObject().getObjectClass();
287: String lastNode = prefix.getText().trim();
288:
289: for (int i = 1; i < expression.jjtGetNumChildren(); i++) {
290: SimpleNode suffix = expression.getChild(i);
291: if (suffix.jjtGetNumChildren() == 1
292: && suffix.getChild(0).getId() == JavaParserTreeConstants.JJTARGUMENTS) {
293: if (suffix.getChild(0).jjtGetNumChildren() == 0) { // at the moment only no-argument methods are trackable
294: contextClass = scanCompoundSymbol(lastNode,
295: contextClass, true, listenerId);
296: if (contextClass == null)
297: return null;
298: int dotPos = lastNode.lastIndexOf(".");
299: String objectCode = dotPos == -1 ? "" : lastNode
300: .substring(0, dotPos);
301: for (int j = i - 2; j >= 0; j--)
302: objectCode = expression.getChild(j).getText()
303: + objectCode;
304: if (objectCode.length() == 0)
305: objectCode = compiler.getRootObject()
306: .getJavaCode();
307: String methodName = lastNode.substring(dotPos + 1)
308: .trim();
309: try {
310: MethodDescriptor method = contextClass
311: .getMethodDescriptor(methodName,
312: new ClassDescriptor[0]);
313: trackMemberIfPossible(objectCode, contextClass,
314: method.getName(), true, listenerId);
315: return method.getReturnType();
316: } catch (NoSuchMethodException e) {
317: // happens for methods defined in the current JAXX file via scripts
318: String propertyName = null;
319: if (methodName.startsWith("is"))
320: propertyName = Introspector
321: .decapitalize(methodName
322: .substring("is".length()));
323: else if (methodName.startsWith("get"))
324: propertyName = Introspector
325: .decapitalize(methodName
326: .substring("get".length()));
327: if (propertyName != null) {
328: MethodDescriptor[] newMethods = compiler
329: .getScriptMethods();
330: for (int j = 0; j < newMethods.length; j++) {
331: if (newMethods[j].getName().equals(
332: methodName)) {
333: addListener(
334: compiler.getRootObject()
335: .getId(),
336: null,
337: "addPropertyChangeListener(\""
338: + propertyName
339: + "\", "
340: + listenerId
341: + ");"
342: + JAXXCompiler
343: .getLineSeparator(),
344: "removePropertyChangeListener(\""
345: + propertyName
346: + "\", "
347: + listenerId
348: + ");"
349: + JAXXCompiler
350: .getLineSeparator());
351: contextClass = newMethods[j]
352: .getReturnType();
353: break;
354: }
355: }
356: }
357: }
358: }
359: }
360: lastNode = suffix.getText().trim();
361: if (lastNode.startsWith("."))
362: lastNode = lastNode.substring(1);
363: }
364:
365: return null;
366: }
367:
368: private void trackMemberIfPossible(String objectCode,
369: ClassDescriptor objectClass, String memberName,
370: boolean method, String listenerId) {
371: if (objectClass.isInterface()) // might be technically possible to track in some cases, but for now
372: return; // we can't create a DefaultObjectHandler for interfaces
373:
374: DefaultObjectHandler handler = TagManager
375: .getTagHandler(objectClass);
376: try {
377: if (handler.isMemberBound(memberName)) {
378: addListener(objectCode + "." + memberName
379: + (method ? "()" : ""), objectCode, handler
380: .getAddMemberListenerCode(objectCode, id,
381: memberName, listenerId, compiler),
382: handler.getRemoveMemberListenerCode(objectCode,
383: id, memberName, listenerId, compiler));
384: }
385: } catch (UnsupportedAttributeException e) {
386: // ignore -- this is thrown for methods like toString(), for which there is no tracking and
387: // no setting support
388: }
389: }
390:
391: /** Adds type information to nodes where possible, and as a side effect adds event listeners to nodes which
392: * can be tracked.
393: */
394: private void determineNodeType(SimpleNode node, String listenerId) {
395: ClassDescriptor type = null;
396: if (node.jjtGetNumChildren() == 1)
397: type = node.getChild(0).getJavaType();
398: switch (node.getId()) {
399: case JavaParserTreeConstants.JJTCLASSORINTERFACETYPE:
400: type = ClassDescriptorLoader
401: .getClassDescriptor(Class.class);
402: break;
403: case JavaParserTreeConstants.JJTPRIMARYEXPRESSION:
404: type = determineExpressionType(node, listenerId);
405: break;
406: case JavaParserTreeConstants.JJTLITERAL:
407: type = determineLiteralType(node);
408: break;
409: case JavaParserTreeConstants.JJTCASTEXPRESSION:
410: type = TagManager.resolveClass(node.getChild(0).getText(),
411: compiler);
412: break;
413: }
414: node.setJavaType(type);
415: }
416:
417: private void addListener(String dependencySymbol,
418: String objectCode, String addCode, String removeCode) {
419: if (!dependencySymbols.contains(dependencySymbol)) {
420: dependencySymbols.add(dependencySymbol);
421: if (objectCode != null) {
422: addListenerCode.append("if (" + objectCode
423: + " != null) {"
424: + JAXXCompiler.getLineSeparator());
425: addListenerCode.append(" ");
426: }
427: addListenerCode.append(addCode);
428: if (objectCode != null)
429: addListenerCode.append("}");
430:
431: if (objectCode != null) {
432: removeListenerCode.append("if (" + objectCode
433: + " != null) {"
434: + JAXXCompiler.getLineSeparator());
435: removeListenerCode.append(" ");
436: }
437: removeListenerCode.append(removeCode);
438: if (objectCode != null)
439: removeListenerCode.append("}");
440: }
441: }
442:
443: private void compileListeners() {
444: if (addListenerCode.length() > 0) {
445: if (compiler.applyDataBinding.length() > 0)
446: compiler.applyDataBinding.append("else ");
447: compiler.applyDataBinding.append("if ($binding.equals("
448: + TypeManager.getJavaCode(id) + ")) {"
449: + JAXXCompiler.getLineSeparator());
450: compiler.applyDataBinding.append(" " + addListenerCode
451: + JAXXCompiler.getLineSeparator());
452: compiler.applyDataBinding.append("}"
453: + JAXXCompiler.getLineSeparator());
454: }
455:
456: if (removeListenerCode.length() > 0) {
457: if (compiler.removeDataBinding.length() > 0)
458: compiler.removeDataBinding.append("else ");
459: compiler.removeDataBinding.append("if ($binding.equals("
460: + TypeManager.getJavaCode(id) + ")) {"
461: + JAXXCompiler.getLineSeparator());
462: compiler.removeDataBinding.append(" "
463: + removeListenerCode
464: + JAXXCompiler.getLineSeparator());
465: compiler.removeDataBinding.append("}"
466: + JAXXCompiler.getLineSeparator());
467: }
468: }
469: }
|