View Javadoc

1   /* ===================================================================
2    * Copyright 2002-04 SRI International.
3    * Released under the MOZILLA PUBLIC LICENSE Version 1.1
4    * which can be obtained at http://www.mozilla.org/MPL/MPL-1.1.html
5    * This software is distributed on an "AS IS"
6    * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.
7    * See the License for the specific language governing rights and
8    * limitations under the License.
9    * =================================================================== */
10  package com.sri.emo.dbobj;
11  
12  import com.jcorporate.expresso.core.controller.*;
13  import com.jcorporate.expresso.core.dataobjects.jdbc.JoinedDataObject;
14  import com.jcorporate.expresso.core.db.DBConnection;
15  import com.jcorporate.expresso.core.db.DBException;
16  import com.jcorporate.expresso.core.db.exception.DBRecordNotFoundException;
17  import com.jcorporate.expresso.core.dbobj.*;
18  import com.jcorporate.expresso.core.misc.DateTime;
19  import com.jcorporate.expresso.core.registry.RequestRegistry;
20  import com.jcorporate.expresso.core.security.ReadOnlyUser;
21  import com.jcorporate.expresso.core.security.SuperUser;
22  import com.jcorporate.expresso.core.security.User;
23  import com.jcorporate.expresso.core.security.filters.AllowedHtmlPlusURLFilter;
24  import com.jcorporate.expresso.core.security.filters.Filter;
25  import com.jcorporate.expresso.core.security.filters.RawFilter;
26  import com.jcorporate.expresso.core.security.filters.XmlFilter;
27  import com.jcorporate.expresso.services.dbobj.RowPermissions;
28  import com.sri.common.controller.AbstractDBController;
29  import com.sri.emo.annotations.NodeTag;
30  import com.sri.emo.controller.AddNodeAction;
31  import com.sri.emo.dbobj.model_tree.ModelVisitable;
32  import com.sri.emo.dbobj.model_tree.ModelVisitor;
33  import com.sri.emo.dbobj.selectiontree.TreeSelectionFactory;
34  import org.dom4j.DocumentFactory;
35  import org.dom4j.Element;
36  
37  import java.util.*;
38  
39  
40  /***
41   * Encapsulate an element in a linked graph.
42   *
43   * @author larry hamel
44   * @todo Implement equals and hashcode for handling in collections.
45   */
46  public class Node extends RowSecuredDBObject implements Comparable, ModelVisitable, IViewable {
47      /***
48  	 * 
49  	 */
50  	private static final long serialVersionUID = 1L;
51  	public static final String NODE_ID = "NODE_ID";
52      public static final String NODE_TYPE = "NODE_TYPE";
53      public static final String NODE_OWNER = "NODE_OWNER";
54      public static final String NODE_TITLE = "NODE_TITLE";
55      public static final String NODE_ANNOTATION = "NODE_ANNOTATION";
56      public static final String NODE_CREATED = "NODE_CREATED";
57      public static final String NODE_MODIFIED = "NODE_MODIFIED";
58      public static final String NODE_PATH = "NODE_PATH";
59      public static final String NODE_MIME_TYPE = "NODE_MIME_TYPE";
60      public static final String NODE_COMMENT = "NODE_COMMENT";
61  
62      // used for MultiDBObject
63      public static final String RELATION_JOIN = "relation";
64      public static final String NODE_TABLE = "node";
65      public static final String NODE_JOIN = NODE_TABLE;
66      public static final String ATTRIBUTE_JOIN = "attribute";
67  
68      /***
69       * XML: used to seperate 2 halves of name for related node tags.
70       */
71      public static final String RELATED_DELIMITER = "__-__";
72      public static final String REF_ID = "REF_ID";
73      public static final String RELATED = "RELATED";
74      public static final String REFERENCE = "REFERENCE";
75  
76      /***
77       * Used to separate machine name from internal ID,
78       * for creating universally unique IDs ("global" ids).
79       *
80       * @see #getGlobalId
81       */
82      public static final String ID_DELIMITER = "|||";
83      public static final String IDENT_TAG_NAME = "ident";
84  
85      /***
86       * virtual field; convenience for sorting
87       */
88      public static final String GROUP_OF_OWNER = "GROUP_OF_OWNER";
89      public static final String REFS_ONLY = "refs_only";
90      public static final String IGNORE_RELATIONS = "IGNORE_RELATIONS";
91  
92      /***
93       * optional attribute which will hold indentation level for tree access
94       */
95      public static final String INDENT = "indent";
96      public static final String FULL_XML = "FULL_XML";
97      public static final String RELATION_TYPE_JOIN = "Relation_type";
98  
99      public Node(final ReadOnlyUser user) throws DBException {
100         super(user);
101     }
102 
103     /***
104      * Constructor without parameters is required by framework,
105      * but if you use this constructor,
106      * be sure to setRequestingUid() after instantiation.
107      *
108      * @throws DBException upon error.
109      */
110     public Node() throws DBException {
111         super();
112 
113         //setRequestingUid();
114     }
115 
116     /***
117      * Constructor.
118      *
119      * @param theConnection DBConnection to be used to
120      *                      communicate with the database
121      * @param theUser       User name attempting to access the
122      *                      object
123      * @throws DBException If the user cannot access this
124      *                     object or the object cannot be initialized
125      */
126     public Node(DBConnection theConnection, int theUser) throws DBException {
127         super(theConnection, theUser);
128     }
129 
130     /***
131      * Constructs a Node with the given 'context' and the given Id.
132      *
133      * @param id      String the node id. (String)
134      * @param request the <code>ExpressoRequest</code> object.
135      * @throws DBException If the user cannot access this
136      *                     object or the object cannot be initialized
137      */
138     public Node(ExpressoRequest request, String id) throws DBException {
139         super(request);
140         setNodeId(id);
141     }
142 
143     /***
144      * Constructs a Node that has the given string id.  Security Parameters
145      * are set by RowSecuredDBObject's default constructors using
146      * the <tt>RequestRegistry</tt>
147      *
148      * @param id String the node id. (String)
149      * @throws DBException upon error.
150      */
151     public Node(String id) throws DBException {
152         setNodeId(id);
153     }
154 
155     /***
156      * @param request the <code>ExpressoRequest</code> object.
157      * @throws DBException If the user cannot access this
158      *                     object or the object cannot be initialized
159      */
160     public Node(ExpressoRequest request) throws DBException {
161         super(request);
162     }
163 
164     /***
165      * Constructs a node using another object as the security credentials.
166      *
167      * @param node SecuredDBObject
168      * @throws DBException upon construction error.
169      */
170     public Node(SecuredDBObject node) throws DBException {
171         super(node.getRequestingUser());
172         setDataContext(node.getDataContext());
173     }
174 
175     /***
176      * Constructs a Node from an XML Element.
177      *
178      * @param root Element The Element to parse.
179      * @throws DBException upon error.
180      */
181     public Node(Element root) throws DBException {
182         createFromXml(root);
183     }
184 
185 
186     /***
187      * Construct node from XML--just basic XML attributes,
188      * NOT related nodes, or model-app Attributes
189      * side-effect: also adds DBObject.setAttribute()
190      * items for root param, and one for ident.
191      *
192      * @param request the <code>ExpressoRequest</code> object.
193      * @param root    The Root Element to load.
194      * @throws DBException upon database access error.
195      */
196     public Node(ExpressoRequest request, Element root) throws DBException {
197         super(request);
198         createFromXml(root);
199     }
200 
201     private void createFromXml(Element root) throws DBException {
202         setNodeType(root.getName());
203         setAttribute("root", root); // for second-phase parsing
204 
205         Part[] parts = PartsFactory.getParts(getNodeType());
206 
207         if (parts.length == 0) {
208             throw new DBException("Cannot find type: " + getNodeType());
209         }
210 
211         // String nodetypeversion = root.attributeValue(NodeType.NODE_TYPE_VERSION); //  handle legacy
212         setNodeTitle(root.attributeValue(NODE_TITLE));
213 
214         //        setAttribute("root", root);
215         String ident = root.attributeValue(IDENT_TAG_NAME);
216         setAttribute(IDENT_TAG_NAME, ident); // original ID, used for matching relations within this import
217 
218         Element ann = (Element) root.selectSingleNode("./" + NODE_ANNOTATION);
219 
220         if (ann != null) {
221             setNodeAnnotation(ann.getTextTrim());
222         }
223 
224         Element comment = (Element) root.selectSingleNode("./" + NODE_COMMENT);
225 
226         if (comment != null) {
227             setNodeComment(comment.getTextTrim());
228         }
229 
230         setRecentEditor(RequestRegistry.getUser().getLoginName());
231     }
232 
233     /***
234      * @return list of NodeTag instances for this node
235      */
236     public List getTags() throws DBException {
237         NodeTag tags = new NodeTag(SuperUser.INSTANCE);
238         tags.setNodeId(this.getNodeId());
239         return tags.searchAndRetrieveList(NodeTag.TAG_ADDED_ON);
240     }
241 
242     /***
243      * Defines the database table name and fields for this DB object.
244      *
245      * @throws DBException if the operation cannot be performed
246      */
247     protected synchronized void setupFields() throws DBException {
248         setTargetTable(NODE_TABLE);
249         setDescription("Node, a sample record");
250         addField(NODE_ID, DBField.AUTOINC_TYPE, 0, false, "Autoincrement ID");
251         addField(NODE_TYPE, DBField.VARCHAR_TYPE, 100, false, "Type of node");
252 
253         // node owner is actually the last person who edited the node
254         addField(NODE_OWNER, DBField.VARCHAR_TYPE, 245, false,
255                 "Last editor"); // 245 to permit indexing on both owner, type
256         addField(NODE_TITLE, DBField.VARCHAR_TYPE, 255, false, "Title");
257         addField(NODE_ANNOTATION, DBField.LONGVARCHAR_TYPE, 0, true, "Summary");
258 
259         // set special filter on command and annotation fields
260         // so that if the user types in a URL, we make it a link during output
261         DBField fieldMeta = (DBField) getMetaData().getFieldMetadata(NODE_ANNOTATION);
262         fieldMeta.setFilterClass(AllowedHtmlPlusURLFilter.class);
263         addField(NODE_COMMENT, DBField.LONGVARCHAR_TYPE, 0, true, "Comment");
264         fieldMeta = (DBField) getMetaData().getFieldMetadata(NODE_COMMENT);
265         fieldMeta.setFilterClass(AllowedHtmlPlusURLFilter.class);
266 
267         addField(NODE_CREATED, DBField.DATETIME_TYPE, 0, false, "Created");
268         addField(NODE_MODIFIED, DBField.DATETIME_TYPE, 0, false,
269                 "Modified Timestamp");
270 
271         // unused
272         addField(NODE_PATH, DBField.LONGVARCHAR_TYPE, 0, true, "Path to File"); // unused currently
273         addField(NODE_MIME_TYPE, DBField.VARCHAR_TYPE, 255, true,
274                 "MIME type of File"); // unused currently
275 
276         setLookupObject(NODE_TYPE, NodeType.class.getName());
277         addKey(NODE_ID);
278 
279         addDetail(Attribute.class.getName(), NODE_ID, Attribute.NODE_ID);
280         addDetail(Relation.class.getName(), NODE_ID, Relation.RELATION_SRC);
281         addDetail(NodeTag.class.getName(), NODE_ID, NodeTag.TAG_PARENT_NODE);
282 
283         //		addDetail(Relation.class.getName(), NODE_ID, Relation.RELATION_DEST);
284         /*
285                  patch other relation detail by overriding NodeAction.runDelete
286 
287                  Hello,
288 
289          I think that there is a bug in the underlying representation of details (foreign key relations) in DBObject.
290 
291                  For example, consider a Node class which has relations with other node instances, and these relations are represented in a Relation class.  A given node could be the source of a relation, or the destination of a relation.  The Node class then would have two addDetail() lines with the Relation class:
292 
293          addDetail(Relation.class.getName(), NODE_ID, Relation.RELATION_SRC);
294          addDetail(Relation.class.getName(), NODE_ID, Relation.RELATION_DEST);
295 
296                  But this does the wrong thing because the underlying implementation in DBObjectDef which captures the details is a hash table:
297 
298          synchronized void addDetail(String objName, String keyFieldsLocal,
299                  String keyFieldsForeign)
300                  throws DBException {
301                  detailObjsLocal.put(objName, keyFieldsLocal);
302                  detailObjsForeign.put(objName, keyFieldsForeign);
303                  }
304 
305                  With this implementation, there can be only one detail association between any two classes.
306 
307          I'm working on an old version of Expresso right now, so it will take me a while to get around to fixing this.
308 
309                  Let me know if you see a problem with this analysis, and feel free to fix this before I get there.
310 
311                  Larry
312          */
313         /***
314          * @todo make name of node unique via index; may revisit later
315          */
316         addIndex("node_name_idx", Node.NODE_TITLE, true);
317         addIndex("node_type_idx", Node.NODE_TYPE, false);
318 
319         //addIndex("node_owner_idx",Node.NODE_OWNER,false);
320         //addIndex("node_type_owner_idx", Node.NODE_OWNER + "," + Node.NODE_TYPE, false);
321     }
322 
323     /* setupFields() */
324 
325     /***
326      * Add sample record.
327      *
328      * @throws DBException if the operation cannot be performed
329      */
330     public synchronized void populateDefaultValues() throws DBException {
331     }
332 
333     public void setNodeId(String id) throws DBException {
334         setField(NODE_ID, id);
335     }
336 
337     public String getNodeType() throws DBException {
338         return getField(Node.NODE_TYPE);
339     }
340 
341     public void setNodeType(String type) throws DBException {
342         setField(Node.NODE_TYPE, type);
343     }
344 
345     public String getNodeId() throws DBException {
346         return getField(Node.NODE_ID);
347     }
348 
349 
350     /***
351      * Adds a new attribute to be associated with this node.
352      *
353      * @param attribType String the attribute type.
354      * @param value      String the attribute value.
355      * @param comment    String the attribute comment.
356      * @return Attribute the resulting saved attribute.
357      * @throws DBException upon general database error.
358      */
359     public Attribute addAttribute(final String attribType, final String value, final String comment) throws
360             DBException {
361         Attribute attribute = new Attribute();
362         attribute.setField(Attribute.ATTRIBUTE_TYPE, attribType);
363         attribute.setField(Attribute.NODE_ID, this.getNodeId());
364         attribute.setField(Attribute.NODE_TYPE, this.getNodeType());
365 
366         // is this attrib type limited to being a single value?
367         if (attribute.getPart().isSingleValued()) {
368             // check for other values already
369             List existing = attribute.searchAndRetrieveList();
370             if (existing.size() > 0) {
371                 // ok, have a conflict.  instead of adding, we must update
372                 Attribute existingAttrib = (Attribute) existing.get(0);
373                 existingAttrib.setField(Attribute.ATTRIBUTE_VALUE, value);
374                 existingAttrib.setField(Attribute.ATTRIBUTE_COMMENT, comment);
375                 existingAttrib.update();
376                 return existingAttrib;
377             }
378         }
379 
380         // ok, set other fields and add
381         attribute.setField(Attribute.ATTRIBUTE_VALUE, value);
382         attribute.setField(Attribute.ATTRIBUTE_COMMENT, comment);
383         attribute.add();
384         touch();
385         return attribute;
386     }
387 
388 
389     /***
390      * Saves an attributes for the node, touching the node as necessary, etc.
391      *
392      * @param attributeId the attribute to update.
393      * @return Attribute the resulting attribute.
394      * @throws DBException               upon general DBObject error.
395      * @throws DBRecordNotFoundException if the attribute wasn't found.
396      */
397     public Attribute updateAttribute(String attributeId, String value, String comment) throws DBException,
398             DBRecordNotFoundException {
399         Attribute attribute = new Attribute();
400 
401         attribute.setField(Attribute.ATTRIBUTE_ID, attributeId);
402 
403         boolean found = attribute.find();
404         boolean isSame = found &&
405                 value.equals(attribute.getAttribValue()) &&
406                 comment.equals(attribute.getAttribComment());
407         boolean shouldDelete = found && !isSame &&
408                 (value.length() == 0) && (comment.length() == 0);
409 
410         if (isSame) {
411             // no saving necessary
412             return null;
413         }
414 
415         // edit is just to remove all text, signalling deletion
416         if (shouldDelete) {
417             attribute.delete();
418             touch();
419 
420             return attribute;
421         }
422 
423         if (found) {
424             attribute.setField(Attribute.ATTRIBUTE_VALUE, value);
425             attribute.setField(Attribute.ATTRIBUTE_COMMENT, comment);
426             attribute.update();
427             touch();
428             return attribute;
429         } else {
430             throw new DBRecordNotFoundException("Cannot find attribute with id = " + attributeId);
431         }
432 
433     }
434 
435 
436     /***
437      * Get the node's title.
438      *
439      * @return current title or "" -- never null
440      * @throws DBException upon database access error.
441      */
442     public String getNodeTitle() throws DBException {
443         return getField(Node.NODE_TITLE);
444     }
445 
446     /***
447      * @param relation
448      * @param nodeType
449      * @return a list of  MultiDBObject which are products of an SQL join, in order by relation order
450      * @throws DBException upon database access error.
451      */
452     public List getRawRelated(String relation, String nodeType) throws DBException {
453         MultiDBObject joinObject = new MultiDBObject();
454         joinObject.setDataContext(getDataContext());
455         joinObject.addDBObj(Relation.class.getName(), RELATION_JOIN);
456 
457         Node sampleNode = new Node(getRequestingUser());
458         sampleNode.setNodeType(nodeType);
459         joinObject.addDBObj(sampleNode, NODE_JOIN);
460 
461         joinObject.setForeignKey(RELATION_JOIN, Relation.RELATION_DEST, NODE_JOIN, Node.NODE_ID);
462         joinObject.setField(RELATION_JOIN, Relation.RELATION_SRC, getNodeId());
463         joinObject.setField(RELATION_JOIN, Relation.RELATION_TYPE, relation);
464 
465         // sort by node title
466         return joinObject.searchAndRetrieveList(Relation.RELATION_ORDER);
467     }
468 
469     static int numCalls = 0;
470 
471     /***
472      * return all nodes related to this node as source
473      *
474      * @return a list of  MultiDBObject which are products of an SQL join, in order by relation order
475      * @throws DBException upon database access error.
476      */
477     public List getRawRelated() throws DBException {
478         MultiDBObject joinObject = new MultiDBObject();
479         joinObject.setDataContext(getDataContext());
480 
481         Relation rel = new Relation();
482         rel.setSrcId(getNodeId());
483         joinObject.addDBObj(rel, RELATION_JOIN);
484 
485         Node sampleNode = new Node();
486         joinObject.addDBObj(sampleNode, NODE_JOIN);
487 
488         joinObject.setForeignKey(RELATION_JOIN, Relation.RELATION_DEST,
489                 NODE_JOIN, Node.NODE_ID);
490         joinObject.setField(RELATION_JOIN, Relation.RELATION_SRC, getNodeId());
491 
492         // sort by node title
493 
494         return joinObject.searchAndRetrieveList(Relation.RELATION_ORDER);
495     }
496 
497 
498     /***
499      * Grabs a list of joined data objects based on the matrix join.
500      *
501      * @return List of JoinedDataObjects
502      * @throws DBException upon join creation/execution error.
503      */
504     public List getRawRelatedUsingDataObjects() throws DBException {
505         JoinedDataObject joinObject = new JoinedDataObject("/com/sri/emo/dbobj/NodeRelationsJoin.xml");
506         joinObject.setRequestingUser(this.getRequestingUser());
507         joinObject.set("Relation." + Relation.RELATION_SRC, getNodeId());
508         return joinObject.searchAndRetrieveList("Relation." + Relation.RELATION_ORDER);
509     }
510 
511     /***
512      * Get related nodes, without ID test (for speed).
513      *
514      * @param dbname     the database object to load from.
515      * @param relation
516      * @param typeOfPart
517      * @return a list of  MultiDBObject which are products of an SQL join, in alpha order by title
518      * @throws DBException upon database access error.
519      */
520     public List getRawRelatedAssumeSecure(String dbname, String relation,
521                                           String typeOfPart) throws DBException {
522         MultiDBObject joinObject = new MultiDBObject();
523         joinObject.setDataContext(dbname);
524         joinObject.addDBObj(Relation.class.getName(), RELATION_JOIN);
525 
526         Node sampleNode = new Node(SuperUser.INSTANCE);
527         sampleNode.setDataContext(dbname);
528         sampleNode.setNodeType(typeOfPart);
529         joinObject.addDBObj(sampleNode, NODE_JOIN);
530 
531         joinObject.setForeignKey(RELATION_JOIN, Relation.RELATION_DEST,
532                 NODE_JOIN, Node.NODE_ID);
533         joinObject.setField(RELATION_JOIN, Relation.RELATION_SRC, getNodeId());
534         joinObject.setField(RELATION_JOIN, Relation.RELATION_TYPE, relation);
535 
536         // sort by node title
537         return joinObject.searchAndRetrieveList(Relation.RELATION_ORDER);
538     }
539 
540     /***
541      * @return a list of Nodes which are products of an SQL join, in by relation order; can be empty list; never returns null
542      * @throws DBException upon database access error.
543      */
544     public Node[] getRelatedNodes(String relation, String typeOfPart) throws DBException {
545         List list = getRawRelated(relation, typeOfPart);
546         ArrayList resultList = new ArrayList();
547 
548         for (Iterator iterator = list.iterator(); iterator.hasNext();) {
549             MultiDBObject aMultiObject = (MultiDBObject) iterator.next();
550             String id = aMultiObject.getField(Node.NODE_JOIN, Node.NODE_ID);
551             Node node = new Node(this);
552             node.setNodeId(id);
553             node.retrieve();
554             resultList.add(node);
555         }
556 
557         return (Node[]) resultList.toArray(new Node[resultList.size()]);
558     }
559 
560     /***
561      * Fetch any attributes of this node, of this type, in alpha order by value.
562      *
563      * @return an array of Attributes; NEVER null, but could be empty
564      * @throws DBException upon database access error.
565      */
566     public Attribute[] getAttributes(String attribType) throws DBException {
567         Attribute attribute = new Attribute(getRequestingUser());
568         attribute.setDataContext(getDataContext());
569         attribute.setField(Attribute.ATTRIBUTE_TYPE, attribType);
570         attribute.setField(Attribute.NODE_ID, getNodeId());
571 
572         return (Attribute[]) attribute.searchAndRetrieveList(Attribute.
573                 ATTRIBUTE_ORDER)
574                 .toArray(new Attribute[0]);
575     }
576 
577 
578     public ValidValue[] getMultiValuedAttributeMenu() throws DBException {
579         Attribute attrib = new Attribute();
580         attrib.setField(NODE_ID, getNodeId());
581         List attribs = attrib.searchAndRetrieveList(Attribute.ATTRIBUTE_ORDER);
582         ValidValue[] returnValue = new ValidValue[attribs.size()];
583         for (int i = 0; i < attribs.size(); i++) {
584             returnValue[i] = new ValidValue(attrib.getAttribId(), attrib.getAttribValue());
585         }
586 
587         return returnValue;
588     }
589 
590     /***
591      * Fetch any attributes of this node, of this type, in alpha order by value,
592      * using System ID to speed recall w/o permissions check.
593      *
594      * @return an array of Attributes; NEVER null, but could be empty
595      * @throws DBException upon database access error.
596      */
597     public Attribute[] getAttributesAssumeSecure(String attribType) throws
598             DBException {
599         Attribute attribute = new Attribute(User.getAdmin(getDataContext()));
600         attribute.setDataContext(getDataContext());
601         attribute.setField(Attribute.ATTRIBUTE_TYPE, attribType);
602         attribute.setField(Attribute.NODE_ID, getNodeId());
603 
604         return (Attribute[]) attribute.searchAndRetrieveList(Attribute.ATTRIBUTE_ORDER).toArray(new Attribute[0]);
605     }
606 
607     /***
608      * Fetch all attributes of this node OF ALL KINDS.
609      *
610      * @return a list of Attributes
611      * @throws DBException upon database access error.
612      */
613     public List getAttributes() throws DBException {
614         Attribute attribute = new Attribute(getRequestingUser());
615         attribute.setDataContext(getDataContext());
616         attribute.setField(Attribute.NODE_ID, getNodeId());
617 
618         return attribute.searchAndRetrieveList(Attribute.ATTRIBUTE_ORDER);
619     }
620 
621     public String getNodeAnnotation() throws DBException {
622         return getField(Node.NODE_ANNOTATION);
623     }
624 
625     public String getNodeAnnotationRaw() throws DBException {
626         Filter old = setFilterClass(new RawFilter());
627         String raw = getNodeAnnotation();
628         setFilterClass(old);
629 
630         return raw;
631     }
632 
633     public String getNodeCommentRaw() throws DBException {
634         Filter old = setFilterClass(new RawFilter());
635         String raw = getNodeComment();
636         setFilterClass(old);
637 
638         return raw;
639     }
640 
641     public String getNodeComment() throws DBException {
642         return getField(Node.NODE_COMMENT);
643     }
644 
645     public void setNodeTitle(String title) throws DBException {
646         setField(NODE_TITLE, title);
647     }
648 
649     /***
650      * Creates and returns a deep copy of this object--everything, including
651      * links to "contained by" objects above this one.
652      *
653      * @param title The Node Title.
654      * @return a cloned sibling.
655      * @throws DBException upon database access error.
656      * @see #cloneOrphan
657      */
658     public Node cloneSibling(String title) throws DBException {
659         Node clone = cloneAllButRelations(title);
660         cloneRelations(clone);
661 
662         return clone;
663     }
664 
665     /***
666      * Fet relations for which my ID is marked as source.
667      *
668      * @return an array of relations.
669      * @throws DBException upon database access error.
670      */
671     public Relation[] getSrcRelations() throws DBException {
672         return getRelations(Relation.RELATION_SRC);
673     }
674 
675     /***
676      * Get relations.
677      *
678      * @param srcOrDest field name for how to interpret getID():
679      *                  either as src (pass in Relation.RELATION_SRC) or dest (pass in Relation.RELATION_DEST)
680      * @return an array of relations, never null
681      * @throws DBException upon database access error.
682      * @see Relation#RELATION_SRC
683      * @see Relation#RELATION_DEST
684      */
685     private Relation[] getRelations(String srcOrDest) throws DBException {
686         Relation rel = new Relation();
687         rel.setField(srcOrDest, getNodeId());
688 
689         return (Relation[]) rel.searchAndRetrieveList(Relation.RELATION_ORDER).toArray(new Relation[0]);
690     }
691 
692     /***
693      * Get relations for which my ID is marked as destination.
694      *
695      * @return an array of relations, never null
696      * @throws DBException upon database access error.
697      */
698     public Relation[] getDestRelations() throws DBException {
699         return getRelations(Relation.RELATION_DEST);
700     }
701 
702     /***
703      * Delete row.
704      *
705      * @throws DBException upon database access error.
706      */
707     public synchronized void delete(boolean deleteDetails) throws DBException {
708 
709         /*** @todo fix expresso details so that this hack is unnecessary
710          * currently detail can only support one association between two tables
711          * so manually delete any destination relations
712          */
713         Relation relation = new Relation();
714         relation.setDataContext(getDataContext());
715         relation.setField(Relation.RELATION_DEST, getNodeId());
716 
717         List list = relation.searchAndRetrieveList();
718 
719         for (Iterator iter = list.iterator(); iter.hasNext();) {
720             Relation rel = (Relation) iter.next();
721             rel.delete(true);
722         }
723 
724         super.delete(deleteDetails);
725     }
726 
727     /* delete() */
728 
729     public String getNodeOwner() throws DBException {
730         return getField(Node.NODE_OWNER);
731     }
732 
733     public String getNodeCreated() throws DBException {
734         return getField(Node.NODE_CREATED);
735     }
736 
737     public String getNodeModified() throws DBException {
738         return getField(Node.NODE_MODIFIED);
739     }
740 
741     /***
742      * @return version number for this node type
743      * @throws DBException upon database access error.
744      */
745     public String getVersion() throws DBException {
746         NodeType type = new NodeType(getRequestingUser());
747         type.setDataContext(getDataContext());
748         type.setEntityName(getNodeType());
749 
750         if (!type.find()) {
751             return "1.0";
752         }
753 
754         return type.getVersion();
755     }
756 
757     /***
758      * We override to set dates
759      * (by superclass) but rather to add default permissions.
760      *
761      * @throws DBException upon database access error.
762      * @see #add(String, int) for a better way to add() with specific permissions
763      */
764     public synchronized void add() throws DBException {
765         String datestamp = DateTime.getDateTimeForDB();
766         setField(NODE_CREATED, datestamp);
767         setField(NODE_MODIFIED, datestamp);
768 
769         super.add();
770 
771         // does this node have a special handler?
772         NodeType entity = getEntity();
773 
774         if (entity.hasCustomHandler()) {
775             entity.getCustomHandler().init(this);
776         }
777     }
778 
779     /***
780      * We override to set dates, and make sure perms are complete.
781      *
782      * @throws DBException upon database access error.
783      */
784     public synchronized void update() throws DBException {
785         String datestamp = DateTime.getDateTimeForDB();
786         setField(NODE_MODIFIED, datestamp);
787         super.update();
788 
789         // adjust group write permissions to new owner
790         User user = User.getUserFromId(getRequestingUid(), getDataContext());
791         String userPrimaryGroup = user.getPrimaryGroup();
792         List nodegroups = getWriteGroups();
793 
794         if (!nodegroups.contains(userPrimaryGroup)) {
795             addGroupPerm(userPrimaryGroup,
796                     RowPermissions.OTHERS_READ_AND_GROUP_WRITES_PERMISSIONS);
797         }
798     }
799 
800     /*  update() */
801 
802     /***
803      * Format XML for this node.
804      * Note that double quotes are removed from regular
805      * fields (varchar fields) by the JDBC escape handler.  longvarchar fields,
806      * on the other hand, are not filtered by this handler, NOR by standard calls
807      * to getField which would normally use the FilterManager.  so longvarchar
808      * fields must be filtered manually.
809      * <p/>
810      * This routine is intensive on DB.  To lighten load, security checks are reduced after first node.
811      * related nodes are assumed readable, so we use "superuser"
812      * </p>
813      *
814      * @param alreadyIncluded list of nodes already included in XML; used to prevent infinite loop if a circular relation is found
815      * @param request         the <code>ExpressoRequest</code> object.
816      * @return dom4j Element.
817      * @throws DBException upon database access error.
818      */
819     public Element getXML(ExpressoRequest request, ArrayList alreadyIncluded) throws DBException {
820         Filter old = setFilterClass(new XmlFilter());
821 
822         // protect against infinite loop if a referenced node loops back to this node
823         if (alreadyIncluded.contains(this)) {
824             Element root = DocumentFactory.getInstance().createElement(REFERENCE);
825 
826             // we already included this node; just write an attribute for ID
827             root.addAttribute(REF_ID, getGlobalId()).addAttribute(Node.NODE_TITLE, getNodeTitle());
828 
829             return root;
830         }
831 
832         Element root = DocumentFactory.getInstance().createElement(getNodeType());
833         alreadyIncluded.add(this);
834 
835         // are we changing title for a duplication of tree?
836         String title = getNodeTitle();
837         boolean isChangingTitle = request.getParameter(AddNodeAction.IS_CHANGE_TITLE) != null;
838 
839         if (isChangingTitle) {
840             String append = request.getParameter(AddNodeAction.APPEND_TO_TITLE);
841 
842             // super geeks can put in regexp in format 's/old/new/'
843             if (append != null) {
844                 if (append.startsWith("s/") && append.endsWith("/")) {
845                     String[] pieces = append.split("/");
846 
847                     if (pieces.length != 3) {
848                         throw new DBException("found substitution string: " +
849                                 append +
850                                 " and expected it to parse into 3 pieces by key '/', " +
851                                 "but found num pieces: " + pieces.length);
852                     }
853 
854                     title = title.replaceFirst(pieces[1], pieces[2]);
855                 } else {
856                     title += append;
857                 }
858             }
859         }
860 
861         root.addAttribute(Node.NODE_TITLE, title);
862 
863         root.addAttribute(NodeType.NODE_TYPE_VERSION, getVersion());
864         root.addAttribute(IDENT_TAG_NAME, getGlobalId());
865 
866         //        root.addAttribute(Node.NODE_OWNER, getNodeOwner());
867         //        root.addAttribute(Node.NODE_CREATED, getNodeCreated());
868         //        root.addAttribute(Node.NODE_MODIFIED, getNodeModified());
869         String comment = getNodeComment();
870 
871         if (comment.length() > 0) {
872             root.addElement(Node.NODE_COMMENT).setText(comment);
873         }
874 
875         String annotation = getNodeAnnotation();
876 
877         if (annotation.length() > 0) {
878             root.addElement(Node.NODE_ANNOTATION).setText(annotation);
879         }
880 
881         HashMap refsonlymap = getNodeTypesWithRefOnly_Map(request);
882         HashMap ignoredRelationsMap = getIgnoredRelations(request);
883 
884         Part[] parts = PartsFactory.getParts(getNodeType());
885 
886         for (int i = 0; i < parts.length; i++) {
887             Part part = parts[i];
888 
889             if (part.isSharedNodeAttrib()) {
890                 if (ignoredRelationsMap.get(part.getNodeRelation()) != null) {
891                     continue;
892                 }
893 
894                 Node[] related = getRelatedNodesAssumeSecure(part.
895                         getNodeRelation(),
896                         part.getPartType());
897 
898                 if (related.length > 0) {
899                     Element relatedElmCollection = root.addElement(RELATED);
900                     relatedElmCollection.addAttribute(Part.PART_LABEL, part.getPartLabel());
901                     relatedElmCollection.addAttribute(Part.PART_TYPE, part.getPartType());
902                     relatedElmCollection.addAttribute(Part.NODE_PART_RELATION_TYPE, part.getNodeRelation());
903                     relatedElmCollection.addAttribute(Part.PART_NUM, part.getPartNum());
904 
905                     for (int j = 0; j < related.length; j++) {
906                         Node aRelatedNode = related[j];
907                         NodeType nodeType = aRelatedNode.getEntity();
908 
909                         // just use references to related nodes that 'prefer' to be seen as IDs
910                         boolean useIdForRef = refsonlymap.get(nodeType.getEntityName()) != null;
911 
912                         String order = null;
913 
914                         if (related.length > 1) {
915                             order = "" + (j + 1); // assumes order is linear with no gaps
916                         }
917 
918                         if (useIdForRef) {
919                             // just output ID
920                             aRelatedNode.addRef(relatedElmCollection, order);
921                         } else {
922                             // output entire xml tree
923                             Element relXml = aRelatedNode.getXML(request, alreadyIncluded);
924 
925                             // add relation order attrib
926                             if (order != null) {
927                                 relXml.addAttribute(Relation.RELATION_ORDER, order);
928                             }
929 
930                             relatedElmCollection.add(relXml);
931                         }
932                     }
933                 }
934             } else {
935                 Attribute[] attribs = getAttributesAssumeSecure(part.getPartType());
936                 boolean addOrder = attribs.length > 1;
937 
938                 if (attribs.length > 0) {
939                     if (part.isSingleValued() && (attribs.length > 1)) {
940                         // make sure we only output one; additional are programming error
941                         attribs = new Attribute[]{attribs[0]};
942                     }
943 
944 
945                     for (int j = 0; j < attribs.length; j++) {
946                         Attribute attrib = attribs[j];
947                         root.add(attrib.getXML(request, addOrder));
948                     }
949                 } else {
950 
951                     // check if we should create default
952                     if (part.isHaveCustomHandler()) {
953                         IPartHandler handler = part.getCustomHandler();
954                         if (handler.isNeededInFullXML()) {
955                             // need to create matrix attrib with node-owners perm.
956                             ReadOnlyUser oldUser = getRequestingUser();
957                             this.setRequestingUser(User.getUserFromId(ownerID()));
958                             Attribute attrib = AbstractMatrixHandler.createAttrib(this, part.getPartType());
959                             this.setRequestingUser(oldUser);
960                             getLogger().debug("creating new attribute of type: " +
961                                     attrib.getAttribType());
962 
963                             root.add(attrib.getXML(request, addOrder));
964                         }
965                     }
966                 }
967 
968             }
969         }
970 
971         setFilterClass(old); // reset
972 
973         return root;
974     }
975 
976     /***
977      * @param request the <code>ExpressoRequest</code> object.
978      * @return hashmap of param values which have name 'refs_only'
979      */
980     public static HashMap getNodeTypesWithRefOnly_Map(ExpressoRequest request) {
981         HashMap refs_only = new HashMap();
982 
983         String[] ref_only = Controller.getParamValues((ServletControllerRequest)
984                 request,
985                 REFS_ONLY);
986 
987         for (int i = 0; (ref_only != null) && (i < ref_only.length); i++) {
988             String referenceOnlyType = ref_only[i];
989             refs_only.put(referenceOnlyType, referenceOnlyType);
990         }
991 
992         return refs_only;
993     }
994 
995     /***
996      * @param request the <code>ExpressoRequest</code> object.
997      * @return map of param values which have name 'IGNORE_RELATIONS'
998      */
999     public static HashMap getIgnoredRelations(ExpressoRequest request) {
1000         HashMap ignoredRelmap = new HashMap();
1001 
1002         String[] ignored = Controller.getParamValues((ServletControllerRequest) request, IGNORE_RELATIONS);
1003 
1004         for (int i = 0; (ignored != null) && (i < ignored.length); i++) {
1005             String ignoredRel = ignored[i];
1006             ignoredRelmap.put(ignoredRel, ignoredRel);
1007         }
1008 
1009         return ignoredRelmap;
1010     }
1011 
1012     /***
1013      * Add a reference ID tag to the xml for this node.
1014      *
1015      * @throws DBException upon database access error.
1016      */
1017     private void addRef(Element elem, String order) throws DBException {
1018         elem.addElement(REFERENCE)
1019                 .addAttribute(REF_ID, getGlobalId())
1020                 .addAttribute(Node.NODE_TITLE, getNodeTitle());
1021 
1022         if (order != null) {
1023             elem.addAttribute(Relation.RELATION_ORDER, order);
1024         }
1025     }
1026 
1027     /***
1028      * Assuming all access is privileged, get related nodes.
1029      *
1030      * @return an array of related nodes.
1031      * @throws DBException upon database access error.
1032      */
1033     private Node[] getRelatedNodesAssumeSecure(String relation, String partType) throws DBException {
1034         List list = getRawRelatedAssumeSecure(getDataContext(), relation, partType);
1035         ArrayList resultList = new ArrayList();
1036 
1037         for (Iterator iterator = list.iterator(); iterator.hasNext();) {
1038             MultiDBObject aMultiObject = (MultiDBObject) iterator.next();
1039             String id = aMultiObject.getField(Node.NODE_JOIN, Node.NODE_ID);
1040             Node node = new Node(SuperUser.INSTANCE);
1041             node.setNodeId(id);
1042             node.setDataContext(getDataContext());
1043             node.retrieve();
1044             resultList.add(node);
1045         }
1046 
1047         return (Node[]) resultList.toArray(new Node[resultList.size()]);
1048     }
1049 
1050     /***
1051      * For creating universally unique IDs ("global" ids)
1052      * by combining machine name with internal ID.
1053      *
1054      * @return node id.
1055      * @throws DBException upon database access error.
1056      */
1057     public String getGlobalId() throws DBException {
1058         //        String serverName = Setup.getValue(DBConnection
1059         // .DEFAULT_DB_CONTEXT_NAME, "HTTPServ");
1060         //        return serverName + ID_DELIMITER + getNodeId();
1061         return getNodeId();
1062     }
1063 
1064     public String getRecentEditor() throws DBException {
1065         return getField(NODE_OWNER);
1066     }
1067 
1068     public void setRecentEditor(String username) throws DBException {
1069         setField(NODE_OWNER, username);
1070     }
1071 
1072     /***
1073      * Get node type; will throw if node type is not set or cannot be found.
1074      *
1075      * @return entity named by the node type of this node
1076      * @throws DBException upon database access error.
1077      */
1078     public NodeType getEntity() throws DBException {
1079         NodeType type = new NodeType(getRequestingUser());
1080         type.setDataContext(getDataContext());
1081         type.setEntityName(getNodeType());
1082 
1083         if (!type.find()) {
1084             throw new DBException("cannot find node type with entity name: " +
1085                     getNodeType());
1086         }
1087 
1088         return type;
1089     }
1090 
1091     /***
1092      * Create clone which copies all EXCEPT &quot;upstream&quot; links.
1093      *
1094      * @param title The Node Title.
1095      * @return cloned Node
1096      * @throws DBException upon database access error.
1097      * @see #cloneSibling
1098      */
1099     public Node cloneOrphan(String title) throws DBException {
1100         Node clone = cloneAllButRelations(title);
1101         cloneOrphanRelations(clone);
1102 
1103         return clone;
1104     }
1105 
1106     private void cloneOrphanRelations(Node clone) throws DBException {
1107         cloneRelations(clone, false);
1108     }
1109 
1110     private Node cloneAllButRelations(String title) throws DBException {
1111         Node clone = new Node(this);
1112         clone.setNodeId(getNodeId());
1113         clone.retrieve();
1114 
1115         clone.setNodeTitle(title);
1116         clone.setNodeId(
1117                 "0"); // resetting id isn't technically necessary, since auto-inc field WILL get new value with add() method
1118 
1119         // clone by just adding -- auto-inc magic
1120         clone.add();
1121 
1122         copyAttribsInto(clone);
1123 
1124         return clone;
1125     }
1126 
1127     /***
1128      * Copies all the node attributes into the specified node.
1129      *
1130      * @param clone Node the node we're copying into.
1131      * @throws DBException upon error.
1132      */
1133     private void copyAttribsInto(Node clone) throws DBException {
1134         List attribs = getAttributes();
1135         for (Iterator iterator = attribs.iterator(); iterator.hasNext();) {
1136             Attribute attrib = (Attribute) iterator.next();
1137             attrib.clone(clone.getNodeId());
1138         }
1139     }
1140 
1141     /***
1142      * Clone ALL relations.
1143      *
1144      * @param clone The node to clone.
1145      * @throws DBException upon database access error.
1146      */
1147     private void cloneRelations(Node clone) throws DBException {
1148         cloneRelations(clone, true);
1149     }
1150 
1151     /***
1152      * clone relations--all or some, depending on boolean
1153      *
1154      * @param cloneNode  The node to clone.
1155      * @param isCloneAll true if all relations are cloned;
1156      *                   false if we omit some in order to create an "orphan" clone
1157      * @throws DBException upon database access error.
1158      */
1159     private void cloneRelations(Node cloneNode, boolean isCloneAll) throws DBException {
1160         Relation[] src = getSrcRelations();
1161 
1162         for (int i = 0; i < src.length; i++) {
1163             Relation relation = src[i];
1164 
1165             if (!isCloneAll && relation.isUpstreamLink(Relation.RELATION_SRC)) {
1166                 continue;
1167             }
1168 
1169             relation.clone(cloneNode.getNodeId(), Relation.RELATION_SRC);
1170         }
1171 
1172         Relation[] dest = getDestRelations();
1173 
1174         for (int i = 0; i < dest.length; i++) {
1175             Relation relation = dest[i];
1176 
1177             if (!isCloneAll && relation.isUpstreamLink(Relation.RELATION_DEST)) {
1178                 continue;
1179             }
1180 
1181             relation.clone(cloneNode.getNodeId(), Relation.RELATION_DEST);
1182         }
1183     }
1184 
1185     /***
1186      * Compares this object with the specified object for order.  Returns a
1187      * negative integer, zero, or a positive integer as this object is less
1188      * than, equal to, or greater than the specified object.<p>
1189      * <p/>
1190      * In the foregoing description, the notation
1191      * <tt>sgn(</tt><i>expression</i><tt>)</tt> designates the mathematical
1192      * <i>signum</i> function, which is defined to return one of <tt>-1</tt>,
1193      * <tt>0</tt>, or <tt>1</tt> according to whether
1194      * the value of <i>expression</i>
1195      * is negative, zero or positive.
1196      * <p/>
1197      * The implementor must ensure <tt>sgn(x.compareTo(y)) ==
1198      * -sgn(y.compareTo(x))</tt> for all <tt>x</tt> and <tt>y</tt>.  (This
1199      * implies that <tt>x.compareTo(y)</tt> must throw an exception iff
1200      * <tt>y.compareTo(x)</tt> throws an exception.)<p>
1201      * <p/>
1202      * The implementor must also ensure that the relation is transitive:
1203      * <tt>(x.compareTo(y)&gt;0 &amp;&amp; y.compareTo(z)&gt;0)</tt> implies
1204      * <tt>x.compareTo(z)&gt;0</tt>.<p>
1205      * <p/>
1206      * Finally, the implementer must ensure that <tt>x.compareTo(y)==0</tt>
1207      * implies that <tt>sgn(x.compareTo(z)) == sgn(y.compareTo(z))</tt>, for
1208      * all <tt>z</tt>.<p>
1209      * <p/>
1210      * It is strongly recommended, but <i>not</i> strictly required that
1211      * <tt>(x.compareTo(y)==0) == (x.equals(y))</tt>.  Generally speaking, any
1212      * class that implements the <tt>Comparable</tt> interface and violates
1213      * this condition should clearly indicate this fact.  The recommended
1214      * language is "Note: this class has a natural ordering that is
1215      * inconsistent with equals."
1216      *
1217      * @param o the Object to be compared.
1218      * @return a negative integer, zero, or a positive integer as this object
1219      *         is less than, equal to, or greater than the specified object.
1220      * @throws ClassCastException if the specified object's type prevents it
1221      *                            from being compared to this Object.
1222      */
1223     public int compareTo(Object o) {
1224         try {
1225             return getNodeTitle().compareTo(((Node) o).getNodeTitle());
1226         } catch (DBException e) {
1227             throw new RuntimeException(e);
1228         }
1229     }
1230 
1231     /***
1232      * Allow a special hander to create custom view,
1233      * while providing default view also.
1234      *
1235      * @param defaultaction the default controller.
1236      * @param request       the ExpressoRequest object.
1237      * @param response      the ExpressoResponse object.
1238      * @throws DBException         upon database access error.
1239      * @throws ControllerException upon controller element related error.
1240      */
1241     public void view(final AbstractDBController defaultaction,
1242                      final ExpressoRequest request,
1243                      final ExpressoResponse response) throws DBException, ControllerException {
1244 
1245         NodeType entity = getEntity();
1246 
1247         if (entity.hasCustomHandler()) {
1248             INodeHandler handler = entity.getCustomHandler();
1249             handler.view(this, defaultaction, (ControllerRequest) request, (ControllerResponse) response);
1250         } else {
1251             // use default handler
1252             ((AddNodeAction) defaultaction).view(this, (ControllerRequest) request, (ControllerResponse) response);
1253         }
1254     }
1255 
1256     public void setNodeAnnotation(final String annotation) throws DBException {
1257         setField(NODE_ANNOTATION, annotation);
1258     }
1259 
1260     public void setNodeComment(final String s) throws DBException {
1261         setField(NODE_COMMENT, s);
1262     }
1263 
1264     /***
1265      * @param nodeOwner login name of last editor
1266      */
1267     public void setNodeOwner(final String nodeOwner) throws DBException {
1268         setField(NODE_OWNER, nodeOwner);
1269     }
1270 
1271     /***
1272      * Find nodes contained just beneath this one.
1273      *
1274      * @param request the <code>ExpressoRequest</code> object.
1275      * @return array of nodes which have relation RelationType
1276      *         .DEST_IS_PART_OF_SRC with our node as "src"
1277      * @throws DBException upon database access error.
1278      */
1279     public Node[] getShallowContainedNodes(final String type, final ExpressoRequest request) throws DBException {
1280         return getRelatedNodes(RelationType.DEST_IS_PART_OF_SRC, type);
1281     }
1282 
1283     /***
1284      * Recursively clone contained tree beneath this node, including this node.
1285      *
1286      * @return cloned node instance.
1287      * @throws DBException upon database access error.
1288      */
1289     public Node cloneTree(final String suffix, final HashMap itemInfo) throws DBException {
1290         // make sure title & suffix are ok
1291         Node queryNode = new Node(getRequestingUser());
1292         queryNode.setDataContext(getDataContext());
1293         queryNode.setNodeTitle(getNodeTitle() + suffix);
1294 
1295         if (queryNode.find()) {
1296             throw new DBException("A node named: '" + queryNode.getNodeTitle() +
1297                     "' already exists.");
1298         }
1299 
1300         Relation[] src = getSrcRelations();
1301 
1302         for (int i = 0; i < src.length; i++) {
1303             Relation relation = src[i];
1304 
1305             // todo need to recurse into testing all
1306             //contained node titles in tree
1307             if (RelationType.DEST_IS_PART_OF_SRC.equals(relation.getRelationTypeName())) {
1308                 Node destNode = relation.getDestNode();
1309                 queryNode.clear();
1310                 queryNode.setNodeTitle(destNode.getNodeTitle() + suffix);
1311 
1312                 if (queryNode.find()) {
1313                     throw new DBException("A node named: '" +
1314                             queryNode.getNodeTitle() + "' already exists.");
1315                 }
1316             }
1317         }
1318 
1319         Relation[] dest = getDestRelations();
1320 
1321         for (int i = 0; i < dest.length; i++) {
1322             Relation relation = dest[i];
1323 
1324             if (RelationType.DEST_CONTAINS_SRC.equals(relation.getRelationTypeName())) {
1325                 Node srcNode = relation.getSrcNode();
1326                 queryNode.clear();
1327                 queryNode.setNodeTitle(srcNode.getNodeTitle() + suffix);
1328 
1329                 if (queryNode.find()) {
1330                     throw new DBException("A node named: '" +
1331                             queryNode.getNodeTitle() + "' already exists.");
1332                 }
1333             }
1334         }
1335 
1336         return cloneAllButRelations(getNodeTitle() + suffix);
1337     }
1338 
1339     /***
1340      * Parse xml to create relations; all nodes from xml are assumed
1341      * already created by
1342      * Node(xml) constructor.
1343      *
1344      * @param allNodesByXML_ID all nodes put in hash with key of the ID
1345      *                         in the import XML, to ease
1346      *                         relationship-building.
1347      * @return a list of nodes which where discovered, and which need processing
1348      * @throws Exception upon DOM or database error.
1349      */
1350     public List parseXMLRelations(final Map allNodesByXML_ID, final boolean isExternalRefRequired) throws Exception {
1351         ArrayList containedNodesNeedingParsing = new ArrayList();
1352         Element root = (Element) getAttribute("root");
1353 
1354         if (root == null) {
1355             return containedNodesNeedingParsing;
1356         }
1357 
1358         List list = root.selectNodes("./" + RELATED);
1359 
1360         for (Iterator iterator = list.iterator(); iterator.hasNext();) {
1361             Element relElem = (Element) iterator.next();
1362             String reltype = relElem.attributeValue(Part.
1363                     NODE_PART_RELATION_TYPE); // can be null
1364             String parttype = relElem.attributeValue(Part.PART_TYPE);
1365 
1366             // sanity check that there is a part of this type
1367             Part part = PartsFactory.getPart(getNodeType(), parttype, reltype);
1368 
1369             if (part == null) {
1370                 throw new DBException("cannot find part/relation: '" +
1371                         parttype + "/" + reltype +
1372                         "' which is expected in node of type: '" + getNodeType() +
1373                         "' because the node being processed, with title: '" +
1374                         getNodeTitle() + "' contains the XML segment: " +
1375                         AbstractDBController.getPrettyXML(relElem));
1376             }
1377 
1378             // get all children, which could be 'inline' nodes, or references
1379             List children = relElem.elements();
1380 
1381             for (Iterator childiter = children.iterator();
1382                  childiter.hasNext();) {
1383                 Element childElem = (Element) childiter.next();
1384 
1385                 // we expect to create a new relation between our
1386                 //parent node and this related node
1387                 Relation rel = new Relation();
1388                 rel.setSrcId(getNodeId());
1389                 rel.setRelationTypeName(part.getNodeRelation());
1390 
1391                 Node destnode = null;
1392                 String order = null;
1393 
1394                 if (REFERENCE.equals(childElem.getName())) { // reference
1395 
1396                     // is this a local node?
1397                     String refid = childElem.attributeValue(Node.REF_ID);
1398                     order = childElem.attributeValue(Relation.RELATION_ORDER);
1399                     destnode = (Node) allNodesByXML_ID.get(refid);
1400 
1401                     if (destnode == null) {
1402                         // external reference;
1403                         if (isExternalRefRequired) {
1404                             // search for match
1405                             Node searchnode = new Node(refid);
1406 
1407                             if (!searchnode.find()) {
1408                                 throw new DBException("Cannot find referenced node ID: " + refid +
1409                                         " neither inside XML nor in this system.");
1410                             }
1411 
1412                             destnode = searchnode;
1413                         } else {
1414                             // just ignore external refs
1415                             if (getLogger().isDebugEnabled()) {
1416                                 getLogger().debug("Ignore external refs.");
1417                             }
1418                         }
1419                     } // handle non local node w/i preparsed allnodes hash
1420                 } else {
1421                     // inline node--we have already parsed basic node,
1422                     // just parse out internal id
1423                     String id = childElem.attributeValue(IDENT_TAG_NAME);
1424                     order = childElem.attributeValue(Relation.RELATION_ORDER);
1425                     destnode = (Node) allNodesByXML_ID.get(id);
1426 
1427                     if (destnode == null) {
1428                         throw new DBException("Cannot find inline node ID: "
1429                                 + id
1430                                 + " which was supposed to be already parsed and "
1431                                 + "waiting in allNodes hash.");
1432                     }
1433 
1434                     containedNodesNeedingParsing.add(destnode);
1435                 }
1436 
1437                 // we may have not found a dest node
1438                 if (destnode != null) {
1439                     // for import final display, add attribute for relation label
1440                     //                    destnode.setAttribute("rel_label",
1441                     //part.getPartLabel());
1442                     // add this relation
1443                     rel.setDestId(destnode.getNodeId());
1444 
1445                     if (!rel.find()) {
1446                         if (order == null) {
1447                             order = "1";
1448                         }
1449 
1450                         rel.setOrder(order);
1451                         rel.add();
1452                     }
1453                 }
1454             }
1455         } // all related tags (shared parts)
1456 
1457         return containedNodesNeedingParsing;
1458     }
1459 
1460     /***
1461      * Parse xml to create relations and attributes; nodes are assumed already
1462      * created by Node(xml) constructor, and allNodes has map of all nodes.
1463      * <p><b>SIDE-EFFECT:</b> removes attribute for xml from this node--we are
1464      * finished with xml, and this is a flag that no more parsing is
1465      * required/allowed
1466      * </p>
1467      *
1468      * @param allNodesByXML_ID     java.util.Map
1469      * @param request              The ExpressoRequest Object
1470      * @param translatePicklistMap map of old ID to new picklist item, for translation
1471      * @throws Exception upon database or DOM error.
1472      */
1473     public void parseXMLAttributes(Map allNodesByXML_ID,
1474                                    ExpressoRequest request,
1475                                    Map translatePicklistMap
1476     ) throws Exception {
1477         Element root = (Element) getAttribute("root");
1478 
1479         if (root == null) {
1480             return;
1481         }
1482 
1483         // owned attributes
1484         Part[] parts = PartsFactory.getParts(getNodeType());
1485 
1486         for (int i = 0; i < parts.length; i++) {
1487             Part part = parts[i];
1488 
1489             if (part.isOwnedAttribute()) {
1490                 // is this attribute present in xml?
1491                 List list = root.selectNodes("./" + part.getPartType());
1492 
1493                 for (Iterator iterator = list.iterator();
1494                      iterator.hasNext();) {
1495                     Element attElem = (Element) iterator.next();
1496                     Attribute attrib = new Attribute();
1497                     attrib.setAttributeType(part.getPartType());
1498                     attrib.setParentNodeType(getNodeType());
1499                     attrib.setParentNodeId(getNodeId());
1500 
1501                     String attcomment = attElem.attributeValue(Attribute.
1502                             ATTRIBUTE_COMMENT);
1503 
1504                     if (attcomment != null) {
1505                         attrib.setAttributeComment(attcomment);
1506                     }
1507 
1508                     String value = attElem.attributeValue(Attribute.
1509                             ATTRIBUTE_VALUE);
1510 
1511                     if (value != null) {
1512                         attrib.setAttributeValue(value);
1513                         if (attrib.hasPicklist() && translatePicklistMap != null) {
1514                             // translate the picklist ID
1515                             PickList item = (PickList) translatePicklistMap.get(value);
1516                             if (item == null) {
1517                                 getLogger().error("Cannot find picklist translation for incoming picklist ID: " + value);
1518                             } else {
1519                                 attrib.setAttributeValue(item.getID());
1520                             }
1521                         }
1522                     }
1523 
1524                     String order = attElem.attributeValue(Attribute.
1525                             ATTRIBUTE_ORDER);
1526 
1527                     if (order == null) {
1528                         order = "1";
1529                     }
1530 
1531                     attrib.setOrder(order);
1532 
1533                     attrib.add(); // need id before parsing for custom handler
1534 
1535                     if (attrib.hasCustomHandler()) {
1536                         attrib.getCustomHandler().parseXML(attElem, attrib,
1537                                 allNodesByXML_ID, (ControllerRequest) request);
1538                     }
1539                 }
1540             }
1541         }
1542 
1543         // we are done with parsing, and remove the attribute
1544         removeAttribute("root");
1545     } // parsexml
1546 
1547     /***
1548      * Get ALL related nodes in tree beneath this node EXCEPT types
1549      * indicated for omission
1550      * recurses into tree; side-effect: adds attribute 'level' with node
1551      * level w/i tree to each node.
1552      * <p/>
1553      * Uses optional INDENT attribute to determine formatting indentation
1554      * </p>
1555      *
1556      * @param request   The ExpressoRequest object.
1557      * @param outputMap which will end up containing all nodes in tree;
1558      *                  hand in empty map to begin with
1559      * @throws DBException upon database access error.
1560      * @see #INDENT
1561      */
1562     public void getNodesInStronglyRelatedTree(ExpressoRequest request, HashMap outputMap) throws DBException {
1563         getNodesInStronglyRelatedTree(outputMap);
1564     }
1565 
1566     /***
1567      * Get ALL related nodes in tree beneath this node EXCEPT types indicated
1568      * for omission
1569      * recurses into tree; side-effect: adds attribute 'level' with node level
1570      * w/i tree to each node.
1571      * <p/>
1572      * uses optional INDENT attribute to determine formatting indentation
1573      * </p>
1574      *
1575      * @param outputMap which will end up containing all nodes in tree;
1576      *                  hand in empty map to begin with
1577      * @throws DBException upon database access error.
1578      * @see #INDENT
1579      */
1580     public void getNodesInStronglyRelatedTree(Map outputMap) throws DBException {
1581         TreeSelectionFactory selectionFactory = new TreeSelectionFactory(this);
1582         outputMap.putAll(selectionFactory.getNodesInStronglyRelatedTree());
1583     }
1584 
1585     /***
1586      * Get all parts within this node.
1587      *
1588      * @return An array of Parts for this node.
1589      * @throws DBException upon database error.
1590      */
1591     public Part[] getParts() throws DBException {
1592         return PartsFactory.getParts(getNodeType());
1593     }
1594 
1595     /***
1596      * Set recent editor and modification date.  To save bandwidth and
1597      * ease, this function will not update if all the conditions are true.
1598      * <ol>
1599      * <li>The current user is the same as the last editor </li>
1600      * <li>Less then a second has passed since the last update.</li>
1601      * </ol>
1602      *
1603      * @throws DBException upon database error.
1604      */
1605     public void touch() throws DBException {
1606         //If last editor has not changed.
1607         if (RequestRegistry.getUser().getLoginName().equals(getRecentEditor())) {
1608             Date now = new Date();
1609             Date lastTouched = this.getFieldDate(NODE_MODIFIED);
1610 
1611             //And the update time is less than 1000 milliseconds
1612             if (lastTouched != null && Math.abs(now.getTime() - lastTouched.getTime()) < 1000) {
1613 
1614                 //Then do not touch.
1615                 return;
1616             }
1617         }
1618 
1619         setRecentEditor(RequestRegistry.getUser().getLoginName());
1620         update(true); // will touch mod date
1621     }
1622 
1623     /***
1624      * provide a transition for viewing this object, suitable for creating an
1625      * HTTP link
1626      *
1627      * @return transtion for viewing, including label for name of object; never null
1628      */
1629     public Transition getViewTrans() throws DBException {
1630         Transition trans = new Transition(getNodeTitle(), AddNodeAction.class, AddNodeAction.VIEW_NODE);
1631         trans.addParam(Node.NODE_ID, getNodeId());
1632         return trans;
1633     }
1634 
1635     public void acceptVisitor(ModelVisitor visitor) {
1636         visitor.visitNode(this);
1637     }
1638 
1639 
1640     public String getNodeTitleRaw() throws DBException {
1641         Filter old = setFilterClass(new RawFilter());
1642         String raw = getNodeTitle();
1643         setFilterClass(old);
1644 
1645         return raw;
1646     }
1647 
1648     /***
1649      * Find all properly-typed nodes related to this source node, with
1650      * relation of given type.
1651      *
1652      * @param relationType               the relationship type
1653      * @param targetNodeType             nodes of this type (only) will be returned
1654      * @param toPopulateOldRemainingList (returned param) list to receive ordered list of
1655      *                                   MultiDBObjects; pass in null if you don't want this list added
1656      * @return hash with key = node Id, value = node
1657      * @throws DBException upon error.
1658      */
1659     public Map getRelatedNodesHash(String relationType,
1660                                    String targetNodeType,
1661                                    List toPopulateOldRemainingList) throws DBException {
1662 
1663         List list = this.getRawRelated(relationType, targetNodeType);
1664 
1665         if (toPopulateOldRemainingList != null) {
1666             toPopulateOldRemainingList.addAll(list);
1667         }
1668 
1669         Hashtable alreadySelected = new Hashtable(list.size());
1670 
1671         for (int i = 0; i < list.size(); i++) {
1672             MultiDBObject aMultiObject = (MultiDBObject) list.get(i);
1673             String nodeId = aMultiObject.getField(Node.NODE_JOIN,
1674                     Node.NODE_ID);
1675             alreadySelected.put(nodeId, aMultiObject);
1676         }
1677 
1678         return alreadySelected;
1679     }
1680 
1681     /***
1682      * Updates the specified node relation to reflect the new values specified
1683      * in the targetNodeIds.
1684      *
1685      * @param targetNodeType String
1686      * @param relationType   String
1687      * @param targetNodeIds  String[]
1688      * @throws DBException
1689      */
1690     public void updateNodeRelations(String targetNodeType, String relationType, String[] targetNodeIds) throws
1691             DBException {
1692 
1693         List added = new ArrayList();
1694         List oldRemainingList = new ArrayList();
1695         Map toRemoveHash = getRelatedNodesHash(relationType,
1696                 targetNodeType, oldRemainingList); // we will remove items from hash if they remain checked
1697 
1698         //
1699         //Then add.
1700         //
1701         for (int k = 0; (targetNodeIds != null) && (k < targetNodeIds.length); k++) {
1702             String nodeId = targetNodeIds[k];
1703             if (toRemoveHash.get(nodeId) != null) {
1704                 // no need to set this relation--it is already set
1705                 // remove it from our list so that we'll know the items that
1706                 // were relatedBefore but now are NOT related
1707                 toRemoveHash.remove(nodeId);
1708 
1709                 continue;
1710             }
1711 
1712             // new item to add
1713             Relation relation = new Relation(RequestRegistry.getUser());
1714             relation.setField(Relation.RELATION_SRC, this.getNodeId());
1715             relation.setField(Relation.RELATION_DEST, nodeId);
1716             relation.setField(Relation.RELATION_TYPE, relationType);
1717             relation.setOrder(oldRemainingList.size() + 1 + added.size());
1718             relation.add();
1719 
1720             added.add(relation);
1721         }
1722 
1723         //Now prune
1724         // anything left in toRemoveHash should be removed
1725         for (Iterator enumeration = toRemoveHash.values().iterator();
1726              enumeration.hasNext();) {
1727             MultiDBObject join = (MultiDBObject) enumeration.next();
1728             String destId = join.getField(Node.RELATION_JOIN,
1729                     Relation.RELATION_DEST);
1730             Relation relation = new Relation();
1731             relation.setField(Relation.RELATION_SRC, this.getNodeId());
1732             relation.setField(Relation.RELATION_DEST, destId);
1733             relation.setField(Relation.RELATION_TYPE, relationType);
1734             relation.delete();
1735         }
1736     }
1737 
1738     /***
1739      * get all nodes that have this node as 'src' for some strong relation
1740      *
1741      * @return array, never null
1742      */
1743     public Node[] getStronglyRelatedNodes() throws DBException {
1744         MultiDBObject joinObject = new MultiDBObject();
1745         joinObject.setDataContext(getDataContext());
1746 
1747         // we join a relation
1748         joinObject.addDBObj(Relation.class.getName(), RELATION_JOIN);
1749 
1750         // and a node
1751         joinObject.addDBObj(Node.class.getName(), NODE_JOIN);
1752 
1753         // and a relation type
1754         joinObject.addDBObj(RelationType.class.getName(), RELATION_TYPE_JOIN);
1755 
1756         // and make sure the relation is for this node as source
1757         joinObject.setForeignKey(RELATION_JOIN, Relation.RELATION_DEST, NODE_JOIN, Node.NODE_ID);
1758         joinObject.setField(RELATION_JOIN, Relation.RELATION_SRC, getNodeId());
1759 
1760         // relation type must be strong
1761         joinObject.setForeignKey(RELATION_TYPE_JOIN, RelationType.RELATION_TYPE_NAME, RELATION_JOIN, Relation.RELATION_TYPE);
1762         joinObject.setField(RELATION_TYPE_JOIN, RelationType.RELATION_STRENGTH, RelationType.STRONG_RELATION);
1763 
1764         // sort by node title
1765         List list = joinObject.searchAndRetrieveList(Relation.RELATION_ORDER);
1766 
1767         ArrayList resultList = new ArrayList();
1768 
1769         for (Iterator iterator = list.iterator(); iterator.hasNext();) {
1770             MultiDBObject aMultiObject = (MultiDBObject) iterator.next();
1771             Node node = (Node) aMultiObject.getDBObject(NODE_JOIN);
1772             resultList.add(node);
1773         }
1774 
1775         return (Node[]) resultList.toArray(new Node[resultList.size()]);
1776     }
1777 }