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 java.util.ArrayList;
13  import java.util.Collections;
14  import java.util.Iterator;
15  import java.util.List;
16  import java.util.Map;
17  import com.jcorporate.expresso.core.controller.ControllerRequest;
18  import com.jcorporate.expresso.core.controller.ExpressoRequest;
19  import com.jcorporate.expresso.core.controller.Transition;
20  import com.jcorporate.expresso.core.db.DBConnection;
21  import com.jcorporate.expresso.core.db.DBConnectionPool;
22  import com.jcorporate.expresso.core.db.DBException;
23  import com.jcorporate.expresso.core.dbobj.DBField;
24  import com.jcorporate.expresso.core.dbobj.SecurDBObject;
25  import com.jcorporate.expresso.core.dbobj.SecuredDBObject;
26  import com.jcorporate.expresso.core.dbobj.ValidValue;
27  import com.jcorporate.expresso.core.misc.StringUtil;
28  import com.jcorporate.expresso.core.registry.RequestRegistry;
29  import com.jcorporate.expresso.core.security.SuperUser;
30  import com.jcorporate.expresso.core.security.User;
31  import com.jcorporate.expresso.core.security.filters.Filter;
32  import com.jcorporate.expresso.core.security.filters.RawFilter;
33  import com.sri.common.controller.AbstractDBController;
34  import com.sri.emo.controller.NodeAction;
35  import com.sri.emo.controller.PartAction;
36  import com.sri.emo.dbobj.model_tree.ModelVisitable;
37  import com.sri.emo.dbobj.model_tree.ModelVisitor;
38  
39  
40  /***
41   * A part of an "entity" (a Node object). Our modeling environment allows dynamic building
42   * of objects.  Parts have a complex typing system because the "type"
43   * field is overloaded for two kinds of parts: owned attributes and shared nodes.
44   * <p/>
45   * For owned attributes, the Type field is a simple string, unique within
46   * the parent object.
47   * </p>
48   * <p/>
49   * For shared nodes, the Type field is the nodeType of the shared nodes. However,
50   * multiple parts within a single entity can be of &quot;shared node&quot; type AND share
51   * the same kind of node. For example, Is a kind of/These are kinds of me -- these
52   * two part labels apply to the same kind of shared node, so their part types
53   * are the same. Parts which are shared-node parts are distinguished (uniquely)
54   * by a combination of part type, relation type, and parent type.
55   * </p>
56   *
57   * @author larry hamel
58   */
59  public class Part extends SecurDBObject implements Comparable, ModelVisitable {
60      /***
61  	 * 
62  	 */
63  	private static final long serialVersionUID = 1L;
64  	public static final String PART_TYPE_TABLE_NAME = "part2";
65      public static final String KEEP_DETAILS = "keepDetails";
66  
67      public static final String DELIMITER = "_x_";
68  
69      /***
70       * field names
71       */
72      public static final String PART_ID = "PART_ID";
73      public static final String PARENT_TYPE = "PARENT_TYPE";
74      public static final String PART_NUM = "PART_NUM";
75      public static final String PART_LABEL = "PART_LABEL";
76      public static final String SPECIAL_HANDLER = "SPECIAL_HANDLER";
77      public static final String PART_TYPE = "PART_TYPE";
78      public static final String NODE_PART_RELATION_TYPE = RelationType.RELATION_TYPE_NAME;
79      public static final String CARDINALITY = "CARDINALITY";
80      public static final String PART_DESCRIP = "PART_DESCRIP";
81      public static final String PART_IS_ATTRIB = "PART_IS_ATTRIB";
82  
83      /***
84       * attribute limitation constants (could be in other "def" table)
85       */
86      public static final String MULTIPLE_ALLOWED = "MULTIPLE_ALLOWED";
87      public static final String SINGLE_ALLOWED = "SINGLE_ALLOWED";
88      public static final String SUMMARY_SINGLE_ALLOWED = "SUMMARY_SINGLE_ALLOWED";
89      public static final String PICKLIST_SINGLE_ALLOWED = "PICKLIST_SINGLE_ALLOWED";
90      public static final String NOT_SPECIFIED_ID = "NOT_SPECIFIED_ID";
91      public static final String[][] CARDINALITY_CHOICES = {{NOT_SPECIFIED_ID, PickList.NOT_SPECIFIED_DISPLAY},
92              {MULTIPLE_ALLOWED, "Multiple attributes allowed"}, {SINGLE_ALLOWED, "Single attribute only"},
93              {SUMMARY_SINGLE_ALLOWED, "Summary single attribute"}, {PICKLIST_SINGLE_ALLOWED, "Picklist single attribute"},
94      };
95      public static final String[] SINGLE_CARDINALITY_CHOICES = {
96              SINGLE_ALLOWED, SUMMARY_SINGLE_ALLOWED, PICKLIST_SINGLE_ALLOWED,
97      };
98      private String REFERENCES_DEFINITION = " are notes about relevant items, such as academic articles.";
99      private String ONLINE_RESOURCES_DEFINITION = " are relevant items that can be found online (URLs).";
100     public static final String PART_DISPLAY_NAME = "PART_DISPLAY_NAME";
101     public static final String PART_HELP_STRING = "PART_HELP_STRING";
102     public static final String REL_REF = "REL_REF";
103     public static final String SRC_NODE_REF = "SRC_NODE_REF";
104     public static final String DEST_NODE_REF = "DEST_NODE_REF";
105 
106     /***
107      * data member, caching value
108      */
109     private transient IPartHandler mSpecialHandler = null;
110     private transient String mSpecialHandlerStr = null;
111 
112     /***
113      * Default constructor for <code>Part</code>
114      * creates a new object of this type with no connection
115      * yet allocated.
116      *
117      * @throws DBException If the new object cannot be
118      *                     created
119      */
120     public Part() throws DBException {
121     }
122 
123     /***
124      * @param request ControllerRequest
125      * @throws DBException
126      * @deprecated
127      */
128     public Part(ControllerRequest request) throws DBException {
129         super(request);
130     }
131 
132     public Part(SecuredDBObject obj) throws DBException {
133         super();
134         setRequestingUser(this.getRequestingUser());
135         setDataContext(obj.getDataContext());
136     }
137 
138     /***
139      * Defines the database table name and fields for this DB object
140      *
141      * @throws DBException if the operation cannot be performed
142      */
143     protected synchronized void setupFields() throws DBException {
144         setTargetTable(PART_TYPE_TABLE_NAME);
145         setDescription("Parts of Objects");
146         addField(PART_ID, DBField.AUTOINC_TYPE, 0, false, "Primary key id");
147         addField(PARENT_TYPE, DBField.VARCHAR_TYPE, 100, false,
148                 "Parent node type");
149         addField(PART_NUM, DBField.INT_TYPE, 0, false,
150                 "Order of part within parent");
151         addField(PART_LABEL, DBField.VARCHAR_TYPE, 255, false, "Part Label");
152         addField(PART_TYPE, DBField.VARCHAR_TYPE, 100, false,
153                 "part type--either node type (if node part), or attribute key (if attribute part");
154         addField(PART_IS_ATTRIB, DBField.BOOLEAN_TYPE, 0, false,
155                 "true if part is attribute (otherwise, it is a node)");
156 
157         // this field stores any limits (e.g. single/multiple attrib's allowed) for attributes
158         addField(CARDINALITY, DBField.VARCHAR_TYPE, 100, true,
159                 "Attribute limits");
160         addField(NODE_PART_RELATION_TYPE, DBField.VARCHAR_TYPE, 100, true,
161                 "Relation of part to parent (all node parts fill this field, AND attributes which are summaries)");
162         addField(SPECIAL_HANDLER, DBField.VARCHAR_TYPE, 255, true,
163                 "Class name of handler class if any");
164         addField(PART_DESCRIP, DBField.LONGVARCHAR_TYPE, 0, true,
165                 "Description of this part");
166 
167         addKey(PART_ID);
168 
169         addIndex("parent_idx", PARENT_TYPE, false);
170 
171         // unique index; would be PK but it is convenient to have an ID for changing these fields
172         addIndex("parent_parttype_idx",
173                 PARENT_TYPE + "," + PART_TYPE + "," + NODE_PART_RELATION_TYPE, true);
174     }
175     /* setupFields() */
176 
177     public void setParentType(String type) throws DBException {
178         setField(PARENT_TYPE, type);
179     }
180 
181     public String getPartLabel() throws DBException {
182         return getField(PART_LABEL);
183     }
184 
185     public String getPartType() throws DBException {
186         return getField(PART_TYPE);
187     }
188 
189     public boolean isOwnedAttribute() throws DBException {
190         // node parts have relationships; this is null for attribute
191         return getFieldBoolean(PART_IS_ATTRIB);
192     }
193 
194     public String getPartNum() throws DBException {
195         return getField(PART_NUM);
196     }
197 
198     public String getSpecialHandlerName() throws DBException {
199         return getField(SPECIAL_HANDLER);
200     }
201 
202     public String getCardinality() throws DBException {
203         return getField(CARDINALITY);
204     }
205 
206     public String getPartDescription() throws DBException {
207         return getField(PART_DESCRIP);
208     }
209 
210     public void setPartType(String partType) throws DBException {
211         setField(PART_TYPE, partType);
212     }
213 
214     public String getParentType() throws DBException {
215         return getField(PARENT_TYPE);
216     }
217 
218     /***
219      * Return code for node relation type.
220      *
221      * @return String, the node relation type.
222      * @throws DBException upon database exception error.
223      */
224     public String getNodeRelation() throws DBException {
225         return getField(NODE_PART_RELATION_TYPE);
226     }
227 
228     public void setNodeRelation(String nodeRelation) throws DBException {
229         setField(NODE_PART_RELATION_TYPE, nodeRelation);
230     }
231 
232     public NodeType getParentEntity() throws DBException {
233         NodeType parent = new NodeType();
234         parent.setRequestingUser(this.getRequestingUser());
235         parent.setDBName(getDataContext());
236         parent.setEntityName(getParentType());
237 
238         // cannot use retrieve because it expects primary key set
239         if (!parent.find()) {
240             throw new DBException("cannot find NodeType with name: " +
241                     getPartType());
242         }
243 
244         return parent;
245     }
246 
247     /***
248      * for definition page,
249      *
250      * @param isHTML signals whether to use HTML tags, or if false, to use Docbook tags
251      * @return true The definition, in Docbook or Html text
252      * @throws DBException upon database exception error.
253      */
254     public String getDefinition(boolean isHTML) throws DBException {
255         String result = getPartDescription();
256 
257         if (!isOwnedAttribute() && (result.length() == 0)) {
258             // is this node association with same type node as parent?
259             if (getParentType().equals(getPartType())) {
260                 // use relation definition
261                 RelationType relType = new RelationType();
262                 relType.setRequestingUser(this.getRequestingUser());
263                 relType.setDataContext(getDataContext());
264                 relType.setRelType(getNodeRelation());
265                 relType.retrieve();
266 
267                 Filter old = null;
268 
269                 if (!isHTML) {
270                     old = relType.setFilterClass(new RawFilter());
271                 }
272 
273                 result = relType.getRelTypeDescrip();
274 
275                 if (!isHTML) {
276                     relType.setFilterClass(old);
277                 }
278             } else {
279                 NodeType type = new NodeType(getRequestingUser());
280                 type.setEntityName(getPartType());
281 
282                 // cannot use retrieve because it expects primary key set
283                 if (!type.find()) {
284                     getLogger().error("cannot find NodeType with name: " +
285                             getPartType());
286                     result = "(unknown type)";
287                 }
288 
289                 if (isHTML) {
290                     result = " are associations with (potentially shared) objects of type: <a href=\"#" +
291                             getPartType() + "\">" + type.getDisplayName() + "</a>.";
292                 } else {
293                     // docbook
294                     result = " are associations with (potentially shared) objects of type: " +
295                             type.getDisplayName();
296                 }
297             }
298         } else if (AbstractParts.REFERENCES.equals(getPartType())) {
299             result = REFERENCES_DEFINITION;
300         } else if (AbstractParts.ONLINE_RESOURCE.equals(getPartType())) {
301             result = ONLINE_RESOURCES_DEFINITION;
302         }
303 
304         return result;
305     }
306 
307     public void setId(String id) throws DBException {
308         setField(PART_ID, id);
309     }
310 
311     public String getId() throws DBException {
312         return getField(PART_ID);
313     }
314 
315     public void setPartNum(int i) throws DBException {
316         if (i < 0) {
317             i = 0;
318         }
319         setField(PART_NUM, i);
320     }
321 
322     /***
323      * @return true if this attribute has a picklist
324      * @throws DBException upon database exception error.
325      */
326     public boolean hasPicklist() throws DBException {
327         return isOwnedAttribute() &&
328                 Part.PICKLIST_SINGLE_ALLOWED.equals(getCardinality());
329     }
330 
331     public boolean isHaveCustomHandler() throws DBException {
332         return getCustomHandler() != null;
333     }
334 
335     /***
336      * @return the custom editting handler or null if this part does not have one
337      * @throws DBException upon database exception error.
338      */
339     public IPartHandler getCustomHandler() throws DBException {
340         // mSpecialHandlerStr is the flag for cached value; first
341         // access will set cache value to "" or classname
342         if (mSpecialHandlerStr == null) {
343             mSpecialHandlerStr = getField(Part.SPECIAL_HANDLER);
344 
345             if ((mSpecialHandlerStr != null) &&
346                     (mSpecialHandlerStr.length() > 0)) {
347                 try {
348                     Class myclass = Class.forName(mSpecialHandlerStr);
349                     mSpecialHandler = (IPartHandler) myclass.newInstance();
350                 } catch (Exception e) {
351                     throw new DBException(e);
352                 }
353             }
354         }
355 
356         IPartHandler result = mSpecialHandler;
357         if (result != null) {
358             try {
359                 result = (IPartHandler) mSpecialHandler.clone();
360             } catch (CloneNotSupportedException e) {
361                 throw new DBException("cannot clone: ", e);
362             }
363         }
364 
365         return result;
366     }
367 
368     public void setCustomHandler(String customHandler) throws DBException {
369         setField(Part.SPECIAL_HANDLER, customHandler);
370     }
371 
372     /***
373      * Return display title of node type of parent node, or empty string "" if not found.
374      *
375      * @param requestingUid the requesting uid.
376      * @return The display title.
377      */
378     public String getDisplayTitleOfParentNodeType(int requestingUid) {
379         String result = "";
380 
381         try {
382             NodeType nodeType = new NodeType();
383             nodeType.setRequestingUser(User.getUserFromId(requestingUid, getDataContext()));
384             nodeType.setField(NodeType.NODE_TYPE_NAME, getParentType());
385 
386             if (!nodeType.find()) {
387                 throw new DBException("cannot find node type: " +
388                         getParentType());
389             }
390 
391             result = nodeType.getField(NodeType.DISPLAY_TITLE);
392 
393             if (result == null) {
394                 result = "";
395             }
396         } catch (DBException e) {
397             e.printStackTrace();
398         }
399 
400         return result;
401     }
402 
403     /***
404      * @param request the ControllerRequest for parameters.
405      * @return array of values in picklist, with each row containing ID, display; null if attribute has no picklist
406      * @throws DBException upon database exception error.
407      */
408     public String[][] getPicklistArray(ExpressoRequest request) throws DBException {
409         if (!hasPicklist()) {
410             return null;
411         }
412 
413         List found = getPicklist(request);
414 
415         if (found == null) {
416             return new String[0][2];
417         }
418 
419         // sort list alphabetically (picklist items implement Comparable)
420         if (found.size() > 0) {
421             Collections.sort(found);
422         }
423 
424         // convert to array
425         String[][] result = new String[found.size()][2];
426         int i = 0;
427 
428         for (Iterator iter = found.iterator(); iter.hasNext();) {
429             PickList pickList = (PickList) iter.next();
430             result[i][0] = pickList.getField(PickList.LIST_ID);
431             result[i][1] = pickList.getField(PickList.DISPLAY_IN_PICKLIST);
432             i++;
433         }
434 
435         return result;
436     }
437 
438     public List getPicklist(ExpressoRequest request) throws DBException {
439 
440         return getPickList(request.getDataContext(), RequestRegistry.getUser().getUid());
441     }
442 
443     public List getPickList(String dataContext, int uid) throws DBException {
444         if (!hasPicklist()) {
445             return null;
446         }
447 
448         PickList listItem = new PickList(getParentType(), getPartType());
449         listItem.setRequestingUser(User.getUserFromId(uid, dataContext));
450 
451         return (List) listItem.searchAndRetrieveList(PickList.LIST_ID);
452     }
453 
454     /***
455      * @return list of PickList items; never null
456      */
457     public List getPickList() throws DBException {
458         if (!hasPicklist()) {
459             return null;
460         }
461         //ataContext, getParentType(), getPartType()
462         PickList listItem = new PickList();
463         listItem.setNodeType(getParentType());
464         listItem.setPickAttribType(getPartType());
465 
466         return (List) listItem.searchAndRetrieveList(PickList.LIST_ID);
467     }
468 
469     public ValidValue[] getPickListValidValues() throws DBException {
470         if (!hasPicklist()) {
471             return new ValidValue[0];
472         }
473 
474         List list = this.getPickList();
475 
476         ValidValue[] returnValue = new ValidValue[list.size()];
477         for (int i = 0; i < list.size(); i++) {
478             PickList oneItem = (PickList) list.get(i);
479             returnValue[i] = new ValidValue(oneItem.getField(PickList.LIST_ID),
480                     oneItem.getField(PickList.DISPLAY_IN_PICKLIST));
481         }
482 
483         return returnValue;
484     }
485 
486 
487     /***
488      * @return true if this attribute is a summary
489      * @throws DBException upon database exception error.
490      */
491     public boolean isSummary() throws DBException {
492         return isOwnedAttribute() &&
493                 Part.SUMMARY_SINGLE_ALLOWED.equals(getCardinality());
494     }
495 
496     /***
497      * @return true if this attribute allows multiple entries
498      * @throws DBException upon database exception error.
499      */
500     public boolean areMultipleAttributesAllowed() throws DBException {
501         return Part.MULTIPLE_ALLOWED.equals(getCardinality());
502     }
503 
504     public int numDisplayLines() throws DBException {
505         int numLines = AbstractDBController.SINGLE_TEXTAREA_NUM_LINES; //default for multiple
506 
507         if (areMultipleAttributesAllowed()) {
508             numLines = AbstractDBController.MULTIPLE_TEXTAREA_NUM_LINES;
509         }
510 
511         return numLines;
512     }
513 
514     public void refreshSpecialHandlerCache() {
515         mSpecialHandlerStr = null; // signal to refresh cache next access
516     }
517 
518     /***
519      * Delete a part;
520      * deletes all attribs or relations associated with this part, unless
521      * the part has a KEEP_DETAILS attribute (an expresso attribute associated with the dbobject)
522      *
523      * @throws DBException if delete is not allowed for the current user
524      * @see #setAttribute
525      * @see #KEEP_DETAILS
526      */
527     public synchronized void delete() throws DBException {
528         // delete any attributes or relations unless there is a flag to keep 'em
529         if (!isRowAllowed(SecuredDBObject.DELETE)) {
530             return; // will throw anyway
531         }
532 
533         if (getAttribute(KEEP_DETAILS) == null) {
534             if (isOwnedAttribute()) {
535                 // use superuser privileges because if we own model, we own attribs
536                 Attribute attribs = new Attribute(SuperUser.INSTANCE);
537                 attribs.setDataContext(getDataContext());
538                 attribs.setParentNodeType(getParentType());
539                 attribs.setAttributeType(getPartType());
540                 attribs.deleteAll();
541             } else {
542                 deleteRelations();
543             }
544         }
545 
546         super.delete();
547 
548         // tell cache to refresh
549         PartsFactory.refreshCache(getParentType());
550         getParentEntity().incrementVersion();
551     }
552     /* delete() */
553 
554     /***
555      * override to use permissions of parent node type
556      *
557      * @param requestedFunction code for function -- Add, Update, Delete, Search (read)
558      * @return true if this function is allowed for this requesting user
559      * @throws SecurityException (unchecked) if not allowed
560      * @throws com.jcorporate.expresso.core.db.DBException
561      *                           for other data-related errors.
562      */
563     public boolean isRowAllowed(String requestedFunction) throws DBException {
564         if (User.isAdmin(getRequestingUid()) || SuperUser.SYSTEM_UID == getRequestingUid()) {
565             return true;
566         }
567         NodeType nodeType = this.getParentEntity();
568         return nodeType.isRowAllowed(requestedFunction);
569     }
570 
571     /***
572      * See if the current user of this DB object is allowed to perform the
573      * requested function, given the function's code.
574      *
575      * @param requestedFunction The code of the requested function. The codes are:
576      *                          <ol><li>A: Add<li>
577      *                          <li>S: Search<li>
578      *                          <li>U: Update<li>
579      *                          <li>D: Delete<li>
580      *                          </ol>
581      * @throws com.jcorporate.expresso.core.db.DBException
582      *                           If the requested operation is not permitted to this user
583      * @throws SecurityException if the user is not allowed access to the object.
584      */
585     public void isAllowed(String requestedFunction) throws SecurityException, DBException {
586         // if we are retrieving, allow always
587         if (SEARCH.equals(requestedFunction)) {
588             return;
589         }
590 
591         if (getRequestingUser() == SuperUser.INSTANCE) {
592             return;
593         }
594 
595         if (getRequestingUser().isAdmin()) {
596             return;
597         }
598 
599         if (getPartType().length() == 0) {
600             // no parent to judge from, so we allow
601             return;
602         }
603         NodeType nodeType = this.getParentEntity();
604         nodeType.isAllowed(requestedFunction);
605     }
606     /* isAllowed(String) */
607 
608     /***
609      * add cache maintenance
610      *
611      * @throws DBException upon database exception error.
612      */
613     public synchronized void update() throws DBException {
614         Part oldPart = new Part();
615         oldPart.setRequestingUser(getRequestingUser());
616         oldPart.setDataContext(getDataContext());
617         oldPart.setId(getId());
618         oldPart.retrieve();
619 
620         super.update();
621         refreshSpecialHandlerCache();
622         PartsFactory.refreshCache(getParentType());
623 
624         getParentEntity().incrementVersion();
625 
626         // todo NEED TRANSACTION
627         // handle relation name-change
628         if ((oldPart.getNodeRelation().length() > 0) &&
629                 !oldPart.getNodeRelation().equals(getNodeRelation())) {
630             changeRelations(oldPart.getNodeRelation());
631         }
632 
633         // handle change in type
634         if (!oldPart.getPartType().equals(getPartType())) {
635             // must rename all existing records
636             if (oldPart.isOwnedAttribute()) {
637                 Attribute attribs = new Attribute();
638                 attribs.setRequestingUser(getRequestingUser());
639                 attribs.setParentNodeType(getParentType());
640                 attribs.setAttributeType(getPartType());
641 
642                 List all = attribs.searchAndRetrieveList();
643 
644                 for (Iterator iter = all.iterator(); iter.hasNext();) {
645                     Attribute attribute = (Attribute) iter.next();
646                     attribute.setAttributeType(getPartType());
647                     attribute.update();
648                 }
649             } else {
650                 // we just delete all relations if the type changes, since we cannot
651                 // use the old related nodes of the old type
652                 deleteRelations();
653             }
654         }
655     }
656     /*  update() */
657 
658     /***
659      * we override not to check permissions (which is done at the table level
660      * by superclass) but rather to add default permissions
661      *
662      * @throws DBException upon database exception error.
663      */
664     public synchronized void add() throws DBException {
665         NodeType parent = getParentEntity();
666         if (!parent.canRequesterWrite()) {
667             throw new DBException("User " + User.getLoginFromId(this.getRequestingUid())
668                     + " does not have permission to change model: "
669                     + parent.getEntityName());
670         }
671         super.add();
672 
673         getParentEntity().incrementVersion();
674 
675         // tell cache to refresh
676         PartsFactory.refreshCache(getParentType());
677     }
678     /* add() */
679 
680     public boolean isSharedNodeAttrib() throws DBException {
681         return !isOwnedAttribute();
682     }
683 
684     /***
685      * @param nodeRelation the relationship to be queried; useful for getting old node relations for updating; returned Relation object only have primary key set--the relations are from a 'join', not retrieved from DB
686      * @return list of Relations specified by this part;
687      * @throws DBException upon database exception error.
688      */
689     private List getRelations(String nodeRelation) throws DBException {
690         if (isOwnedAttribute()) {
691             // no relations for owned attribs
692             return new ArrayList();
693         }
694 
695         String sql = "SELECT relation.* " +
696                 " FROM node as src, node as dest, relation " +
697                 " WHERE   src.NODE_TYPE LIKE '" + getParentType() + "' " +
698                 " AND     dest.NODE_TYPE LIKE '" + getPartType() + "' " +
699                 " AND relation.RELATION_TYPE LIKE '" + nodeRelation + "' " +
700                 " AND relation.RELATION_SRC = src.NODE_ID " +
701                 " AND relation.RELATION_DEST = dest.NODE_ID ";
702 
703         ArrayList relations = new ArrayList();
704         DBConnection conn = DBConnectionPool.getInstance(getDataContext())
705                 .getConnection();
706 
707         try {
708             conn.execute(sql);
709 
710             while (conn.next()) {
711                 Relation rel = new Relation();
712                 rel.setRequestingUser(getRequestingUser());
713                 rel.setDataContext(getDataContext());
714                 rel.setSrcId(conn.getString(Relation.RELATION_SRC));
715                 rel.setDestId(conn.getString(Relation.RELATION_DEST));
716                 rel.setRelationTypeName(conn.getString(Relation.RELATION_TYPE));
717 
718                 if (conn.getString(Relation.RELATION_ORDER) != null) {
719                     rel.setOrder(conn.getString(Relation.RELATION_ORDER));
720                 }
721 
722                 relations.add(rel);
723             }
724         } finally {
725             conn.release();
726         }
727 
728         return relations;
729 
730         // does NOT work because of node table used twice
731         //        MultiDBObject joinObject = new MultiDBObject();
732         //        joinObject.setDBName(getDataContext());
733         //        joinObject.addDBObj(Relation.class.getName(), REL_REF);
734         //        joinObject.addDBObj(Node.class.getName(), SRC_NODE_REF);
735         //        joinObject.addDBObj(Node.class.getName(), DEST_NODE_REF);
736         //        joinObject.setForeignKey(
737         //                REL_REF, Relation.RELATION_SRC, SRC_NODE_REF, Node.NODE_ID);
738         //        joinObject.setForeignKey(
739         //                REL_REF, Relation.RELATION_DEST, DEST_NODE_REF, Node.NODE_ID);
740         //        joinObject.setField(REL_REF, Relation.RELATION_TYPE, nodeRelation);
741         //        joinObject.setField(SRC_NODE_REF, Node.NODE_TYPE, getParentType());
742         //        joinObject.setField(DEST_NODE_REF, Node.NODE_TYPE, getPartType());
743         //
744         //        return joinObject.searchAndRetrieveList();
745     }
746 
747     /***
748      * @return list of Relations specified by this part; Relation object retrieved from DB via custom SQL
749      * @throws DBException upon database exception error.
750      */
751     public List getRelations() throws DBException {
752         return getRelations(getNodeRelation());
753     }
754 
755     /***
756      * Delete all relations associated with this part.
757      *
758      * @throws DBException upon database exception error.
759      */
760     public void deleteRelations() throws DBException {
761         List list = getRelations();
762 
763         for (Iterator iterator = list.iterator(); iterator.hasNext();) {
764             Relation aRelation = (Relation) iterator.next();
765 
766             getLogger().debug("deleting Relation src/type: " +
767                     aRelation.getSrcNode().getNodeTitle() + "/" +
768                     aRelation.getRelationTypeName());
769 
770             aRelation.delete();
771         }
772     }
773 
774     /***
775      * change all relations associated with this part
776      * IFF the nodeRelation changed
777      *
778      * @param oldNodeRelation the old relation; assumes new relation is in "this"
779      * @throws DBException upon database exception error.
780      */
781     public void changeRelations(String oldNodeRelation) throws DBException {
782         // no work if equal
783         if (oldNodeRelation.equals(getNodeRelation())) {
784             return;
785         }
786 
787         List list = getRelations(oldNodeRelation);
788 
789         for (Iterator iterator = list.iterator(); iterator.hasNext();) {
790             Relation relation = (Relation) iterator.next();
791 
792             // instead of doing update, do delete & add, so
793             // that Relation handles inverses for us
794             relation.delete();
795 
796             relation.setRelationTypeName(getNodeRelation());
797             relation.add();
798 
799             getLogger().debug("changed Relation src/oldtype/newtype: " +
800                     relation.getSrcNode().getNodeTitle() + "/" + oldNodeRelation +
801                     "/" + relation.getRelationTypeName());
802         }
803     }
804 
805     public int getPartNumInt() throws DBException {
806         return Integer.parseInt(getPartNum());
807     }
808 
809     public void setPartLabel(String s) throws DBException {
810         setField(PART_LABEL, s);
811     }
812 
813     /***
814      * @param card cardinality type; see constants in this file
815      * @throws DBException upon database exception error.
816      */
817     public void setPartCardinality(String card) throws DBException {
818         setField(CARDINALITY, card);
819     }
820 
821     public void isOwnedAttribute(boolean attribute) throws DBException {
822         setField(PART_IS_ATTRIB, attribute);
823     }
824 
825     /***
826      * @return either parentType + partType for owned attribs, or partType + relationship for shared
827      * @throws DBException upon database exception error.
828      */
829     public String getPartUniqueId() throws DBException {
830         String result = getParentType() + "|" + getPartType();
831 
832         if (isSharedNodeAttrib()) {
833             result += ("|" + getNodeRelation());
834         }
835 
836         return result;
837     }
838 
839     /***
840      * Can be for attribute OR shared relation; simply describes cardinality.
841      *
842      * @return true if this part allows only a single item
843      * @throws DBException upon getField exceptions.
844      * @see #isMultipleAllowed()
845      */
846     public boolean isSingleValued() throws DBException {
847         boolean result = false;
848 
849         for (int i = 0; i < SINGLE_CARDINALITY_CHOICES.length; i++) {
850             String cardinalityChoice = SINGLE_CARDINALITY_CHOICES[i];
851 
852             if (cardinalityChoice.equals(getCardinality())) {
853                 result = true;
854 
855                 break;
856             }
857         }
858 
859         return result;
860     }
861 
862     /***
863      * Retrieves the label for the cardinality.
864      *
865      * @return display label for this part's cardinality
866      * @throws DBException upon database exception error.
867      */
868     public String getCardinalityLabel() throws DBException {
869         String result = CARDINALITY_CHOICES[1][1];
870 
871         for (int i = 0; i < CARDINALITY_CHOICES.length; i++) {
872             String[] choice = CARDINALITY_CHOICES[i];
873 
874             if (choice[0].equals(getCardinality())) {
875                 result = choice[1];
876 
877                 break;
878             }
879         }
880 
881         return result;
882     }
883 
884     /***
885      * @param request the ControllerRequest object.
886      * @return display label of node relation for this part
887      * @throws DBException upon database exception error.
888      */
889     public String getNodeRelationLabel(ExpressoRequest request) throws DBException {
890         String result = "";
891 
892         String rel = getNodeRelation();
893         result = StringUtil.notNull(RelationType.getRelTypeDisplayName(rel));
894 
895         return result;
896     }
897 
898     /***
899      * @return the label of the type of node allowed for relation
900      * @throws DBException upon database exception error.
901      */
902     public String getNodeRelationObjectLabel() throws DBException {
903         String result = "";
904 
905         NodeType nodeType = new NodeType();
906         nodeType.setDBName(getDataContext());
907         nodeType.setRequestingUser(getRequestingUser());
908         nodeType.setField(NodeType.NODE_TYPE_NAME, getPartType());
909 
910         if (!nodeType.find()) {
911             throw new DBException("cannot find node type: " +
912                     getParentType());
913         }
914 
915         result = StringUtil.notNull(nodeType.getField(NodeType.DISPLAY_TITLE));
916 
917         return result;
918     }
919 
920     public void setPartDescrip(String s) throws DBException {
921         setField(PART_DESCRIP, s);
922     }
923 
924     public void setSpecialHandler(String s) throws DBException {
925         setField(SPECIAL_HANDLER, s);
926     }
927 
928     /***
929      * Compares this object with the specified object for order.  Returns a
930      * negative integer, zero, or a positive integer as this object is less
931      * than, equal to, or greater than the specified object.<p>
932      * <p/>
933      * In the foregoing description, the notation
934      * <tt>sgn(</tt><i>expression</i><tt>)</tt> designates the mathematical
935      * <i>signum</i> function, which is defined to return one of <tt>-1</tt>,
936      * <tt>0</tt>, or <tt>1</tt> according to whether the value of <i>expression</i>
937      * is negative, zero or positive.
938      * <p/>
939      * The implementor must ensure <tt>sgn(x.compareTo(y)) ==
940      * -sgn(y.compareTo(x))</tt> for all <tt>x</tt> and <tt>y</tt>.  (This
941      * implies that <tt>x.compareTo(y)</tt> must throw an exception iff
942      * <tt>y.compareTo(x)</tt> throws an exception.)<p>
943      * <p/>
944      * The implementor must also ensure that the relation is transitive:
945      * <tt>(x.compareTo(y)&gt;0 &amp;&amp; y.compareTo(z)&gt;0)</tt> implies
946      * <tt>x.compareTo(z)&gt;0</tt>.<p>
947      * <p/>
948      * Finally, the implementer must ensure that <tt>x.compareTo(y)==0</tt>
949      * implies that <tt>sgn(x.compareTo(z)) == sgn(y.compareTo(z))</tt>, for
950      * all <tt>z</tt>.<p>
951      * <p/>
952      * It is strongly recommended, but <i>not</i> strictly required that
953      * <tt>(x.compareTo(y)==0) == (x.equals(y))</tt>.  Generally speaking, any
954      * class that implements the <tt>Comparable</tt> interface and violates
955      * this condition should clearly indicate this fact.  The recommended
956      * language is "Note: this class has a natural ordering that is
957      * inconsistent with equals."
958      *
959      * @param o the Object to be compared.
960      * @return a negative integer, zero, or a positive integer as this object
961      *         is less than, equal to, or greater than the specified object.
962      * @throws ClassCastException if the specified object's type prevents it
963      *                            from being compared to this Object.
964      */
965     public int compareTo(Object o) {
966         int result = 0;
967         Part other = (Part) o;
968 
969         try {
970             result = getParentType().compareTo(other.getParentType());
971 
972             if (result == 0) {
973                 result = getPartNum().compareTo(other.getPartNum());
974             }
975         } catch (DBException e) {
976             getLogger().error(e);
977         }
978 
979         return result;
980     }
981 
982     public String[][] getPicklistArrayAssumeSecure() throws DBException {
983         if (!hasPicklist()) {
984             return null;
985         }
986 
987         List found = getPicklistAssumeSecure();
988 
989         if (found == null) {
990             return new String[0][2];
991         }
992 
993         // sort list (picklist items implement Comparable)
994         if (found.size() > 0) {
995             Collections.sort(found);
996         }
997 
998         // convert to array
999         String[][] result = new String[found.size()][3];
1000         int i = 0;
1001 
1002         for (Iterator iter = found.iterator(); iter.hasNext();) {
1003             PickList pickList = (PickList) iter.next();
1004             result[i][0] = pickList.getID();
1005             result[i][1] = pickList.getDisplayString();
1006             result[i][2] = pickList.getOrderNum();
1007             i++;
1008         }
1009 
1010         return result;
1011     }
1012 
1013     private List getPicklistAssumeSecure() throws DBException {
1014         if (!hasPicklist()) {
1015             return null;
1016         }
1017 
1018         PickList listItem = new PickList(SuperUser.INSTANCE); // superuser
1019         listItem.setField(PickList.NODE_TYPE, getParentType());
1020         listItem.setField(PickList.ATTRIBUTE_TYPE, getPartType());
1021 
1022         return (List) listItem.searchAndRetrieveList(PickList.LIST_ID);
1023     }
1024 
1025     public void setPartId(int partId) throws DBException {
1026         setField(PART_ID, partId);
1027     }
1028 
1029     public void setPartId(String partId) throws DBException {
1030         setField(PART_ID, partId);
1031     }
1032 
1033     public String getPartDescriptionRaw() throws DBException {
1034         Filter old = setFilterClass(new RawFilter());
1035         String raw = getPartDescription();
1036         setFilterClass(old);
1037 
1038         return raw;
1039     }
1040 
1041     /***
1042      * provide a transition for editing this part, suitable for creating an
1043      * HTTP link
1044      *
1045      * @param nodeId The parent node id.
1046      * @param params request parameters; assumes NODE_ID is within this map
1047      * @return transtion for viewing, including label for name of object; never null
1048      * @throws DBException upon DBObject-related error.
1049      */
1050     public Transition getEditTrans(String nodeId, Map params) throws DBException {
1051         Transition trans = null;
1052 
1053         if (isHaveCustomHandler() && params != null) {
1054 
1055             // custom handler will get node id from params; need to set it
1056             String oldNodeId = (String) params.get(Node.NODE_ID);
1057             params.put(Node.NODE_ID, nodeId);
1058             trans = getCustomHandler().getEditTransition(params);
1059             if (trans != null) {
1060                 trans.setName(NodeAction.PROMPT_EDIT_ATTRIB);
1061                 trans.setLabel(getPartLabel());
1062             }
1063             params.put(Node.NODE_ID, oldNodeId);
1064         }
1065 
1066         // use standard algorithm if no custom, or if custom returns null
1067         if (!isHaveCustomHandler() || trans == null) {
1068             if (isOwnedAttribute()) {
1069                 trans = new Transition(getPartLabel(), NodeAction.class, NodeAction.PROMPT_EDIT_ATTRIB);
1070                 trans.addParam(Attribute.ATTRIBUTE_TYPE, getPartType());
1071                 if (nodeId != null) {
1072                     trans.addParam(Node.NODE_ID, nodeId);
1073                 }
1074 
1075             } else {
1076                 // relations: show picklist
1077                 trans = new Transition(getPartLabel(), NodeAction.class, NodeAction.PROMPT_PICKLIST_NODE);
1078                 trans.addParam(Node.NODE_TYPE, getPartType());
1079                 trans.addParam(Relation.RELATION_TYPE, getNodeRelation());
1080                 if (nodeId != null) {
1081                     trans.addParam(Node.NODE_ID, nodeId);
1082                 }
1083             }
1084         }
1085 
1086         return trans;
1087     }
1088 
1089     /***
1090      * provide a transition for viewing this object, suitable for creating an
1091      * HTTP link
1092      *
1093      * @return transtion for viewing, including label for name of part; never null
1094      * @throws DBException upon DBObject-related error.
1095      */
1096     public Transition getViewTrans() throws DBException {
1097         Transition trans = new Transition(getPartLabel(), PartAction.class, PartAction.PROMPT_EDIT_PART);
1098         trans.addParam(Part.PART_ID, getId());
1099         return trans;
1100     }
1101 
1102     public void acceptVisitor(ModelVisitor visitor) {
1103         visitor.visitPart(this);
1104     }
1105 
1106     /***
1107      * can be for attribute OR shared relation; simply describes cardinality
1108      *
1109      * @return true if this part allows multiple items
1110      * @throws DBException upon DBObject-related error.
1111      * @see #isSingleValued()
1112      */
1113     public boolean isMultipleAllowed() throws DBException {
1114         return !isSingleValued();
1115     }
1116 
1117     /***
1118      * @return the relation type for this shared relation; null if owned attribute.
1119      * @throws DBException upon DBObject-related error.
1120      */
1121     public RelationType getNodeRelationEntity() throws DBException {
1122         if (isOwnedAttribute()) {
1123             return null;
1124         }
1125 
1126         RelationType relType = new RelationType();
1127         relType.setRelType(getNodeRelation());
1128         relType.retrieve();
1129         return relType;
1130     }
1131 
1132     public String getPicklistXMLName() throws DBException {
1133         return getParentType() + DELIMITER + getPartType();
1134     }
1135 
1136     /***
1137      * @return part found; null if not found
1138      */
1139     public static Part getPartFromXMLName(String name) throws DBException {
1140         if (name == null || name.length() == 0) throw new DBException("Cannot find part for empty name.");
1141         String[] split = name.split(DELIMITER);
1142         if (split.length != 2) {
1143             return null;
1144         }
1145 
1146         Part result = PartsFactory.getPart(split[0], split[1], null);
1147         if (result == null) {
1148             return null;
1149         }
1150 
1151         return result;
1152     }
1153 } // fini