001: package net.xoetrope.xui.data;
002:
003: import net.xoetrope.debug.DebugLogger;
004: import net.xoetrope.xui.build.BuildProperties;
005: import net.xoetrope.xui.helper.NumberFormatter;
006:
007: /**
008: * The basic implementation of the XModel is handled by this class. Static data
009: * is loaded from an XML file pointed to by the startup.properties file by
010: * default using an XDataSource. The model is central to XUI and allows the UI
011: * components to be separated from the data in an abstract fashion. This model
012: * element is designed to support static data, text, list and tabular structures.
013: * <p>Copyright (c) Xoetrope Ltd., 1998-2003<br>
014: * License: see license.txt
015: * @version $Revision: 1.27 $
016: */
017: public class XBaseModel extends XModel implements Cloneable {
018: private boolean hasAutoId = false;
019: protected XModel[] values;
020: private volatile int hashcode;
021: private Object[] attributeValues;
022: private String[] attributeNames;
023: private int numValidChildren = 0;
024: protected XModel parentModel;
025:
026: protected boolean addByDefault = true;
027: protected static boolean appendByDefault = true;
028:
029: public static final int VALUE_ATTRIBUTE = 0;
030: public static final int ID_ATTRIBUTE = 1;
031: public static final int NUM_FIXED_ATTRIBUTE = 2;
032:
033: /**
034: * Constructs an instance of the model node.
035: */
036: public XBaseModel(XModel parent) {
037: parentModel = parent;
038: values = null;
039: numValidChildren = 0;
040:
041: setNumAttributes(NUM_FIXED_ATTRIBUTE);
042:
043: attributeNames[VALUE_ATTRIBUTE] = "value";
044: attributeNames[ID_ATTRIBUTE] = "id";
045:
046: tagName = "data";
047: }
048:
049: /**
050: * Insert a node at a specified index in the list of children
051: * @param newNode the new model node
052: * @param idx the index at which to insert
053: */
054: public void insertChildAt(XModel newNode, int idx) {
055: // Add a new node to the model
056: int numChildren = getNumChildren();
057: setNumChildren(Math.max(numValidChildren + 1, numChildren));
058: numValidChildren++;
059:
060: // Reposition the 'following' nodes
061: for (int i = numChildren - 1; i > idx; i--)
062: values[i] = values[i - 1];
063:
064: values[idx] = newNode;
065: }
066:
067: /**
068: * Check to see if the specified child node exists. Doing a get() creates the
069: * named node by design, so it will always result in a value
070: * @param name the name of the child we are looking for
071: * @return boolean indicating the existance of the child.
072: */
073: public boolean getChildExists(String name) {
074: for (int i = 0; i < values.length; i++) {
075: if ((values[i] != null)
076: && (values[i].getId().compareTo(name) == 0))
077: return true;
078: }
079: return false;
080: }
081:
082: public XBaseModel() {
083: this (null);
084: }
085:
086: public XModel getParent() {
087: return parentModel;
088: }
089:
090: /**
091: * Get the value of the element located at the path in the element parameter.
092: * The element name parameter can include an attribute name by appending
093: * '@attributeName' to the path, where attributeName is the name of the
094: * attribute.
095: * @param element The path to the XModel required
096: * @return The value of the XModel or the attribute
097: */
098: public Object get(String element) {
099: if ((element == null) || (element.length() == 0))
100: return null;
101: if (element.charAt(0) == '/')
102: element = element.substring(1);
103: String attribName = getAttribFromPath(element);
104: String attribValue = null;
105: int numValues = values == null ? 0 : values.length;
106: int i = 0;
107: String subPath;
108:
109: int pos = element.indexOf('/');
110: if (attribName != null) {
111: int equalsPos = attribName.indexOf('=');
112: if (equalsPos > 0) {
113: if (attribName.charAt(equalsPos + 1) == '[') {
114: int endPos = attribName.indexOf(']');
115: attribValue = attribName.substring(equalsPos + 2,
116: endPos).trim();
117: pos = element.indexOf('/', element.indexOf(']'));
118: } else
119: attribValue = attribName.substring(equalsPos + 1)
120: .trim();
121: attribName = attribName.substring(0, equalsPos).trim();
122: } else
123: return getAttribValue(getAttribute(attribName));
124:
125: subPath = getBaseFromPath(element);
126: } else
127: subPath = pos > -1 ? element.substring(0, pos) : element;
128:
129: int spHash = subPath.hashCode();
130:
131: // Check for existing values/children
132: for (; i < numValues; i++) {
133: XModel node = (XModel) values[i];
134: if (node != null) {
135: // Check this node
136: if (node.hashCode() == spHash) {
137: if (attribName == null) {
138: if (pos < 0) { // No further attrib or path specified (this is a leaf node and no attrib is requested)
139: if (BuildProperties.DEBUG) {
140: String nodeValue = "";
141: if (node.getClass().getName().indexOf(
142: "XBaseModel") > 0) {
143: Object objValue = node.get();
144: if (objValue != null)
145: nodeValue = objValue.toString();
146: } else
147: nodeValue = node.toString();
148: DebugLogger
149: .trace("==> get( "
150: + (String) attributeValues[ID_ATTRIBUTE]
151: + ", " + node.getId()
152: + " ) : " + nodeValue);
153: }
154: return node;
155: } else
156: // An extra path name is specified, so get the child (we're not at the leaf node yet)
157: return node.get(element.substring(pos + 1));
158: } else {
159: if (pos < 0) {
160: // The leaf node has been reached so get the named attribute
161: if (BuildProperties.DEBUG)
162: DebugLogger
163: .trace("==> get( "
164: + (String) attributeValues[ID_ATTRIBUTE]
165: + ", "
166: + node.getId()
167: + " ) : "
168: + node
169: .getAttribValueAsString(0));
170:
171: if (attribValue == null)
172: return node.getAttribValue(node
173: .getAttribute(attribName));
174: else {
175: String testAttribValue = node
176: .getAttribValueAsString(node
177: .getAttribute(attribName));
178: if ((testAttribValue != null)
179: && testAttribValue
180: .equals(attribValue))
181: return node;
182: }
183: } else { // An extra path name is specified, so get the child (we're not at the leaf node yet)
184: if (attribValue == null)
185: return node.get(element
186: .substring(pos + 1));
187: else {
188: int attribPos = node
189: .getAttribute(attribName);
190: if (attribPos > -1) {
191: String attribStr = node
192: .getAttribValueAsString(attribPos);
193: if ((attribStr != null)
194: && attribStr
195: .equals(attribValue))
196: return node.get(element
197: .substring(pos + 1));
198: }
199: }
200: }
201: }
202: }
203: } else
204: break;
205: }
206:
207: // Add a new element if necessary
208: if (appendByDefault)
209: return append(element);
210:
211: return null;
212: }
213:
214: /**
215: * Set the value of the element in the child XModel located at the elementName.
216: * The child node matching the element is first retrieved and then the named
217: * attribute is updated. The attributeName can be specified by appending
218: * '@attributeName' to the path, where attributeName is the name of the
219: * attribute (e.g. '/fonts/arial/@bold' where 'bold' is the attributeName.
220: * If the attributeName is not specified the retreived node's value is updated.
221: * <br>
222: * To update this node's value use <code>set( newObject )</code>
223: * @param elementName The path to the XModel in the format 'base/foo
224: * @param newObject The new value of the attribute
225: */
226: public void set(String elementName, Object newObject) {
227: String attribName = getAttribFromPath(elementName);
228: String path = getBaseFromPath(elementName);
229:
230: XModel node = path == null ? this : (XModel) get(path);
231: if (attribName == null)
232: node.setAttribValue(node.getAttribute("value"), newObject);
233: else
234: node.setAttribValue(node.getAttribute(attribName),
235: newObject);
236: }
237:
238: /**
239: * returns the index of the attribiteNames array whose value is the same
240: * as the attribName
241: * @param attribName The name of the attribute we are trying to locate
242: * @return The index of the attributeNames array containg the name
243: */
244: public int getAttribute(String attribName) {
245: int attribHashcode = attribName.hashCode();
246: int numAttributes = attributeValues.length;
247: int j = 0;
248: for (; j < numAttributes; j++) {
249: if (attributeNames[j] != null) {
250: if (attributeNames[j].hashCode() == attribHashcode) {
251: return j;
252: }
253: } else
254: break;
255: }
256:
257: if (addByDefault && appendByDefault) {
258: // Add a new attribute
259: if (j == numAttributes) {
260: setNumAttributes(numAttributes + 1);
261: }
262: attributeNames[j] = attribName;
263: return j;
264: }
265: return -1;
266: }
267:
268: /**
269: * Get the XModel at element i
270: * @param i The index of the values array
271: * @return The XModel at location i
272: */
273: public XModel get(int i) {
274: return values[i];
275: }
276:
277: /**
278: * gets the value attribute
279: * @return the value of the model
280: */
281: public Object get() {
282: return attributeValues[VALUE_ATTRIBUTE];
283: }
284:
285: /**
286: * Sets the model value of this node.
287: * @param s the new value
288: */
289: public void set(Object s) {
290: boolean changed = !(attributeValues[VALUE_ATTRIBUTE] == s);
291: if (BuildProperties.DEBUG)
292: DebugLogger.trace((String) attributeValues[ID_ATTRIBUTE]
293: + "=" + (s == null ? "null" : s.toString()));
294:
295: attributeValues[VALUE_ATTRIBUTE] = s;
296: if (changed)
297: fireModelUpdated();
298: }
299:
300: /**
301: * Used for elements which need a name assigned temporarily because one doesn't
302: * exist in the DataSource.
303: * @param b true if there was no name in the DataSource
304: */
305: public void hasAutoId(boolean b) {
306: hasAutoId = b;
307: }
308:
309: /**
310: * @return true if there was no name for the element in the DataSource
311: */
312: public boolean hasAutoId() {
313: return hasAutoId;
314: }
315:
316: /**
317: * Gets the name attribute
318: * @return the name attribute
319: */
320: public String getId() {
321: return (String) attributeValues[ID_ATTRIBUTE];
322: }
323:
324: /**
325: * Sets the name attribute
326: * @param newName the new name
327: */
328: public void setId(String newName) {
329: attributeValues[ID_ATTRIBUTE] = newName;
330: hashcode = newName.hashCode();
331: }
332:
333: /**
334: * @param i The index of the attributeNames array whose value we want
335: * @return The string value of the attributeNames array at position i
336: */
337: public String getAttribName(int i) {
338: return (String) attributeNames[i];
339: }
340:
341: /**
342: * @param i The index of the attributeValues array whose value we want
343: * @return The (Object) value of the attributeValues array at position i
344: */
345: public Object getAttribValue(int i) {
346: return attributeValues[i];
347: }
348:
349: /**
350: * @param i The index of the attributeValues array whose value we want
351: * @return The string value of the attributeValues array at position i
352: */
353: public String getAttribValueAsString(int i) {
354: try {
355: return (String) attributeValues[i];
356: } catch (ClassCastException e) {
357: return attributeValues[i].toString();
358: }
359: }
360:
361: /**
362: * @param i The index of the attributeValues array whose value we want
363: * @return The double value of the attributeValues array at position i
364: */
365: public double getAttribValueAsDouble(int i) {
366: if (attributeValues[i] instanceof Double)
367: return ((Double) attributeValues[i]).doubleValue();
368: else
369: return NumberFormatter.parseDouble(attributeValues[i]
370: .toString());
371: }
372:
373: /**
374: * @param index The index of the attributeValues array whose value we want
375: * @return The int value of the attributeValues array at position i
376: */
377: public int getAttribValueAsInt(int index) {
378: if (attributeValues[index] instanceof Double)
379: return Integer.parseInt(attributeValues[index].toString());
380: else
381: return NumberFormatter.parseInt(attributeValues[index]
382: .toString());
383: }
384:
385: /**
386: * Sets the attribute value
387: * @param index The index of the attributeValues array whose value we want
388: * @param value the value object
389: */
390: public void setAttribValue(int index, Object value) {
391: if (index == ID_ATTRIBUTE)
392: setId((String) value);
393: else
394: attributeValues[index] = value;
395: }
396:
397: /**
398: * Sets the attribute value
399: * @param index The index of the attributeValues array whose value we want
400: * @param attribName the name of the attribute
401: * @param value the value object
402: */
403: public void setAttribValue(int index, String attribName,
404: Object value) {
405: if (index >= NUM_FIXED_ATTRIBUTE)
406: attributeNames[index] = attribName;
407: attributeValues[index] = value;
408: }
409:
410: /**
411: * Gets the value attribute as a Double value
412: * @param elementName
413: * @return the value as a double
414: */
415: public double getValueAsDouble(String elementName) {
416: XModel node = (XModel) get(elementName);
417: if (node.get() instanceof Double)
418: return ((Double) node.get()).doubleValue();
419: else
420: return NumberFormatter.parseDouble(node
421: .getAttribValueAsString(VALUE_ATTRIBUTE));
422: }
423:
424: /**
425: * Gets the value attribute of the specified node as an int.
426: * @param elementName
427: * @return the value as an int
428: */
429: public int getValueAsInt(String elementName) {
430: XModel node = (XModel) get(elementName);
431: if (node.get() instanceof Double)
432: return Integer.parseInt(node
433: .getAttribValueAsString(VALUE_ATTRIBUTE));
434: else
435: return NumberFormatter.parseInt(node
436: .getAttribValueAsString(VALUE_ATTRIBUTE));
437: }
438:
439: /**
440: * Gets the value attribute of the specified node as a string.
441: * @param elementName
442: * @return the value as a string
443: */
444: public String getValueAsString(String elementName) {
445: XModel node = (XModel) get(elementName);
446: return node.getAttribValueAsString(VALUE_ATTRIBUTE);
447: }
448:
449: public int hashCode() {
450: return hashcode;
451: }
452:
453: /**
454: * Gets the number of immediate children of this node
455: * @return the number of child nodes
456: */
457: public int getNumChildren() {
458: if (values == null)
459: return 0;
460:
461: return numValidChildren;
462: }
463:
464: /**
465: * Gets the number of attributes of this node
466: * @return the number of attributes
467: */
468: public int getNumAttributes() {
469: return attributeValues.length;
470: }
471:
472: /**
473: * Set the number of children of this node
474: * @param num the new number of children
475: */
476: public void setNumChildren(int num) {
477: XModel[] temp = values;
478: values = new XModel[num];
479: if (temp != null) {
480: int numChildren = Math.min(num, temp.length);
481: for (int i = 0; i < numChildren; i++)
482: values[i] = temp[i];
483: }
484: }
485:
486: /**
487: * Append a new node with the specified name. This method does not replace any
488: * existing nodes.
489: * @param element The immediate path to the XModel required
490: * @return The value of the XModel or the attribute
491: */
492: public Object append(String elementName) {
493: String path = getBaseFromPath(elementName);
494:
495: int numValues = values == null ? 0 : values.length;
496:
497: // Add a new element if necessary
498: int i = 0;
499: for (; i < numValues; i++) {
500: if (values[i] == null)
501: break;
502: }
503:
504: if (i == numValues)
505: setNumChildren(numValidChildren + 1);
506:
507: int pos = path.lastIndexOf('/');
508: values[i] = new XBaseModel(this );
509: numValidChildren++;
510: if (pos < 0)
511: values[i].setId(path);
512: else {
513: pos = path.indexOf('/');
514: values[i].setId(path.substring(0, pos));
515: return values[i].append(path.substring(pos + 1));
516: }
517: return values[i];
518: }
519:
520: /**
521: * Appends a node to the model. If a node of the same name is found it is
522: * replaced. If there is insufficient space to store the new node then the
523: * storage is automatically expanded.
524: * <br>
525: * The child nodes should be named uniquely
526: * or not at all (i.e. they should be annonymous). If the new childNode has a
527: * name then it is compared to the existing nodes and will replace a node of
528: * the same name if one exists.
529: * @param childNode the child node
530: */
531: public void append(XModel childNode) {
532: if (values == null)
533: values = new XModel[1];
534:
535: String childName = childNode.getId();
536: int numChildren = numValidChildren;
537:
538: if (childName == null) {
539: for (int i = 0; i < values.length; i++) {
540: if ((values[i] == null)) {
541: values[i] = childNode;
542: numValidChildren++;
543: return;
544: }
545: }
546: } else {
547: for (int i = 0; i < numChildren; i++) {
548: if ((values[i].getId() != null)
549: && (values[i].getId().compareTo(childName) == 0)) {
550: values[i] = childNode;
551: return;
552: }
553: }
554: }
555:
556: // There was no room left so we need to expand the array and append the new
557: // element as the last array item.
558: setNumChildren(numChildren + 5);
559: values[numValidChildren] = childNode;
560: numValidChildren = numChildren + 1;
561: }
562:
563: /**
564: * Remove a child node from this XModel instance. Squeeze the following
565: * children so that they are contiguous.
566: * @param child the child to be removed.
567: */
568: public void remove(XModel child) {
569: boolean found = false;
570: int numChildren = values.length;
571: for (int i = 0; i < numChildren; i++) {
572: if (!found) {
573: if (values[i] == child) {
574: found = true;
575: values[i] = null;
576: numValidChildren--;
577: }
578: } else
579: values[i - 1] = values[i];
580: }
581:
582: // if the child was removed then we need to initialise the last item in the list.
583: if (found && (numChildren == (numValidChildren + 1)))
584: values[numChildren - 1] = null;
585: }
586:
587: /**
588: * Remove the nodes attributes and attribute names
589: */
590: public void removeAttributes() {
591: attributeValues = new Object[NUM_FIXED_ATTRIBUTE];
592: attributeNames = new String[NUM_FIXED_ATTRIBUTE];
593: }
594:
595: /**
596: * Remove the children of this node
597: */
598: public void removeChildren() {
599: values = null;
600: numValidChildren = 0;
601: }
602:
603: /**
604: * Remove a child node
605: * @param name the ID or name of the node
606: */
607: public void removeChild(String name) {
608: boolean found = false;
609: for (int i = 0; i < values.length; i++) {
610: if (!found) {
611: if (((XModel) values[i]).getId().compareTo(name) == 0) {
612: values[i] = null;
613: found = true;
614: numValidChildren--;
615: }
616: }
617:
618: // Squeeze the entries
619: if (i < (values.length - 1))
620: values[i] = values[i + 1];
621: }
622: if (found)
623: values[values.length - 1] = null;
624: }
625:
626: /**
627: * Remove a child node
628: * @param name the ID or name of the node
629: * @param value the value of the matching node to remove
630: */
631: public void removeChild(String name, String value) {
632: boolean found = false;
633: for (int i = 0; i < values.length; i++) {
634: if (!found) {
635: if (((XModel) values[i]).getId().equals(name)) {
636: // Check the child value
637: if (values[i].get().equals(value)) {
638: values[i] = null;
639: found = true;
640: numValidChildren--;
641: }
642: }
643: }
644:
645: // Squeeze the entries
646: if (i < (values.length - 1))
647: values[i] = values[i + 1];
648: }
649: if (found)
650: values[values.length - 1] = null;
651: }
652:
653: /**
654: * Setup the attributeNames and attributeValues arrays. If not already
655: * initialised set the size of each to 2 otherwise store them temporarily
656: * and reassign to the increased size arrays.
657: * @param num The new size of the array
658: */
659: public void setNumAttributes(int num) {
660: num = Math.max(num, 2);
661: Object[] temp = new Object[num];
662: String[] tempNames = new String[num];
663: if (attributeNames != null) {
664: if (num != attributeNames.length) {
665: int numAttributes = Math
666: .min(num, attributeNames.length);
667: System.arraycopy(attributeNames, 0, tempNames, 0,
668: numAttributes);
669: System.arraycopy(attributeValues, 0, temp, 0,
670: numAttributes);
671: } else
672: return;
673: }
674: attributeValues = temp;
675: attributeNames = tempNames;
676: }
677:
678: /**
679: * Get the attribute from a path e.g. <br>
680: * returns 'attrib' from 'a/b/c/@attrib'
681: * returns null from 'a/b/c/'
682: * @param path the path to split
683: * @return the attribute name
684: */
685: protected String getAttribFromPath(String path) {
686: int pos = path.indexOf('@');
687: if (pos >= 0) {
688: int endPos = path.indexOf('/');
689: int startEscapePos = path.indexOf('[');
690: if ((startEscapePos > pos) && (startEscapePos < endPos))
691: endPos = path.indexOf(']') + 1;
692: if (endPos < 0)
693: return path.substring(pos + 1);
694: if (endPos < pos)
695: return null;
696: return path.substring(pos + 1, endPos);
697: }
698:
699: return null;
700: }
701:
702: /**
703: * Get the base path from a path e.g. <br>
704: * returns 'a/b/c/' from 'a/b/c/@attrib'
705: * returns 'a/b/c/' from 'a/b/c/'
706: * returns null from 'a/b/c/@attrib'
707: * @param path the path to split
708: * @return the path stripped of attributes
709: */
710: protected String getBaseFromPath(String path) {
711: int pos = path.indexOf('@');
712: if (pos < 0)
713: return path;
714: else if (pos > 0)
715: return path.substring(0, pos);
716:
717: return null;
718: }
719:
720: /**
721: * Set the flags that determines if attributes are added when queried
722: * @param state true to add an attribute if it is missing, false to return -1
723: * when querying a node attribute with "getAttribute( name )".
724: */
725: public void setAddByDefault(boolean state) {
726: addByDefault = state;
727: }
728:
729: /**
730: * Get the flags that determines if attributes are added when queried
731: * @return true if an attribute is added if it is missing when queried, or false to return -1
732: * when querying a node attribute with "getAttribute( name )".
733: */
734: public boolean getAddByDefault() {
735: return addByDefault;
736: }
737:
738: /**
739: * Set the flags that determines if attributes are added when queried
740: * @param state true to add an attribute if it is missing, false to return -1
741: * when querying a node attribute with "getAttribute( name )".
742: */
743: public static void setAppendByDefault(boolean state) {
744: appendByDefault = state;
745: }
746:
747: /**
748: * Get the flags that determines if attributes are added when queried
749: * @return true if an attribute is added if it is missing when queried, or false to return -1
750: * when querying a node attribute with "getAttribute( name )".
751: */
752: public static boolean getAppendByDefault() {
753: return appendByDefault;
754: }
755: }
|