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.ControllerRequest;
13  import com.jcorporate.expresso.core.controller.ExpressoRequest;
14  import com.jcorporate.expresso.core.controller.Output;
15  import com.jcorporate.expresso.core.controller.Transition;
16  import com.jcorporate.expresso.core.dataobjects.BaseDataObject;
17  import com.jcorporate.expresso.core.dataobjects.DataFieldMetaData;
18  import com.jcorporate.expresso.core.db.DBConnection;
19  import com.jcorporate.expresso.core.db.DBException;
20  import com.jcorporate.expresso.core.dbobj.DBField;
21  import com.jcorporate.expresso.core.dbobj.RowSecuredDBObject;
22  import com.jcorporate.expresso.core.misc.DateTime;
23  import com.jcorporate.expresso.core.security.ReadOnlyUser;
24  import com.jcorporate.expresso.core.security.SuperUser;
25  import com.jcorporate.expresso.core.security.filters.AllowedHtmlPlusURLFilter;
26  import com.jcorporate.expresso.core.security.filters.Filter;
27  import com.jcorporate.expresso.core.security.filters.RawFilter;
28  import com.jcorporate.expresso.core.security.filters.XmlFilter;
29  import com.sri.emo.controller.AddNodeAction;
30  import com.sri.emo.controller.NodeAction;
31  import com.sri.emo.dbobj.model_tree.ModelVisitable;
32  import com.sri.emo.dbobj.model_tree.ModelVisitor;
33  import org.dom4j.DocumentFactory;
34  import org.dom4j.Element;
35  
36  import java.util.Calendar;
37  import java.util.Map;
38  
39  
40  /***
41   * encapsulate a name-value pair which is associated with a Node object
42   *
43   * @author larry hamel
44   */
45  public class Attribute extends RowSecuredDBObject implements ModelVisitable, IViewable {
46      /***
47  	 * 
48  	 */
49  	private static final long serialVersionUID = 1L;
50  	/***
51       * incrementing PK
52       */
53      public static final String ATTRIBUTE_ID = "ATTRIBUTE_ID";
54      public static final String ATTRIBUTE_TAG = "ATTRIBUTE";
55      public static final String NODE_ID = Node.NODE_ID; // foreign key
56  
57      /***
58       * Field NODE_TYPE breaks normalization by storing data in two places,
59       * but is convenient for finding
60       * all attributes for a given node type;
61       *
62       * @todo remove this abomination; use join where only nodetype is used for searching
63       */
64      public static final String NODE_TYPE = Node.NODE_TYPE;
65  
66      /***
67       * Name is internal string, type of attribute (would be PK with nodeId
68       * if we didn't save multiple attributes of same type per node ).
69       */
70      public static final String ATTRIBUTE_TYPE = "ATTRIBUTE_TYPE";
71      public static final String ATTRIBUTE_VALUE = "ATTRIBUTE_VALUE";
72      public static final String ATTRIBUTE_CREATED = "ATTRIBUTE_CREATED";
73      public static final String ATTRIBUTE_MODIFIED = "ATTRIBUTE_MODIFIED";
74      public static final String ATTRIBUTE_COMMENT = "ATTRIBUTE_COMMENT";
75      public static final String ATTRIBUTE_PICKLIST_DISPLAY = "ATTRIB_PLIST_DISP";
76      public static final String ATTRIBUTE_ORDER = "ATTRIBUTE_ORDER";
77  
78      /***
79       * For now, attribute display name is stored in
80       * subclasses of PartDescriptor, as implemented in PartDescriptor.getParts().
81       */
82      public static final String ATTRIBUTE_DISPLAY_NAME = "ATTRIBUTE_DISPLAY_NAME";
83  
84      // prefix string for putting at front of param names
85      // used for POSTing input of attributes;
86      // suffix is number
87      public static final String ATTRIBUTE_VALUE_PREFIX = "ATTR_VALUE_";
88      public static final String ATTRIBUTE_COMMENT_PREFIX = "ATTR_COMMENT_";
89      public static final String ATTRIBUTE_ID_PREFIX = "ATTR_ID_";
90  
91      /***
92       * Attribute id is used to discern between multiple attributes with the
93       * same name for the same owner node.  before an attribute is added to the
94       * node, this placeholder value is used to indicate that the attribute is
95       * new--unsaved as yet.
96       */
97      public static final String ATTRIBUTE_ID_UNKNOWN = "-1";
98      public static final String ATTRIBUTE_DESCRIPTION = "Attribute";
99      public static final String CUSTOM_ATTRIBUTE = "CUSTOM_ATTRIBUTE";
100 
101 
102     /***
103      * Field that defined the value for getPart() explicitly, potentially
104      * saving database lookups to retrieve it.
105      */
106     private Part explicitPart = null;
107     public static final String TABLE_NAME = "attribute";
108 
109     /***
110      * Default constructor for <code>Attribute</code>
111      * creates a new object of this type with no connection
112      * yet allocated.
113      *
114      * @throws DBException If the new object cannot be
115      *                     created
116      */
117     public Attribute() throws DBException {
118     }
119 
120     /***
121      * Constructor
122      *
123      * @param theConnection DBConnection to be used to
124      *                      communicate with the database
125      * @param theUser       User name attempting to access the
126      *                      object
127      * @throws DBException If the user cannot access this
128      *                     object or the object cannot be initialized
129      */
130     public Attribute(DBConnection theConnection, int theUser)
131             throws DBException {
132         super(theConnection, theUser);
133     }
134 
135     public Attribute(String attribId)
136             throws DBException {
137         if (attribId != null) {
138             setAttributeId(attribId);
139         }
140     }
141 
142     /***
143      * attribute for this parent
144      */
145     public Attribute(Node parent) throws DBException {
146         super(parent.getRequestingUser());
147         setDBName(parent.getDataContext());
148         setParentNodeType(parent.getNodeType());
149         setParentNodeId(parent.getNodeId());
150     }
151 
152 
153     /***
154      * Constructor
155      *
156      * @param theUser User name attempting to access the object
157      * @throws DBException If the user cannot access this
158      *                     object or the object cannot be initialized
159      */
160     public Attribute(ReadOnlyUser theUser) throws DBException {
161         super(theUser);
162     }
163 
164     /***
165      * Defines the database table name and fields for this DB object
166      *
167      * @throws DBException if the operation cannot be performed
168      */
169     protected synchronized void setupFields() throws DBException {
170         setTargetTable(TABLE_NAME);
171         setDescription(ATTRIBUTE_DESCRIPTION);
172         addField(NODE_ID, "int", 0, false, "foreign key to node table");
173 
174         /*** field NODE_TYPE breaks normal form, but is convenient for finding
175          all attributes for a given node type; since node type
176          is never expected to change after node creation, this seems reasonable
177          */
178         addField(NODE_TYPE, DBField.VARCHAR_TYPE, 255, false, "Node Type");
179         addField(ATTRIBUTE_TYPE, DBField.VARCHAR_TYPE, 255, false, "Name");
180         addField(ATTRIBUTE_VALUE, DBField.LONGVARCHAR_TYPE, 0, true, "Value");
181 
182         DataFieldMetaData fieldMeta = getMetaData().getFieldMetadata(ATTRIBUTE_VALUE);
183         ((DBField) fieldMeta).setFilterClass(AllowedHtmlPlusURLFilter.class);
184 
185         addField(ATTRIBUTE_CREATED, DBField.DATETIME_TYPE, 0, false, "Created");
186         addField(ATTRIBUTE_MODIFIED, DBField.DATETIME_TYPE, 0, false,
187                 "Modified Timestamp");
188 
189         addField(ATTRIBUTE_COMMENT, DBField.LONGVARCHAR_TYPE, 0, true, "Comment");
190         fieldMeta = getMetaData().getFieldMetadata(ATTRIBUTE_COMMENT);
191         ((DBField) fieldMeta).setFilterClass(AllowedHtmlPlusURLFilter.class);
192 
193         addField(ATTRIBUTE_ORDER, DBField.INT_TYPE, 0, true, "Order");
194         addField(ATTRIBUTE_ID, DBField.AUTOINC_TYPE, 0, false, "auto-inc ID");
195 
196         // proxy
197         addVirtualField(ATTRIBUTE_PICKLIST_DISPLAY, DBField.VARCHAR_TYPE, 253,
198                 ATTRIBUTE_VALUE, "Display value for picklist");
199 
200         // PK is FK node id in combination with attribute name
201         // and ID--CAN have 2 of same attribute for one node
202         addKey(ATTRIBUTE_ID);
203 
204         // lookups for particular attrib kinds are by node+attrib_name
205         addIndex("attrib_nodeid_attrName_idx", NODE_ID + "," + ATTRIBUTE_TYPE,
206                 false);
207         addIndex("attrib_nodeid__idx", NODE_ID, false);
208         addIndex("attrib_order_idx", ATTRIBUTE_ORDER, false);
209 
210         // add index on value since items are sorted this way for display
211         // cannot index on blob/text field
212         //addIndex("attrib_value_idx", ATTRIBUTE_VALUE, false);
213         addDetail(MatrixCell.class.getName(), ATTRIBUTE_ID, MatrixCell.ATTRIB_ID);
214     } /* setupFields() */
215 
216     /***
217      * Extends the normal getField method to handle virtual fields.
218      * Note that OWNER_USERNAME requires that OWNER_ID is already retrieved and
219      * IMAGE_FILE_PATH requires IMAGE_FILE_ID.
220      *
221      * @param fieldName Name of the database field for which a value set is
222      *                  requested
223      * @return The value for the field
224      * @throws DBException If the operation cannot be performed
225      * @todo This code only works with Picklist_single_allowed cardinality.
226      * It doesn't handle multiple attributes or free-text attributes.
227      */
228     public synchronized String getField(String fieldName)
229             throws DBException {
230         String result = null;
231 
232         if (ATTRIBUTE_PICKLIST_DISPLAY.equals(fieldName)) {
233             String val = getAttribValue();
234             try {
235                 PickList listItem = new PickList();
236                 listItem.setField(PickList.LIST_ID, getAttribValue());
237 
238                 if (listItem.find()) {
239                     result = listItem.getField(PickList.DISPLAY_IN_PICKLIST);
240                 } else {
241                     getLogger().error("Can't find picklist item with id: " +
242                             getAttribValue());
243                     result = "CANNOT FIND PICKLIST ITEM WITH ID=" +
244                             getAttribValue();
245                 }
246 
247             } catch (NumberFormatException ex) {
248                 //Not an appropriate number, we're probably dealing with
249                 //multiple attribute.
250                 getLogger().info("Not an integer value: " + val);
251                 return "";
252             }
253         } else {
254             result = super.getField(fieldName);
255         }
256 
257         return result;
258     }
259 
260     public String getAttribId() throws DBException {
261         return getField(ATTRIBUTE_ID);
262     }
263 
264     public String getAttribValue() throws DBException {
265         return getField(ATTRIBUTE_VALUE);
266     }
267 
268     public void setAttributeId(String id) throws DBException {
269         setField(ATTRIBUTE_ID, id);
270     }
271 
272     public String getParentNodeId() throws DBException {
273         return getField(NODE_ID);
274     }
275 
276     /***
277      * @return node which is parent of this attribute, or throw exception if not found.
278      * @throws DBException upon database access error.
279      */
280     public Node getParentNode() throws DBException {
281         if (getParentNodeId().length() == 0) {
282             throw new DBException("cannot find parent node because attribute lacks parentNodeId info.");
283         }
284 
285         Node node = new Node(this);
286         node.setDBName(getDataContext());
287         node.setNodeId(getParentNodeId());
288         node.setRequestingUser(getRequestingUser());
289         node.retrieve();
290 
291         return node;
292     }
293 
294     public String getOrder() throws DBException {
295         return getField(ATTRIBUTE_ORDER);
296     }
297 
298     public String getAttribComment() throws DBException {
299         return getField(ATTRIBUTE_COMMENT);
300     }
301 
302     public void setAttributeComment(String comment) throws DBException {
303         setField(ATTRIBUTE_COMMENT, comment);
304     }
305 
306     public String getAttribType() throws DBException {
307         return getField(ATTRIBUTE_TYPE);
308     }
309 
310     /***
311      * note: new nodes need BOTH id and type from parent
312      *
313      * @see #setParentNode(Node)
314      */
315     public void setParentNodeId(String parentId) throws DBException {
316         setField(NODE_ID, parentId);
317     }
318 
319     /***
320      * note: new nodes need BOTH id and type from parent
321      *
322      * @see #setParentNode(Node)
323      */
324     public void setParentNodeType(String parentType) throws DBException {
325         setField(Attribute.NODE_TYPE, parentType);
326     }
327 
328     public void setAttributeType(String attribType) throws DBException {
329         setField(Attribute.ATTRIBUTE_TYPE, attribType);
330     }
331 
332     public String getAttribCreated() throws DBException {
333         return getField(ATTRIBUTE_CREATED);
334     }
335 
336     public String getAttribModified() throws DBException {
337         return getField(ATTRIBUTE_MODIFIED);
338     }
339 
340     public IPartHandler getCustomHandler() throws DBException {
341         AbstractAttributeHandler result = null;
342         Part part = null;
343         if (getParentNodeId().length() > 0) {
344             try {
345                 if (getParentNode() != null) {
346                     part = getPart();
347                 }
348             } catch (Exception e) {
349                 getLogger().error("Cannot get parent node or part for attribute id=" + getAttribId());
350             }
351         }
352 
353         if (part != null) {
354             result = (AbstractAttributeHandler) part.getCustomHandler();
355             if (result != null) {
356                 result.setParentAttrib(this);
357             }
358         } else {
359             getLogger().error("cannot find part with parent type/attrib type: " +
360                     getParentNode().getNodeType() + "/" + getAttribType());
361         }
362 
363         return result;
364     }
365 
366     /***
367      * deep copy fields to new attribute into attribute which
368      * attaches to node with given ID; expects fully saved Attribute (with ID) as source.
369      * will treat cardinality=single attributes with care to make sure there
370      * is only one in final node (e.g., if constructor already added a "type" attrib).
371      * <p/>
372      * side-effect: deletes this node if it is found to be impossibly valued (i.e., no part of this type w/ this parent)
373      *
374      * @param cloneNodeId
375      * @return cloned attribute, ALREADY SAVED and thereby holding a valid ID; null if this node is impossible to have relational integrity
376      * @throws DBException upon database access error.
377      */
378     public Attribute clone(String cloneNodeId) throws DBException {
379         return clone(cloneNodeId, getAttribType());
380     }
381 
382     /***
383      * deep copy fields to new attribute into attribute which
384      * attaches to node with given ID; expects fully saved Attribute (with ID) as source.
385      * will treat cardinality=single attributes with care to make sure there
386      * is only one in final node (e.g., if constructor already added a "type" attrib).
387      * <p/>
388      * side-effect: deletes this node if it is found to be impossibly valued (i.e., no part of this type w/ this parent)
389      *
390      * @param cloneNodeId
391      * @return cloned attribute, ALREADY SAVED and thereby holding a valid ID; null if this node is impossible to have relational integrity
392      * @throws DBException upon database access error.
393      */
394     public Attribute clone(String cloneNodeId, String destAttribType)
395             throws DBException {
396         // make copy by getting second copy of "this"
397         Attribute clone = null;
398         Attribute orig = new Attribute(getRequestingUser());
399         orig.setDBName(getDataContext());
400         orig.setAttributeId(getAttribId());
401         orig.retrieve();
402 
403         Part part = getPart();
404         if (part == null) {
405             // big problem: we have no part; probably a type that doesn't correspond to the parent node.
406             getLogger().error("No part found for attribute with type: " + getAttribType()
407                     + " which has parent node of type: " + getParentNode().getNodeType()
408                     + " so deleting this attribute.");
409             this.delete();
410             return null;
411         }
412 
413         Node destinationNode = new Node(this);
414         destinationNode.setNodeId(cloneNodeId);
415         destinationNode.retrieve();
416 
417         // see if the destination node already has this attrib, perhaps
418         // created by Node constructor or something.
419         boolean exists = false;
420 
421         if (part.isSingleValued()) {
422             // does destination already have an attrib, perhaps provided by constructor?
423             Attribute[] existing = destinationNode.getAttributes(destAttribType);
424 
425             if (existing.length > 0) {
426                 // have existing, just update contents
427                 clone = existing[0];
428                 clone.setAttributeValue(getAttribValue());
429                 clone.setAttributeComment(getAttribComment());
430                 clone.update();
431                 exists = true;
432             }
433         }
434 
435         if (!exists) {
436             clone = orig;
437             clone.setParentNodeId(cloneNodeId);
438             clone.setParentNodeType(destinationNode.getNodeType());
439             clone.setAttributeType(destAttribType);
440             clone.setAttributeId("0"); // resetting id isn't technically necessary, since auto-inc field WILL get new value with add() method
441 
442             // clone by just adding -- auto-inc magic
443             clone.add();
444         }
445 
446         IPartHandler handler = getCustomHandler();
447 
448         if (handler != null) {
449             // need to duplicate custom items too
450             handler.clone(this, clone);
451         }
452 
453         return clone;
454     }
455 
456     /***
457      * We override to set dates and provide default ordering if nec.
458      *
459      * @throws DBException upon database access error.
460      */
461     public synchronized void add() throws DBException {
462         String datestamp = DateTime.getDateTimeForDB();
463         String created = getAttribCreated();
464         setField(Attribute.ATTRIBUTE_CREATED, datestamp);
465 
466         // keep modified != created if not that way in parent (i.e., during clone() operation)
467         if (created.equals(getAttribModified())) {
468             setField(Attribute.ATTRIBUTE_MODIFIED, datestamp);
469         } else {
470             // set to slightly different to indicate editing
471             Calendar cal = Calendar.getInstance();
472             cal.add(Calendar.SECOND, 1);
473             datestamp = DateTime.getDateTimeForDB(cal.getTime());
474             setField(Attribute.ATTRIBUTE_MODIFIED, datestamp);
475         }
476         if (isFieldNull(ATTRIBUTE_ORDER)) {
477             // find out next order number
478             Attribute[] attribs = getParentNode().getAttributes(getAttribType());
479             setOrder(attribs.length + 1);
480         }
481 
482         super.add();
483     } /* add() */
484 
485     /***
486      * OVERRIDE to use parent node permissions
487      *
488      * @param requestedFunction The code of the requested function. The codes are:
489      *                          <ol><li>A: Add<li>
490      *                          <li>S: Search<li>
491      *                          <li>U: Update<li>
492      *                          <li>D: Delete<li>
493      *                          </ol>
494      * @throws com.jcorporate.expresso.core.db.DBException
495      *                           If the requested operation is not permitted to this user
496      * @throws SecurityException if the user is not allowed access to the object.
497      */
498     public void isAllowed(String requestedFunction) throws SecurityException, DBException {
499         if (getRequestingUser() == SuperUser.INSTANCE) {
500             return;
501         }
502 
503         if (getRequestingUser().isAdmin()) {
504             return;
505         }
506 
507         // if parent node can be found, use it
508         try {
509             // catch db exception, but NOT securityexception; if parent doesn't allow, don't allow attribute either
510             Node node = getParentNode();
511             node.isAllowed(requestedFunction);
512         } catch (DBException e) {
513             // use local permissons
514             super.isAllowed(requestedFunction);
515         }
516     } /* isAllowed(String) */
517 
518     /***
519      * We override to set dates and fix ordering if nec.
520      *
521      * @throws DBException upon database access error.
522      */
523     public synchronized void update() throws DBException {
524         String datestamp = DateTime.getDateTimeForDB();
525         setField(Attribute.ATTRIBUTE_MODIFIED, datestamp);
526 
527         boolean orderChanged = getDataField(ATTRIBUTE_ORDER).isChanged(); // assumes not blind update
528         super.update();
529 
530         // if order changed, manipulate other values
531         if (orderChanged) {
532             reorderAttribs();
533         }
534     } /*  update() */
535 
536     /***
537      * We override to provide call to handler, if any, and reorder remaining, if nec.
538      *
539      * @throws DBException upon database access error.
540      */
541     public synchronized void delete(boolean deleteDetails)
542             throws DBException {
543         super.delete(deleteDetails);
544 
545         if (deleteDetails) {
546             IPartHandler handler = getCustomHandler();
547 
548             if (handler != null) {
549                 // need to duplicate custom items too
550                 handler.delete(this);
551             }
552         }
553 
554         reorderAttribs();
555     } /* delete() */
556 
557     private void reorderAttribs() throws DBException {
558         Attribute[] attribs = getParentNode().getAttributes(getAttribType());
559         int index = 1;
560 
561         for (int i = 0; i < attribs.length; i++) {
562             Attribute attrib = attribs[i];
563 
564             if (attrib.getOrderInt() != index) {
565                 attrib.setOrder(index);
566                 attrib.updateOrder();
567             }
568 
569             index++;
570         }
571     }
572 
573     /***
574      * Update without triggering reordering; useful for 'transaction' where order of part A should be set before manipulating order of part B,
575      * as opposed to a deletion or otherwise, where reordering of all remaining is required.
576      *
577      * @throws DBException upon database access error.
578      */
579     public void updateOrder() throws DBException {
580         super.update();
581     }
582 
583     public Element getXML(ExpressoRequest request, boolean addOrder)
584             throws DBException {
585         Filter old = setFilterClass(new XmlFilter());
586         Element attribElm = DocumentFactory.getInstance().createElement(getAttribType());
587         IPartHandler handler = getCustomHandler();
588         Part part = getPart();
589         attribElm.addAttribute(Part.PART_LABEL, part.getPartLabel());
590 
591         //        attribElm.addAttribute(Part.PART_TYPE, part.getPartType());
592         attribElm.addAttribute(Attribute.ATTRIBUTE_ID, getAttribId());
593 
594         if (getAttribComment().length() > 0) {
595             attribElm.addAttribute(Attribute.ATTRIBUTE_COMMENT,
596                     getAttribComment());
597         }
598 
599         if (addOrder) {
600             attribElm.addAttribute(ATTRIBUTE_ORDER, getOrder());
601         }
602 
603         if (handler == null) {
604             attribElm.addAttribute(Attribute.ATTRIBUTE_VALUE, getAttribValue());
605 
606             // menu choices
607             if (part.hasPicklist()) {
608                 attribElm.addAttribute(PickList.DISPLAY_IN_PICKLIST,
609                         PickList.getPickFast(getAttribValue(), getDataContext()));
610 
611                 // whole picklist is output by calling function in addnodeaction
612             }
613         } else {
614             handler.addXML(this, attribElm, (ControllerRequest) request);
615         }
616 
617         //        attribElm.addAttribute(Attribute.ATTRIBUTE_CREATED, getAttribCreated());
618         //        attribElm.addAttribute(Attribute.ATTRIBUTE_MODIFIED, getAttribModified());
619         setFilterClass(old); // resets
620 
621         return attribElm;
622     }
623 
624     /***
625      * Retrieve the part why which this attribute belongs to.
626      *
627      * @return Part part, or possibly null if no part is found.
628      * @throws DBException upon error.
629      */
630     public Part getPart() throws DBException {
631         Part returnValue = this.getExplicitPart();
632         if (returnValue == null) {
633             return PartsFactory.getPart(getParentNode().getNodeType(),
634                     getAttribType(), null);
635         } else {
636             return returnValue;
637         }
638 
639     }
640 
641     public void setAttributeValue(String value) throws DBException {
642         setField(Attribute.ATTRIBUTE_VALUE, value);
643     }
644 
645     public void setOrder(String value) throws DBException {
646         setOrder(Integer.parseInt(value));
647     }
648 
649     public void setOrder(int order) throws DBException {
650         if (order < 1) {
651             order = 1;
652         }
653 
654         setField(Attribute.ATTRIBUTE_ORDER, order);
655     }
656 
657     public void setParentNode(Node node) throws DBException {
658         setParentNodeId(node.getNodeId());
659         setParentNodeType(node.getNodeType());
660     }
661 
662     public String getAttribCommentRaw() throws DBException {
663         Filter old = setFilterClass(new RawFilter());
664         String raw = getAttribComment();
665         setFilterClass(old);
666 
667         return raw;
668     }
669 
670     public String getAttribValueRaw() throws DBException {
671         Filter old = setFilterClass(new RawFilter());
672         String raw = getAttribValue();
673         setFilterClass(old);
674 
675         return raw;
676     }
677 
678     public boolean hasCustomHandler() throws DBException {
679         return getCustomHandler() != null;
680     }
681 
682     public boolean hasPicklist() throws DBException {
683         return getPart().hasPicklist();
684     }
685 
686     public int getOrderInt() throws DBException {
687         return getFieldInt(ATTRIBUTE_ORDER);
688     }
689 
690     /***
691      * Provide a transition for viewing this object, suitable for creating an
692      * HTTP link.
693      *
694      * @return transition for viewing, including label for name of object;
695      * @throws DBException upon database access error.
696      */
697     public Transition getViewTrans() throws DBException {
698         Transition trans = null;
699 
700         if (hasCustomHandler()) {
701             IPartHandler handler = getCustomHandler();
702             trans = handler.getViewTransition(IPartHandler.NO_PARAMETERS); // can return null
703         }
704 
705         // use standard algorithm if result is null
706         if (trans == null) {
707 
708             // we don't want a value with a <a> anchor in it, which might happen unless we get raw
709             // @todo use protective filter instead
710             String value = getAttribValueRaw();
711 
712             // use translated menu label if this is a picklist value
713             Part part = getPart();
714             if (part.hasPicklist()) {
715                 value = getField(Attribute.ATTRIBUTE_PICKLIST_DISPLAY);
716             }
717 
718             // allow edit trans only for permitted users; otherwise, just view parent node
719             // todo change usages to getEditTrans()
720             if (BaseDataObject.STATUS_NEW == getStatus() || canRequesterWrite()) {
721                 trans = new Transition(value, NodeAction.class, NodeAction.PROMPT_EDIT_ATTRIB);
722                 trans.addParam(ATTRIBUTE_TYPE, getAttribType());
723             } else {
724                 trans = new Transition(value, NodeAction.class, NodeAction.VIEW_SINGLE_ATTRIBUTE);
725                 trans.addParam(ATTRIBUTE_ID, getAttribId());
726                 NodeAction.addEmbeddedParameter(trans);
727             }
728             trans.addParam(Node.NODE_ID, getParentNodeId());
729         }
730         return trans;
731     }
732 
733     /***
734      * Provide a transition for viewing this object, suitable for creating an
735      * HTTP link.
736      *
737      * @return transtion for viewing, including label for name of object;
738      * @throws DBException upon database access error.
739      */
740     public Transition getViewTrans(Map reqParams) throws DBException {
741         Transition trans = null;
742 
743         if (hasCustomHandler()) {
744             IPartHandler handler = getCustomHandler();
745             trans = handler.getViewTransition(reqParams); // can return null
746         }
747 
748         // use standard algorithm if result is null
749         if (trans == null) {
750             trans = getViewTrans();
751         }
752         return trans;
753     }
754 
755     /***
756      * Provide a transition for SPECIAL editing of this object, suitable for creating an
757      * HTTP link. Return null if no special editing transition
758      *
759      * @return transtion for editing, including label for name of object;
760      * @throws DBException upon database access error.
761      */
762     public Transition getEditTrans(Map reqParams) throws DBException {
763         Transition trans = null;
764 
765         if (hasCustomHandler()) {
766             trans = getCustomHandler().getEditTransition(reqParams); // can return null
767         }
768 
769         return trans;
770     }
771 
772     /***
773      * Provide a transition for editing of this object, suitable for creating an
774      * HTTP link.
775      *
776      * @return transtion for editing, including label for name of object;
777      * @throws DBException upon database access error.
778      */
779     public Transition getEditTrans() throws DBException {
780         Transition trans = null;
781 
782 
783         if (hasCustomHandler()) {
784             trans = getCustomHandler().getEditTransition(IPartHandler.NO_PARAMETERS); // can return null
785         } else {
786             String value = getAttribValueRaw();
787 
788             // use translated menu label if this is a picklist value
789             Part part = getPart();
790             if (part.hasPicklist()) {
791                 value = getField(Attribute.ATTRIBUTE_PICKLIST_DISPLAY);
792             }
793 
794             if (BaseDataObject.STATUS_NEW == getStatus() || canRequesterWrite()) {
795                 trans = new Transition(value, NodeAction.class, NodeAction.PROMPT_EDIT_ATTRIB);
796                 trans.addParam(ATTRIBUTE_TYPE, getAttribType());
797             } else {
798                 throw new DBException("User does not have permission to edit attribute with ID=" + getAttribId());
799             }
800         }
801 
802         return trans;
803     }
804 
805     public void acceptVisitor(ModelVisitor visitor) {
806         visitor.visitAttribute(this);
807     }
808 
809 
810     public String getParentNodeType() throws DBException {
811         return getField(NODE_TYPE);
812     }
813 
814     /***
815      * override to use parent node perms for attrib.
816      *
817      * @param requestedFunction code for function -- Add, Update, Delete, Search (read)
818      * @return true if this function is allowed for this requesting user
819      * @throws SecurityException (unchecked) if not allowed
820      * @throws com.jcorporate.expresso.core.db.DBException
821      *                           for other data-related errors.
822      */
823     public boolean isRowAllowed(String requestedFunction) throws DBException {
824         if (SuperUser.SYSTEM_UID == getRequestingUid() || getRequestingUser().isAdmin()) {
825             return true;
826         }
827 
828         boolean result = false;
829         // if parent node can be found, use it
830         try {
831             // catch db exception, but NOT securityexception; if parent doesn't allow, don't allow attribute either
832             Node node = getParentNode();
833             result = node.isRowAllowed(requestedFunction);
834         } catch (DBException e) {
835             // use local permissons
836             result = super.isRowAllowed(requestedFunction);
837         }
838 
839         return result;
840     }
841 
842     /***
843      * Retrieve the view comment as an output.
844      *
845      * @param params map of ExpressoRequest params.
846      * @return Output the resulting comment.
847      * @throws DBException upon Output construction error.
848      */
849     public Output getViewComment(Map params) throws DBException {
850         Output result = null;
851         if (hasCustomHandler()) {
852             result = getCustomHandler().getViewComment(params);
853         }
854 
855         if (result == null) {
856             result = new Output(Attribute.ATTRIBUTE_COMMENT, AddNodeAction.str(getAttribComment()));
857         }
858 
859         return result;
860     }
861 
862     /***
863      * Returns the value for an explicit Part if it has been set.
864      *
865      * @return Part or null if it has never been set by any sort of factory
866      *         object.
867      */
868     public Part getExplicitPart() {
869         return explicitPart;
870     }
871 
872     /***
873      * Sets the part for this attribute explicitly.  While this is not
874      * necessary, it can yield performance gains if set by factory objects.
875      *
876      * @param explicitPart Part
877      */
878     public void setExplicitPart(Part explicitPart) {
879         this.explicitPart = explicitPart;
880     }
881 
882 } // fini