1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 package com.ivata.mask;
77 import java.beans.PropertyDescriptor;
78 import java.io.IOException;
79 import java.io.InputStream;
80 import java.io.Serializable;
81 import java.math.BigDecimal;
82 import java.util.ArrayList;
83 import java.util.Date;
84 import java.util.HashMap;
85 import java.util.Iterator;
86 import java.util.List;
87 import java.util.Map;
88 import java.util.Properties;
89 import org.apache.commons.beanutils.PropertyUtils;
90 import org.dom4j.Document;
91 import org.dom4j.DocumentException;
92 import org.dom4j.Element;
93 import org.dom4j.io.SAXReader;
94 import org.xml.sax.InputSource;
95 import com.ivata.mask.field.Field;
96 import com.ivata.mask.field.FieldImpl;
97 import com.ivata.mask.field.FieldValueConvertor;
98 import com.ivata.mask.field.FieldValueConvertorFactory;
99 import com.ivata.mask.filter.Filter;
100 import com.ivata.mask.filter.FilterImpl;
101 import com.ivata.mask.group.Group;
102 import com.ivata.mask.group.GroupImpl;
103 import com.ivata.mask.util.StringHandling;
104 import com.ivata.mask.util.SystemException;
105 /***
106 * <p>
107 * This factory class is at the heart of ivata masks. Use it to read in a
108 * configuration file (in XML), and then access groups of fields via their
109 * unique identifiers.
110 * </p>
111 *
112 * <p>
113 * It is called <code>DefaultMaskFactory</code> because the <strong>ivata
114 * masks </strong> system actually never refers to this class directly - it uses
115 * the interface {@link MaskFactory}, meaning you could create your own factory
116 * implementation, if you want to.
117 * </p>
118 *
119 * @author Colin MacLeod
120 * <a href='mailto:colin.macleod@ivata.com'>colin.macleod@ivata.com</a>
121 * @since ivata masks 0.1 (2004-02-26)
122 */
123 public final class DefaultMaskFactory implements MaskFactory, Serializable {
124 /***
125 * <p>
126 * The name of the default mask/screen used for user input.
127 * </p>
128 */
129 private String defaultInputMask;
130 /***
131 * <p>
132 * The name of the default mask/screen used to list.
133 * </p>
134 */
135 private String defaultListMask;
136 /***
137 * Used to create convertors to convert field values in the filters.
138 */
139 private FieldValueConvertorFactory fieldValueConvertorFactory;
140 /***
141 * Mapping of all config groups, mapped by identifier.
142 */
143 private Map groups;
144 /***
145 * <p>
146 * Default constructor. Initializes the mask factory with
147 * "inputMask" as the default input mask, and
148 * "inputMask" as the default list mask.
149 * </p>
150 * @param fieldValueConvertorFactory creates convertors to convert field
151 * values in the filters.
152 */
153 public DefaultMaskFactory(final FieldValueConvertorFactory
154 fieldValueConvertorFactory) {
155 this("inputMask", "listMask", fieldValueConvertorFactory);
156 }
157 /***
158 * <p>
159 * Construct an instance of the factory with the default mask/screens
160 * provided.
161 * </p>
162 *
163 * @param defaultInputMaskParam
164 * The name of the default mask/screen used for user input.
165 * @param defaultListMaskParam
166 * The name of the default mask/screen used to list.
167 * @param fieldValueConvertorFactory creates convertors to convert field
168 * values in the filters.
169 */
170 public DefaultMaskFactory(final String defaultInputMaskParam,
171 final String defaultListMaskParam,
172 final FieldValueConvertorFactory fieldValueConvertorFactoryParam) {
173 this.defaultInputMask = defaultInputMaskParam;
174 this.defaultListMask = defaultListMaskParam;
175 this.fieldValueConvertorFactory = fieldValueConvertorFactoryParam;
176 }
177 /***
178 * <p>
179 * Go thro' all the properties of the value object class and add fields for
180 * those properties which were not explicitly defined in the configuration
181 * file.
182 * </p>
183 *
184 * @param mask
185 * Mask for which to add all the default fields
186 * @param parentField
187 * If this mask applies to a field within another mask, (known as
188 * a submask) this is the field to which it applies, otherwise
189 * <code>null</code>.
190 */
191 private void addDefaultFields(final MaskImpl mask,
192 final Field parentField) {
193 Class dOClass = mask.getDOClass();
194 PropertyDescriptor[] descriptors = PropertyUtils
195 .getPropertyDescriptors(dOClass);
196 for (int i = 0; i < descriptors.length; i++) {
197 PropertyDescriptor descriptor = descriptors[i];
198 String fieldName = descriptor.getName();
199
200 if ("class".equals(fieldName)) {
201 continue;
202 }
203 StringBuffer combinedName = new StringBuffer();
204 if (parentField != null) {
205 combinedName.append(parentField.getPath());
206 combinedName.append(".");
207 }
208 combinedName.append(fieldName);
209 if (mask.getField(combinedName.toString()) == null) {
210 FieldImpl field = new FieldImpl(parentField, mask
211 .getField(fieldName), this);
212 field.setName(fieldName);
213 Class fieldClass = descriptor.getPropertyType();
214
215 if (Date.class.isAssignableFrom(fieldClass)) {
216 field.setType(Field.TYPE_DATE);
217 } else if (BigDecimal.class.isAssignableFrom(fieldClass)
218 || Double.class.isAssignableFrom(fieldClass)) {
219 field.setType(Field.TYPE_AMOUNT);
220 } else if (Integer.class.isAssignableFrom(fieldClass)) {
221 field.setType(Field.TYPE_NUMBER);
222 } else if (String.class.isAssignableFrom(fieldClass)) {
223 field.setType(Field.TYPE_STRING);
224 } else {
225 field.setType(null);
226 }
227 mask.addField(field);
228 }
229 }
230 }
231 /***
232 * Extract a field from a dom4j element.
233 *
234 * @param element
235 * dom4j element which represents a field.
236 * @param group
237 * group which will contain this field.
238 * @return field New field represented by the element provided.
239 * TODO: replace NullPointerException thrown here with a mask configuration
240 * exception.
241 */
242 private Field extractField(final Group group, final Element element) {
243
244
245 String extendsField = element.attributeValue("extends");
246 String name = element.attributeValue("name");
247 if (name == null) {
248 throw new NullPointerException("ERROR in mask configuration: "
249 + "mandatory name attribute null for field.");
250 }
251 Field extendedField;
252 if (extendsField == null) {
253
254
255 extendedField = group.getField(name);
256 } else {
257
258 extendedField = group.getField(extendsField);
259
260 if (extendedField == null) {
261 throw new NullPointerException("ERROR in mask configuration: "
262 + "field '" + name + "' extends unknown field '"
263 + extendsField + "'");
264 }
265 }
266 FieldImpl field = new FieldImpl(null, extendedField, this);
267 field.setName(name);
268 String type = element.attributeValue("type");
269 field.setType(type);
270
271 String displayOnly = element.attributeValue("displayOnly");
272 field.setDisplayOnly("true".equalsIgnoreCase(displayOnly));
273 String hidden = element.attributeValue("hidden");
274 field.setHidden("true".equalsIgnoreCase(hidden));
275 String mandatory = element.attributeValue("mandatory");
276 field.setMandatory("true".equalsIgnoreCase(mandatory));
277 String oneToOne = element.attributeValue("oneToOne");
278 field.setOneToOne("true".equalsIgnoreCase(oneToOne));
279 String defaultValue = element.attributeValue("default");
280 field.setDefaultValue(defaultValue);
281 String labelKey = element.attributeValue("labelKey");
282 field.setLabelKey(labelKey);
283 String className = element.attributeValue("class");
284
285 if (className != null) {
286 try {
287 Class dOClass = Class.forName(className);
288 field.setDOClass(dOClass);
289 } catch (ClassNotFoundException e) {
290 throw new RuntimeException("ERROR (" + e.getClass()
291 + ") cannot locate class: " + className + ": "
292 + e.getMessage());
293 }
294 }
295 List choiceList = element.selectNodes("choice");
296 Properties choiceProperties = field.getChoiceProperties();
297 if (choiceList.size() > 0) {
298 choiceProperties = new Properties();
299 List choicePropertyKeys = new ArrayList();
300 for (Iterator iterator = choiceList.iterator();
301 iterator.hasNext();) {
302 Element choice = (Element) iterator.next();
303 String key = choice.attributeValue("value");
304 String text = choice.getText();
305 if (key == null) {
306 key = text;
307 }
308 choiceProperties.setProperty(key, text);
309 choicePropertyKeys.add(key);
310 }
311 field.setChoiceProperties(choiceProperties);
312 field.setChoicePropertyKeys(choicePropertyKeys);
313 }
314
315 if (("select".equals(type) || "radio".equals(type))
316 && (className == null) && (choiceProperties == null)) {
317 throw new RuntimeException(
318 "ERROR in mask configuration: "
319 + "you must specify either choices or value object class for "
320 + "field " + type + ", name '" + name + "'");
321 }
322 return field;
323 }
324 /***
325 * Extract a singel filter from the group provided.
326 *
327 * @param group parent group surrounding the filter.
328 * @param element the document element from which to extract the filter.
329 * @return the extracted filter, represented by the XML in the
330 * <code>element</code>.
331 */
332 private Filter extractFilter(final Group groupParam,
333 final Element element) {
334 String propertyName = element.attributeValue("propertyName");
335 String stringValue = element.attributeValue("value");
336 String className = element.attributeValue("propertyClass");
337 Class propertyClass;
338 try {
339 propertyClass = Class.forName(className);
340 } catch (ClassNotFoundException e) {
341 throw new RuntimeException(e);
342 }
343 FieldValueConvertor convertor;
344 try {
345 convertor = fieldValueConvertorFactory
346 .getFieldValueConvertorForClass(propertyClass);
347 } catch (SystemException e) {
348 throw new RuntimeException(e);
349 }
350 Object value = convertor.convertFromString(propertyClass,
351 stringValue);
352 return new FilterImpl(propertyName, propertyClass, value);
353 }
354 /***
355 * Extracts a single group from the element provided.
356 *
357 * @param element
358 * The document element from which to extract the group.
359 * @return New group represented by the XML in <code>element</code>.
360 * TODO: replace NullPointerException thrown here with a mask configuration
361 * exception.
362 */
363 private Group extractGroup(final Element element) {
364
365 String extendsGroup = element.attributeValue("extends");
366 String name = element.attributeValue("name");
367 if (StringHandling.isNullOrEmpty(name)) {
368 throw new NullPointerException("ERROR in mask configuration: "
369 + "mandatory name attribute null for group.");
370 }
371 Group parent = null;
372 if (extendsGroup != null) {
373 parent = (Group) groups.get(extendsGroup);
374 if (parent == null) {
375 throw new NullPointerException("ERROR in mask configuration: "
376 + "group '" + name + "' extends unknown group '"
377 + extendsGroup + "'");
378 }
379 }
380 GroupImpl group = new GroupImpl(name, parent);
381
382 extractGroupFields(element, group);
383
384 for (Iterator i = element.elementIterator("mask"); i.hasNext();) {
385 Element maskElement = (Element) i.next();
386 Mask mask = extractMask(group, maskElement);
387 String maskId = getMaskId(mask.getDOClass().getName(),
388 mask.getName());
389 groups.put(maskId, mask);
390 }
391 return group;
392 }
393 /***
394 * This section is used by both <code>extractMask</code> and
395 * <code>extractGroup</code>.
396 *
397 * @param element
398 * dom4j element to extract the information from.
399 * @param group
400 * group or mask to set the information into.
401 */
402 private void extractGroupFields(final Element element,
403 final GroupImpl group) {
404 for (Iterator i = element.elementIterator("field"); i.hasNext();) {
405 Element fieldElement = (Element) i.next();
406 Field field = extractField(group, fieldElement);
407 group.addField(field);
408 }
409
410 Element exclude = element.element("exclude");
411 if (exclude != null) {
412 for (Iterator iter = exclude.elementIterator("fieldName"); iter
413 .hasNext();) {
414 Element fieldNameElement = (Element) iter.next();
415 group.addExcludedFieldName(fieldNameElement.getTextTrim());
416 }
417 }
418
419 Element include = element.element("include");
420 if (include != null) {
421 for (Iterator iter = include.elementIterator("fieldName"); iter
422 .hasNext();) {
423 Element fieldNameElement = (Element) iter.next();
424 group.addIncludedFieldName(fieldNameElement.getTextTrim());
425 }
426 }
427
428 String displayOnly = element.attributeValue("displayOnly");
429 if (displayOnly != null) {
430 group.setDisplayOnly("true".equals(displayOnly));
431 }
432
433 Element first = element.element("first");
434 if (first != null) {
435 for (Iterator iter = first.elementIterator("fieldName"); iter
436 .hasNext();) {
437 Element fieldNameElement = (Element) iter.next();
438 group.addFirstFieldName(fieldNameElement.getText());
439 }
440 }
441 Element last = element.element("last");
442 if (last != null) {
443 for (Iterator iter = last.elementIterator("fieldName"); iter
444 .hasNext();) {
445 Element fieldNameElement = (Element) iter.next();
446 group.addLastFieldName(fieldNameElement.getText());
447 }
448 }
449
450 for (Iterator i = element.elementIterator("filter"); i.hasNext();) {
451 Element filterElement = (Element) i.next();
452 Filter filter = extractFilter(group, filterElement);
453 group.addFilter(filter);
454 }
455 }
456 /***
457 * Extracts a single mask from the mask element provided.
458 *
459 * @param group
460 * The parent group surrounding the mask.
461 * @param element
462 * The document element from which to extract the mask.
463 * @return New mask represented by the XML in <code>element</code>.
464 * TODO: replace NullPointerException thrown here with a mask configuration
465 * exception.
466 */
467 private Mask extractMask(final Group group, final Element element) {
468 String name = element.attributeValue("name");
469 String className = element.attributeValue("valueObject");
470
471 if (className == null) {
472 throw new NullPointerException("ERROR in mask configuration: "
473 + "mandatory 'valueObject' attribute null for mask'"
474 + name
475 + "'.");
476 }
477
478
479 String extendsMask = element.attributeValue("extends");
480 Group parent = null;
481 if (extendsMask != null) {
482
483 String id = getMaskId(className, extendsMask);
484 parent = (Group) groups.get(id);
485
486
487 if (parent == null) {
488 parent = (Group) groups.get(extendsMask);
489 if (parent == null) {
490 throw new NullPointerException(
491 "ERROR in mask configuration: mask '"
492 + name
493 + "' extends unknown mask/group '"
494 + extendsMask
495 + "'");
496 }
497 }
498 } else {
499
500
501 parent = group;
502 }
503
504 Class dOClass;
505 try {
506 dOClass = Class.forName(className);
507 } catch (ClassNotFoundException e) {
508 throw new RuntimeException("ERROR (" + e.getClass()
509 + ") cannot locate class: " + className + ": "
510 + e.getMessage());
511 }
512 MaskImpl mask = new MaskImpl(dOClass, parent, name);
513
514 extractGroupFields(element, mask);
515 addDefaultFields(mask, null);
516
517
518 for (Iterator i = element.elementIterator("include"); i.hasNext();) {
519 Element includeElement = (Element) i.next();
520 String path = includeElement.attributeValue("path");
521 if (StringHandling.isNullOrEmpty(path)) {
522 path = includeElement.getTextTrim();
523 }
524 if (StringHandling.isNullOrEmpty(path)) {
525 throw new NullPointerException("ERROR in mask configuration: "
526 + "you must specify either a path or body content "
527 + "for all includes in mask '"
528 + mask.getName()
529 + "'.");
530 }
531 mask.addIncludePath(path);
532 }
533 return mask;
534 }
535 /***
536 * <p>
537 * Get the name of the default mask/screen used for user input.
538 * </p>
539 *
540 * @return name of the default mask/screen used for user input.
541 * @see com.ivata.mask.MaskFactory#getDefaultInputMask()
542 */
543 public String getDefaultInputMask() {
544
545 return defaultInputMask;
546 }
547 /***
548 * <p>
549 * Get the name of the default mask/screen used for lists.
550 * </p>
551 *
552 * @return name of the default mask/screen used for lists.
553 * @see com.ivata.mask.MaskFactory#getDefaultListMask()
554 */
555 public String getDefaultListMask() {
556
557 return defaultListMask;
558 }
559 /***
560 * <p>
561 * Get a group definition referenced by its id.
562 * </p>
563 *
564 * @param id
565 * unique identifier of the group.
566 * @return Group definition with the id provided, or <code>null</code> if
567 * there is no such group.
568 */
569 public Group getGroup(final String id) {
570 if (!isConfigured()) {
571 throw new RuntimeException(
572 "ERROR in MaskFactory: you must first read in configuration"
573 + " by calling readConfiguration.");
574 }
575 return (Group) groups.get(id);
576 }
577 /***
578 * This will return the <u>default input mask</u> for the class provided.
579 * Refer to {@link MaskFactory#getMask}.
580 *
581 * @param valueObjectClassParam Refer to {@link MaskFactory#getMask}.
582 * @return Refer to {@link MaskFactory#getMask}.
583 */
584 public Mask getMask(final Class valueObjectClassParam) {
585 return getMask(valueObjectClassParam, getDefaultInputMask());
586 }
587 /***
588 * <p>
589 * Get a mask, identified by its class and name.
590 * </p>
591 *
592 * @param valueObjectClass
593 * class of value object for the mask to be returned.
594 * @param name
595 * optional parameter defining multiple masks for the same value
596 * object. May be <code>null</code>.
597 * @return Mask definition with the id provided, or <code>null</code> if
598 * there is no such mask.
599 */
600 public Mask getMask(final Class valueObjectClass, final String name) {
601 return getMask(null, valueObjectClass, name);
602 }
603 /***
604 * This will return the <u>default input mask</u> for the class provided
605 * of the subclassed field.
606 * Refer to {@link MaskFactory#getMask}.
607 *
608 * @param valueObjectClassParam Refer to {@link MaskFactory#getMask}.
609 * @return Refer to {@link MaskFactory#getMask}.
610 */
611 public Mask getMask(final Field parentField,
612 final Class valueObjectClassParam) {
613 return getMask(parentField, valueObjectClassParam,
614 getDefaultInputMask());
615 }
616 /***
617 * <p>
618 * Get a mask, identified by its class and name.
619 * </p>
620 *
621 * @param parentField
622 * If this mask applies to a field within another mask, (known as
623 * a submask) this is the field to which it applies, otherwise
624 * use the other <code>getMask</code> method.
625 * @param valueObjectClass
626 * class of value object for the mask to be returned.
627 * @param nameParam
628 * describes this mask uniquely within the value object. (You
629 * can have more than one mask for each value object.)
630 * @return Mask definition with the id provided, or <code>null</code> if
631 * there is no such mask.
632 * TODO: replace NullPointerException thrown here with a mask configuration
633 * exception.
634 */
635 public Mask getMask(final Field parentField, final Class valueObjectClass,
636 final String nameParam) {
637 StringBuffer combinedName = new StringBuffer();
638 if (parentField != null) {
639 combinedName.append(parentField.getPath());
640 combinedName.append(".");
641 }
642 combinedName.append(valueObjectClass.getName());
643 String id = getMaskId(combinedName.toString(), nameParam);
644 Group group = getGroup(id);
645 if (group == null) {
646
647
648 String parentId = getMaskId(valueObjectClass.getName(), nameParam);
649 Group parent = getGroup(parentId);
650
651 if (parent == null) {
652 parent = getGroup(nameParam);
653 if (parent == null) {
654 throw new NullPointerException(
655 "ERROR: no appropriate mask or group called '"
656 + nameParam
657 + "' for class '"
658 + valueObjectClass.getName()
659 + "'.");
660 }
661 }
662
663
664 MaskImpl defaultMask = new MaskImpl(valueObjectClass, parent,
665 nameParam);
666 addDefaultFields(defaultMask, parentField);
667 groups.put(combinedName.toString(), defaultMask);
668 return defaultMask;
669 }
670 if (!(group instanceof Mask)) {
671 throw new RuntimeException("ERROR: the group '"
672 + id
673 + "' does not represent a mask.");
674 }
675 return (Mask) group;
676 }
677 /***
678 * <p>
679 * Create a unique identifier for the mask, based on the class and name.
680 * For groups, we use the name alone as unique.
681 * </p>
682 *
683 * @param className
684 * name of the class of value object for the mask to be returned.
685 * @param name
686 * describes this mask uniquely within the value object. (You
687 * can have more than one mask for each value object.)
688 * @return Unique identifier for the mask based on class name and mask name.
689 */
690 private String getMaskId(final String className, final String name) {
691 if (name == null) {
692 return className;
693 } else {
694 return className + "_" + name;
695 }
696 }
697 /***
698 * <p>
699 * Discover whether or not this object has been configured.
700 * </p>
701 *
702 * @return <code>true</code> if the object has been configured, otherwise
703 * <code>false</code>.
704 * @see com.ivata.mask.MaskFactory#isConfigured()
705 */
706 public boolean isConfigured() {
707
708 return groups != null;
709 }
710 /***
711 * <p>
712 * Read in the configuration represented by the <strong>dom4j </strong>
713 * document provided.
714 * </p>
715 *
716 * @param document
717 * <strong>dom4j </strong> document to read configuration from.
718 * @throws IOException
719 * If there is any problem reading from the stream provided.
720 */
721 private synchronized void readConfiguration(final Document document)
722 throws IOException {
723 Element root = document.getRootElement();
724 groups = new HashMap();
725 for (Iterator i = root.elementIterator("group"); i.hasNext();) {
726 Element groupElement = (Element) i.next();
727 Group group = extractGroup(groupElement);
728 groups.put(group.getName(), group);
729 }
730 }
731 /***
732 * Get the configuration represented by the <i>dom4j </i> document provided.
733 *
734 * @param inputStream
735 * The input stream to read the XML from.
736 * @throws IOException
737 * If there is any problem reading from the stream provided.
738 */
739 public void readConfiguration(final InputStream inputStream)
740 throws IOException {
741 SAXReader reader = new SAXReader();
742 Document document;
743 try {
744 InputSource is = new InputSource();
745 is.setByteStream(inputStream);
746 document = reader.read(is);
747 } catch (DocumentException e) {
748 e.printStackTrace();
749 throw new IOException("ERROR in MaskConfigurationFactory: "
750 + e.getMessage());
751 }
752 readConfiguration(document);
753 }
754 }