001: package jimm.datavision;
002:
003: import jimm.datavision.field.Field;
004: import jimm.datavision.field.SpecialField;
005: import jimm.datavision.source.Column;
006: import jimm.util.StringUtils;
007: import jimm.util.XMLWriter;
008: import jimm.util.Replacer;
009: import jimm.util.I18N;
010:
011: /**
012: * A formula is a Bean Scripting Framework (BSF) script evaluated at runtime.
013: * It may contain database column values, other formulas, special values, and
014: * aggregates.
015: * <p>
016: * Before being evaluated, the following substitutions are made withing
017: * the evaluation string of a formula:
018: * <ul>
019: * <li>{<i>table_name.column_name</i>} is replaced by the current value
020: * of the column <i>table_name.column_name</i>, but only if the column exists.
021: * If it doesn't, then the entire "{...}" text is kept as-is.</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 class Formula extends Expression {
035:
036: protected String language;
037: protected Object cachedEvalResult;
038: protected boolean useCache;
039: protected boolean shouldEvaluate;
040: protected boolean showException;
041:
042: /**
043: * Constructor.
044: *
045: * @param id the unique identifier for the new formula; if <code>null</code>,
046: * generate a new id
047: * @param report the report containing this formula
048: * @param name the formula name
049: */
050: public Formula(Long id, Report report, String name) {
051: this (id, report, name, null);
052: useCache = false;
053: shouldEvaluate = true;
054: showException = true;
055: }
056:
057: /**
058: * Constructor. If <i>id</i> is <code>null</code>, generates a new id number.
059: * This number is one higher than any previously-seen id number. This does
060: * <em>not</em> guarantee that no later formula will be created manually with
061: * the same id number.
062: *
063: * @param id the unique identifier for the new formula; if <code>null</code>,
064: * generate a new id
065: * @param report the report containing this formula
066: * @param name the formula name
067: * @param evalString the string to evaulate at runtime.
068: */
069: public Formula(Long id, Report report, String name,
070: String evalString) {
071: super (id == null ? report.generateNewFormulaId() : id, report,
072: name, evalString, "#");
073: language = report.getScripting().getDefaultLanguage();
074: }
075:
076: public String dragString() {
077: return "formula:" + getId();
078: }
079:
080: public String designLabel() {
081: return "{@" + getName() + "}";
082: }
083:
084: public String formulaString() {
085: return "{@" + getId() + "}";
086: }
087:
088: /**
089: * Tells this formula to evaluate only when <var>shouldEvaluate</var> is
090: * <code>true</code> and to return the cached value otherwise.
091: */
092: public void useCache() {
093: useCache = true;
094: }
095:
096: /**
097: * Tells this formula to evaluate the BSF script the next time
098: * <code>eval</code> is called.
099: */
100: public void shouldEvaluate() {
101: shouldEvaluate = true;
102: }
103:
104: public void setExpression(String newExpression) {
105: super .setExpression(newExpression);
106: showException = true;
107: }
108:
109: /**
110: * Evaluate this formula and return the result as either a
111: * <code>String</code> or a <code>Double</code>. Before evaluation, special
112: * fields, formulas, and column references are subtituted with their
113: * values.
114: * <p>
115: * After substitution, if the formula contains a <code>null</code> anywhere
116: * then <code>null</code> is returned.
117: *
118: * @return the result of evaluating the formula as either a
119: * <code>String</code> or a <code>Double</code>; possibly <code>null</code>
120: */
121: public Object eval() {
122: return eval(null);
123: }
124:
125: /**
126: * Evaluate this formula and return the result. Before evaluation, special
127: * fields, formulas, and column references are subtituted with their
128: * values. <code>null</code> values are replaced by "nil".
129: * <p>
130: * After substitution, if the formula contains a <code>null</code> anywhere
131: * then <code>null</code> is returned. If the BSF script throws an exception,
132: * then <code>null</code> is returned.
133: * <p>
134: * During substitution, we take care not to replace "#{...}" Ruby string
135: * substition operators.
136: * <p>
137: * The <var>formulaField</var> is used when the formula contains a
138: * "group.count" special field.
139: *
140: * @param formulaField the field containing this formula; may be
141: * <code>null</code>
142: * @return the result of evaluating the formula as a BSF script; possibly
143: * <code>null</code>
144: *
145: */
146: public Object eval(Field formulaField) {
147: if (!useCache || shouldEvaluate) {
148: cachedEvalResult = evaluate(formulaField);
149: shouldEvaluate = false;
150: }
151: return cachedEvalResult;
152: }
153:
154: /**
155: * Modifies the formula text so it is ready to evaluate, then gives it to the
156: * report to evaluate and returns the result. {@link #eval} calls this method
157: * and stores the return value into <var>cachedEvalResult</var>.
158: *
159: * @param formulaField the field that is using this formula, used to
160: * evaluate any special fields in the formula; may be <code>null</code>
161: * @return the result of evaluating the formula as a BSF script; possibly
162: * <code>null</code>
163: * @see SpecialField#value
164: */
165: protected Object evaluate(final Field formulaField) {
166: String str = getExpression();
167: if (str == null || str.trim().length() == 0)
168: return null;
169:
170: // Special values
171: str = StringUtils.replaceDelimited("#", "{%", "}",
172: new Replacer() {
173: public Object replace(String str) {
174: Object obj = SpecialField.value(formulaField,
175: str, report);
176: return obj == null ? "nil" : obj;
177: }
178: }, str);
179: if (str == null)
180: return null;
181:
182: // Formula values
183: str = StringUtils.replaceDelimited("#", "{@", "}",
184: new Replacer() {
185: public Object replace(String str) {
186: Formula f = report.findFormula(str);
187: return f == null ? "nil" : f.eval(formulaField);
188: }
189: }, str);
190: if (str == null)
191: return null;
192:
193: // Parameter values
194: str = StringUtils.replaceDelimited("#", "{?", "}",
195: new Replacer() {
196: public Object replace(String str) {
197: Parameter p = report.findParameter(str);
198: return p == null ? "nil" : p.getValue();
199: }
200: }, str);
201: if (str == null)
202: return null;
203:
204: // User column values
205: str = StringUtils.replaceDelimited("#", "{!", "}",
206: new Replacer() {
207: public Object replace(String str) {
208: UserColumn uc = report.findUserColumn(str);
209: return uc == null ? "nil" : report
210: .columnValue(uc);
211: }
212: }, str);
213: if (str == null)
214: return null;
215:
216: // Column values
217: str = StringUtils.replaceDelimited("#", "{", "}",
218: new Replacer() {
219: public Object replace(String str) {
220: Column col = report.findColumn(str);
221: if (col == null)
222: return "{" + str + "}";
223:
224: Object val = null;
225: switch (col.getType()) {
226: case java.sql.Types.CHAR:
227: case java.sql.Types.VARCHAR:
228: case java.sql.Types.DATE:
229: case java.sql.Types.TIME:
230: case java.sql.Types.TIMESTAMP:
231: val = report.columnValue(col);
232: val = val == null ? "nil" : quoted(val);
233: break;
234: default:
235: val = report.columnValue(col);
236: if (val == null)
237: val = "nil";
238: break;
239: }
240: return val;
241: }
242: }, str);
243: if (str == null || str.trim().length() == 0)
244: return null;
245:
246: try {
247: return report.eval(getLanguage(), str, getName());
248: } catch (Exception e) {
249: if (showException) {
250: showException = false;
251: // I don't pass e to error() so we avoid a stack trace, which
252: // will be almost useless to the user or to me.
253: ErrorHandler.error(I18N.get("Formula.script_error")
254: + " \"" + str + '"' + ": " + e.toString(), I18N
255: .get("Formula.script_error_title"));
256: }
257: return null;
258: }
259: }
260:
261: /**
262: * Returns the scripting language this formula uses.
263: *
264: * @return the language to use when evaluating this formula
265: */
266: public String getLanguage() {
267: return language == null ? report.getScripting()
268: .getDefaultLanguage() : language;
269: }
270:
271: public void setLanguage(String newLang) {
272: if (newLang == null)
273: newLang = report.getScripting().getDefaultLanguage();
274:
275: if (!language.equals(newLang)) {
276: language = newLang;
277: setChanged();
278: notifyObservers();
279: }
280: }
281:
282: /**
283: * Returns a string representation of an object enclosed in double quotes.
284: * Each double quote and backslash within the object's string
285: * representation is escaped with a backslash character.
286: *
287: * @param obj any object
288: * @return a double-quoted string representation of the object
289: */
290: protected String quoted(Object obj) {
291: String val = obj.toString();
292: StringBuffer buf = new StringBuffer("\"");
293: int len = val.length();
294: for (int i = 0; i < len; ++i) {
295: char c = val.charAt(i);
296: switch (c) {
297: case '"':
298: case '\\':
299: buf.append('\\');
300: buf.append(c);
301: break;
302: default:
303: buf.append(c);
304: break;
305: }
306: }
307: buf.append('"');
308: return buf.toString();
309: }
310:
311: /**
312: * If <var>obj</var> is a <code>String</code> and it is surrounded by
313: * double quotes, return an unquoted copy. All doubled double quotes
314: * are turned into single double quotes.
315: *
316: * @param obj any object
317: * @return <var>obj</var> if it is not a <code>String</code>, an unquoted
318: * copy if it is a <code>String</code>
319: */
320: public Object unquoted(Object obj) {
321: if (obj instanceof String) {
322: String str = (String) obj;
323: if (str.startsWith("\"") && str.endsWith("\"")) {
324: str = str.substring(1, str.length() - 1);
325: StringBuffer buf = new StringBuffer();
326: int oldPos = 0;
327: for (int pos = str.indexOf("\\"); pos != -1; pos = str
328: .indexOf("\\", oldPos)) {
329: buf.append(str.substring(oldPos, pos));
330: buf.append(str.charAt(pos + 1));
331: oldPos = pos + 2;
332: }
333: buf.append(str.substring(oldPos));
334: return buf.toString();
335: }
336: }
337: return obj;
338: }
339:
340: public void writeXML(XMLWriter out) {
341: writeXML(out, "formula");
342: }
343:
344: protected void writeAdditionalAttributes(XMLWriter out) {
345: if (language != null
346: && language.length() != 0
347: && !language.equals(report.getScripting()
348: .getDefaultLanguage()))
349: out.attr("language", language);
350:
351: }
352:
353: }
|