001: package jimm.datavision;
002:
003: import jimm.datavision.field.Field;
004: import jimm.util.XMLWriter;
005: import jimm.util.StringUtils;
006: import jimm.util.Replacer;
007: import jimm.datavision.source.Column;
008: import jimm.util.I18N;
009: import java.util.*;
010:
011: /**
012: * The abstract superclass of objects that are evaluated, such as formulas
013: * and user columns. An expression contains text that is evaluated. The
014: * text may contain database column values, formulas, special values,
015: * or other types of objects.
016: * <p>
017: * Before being evaluated, the following substitutions are made withing
018: * the evaluation string:
019: * <ul>
020: * <li>{<i>table_name.column_name</i>} is replaced by the current value
021: * of the column <i>table_name.column_name</i>.</li>
022: * <li>{@<i>id_number</i>} is replaced by the results of evaluating the
023: * formula whose id is <i>id_number</i>.</li>
024: * <li> {%<i>special_value_name</i>} is replaced by a special value
025: * (report title, report run date, page number, or record number).</li>
026: * <li> {?<i>id_number</i>} is replaced by a parameter value (string,
027: * number, or date).</li>
028: * <li> {!<i>id_number</i>} is replaced by a user column's value (string,
029: * number, or date).</li>
030: * <ul>
031: *
032: * @author Jim Menard, <a href="mailto:jimm@io.com">jimm@io.com</a>
033: */
034: public abstract class Expression extends Observable implements
035: Identity, Nameable, Writeable, Draggable, Observer {
036:
037: protected Long id;
038: protected Report report;
039: protected String name;
040: protected String expr;
041: protected String exceptAfter;
042: protected ArrayList observedContents;
043:
044: /**
045: * Given a string, returns a string with all instances of formula,
046: * parameter, and user column "formula strings" replaced by "display name"
047: * strings. If there are no such strings, the original string is returned.
048: *
049: * @return a string with all formula strings replaced by display name
050: * strings
051: */
052: public static String expressionToDisplay(Report report, String str) {
053: if (str == null || str.length() == 0 || str.indexOf("{") == -1)
054: return str;
055:
056: StringBuffer buf = new StringBuffer();
057: int len = str.length();
058: for (int i = 0; i < len; ++i) {
059: char c = str.charAt(i);
060: if (c == '{' && (i + 1) < len) {
061: int nameStart, nameEnd;
062: switch (str.charAt(i + 1)) {
063: case '@': // Formula
064: nameStart = i + 2;
065: nameEnd = str.indexOf("}", nameStart);
066: if (nameEnd != -1) {
067: String idAsString = str.substring(nameStart,
068: nameEnd);
069: buf.append("{@");
070: buf.append(report.findFormula(idAsString)
071: .getName());
072: buf.append("}");
073: i = nameEnd;
074: }
075: break;
076: case '?': // Parameter
077: nameStart = i + 2;
078: nameEnd = str.indexOf("}", nameStart);
079: if (nameEnd != -1) {
080: String idAsString = str.substring(nameStart,
081: nameEnd);
082: buf.append("{?");
083: buf.append(report.findParameter(idAsString)
084: .getName());
085: buf.append("}");
086: i = nameEnd;
087: }
088: break;
089: case '!': // User column
090: nameStart = i + 2;
091: nameEnd = str.indexOf("}", nameStart);
092: if (nameEnd != -1) {
093: String idAsString = str.substring(nameStart,
094: nameEnd);
095: buf.append("{!");
096: buf.append(report.findUserColumn(idAsString)
097: .getName());
098: buf.append("}");
099: i = nameEnd;
100: }
101: break;
102: default:
103: buf.append(c);
104: break;
105: }
106: } else {
107: buf.append(c);
108: }
109: }
110: return buf.toString();
111: }
112:
113: /**
114: * Given a string, returns a string with all instances of formula,
115: * parameter, and user column "display names" replaced by "formula
116: * strings". If there are no such strings, the original string is returned.
117: *
118: * @param report a report
119: * @param str a string with display names
120: * @return a string with all display names replaced by formula strings
121: */
122: public static String displayToExpression(Report report, String str) {
123: if (str == null || str.length() == 0 || str.indexOf("{") == -1)
124: return str;
125:
126: StringBuffer buf = new StringBuffer();
127: int len = str.length();
128: for (int i = 0; i < len; ++i) {
129: char c = str.charAt(i);
130: if (c == '{' && (i + 1) < len) {
131: int nameStart, nameEnd;
132: switch (str.charAt(i + 1)) {
133: case '@': // Formula
134: nameStart = i + 2;
135: nameEnd = str.indexOf("}", nameStart);
136: if (nameEnd != -1) {
137: String formulaName = str.substring(nameStart,
138: nameEnd);
139: buf.append("{@");
140: Formula formula = report
141: .findFormulaByName(formulaName);
142: if (formula == null) {
143: str = I18N.get("Utils.in") + " \"" + str
144: + "\": "
145: + I18N.get("Utils.no_such_formula")
146: + ' ' + formulaName;
147: throw new IllegalArgumentException(str);
148: }
149: buf.append(formula.getId());
150: buf.append("}");
151: i = nameEnd;
152: }
153: break;
154: case '?': // Parameter
155: nameStart = i + 2;
156: nameEnd = str.indexOf("}", nameStart);
157: if (nameEnd != -1) {
158: String paramName = str.substring(nameStart,
159: nameEnd);
160: buf.append("{?");
161: Parameter param = report
162: .findParameterByName(paramName);
163: if (param == null) {
164: str = I18N.get("Utils.in") + " \"" + str
165: + "\": "
166: + I18N.get("Utils.no_such_param")
167: + ' ' + paramName;
168: throw new IllegalArgumentException(str);
169: }
170: buf.append(param.getId());
171: buf.append("}");
172: i = nameEnd;
173: }
174: break;
175: case '!': // User column
176: nameStart = i + 2;
177: nameEnd = str.indexOf("}", nameStart);
178: if (nameEnd != -1) {
179: String ucName = str.substring(nameStart,
180: nameEnd);
181: buf.append("{!");
182: UserColumn uc = report
183: .findUserColumnByName(ucName);
184: if (uc == null) {
185: str = I18N.get("Utils.in") + " \"" + str
186: + "\": "
187: + I18N.get("Utils.no_such_usercol")
188: + ' ' + ucName;
189: throw new IllegalArgumentException(str);
190: }
191: buf.append(uc.getId());
192: buf.append("}");
193: i = nameEnd;
194: }
195: break;
196: default:
197: buf.append(c);
198: break;
199: }
200: } else {
201: buf.append(c);
202: }
203: }
204:
205: return buf.toString();
206: }
207:
208: /**
209: * Constructor. If <i>id</i> is <code>null</code>, throws an
210: * <code>IllegalArgumentException</code>. This is because subclasses are
211: * responsible for generating their id number. For example, formulas call
212: * <code>Report.generateNewFormulaId</code>.
213: *
214: * @param id the unique identifier for the new expression; may not be
215: * <code>null</code>
216: * @param report the report containing this expression
217: * @param name the expression name
218: * @param expression the string to evaulate at runtime; may be
219: * <code>null</code>
220: * @param exceptAfter when looking for things inside "{}" braces, ignore
221: * braces immediately after this string
222: */
223: protected Expression(Long id, Report report, String name,
224: String expression, String exceptAfter) {
225: if (id == null) // Need not use I18N; this is a programmer err
226: throw new IllegalArgumentException(
227: "Subclasses of Expression must"
228: + " not pass in a null id");
229:
230: this .report = report;
231: this .id = id;
232: this .name = name;
233: expr = expression;
234: this .exceptAfter = exceptAfter;
235:
236: // I'd like to start observing the contents of the eval string here,
237: // but the other expressions may not yet be defined (for example, when
238: // reading in expressions from an XML file). That's why we start observing
239: // contents when someone asks for the eval string.
240: observedContents = null;
241: }
242:
243: protected void finalize() throws Throwable {
244: stopObservingContents();
245: super .finalize();
246: }
247:
248: public void update(Observable o, Object arg) {
249: setChanged();
250: notifyObservers(arg);
251: }
252:
253: public Object getId() {
254: return id;
255: }
256:
257: /**
258: * Returns the name for this expression.
259: *
260: * @return the name
261: */
262: public String getName() {
263: return name;
264: }
265:
266: /**
267: * Sets the name.
268: *
269: * @param newName the new name
270: */
271: public void setName(String newName) {
272: if (name != newName && (name == null || !name.equals(newName))) {
273: name = newName;
274: setChanged();
275: notifyObservers();
276: }
277: }
278:
279: /**
280: * Returns the expression string.
281: *
282: * @return the eval string
283: */
284: public String getExpression() {
285: // I'd like to start observing the contents of the eval string as soon
286: // as we are constructed, but the other expressions may not yet be defined
287: // (for example, when reading in expressions from an XML file). That's why
288: // we start observing contents when someone asks for the eval string.
289: if (observedContents == null)
290: startObservingContents();
291:
292: return expr;
293: }
294:
295: /**
296: * Sets the eval string.
297: *
298: * @param newExpression the new eval string
299: */
300: public void setExpression(String newExpression) {
301: if (expr != newExpression
302: && (expr == null || !expr.equals(newExpression))) {
303: stopObservingContents();
304: expr = newExpression;
305:
306: // Don't start observing contents yet. Wait until someone calls
307: // getExpression(). See the comment there.
308: // startObservingContents();
309:
310: setChanged();
311: notifyObservers();
312: }
313: }
314:
315: /**
316: * Starts observing all observables referenced by this expression: formulas,
317: * parameters, and user columns.
318: */
319: protected void startObservingContents() {
320: observedContents = new ArrayList(); // Even if expr is null
321:
322: if (expr == null || expr.length() == 0)
323: return;
324:
325: // Here, we are using replacers so we can start observing things.
326: // Usually, they are used to replace strings.
327:
328: // Formulas
329: StringUtils.replaceDelimited(exceptAfter, "{@", "}",
330: new Replacer() {
331: public Object replace(String str) {
332: Formula f = report.findFormula(str);
333: observedContents.add(f);
334: f.addObserver(Expression.this );
335: return ""; // Avoid early bail-out
336: }
337: }, expr);
338:
339: // Parameters
340: StringUtils.replaceDelimited(exceptAfter, "{?", "}",
341: new Replacer() {
342: public Object replace(String str) {
343: Parameter p = report.findParameter(str);
344: observedContents.add(p);
345: p.addObserver(Expression.this );
346: return ""; // Avoid early bail-out
347: }
348: }, expr);
349:
350: // User columns
351: StringUtils.replaceDelimited(exceptAfter, "{!", "}",
352: new Replacer() {
353: public Object replace(String str) {
354: UserColumn uc = report.findUserColumn(str);
355: observedContents.add(uc);
356: uc.addObserver(Expression.this );
357: return ""; // Avoid early bail-out
358: }
359: }, expr);
360: }
361:
362: /**
363: * Stops observing that which we were observing.
364: */
365: protected void stopObservingContents() {
366: if (observedContents != null) {
367: for (Iterator iter = observedContents.iterator(); iter
368: .hasNext();)
369: ((Observable) iter.next()).deleteObserver(this );
370: observedContents = null;
371: }
372: }
373:
374: /**
375: * Returns the expression string fit for human consumption. This mainly means
376: * that we substitute formula, parameter, and user column numbers with names.
377: * Called from any expression editor. This code assumes that curly braces are
378: * never nested.
379: *
380: * @return the eval string with formula, parameter, and user column id numbers
381: * replaced with names
382: */
383: public String getEditableExpression() {
384: return expressionToDisplay(report, getExpression());
385: }
386:
387: /**
388: * Sets the eval string after replacing formula, parameter, and user column
389: * names with their id numbers. Called from a editor.
390: * <p>
391: * This method will throw an <code>IllegalArgumentException</code> if any
392: * formula, parameter, or user column name is not the name of some existing
393: * object.
394: *
395: * @param newExpression the new eval string
396: * @throws IllegalArgumentException
397: */
398: public void setEditableExpression(String newExpression) {
399: setExpression(displayToExpression(report, newExpression));
400: }
401:
402: public abstract String dragString();
403:
404: public abstract String designLabel();
405:
406: public abstract String formulaString();
407:
408: /**
409: * Returns <code>true</code> if this expression contains a reference to the
410: * specified field.
411: *
412: * @param f a field
413: * @return <code>true</code> if this field contains a reference to the
414: * specified field
415: */
416: public boolean refersTo(Field f) {
417: String str = getExpression();
418: if (str != null && str.length() > 0)
419: return str.indexOf(f.formulaString()) != -1;
420: else
421: return false;
422: }
423:
424: /**
425: * Returns <code>true</code> if this expression contains a reference to the
426: * specified expression (formula or user column).
427: *
428: * @param expression an expression
429: * @return <code>true</code> if this field is the same as or contains a
430: * reference to the specified expression
431: */
432: public boolean refersTo(Expression expression) {
433: String str = getExpression();
434: if (str != null && str.length() > 0)
435: return str.indexOf(expression.formulaString()) != -1;
436: else
437: return false;
438: }
439:
440: /**
441: * Returns <code>true</code> if this expression contains a reference to the
442: * specified parameter.
443: *
444: * @param p a parameter
445: * @return <code>true</code> if this field contains a reference to the
446: * specified parameter
447: */
448: public boolean refersTo(Parameter p) {
449: String str = getExpression();
450: if (str != null && str.length() > 0)
451: return str.indexOf(p.formulaString()) != -1;
452: else
453: return false;
454: }
455:
456: /**
457: * Returns a collection of the columns used in the expression. This is used
458: * by the report's query when it is figuring out what columns and tables
459: * are used by the report.
460: *
461: * @return a possibly empty collection of database columns
462: * @see jimm.datavision.source.Query#findSelectablesUsed
463: */
464: public Collection columnsUsed() {
465: final ArrayList list = new ArrayList();
466:
467: // We are using a replacer passively, to look for curly-delimited
468: // expressions. Nothing in the expression text gets modified.
469: StringUtils.replaceDelimited(exceptAfter, "{", "}",
470: new Replacer() {
471: public Object replace(String str) {
472: switch (str.charAt(0)) {
473: case '!': // User column
474: UserColumn uc = report.findUserColumn(str
475: .substring(1));
476: if (uc != null) // Should never be null
477: list.addAll(uc.columnsUsed());
478: break;
479: case '%': // Special field
480: case '@': // Formula
481: case '?': // Parameter
482: break; // ...all are ignored
483: default:
484: Column col = report.findColumn(str);
485: if (col != null) // May be null if language uses braces
486: list.add(col);
487: }
488: return ""; // So we don't quit early
489: }
490: }, getExpression());
491:
492: return list;
493: }
494:
495: /**
496: * Returns a collection of the user columns used in the expression. This
497: * is used by the report's query when it is figuring out what columns,
498: * tables, and user columns are used by the report.
499: *
500: * @return a possibly empty collection of user columns
501: * @see jimm.datavision.source.Query#findSelectablesUsed
502: */
503: public Collection userColumnsUsed() {
504: final ArrayList list = new ArrayList();
505:
506: // We are using a replacer passively, to look for curly-delimited
507: // expressions. Nothing in the expression text gets modified.
508: StringUtils.replaceDelimited(exceptAfter, "{!", "}",
509: new Replacer() {
510: public Object replace(String str) {
511: UserColumn uc = report.findUserColumn(str);
512: if (uc != null) // Should never be null
513: list.add(uc);
514: return ""; // So we don't bail out
515: }
516: }, getExpression());
517:
518: return list;
519: }
520:
521: /**
522: * Writes this expression as an XML tag.
523: *
524: * @param out a writer that knows how to write XML
525: */
526: public abstract void writeXML(XMLWriter out);
527:
528: protected void writeXML(XMLWriter out, String elementName) {
529: out.startElement(elementName);
530: out.attr("id", id);
531: out.attr("name", name);
532: writeAdditionalAttributes(out);
533: out.cdata(getExpression());
534: out.endElement();
535: }
536:
537: /**
538: * Writes additional attributes. Default behavior is to do nothing.
539: *
540: * @param out a writer that knows how to write XML
541: */
542: protected void writeAdditionalAttributes(XMLWriter out) {
543: }
544:
545: }
|