001: package jimm.datavision;
002:
003: import jimm.util.XMLWriter;
004: import jimm.util.I18N;
005: import java.util.Observable;
006: import java.util.ArrayList;
007: import java.util.Iterator;
008: import java.util.Date;
009: import java.io.StringWriter;
010: import java.text.SimpleDateFormat;
011: import java.text.ParsePosition;
012:
013: /**
014: * A parameter is a piece of data the value of which is determined by
015: * asking the user each time a report runs. Default values are only used
016: * when asking the user for values, not when generating values in
017: * <code>getValue</code>.
018: * <p>
019: * I started out with subclasses for each type of parameter. The problem is,
020: * the user gets to pick what kind of data the parameter holds and that
021: * type can be changed any time after the parameter gets created. Therefore,
022: * we hold objects and change our output based on the type of the data.
023: *
024: * @author Jim Menard, <a href="mailto:jimm@io.com">jimm@io.com</a>
025: */
026: public class Parameter extends Observable implements Identity,
027: Nameable, Writeable, Draggable, Cloneable {
028:
029: public static final int TYPE_BOOLEAN = 0;
030: public static final int TYPE_STRING = 1;
031: public static final int TYPE_NUMERIC = 2;
032: public static final int TYPE_DATE = 3;
033:
034: public static final int ARITY_ONE = 0;
035: public static final int ARITY_RANGE = 1;
036: public static final int ARITY_LIST_SINGLE = 2;
037: public static final int ARITY_LIST_MULTIPLE = 3;
038:
039: protected static SimpleDateFormat formatter = new SimpleDateFormat(
040: "yyyy-MM-dd");
041: protected static ParsePosition parsePosition = new ParsePosition(0);
042:
043: protected Long id;
044: protected Report report;
045: protected String name;
046: protected String question;
047: protected int type;
048: protected int arity;
049: protected ArrayList defaultValues;
050: protected ArrayList values;
051:
052: /**
053: * Constructor. Creates a string parameter with no name or question
054: * string.
055: *
056: * @param id the unique identifier for the new parameter; if
057: * <code>null</code>, generate a new id
058: * @param report the report in which this parameter resides
059: */
060: public Parameter(Long id, Report report) {
061: this (id, report, "string", "", "", "single");
062: }
063:
064: /**
065: * Constructor.
066: * <p>
067: * If <i>id</i> is <code>null</code>, generates a new id number. This number
068: * is one higher than any previously-seen id number. This does <em>not</em>
069: * guarantee that no later parameter will be created manually with the same
070: * id number.
071: *
072: * @param id the unique identifier for the new parameter; if
073: * <code>null</code>, generate a new id
074: * @param report the report in which this parameter resides
075: * @param typeName one of "string", "numeric", or "date"; found in report XML
076: * @param name the name of this parameter
077: * @param question the question to ask when getting the parameter's value
078: * from the user
079: * @param arityString arity (single, range, list) as a string
080: */
081: public Parameter(Long id, Report report, String typeName,
082: String name, String question, String arityString) {
083: this .report = report;
084:
085: // Convert type name to type number.
086: if (typeName == null || typeName.length() == 0) {
087: String str = I18N.get("Parameter.param_cap") + " " + id
088: + ": " + I18N.get("Parameter.missing_type");
089: throw new IllegalArgumentException(str);
090: }
091:
092: typeName = typeName.toLowerCase().trim();
093: if ("boolean".equals(typeName))
094: type = TYPE_BOOLEAN;
095: else if ("string".equals(typeName))
096: type = TYPE_STRING;
097: else if ("numeric".equals(typeName))
098: type = TYPE_NUMERIC;
099: else if ("date".equals(typeName))
100: type = TYPE_DATE;
101: else {
102: String str = I18N.get("Parameter.param_cap") + " " + id
103: + ": " + I18N.get("Parameter.illegal_type");
104: throw new IllegalArgumentException(str);
105: }
106:
107: this .name = name;
108: this .question = question;
109:
110: // Convert arity string ("range", "list", or "single").
111: if (arityString == null || arityString.length() == 0) {
112: String str = I18N.get("Parameter.param_cap") + id + ": "
113: + I18N.get("Parameter.missing_arity");
114: throw new IllegalArgumentException(str);
115: }
116: arityString = arityString.toLowerCase().trim();
117: if ("single".equals(arityString))
118: arity = ARITY_ONE;
119: else if ("range".equals(arityString))
120: arity = ARITY_RANGE;
121: else if ("list-single".equals(arityString))
122: arity = ARITY_LIST_SINGLE;
123: else if ("list-multiple".equals(arityString))
124: arity = ARITY_LIST_MULTIPLE;
125: else {
126: String str = I18N.get("Parameter.param_cap") + id + ": "
127: + I18N.get("Parameter.illegal_arity");
128: throw new IllegalArgumentException(str);
129: }
130:
131: initialize(id);
132: }
133:
134: /**
135: * Constructor.
136: * <p>
137: * If <i>id</i> is <code>null</code>, generates a new id number. This number
138: * is one higher than any previously-seen id number. This does <em>not</em>
139: * guarantee that no later parameter will be created manually with the same
140: * id number.
141: *
142: * @param id the unique identifier for the new parameter; if
143: * <code>null</code>, generate a new id
144: * @param report the report in which this parameter resides
145: * @param type one of
146: * <code>TYPE_BOOLEAN</code>, <code>TYPE_STRING</code>,
147: * <code>TYPE_NUMERIC</code>, or <code>TYPE_DATE</code>
148: * @param name the name of this parameter
149: * @param question the name of this parameter
150: * @param arity one of <code>ARITY_ONE</code>, <code>ARITY_RANGE</code>,
151: * <code>ARITY_LIST_SINGLE</code>, or <code>ARITY_LIST_MULTIPLE</code>
152: */
153: public Parameter(Long id, Report report, int type, String name,
154: String question, int arity) {
155: this .report = report;
156: this .type = type;
157: this .name = name;
158: this .question = question;
159: this .arity = arity;
160:
161: initialize(id);
162: }
163:
164: private void initialize(Long id) {
165: if (id == null)
166: id = report.generateNewParameterId();
167: this .id = id;
168:
169: // Check for legal combinations of type and arity
170: switch (type) {
171: case TYPE_BOOLEAN:
172: if (arity != ARITY_ONE) {
173: String str = I18N.get("Parameter.param_cap") + id
174: + ": " + I18N.get("Parameter.yesno_single");
175: throw new IllegalArgumentException(str);
176: }
177: break;
178: case TYPE_DATE:
179: if (arity != ARITY_ONE && arity != ARITY_RANGE) {
180: String str = I18N.get("Parameter.param_cap") + id
181: + ": " + I18N.get("Parameter.date_arity_err");
182: throw new IllegalArgumentException(str);
183: }
184: break;
185: }
186:
187: defaultValues = new ArrayList();
188: values = new ArrayList();
189: }
190:
191: public Object clone() {
192: Parameter p = new Parameter(null, report, type, name, question,
193: arity);
194: for (Iterator iter = defaultValues.iterator(); iter.hasNext();) {
195: Object obj = iter.next();
196: if (obj instanceof Boolean)
197: p.defaultValues.add(obj);
198: else if (obj instanceof String)
199: p.defaultValues.add(new String((String) obj));
200: else if (obj instanceof Number)
201: p.defaultValues.add(obj);
202: else if (obj instanceof Date)
203: p.defaultValues.add(((Date) obj).clone());
204: }
205: return p;
206: }
207:
208: public Object getId() {
209: return id;
210: }
211:
212: /**
213: * Returns the name for this parameter.
214: *
215: * @return the name
216: */
217: public String getName() {
218: return name;
219: }
220:
221: /**
222: * Sets the name.
223: *
224: * @param newName the new name
225: */
226: public void setName(String newName) {
227: if (name != newName && (name == null || !name.equals(newName))) {
228: name = newName;
229: setChanged();
230: notifyObservers();
231: }
232: }
233:
234: /**
235: * Returns the question for this parameter.
236: *
237: * @return the question
238: */
239: public String getQuestion() {
240: return question;
241: }
242:
243: /**
244: * Sets the question.
245: *
246: * @param newQuestion the new question
247: */
248: public void setQuestion(String newQuestion) {
249: if (question != newQuestion
250: && (question == null || !question.equals(newQuestion))) {
251: question = newQuestion;
252: setChanged();
253: notifyObservers();
254: }
255: }
256:
257: /**
258: * Returns the type of this field. Will be one of
259: * <code>TYPE_BOOLEAN</code>, <code>TYPE_STRING</code>,
260: * <code>TYPE_NUMERIC</code>, or <code>TYPE_DATE</code>.
261: *
262: * @return the type number
263: */
264: public int getType() {
265: return type;
266: }
267:
268: /**
269: * Sets the parameter type. Must be one of
270: * <code>TYPE_BOOLEAN</code>, <code>TYPE_STRING</code>,
271: * <code>TYPE_NUMERIC</code>, or <code>TYPE_DATE</code>. If the new type
272: * is different than the old, we also make sure the arity is appropriate
273: * (for example, no boolean lists) and clear the value and default value
274: * lists.
275: *
276: * @param newType the new type; must be one of <code>TYPE_BOOLEAN</code>,
277: * <code>TYPE_STRING</code>, <code>TYPE_NUMERIC</code>, or
278: * <code>TYPE_DATE</code>
279: */
280: public void setType(int newType) {
281: if (type != newType) {
282: type = newType;
283:
284: defaultValues.clear();
285: values.clear();
286:
287: if (type == TYPE_BOOLEAN) {
288: if (arity != ARITY_ONE)
289: arity = ARITY_ONE;
290: } else if (type == TYPE_DATE) {
291: if (arity == ARITY_LIST_SINGLE
292: || arity == ARITY_LIST_MULTIPLE)
293: arity = ARITY_ONE;
294: }
295:
296: setChanged();
297: notifyObservers();
298: }
299: }
300:
301: /**
302: * Returns the arity of this field. Will be one of <code>ARITY_ONE</code>,
303: * <code>ARITY_RANGE</code>, <code>ARITY_LIST_SINGLE</code>, or
304: * <code>ARITY_LIST_MULTIPLE</code>.
305: *
306: * @return the arity number
307: */
308: public int getArity() {
309: return arity;
310: }
311:
312: /**
313: * Returns <code>true</code> if the specified combination of type and arity
314: * are legal.
315: *
316: * @param aType one of <code>TYPE_BOOLEAN</code>, <code>TYPE_STRING</code>,
317: * <code>TYPE_NUMERIC</code>, or <code>TYPE_DATE</code>
318: * @param anArity one of <code>ARITY_ONE</code>, <code>ARITY_RANGE</code>,
319: * <code>ARITY_LIST_SINGLE</code>, or <code>ARITY_LIST_MULTIPLE</code>
320: * @return <code>true</code> if the specified combination of type and arity
321: * are legal
322: */
323: public boolean isLegal(int aType, int anArity) {
324: switch (aType) {
325: case TYPE_BOOLEAN:
326: return anArity == ARITY_ONE;
327: case TYPE_DATE:
328: return anArity != ARITY_LIST_SINGLE
329: && anArity != ARITY_LIST_MULTIPLE;
330: case TYPE_STRING:
331: case TYPE_NUMERIC:
332: default:
333: return true;
334: }
335: }
336:
337: /**
338: * Sets the parameter arity. Must be one of <code>ARITY_ONE</code>,
339: * <code>ARITY_RANGE</code>, <code>ARITY_LIST_SINGLE</code>, or
340: * <code>ARITY_LIST_MULTIPLE</code>. We disallow illegal arity values.
341: * For example, if our type is boolean we disallow a list arity.
342: *
343: * @param newArity one of <code>ARITY_ONE</code>, <code>ARITY_RANGE</code>,
344: * <code>ARITY_LIST_SINGLE</code>, or <code>ARITY_LIST_MULTIPLE</code>
345: */
346: public void setArity(int newArity) {
347: if (arity != newArity) {
348:
349: if (type == TYPE_BOOLEAN) {
350: if (newArity != ARITY_ONE) {
351: String str = I18N.get("Parameter.param_cap") + id
352: + ": " + I18N.get("Parameter.yesno_single");
353: throw new IllegalArgumentException(str);
354: }
355: } else if (type == TYPE_DATE) {
356: if (newArity == ARITY_LIST_SINGLE
357: || newArity == ARITY_LIST_MULTIPLE) {
358: String str = I18N.get("Parameter.param_cap") + id
359: + ": "
360: + I18N.get("Parameter.date_arity_err");
361: throw new IllegalArgumentException(str);
362: }
363: }
364:
365: arity = newArity;
366:
367: defaultValues.clear();
368: values.clear();
369:
370: setChanged();
371: notifyObservers();
372: }
373: }
374:
375: /**
376: * Returns an iterator over the default values for this parameter.
377: *
378: * @return an interator
379: */
380: public Iterator defaultValues() {
381: return defaultValues.iterator();
382: }
383:
384: /**
385: * Returns the i'th defaultValue for this parameter. If none has been
386: * assigned, create and return -- but do not store -- a reasonable default.
387: * The default is obtained by calling {@link #getDefaultForType}.
388: *
389: * @param i the index
390: * @return the defaultValue
391: */
392: public Object getDefaultValue(int i) {
393: Object val;
394: if (i < 0 || i >= defaultValues.size()
395: || (val = defaultValues.get(i)) == null) {
396: return getDefaultForType(type);
397: } else
398: return val;
399: }
400:
401: /**
402: * Returns the default value for a specific parameter type. This is not
403: * the same as the i'th default value; it is called when you have a
404: * parameter that has no value or default value, or when you have one
405: * with a different type and you want to switch types.
406: *
407: * @param type one of <code>TYPE_BOOLEAN</code>, <code>TYPE_STRING</code>,
408: * <code>TYPE_NUMERIC</code>, or <code>TYPE_DATE</code>
409: * @return a new object appropriate for the type
410: */
411: public Object getDefaultForType(int type) {
412: switch (type) {
413: case TYPE_BOOLEAN:
414: return Boolean.valueOf(false);
415: case TYPE_STRING:
416: return "";
417: case TYPE_NUMERIC:
418: return new Integer(0);
419: case TYPE_DATE:
420: return new Date();
421: default:
422: String str = I18N.get("Paramter.illegal_type_value");
423: throw new IllegalArgumentException(str + " " + type);
424: }
425: }
426:
427: /**
428: * Erases all default values.
429: */
430: public void removeDefaultValues() {
431: if (defaultValues.size() > 0) {
432: defaultValues.clear();
433: setChanged();
434: notifyObservers();
435: }
436: }
437:
438: /**
439: * Adds a default value to the list.
440: *
441: * @param newDefaultValue a new default value
442: */
443: public void addDefaultValue(Object newDefaultValue) {
444: // Make sure newDefaultValue is of proper type for the values we hold
445: newDefaultValue = convertType(newDefaultValue);
446:
447: defaultValues.add(newDefaultValue);
448: setChanged();
449: notifyObservers();
450: }
451:
452: /**
453: * Sets the i'th defaultValue. If <var>i</var> is out of range,
454: * the list of default values grows to fit.
455: *
456: * @param i the index
457: * @param newDefaultValue a value
458: */
459: public void setDefaultValue(int i, Object newDefaultValue) {
460: // Make sure newDefaultValue is of proper type for the values we hold
461: newDefaultValue = convertType(newDefaultValue);
462:
463: Object defaultValue = null;
464: if (i < defaultValues.size())
465: defaultValue = getDefaultValue(i);
466: if (defaultValue != newDefaultValue
467: && (defaultValue == null || !defaultValue
468: .equals(newDefaultValue))) {
469: defaultValues.add(i, newDefaultValue);
470: setChanged();
471: notifyObservers();
472: }
473: }
474:
475: /**
476: * Returns an iterator over the values for this parameter.
477: *
478: * @return an interator
479: */
480: public Iterator values() {
481: return values.iterator();
482: }
483:
484: /**
485: * Returns the parameter value(s) the user has previously specified. If
486: * the parameter has one value, return that value or possibly null. Else,
487: * return a copy of our list of values.
488: *
489: * @return values (see description)
490: */
491: public Object getValue() {
492: switch (arity) {
493: case ARITY_ONE:
494: case ARITY_LIST_SINGLE:
495: return getValue(0);
496: case ARITY_RANGE:
497: ArrayList list = new ArrayList();
498: list.add(getValue(0));
499: list.add(getValue(1));
500: return list;
501: case ARITY_LIST_MULTIPLE:
502: return values.clone();
503: }
504: return null; // Will never happen
505: }
506:
507: /**
508: * Returns the current value or, if that is <code>null</code>, the default
509: * value. If the index is out of range, return <code>null</code>.
510: *
511: * @param i the index
512: * @return the current or default value.
513: */
514: public Object getValue(int i) {
515: Object val = null;
516: if (i < values.size())
517: val = values.get(i);
518: if (val == null) {
519: if (i < defaultValues.size())
520: val = defaultValues.get(i);
521: }
522: return val;
523: }
524:
525: /**
526: * Adds a value to the list.
527: *
528: * @param newValue a new value
529: */
530: public void addValue(Object newValue) {
531: values.add(convertType(newValue));
532: setChanged();
533: notifyObservers();
534: }
535:
536: /**
537: * Erases all values.
538: */
539: public void removeValues() {
540: if (values.size() > 0) {
541: values.clear();
542: setChanged();
543: notifyObservers();
544: }
545: }
546:
547: /**
548: * Sets the <var>i</var>'th value. If <var>i</var> is out of range,
549: * the list of values grows to fit.
550: *
551: * param i the index
552: * @param newValue the new value
553: */
554: public void setValue(int i, Object newValue) {
555: // Make sure newValue is of proper type for the values we hold
556: values.add(i, convertType(newValue));
557:
558: setChanged();
559: notifyObservers();
560: }
561:
562: /**
563: * Converts the specified object to the proper type for this parameter.
564: * Whenever we add or set a value or default value, we convert it to the
565: * proper type (string, date, etc.)
566: * <p>
567: * If our type is boolean and the incoming object is:
568: * <ul>
569: * <li>A string, return
570: * a <code>true Boolean</code> if the value matches "true", "t", "yes",
571: * or "y" (ignoring case).
572: * <li>A number, return a <code>true Boolean</code> if the value is non-zero.
573: * <li>Anything else, return a <code>true Boolean</code> (any better
574: * suggestions?)
575: *
576: * @param val any old object
577: * @return some object of the proper type
578: */
579: protected Object convertType(Object val) {
580: if (val == null)
581: return null;
582:
583: switch (type) {
584: case TYPE_BOOLEAN: // Return value as a boolean
585: if (val instanceof Boolean)
586: return val;
587: else if (val instanceof String) {
588: val = ((String) val).toLowerCase().trim();
589: if ("true".equals(val) || "t".equals(val)
590: || "yes".equals(val) || "y".equals(val))
591: return Boolean.valueOf(true);
592: else
593: return Boolean.valueOf(false);
594: } else if (val instanceof Number) {
595: return Boolean
596: .valueOf(((Number) val).doubleValue() == 0);
597: } else {
598: return Boolean.valueOf(true); // What to do here?
599: }
600: case TYPE_STRING: // Return value as a string
601: return val.toString();
602: case TYPE_NUMERIC: // Return value as a number
603: if (val instanceof Number)
604: return val;
605: else { // Convert val to string, then to number
606: String str = val.toString();
607: if (str.length() == 0)
608: return new Integer(0);
609: else if (str.indexOf(".") == -1)
610: return new Integer(str);
611: else
612: return new Double(str);
613: }
614: case TYPE_DATE: // Return value as a date
615: if (val instanceof Date)
616: return val;
617: else { // Convert val to string, then to date
618: String str = val.toString();
619: if (str.length() == 0)
620: return new Date();
621: else {
622: parsePosition.setIndex(0);
623: return formatter.parse(str, parsePosition);
624: }
625: }
626: default: // Should never happen
627: return null;
628: }
629: }
630:
631: /**
632: * Returns the string used as the "type" attribute when writing this
633: * parameter as XML.
634: *
635: * @return the "type" attribute string
636: */
637: protected String typeString() {
638: switch (type) {
639: case TYPE_BOOLEAN:
640: return "boolean";
641: case TYPE_STRING:
642: return "string";
643: case TYPE_NUMERIC:
644: return "numeric";
645: case TYPE_DATE:
646: return "date";
647: default:
648: return "unknown"; // Should never happen
649: }
650: }
651:
652: public String dragString() {
653: return "parameter:" + getId();
654: }
655:
656: public String designLabel() {
657: return "{?" + getName() + "}";
658: }
659:
660: public String formulaString() {
661: return "{?" + getId() + "}";
662: }
663:
664: /**
665: * Writes this parameter as an XML tag.
666: *
667: * @param out a writer that knows how to write XML
668: */
669: public void writeXML(XMLWriter out) {
670: String arityString = null;
671: switch (arity) {
672: case ARITY_ONE:
673: arityString = "single";
674: break;
675: case ARITY_RANGE:
676: arityString = "range";
677: break;
678: case ARITY_LIST_SINGLE:
679: arityString = "list-single";
680: break;
681: case ARITY_LIST_MULTIPLE:
682: arityString = "list-multiple";
683: break;
684: }
685:
686: out.startElement("parameter");
687: out.attr("id", id);
688: out.attr("type", typeString());
689: out.attr("name", name);
690: out.attr("question", question);
691: out.attr("arity", arityString);
692:
693: for (Iterator iter = defaultValues.iterator(); iter.hasNext();)
694: out.textElement("default", iter.next().toString());
695:
696: out.endElement();
697: }
698:
699: public String toString() {
700: StringWriter sw = new StringWriter();
701: writeXML(new XMLWriter(sw));
702: return sw.toString();
703: }
704:
705: }
|