View Javadoc
1   package org.apache.fulcrum.intake.model;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.Serializable;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.ListIterator;
28  import java.util.Map;
29  
30  import javax.xml.bind.Unmarshaller;
31  import javax.xml.bind.annotation.XmlAccessType;
32  import javax.xml.bind.annotation.XmlAccessorType;
33  import javax.xml.bind.annotation.XmlAttribute;
34  import javax.xml.bind.annotation.XmlElement;
35  import javax.xml.bind.annotation.XmlType;
36  import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
37  
38  import org.apache.avalon.framework.logger.LogEnabled;
39  import org.apache.avalon.framework.logger.Logger;
40  import org.apache.commons.lang3.StringUtils;
41  import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
42  import org.apache.commons.pool2.PooledObject;
43  import org.apache.commons.pool2.impl.DefaultPooledObject;
44  import org.apache.fulcrum.intake.IntakeException;
45  import org.apache.fulcrum.intake.IntakeServiceFacade;
46  import org.apache.fulcrum.intake.Retrievable;
47  import org.apache.fulcrum.parser.ValueParser;
48  
49  /**
50   * Holds a group of Fields
51   *
52   * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
53   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
54   * @author <a href="mailto:quintonm@bellsouth.net">Quinton McCombs</a>
55   * @version $Id$
56   */
57  @XmlType(name="group")
58  @XmlAccessorType(XmlAccessType.NONE)
59  public class Group implements Serializable, LogEnabled
60  {
61      /** Serial version */
62      private static final long serialVersionUID = -5452725641409669284L;
63  
64      public static final String EMPTY = "";
65  
66      /*
67       * An id representing a new object.
68       */
69      public static final String NEW = "_0";
70  
71      /** Logging */
72      private transient Logger log;
73  
74      /**
75       * The key used to represent this group in a parameter.
76       * This key is usually a prefix as part of a field key.
77       */
78      @XmlAttribute(name="key", required=true)
79      private String gid;
80  
81      /**
82       * The name used in templates and java code to refer to this group.
83       */
84      @XmlAttribute(required=true)
85      private String name;
86  
87      /**
88       * The number of Groups with the same name that will be pooled.
89       */
90      @XmlAttribute
91      private int poolCapacity = 128;
92  
93      /**
94       * The default map object for this group
95       */
96      @XmlAttribute(name="mapToObject")
97      private String defaultMapToObject;
98  
99      /**
100      * The parent element in the XML tree
101      */
102     private AppData parent;
103 
104     /**
105      * A map of the fields in this group mapped by field name.
106      */
107     private Map<String, Field<?>> fieldsByName;
108 
109     /**
110      * Map of the fields by mapToObject
111      */
112     private Map<String, Field<?>[]> mapToObjectFields;
113 
114     /**
115      * A list of fields in this group.
116      */
117     private LinkedList<Field<?>> fields;
118 
119     /**
120      * The object id used to associate this group to a bean
121      * for one request cycle
122      */
123     private String oid;
124 
125     /**
126      * The object containing the request data
127      */
128     private transient ValueParser pp;
129 
130     /**
131      * A flag to help prevent duplicate hidden fields declaring this group.
132      */
133     private boolean isDeclared;
134 
135     /**
136      * Default constructor
137      */
138     public Group()
139     {
140         super();
141         this.fields = new LinkedList<Field<?>>();
142     }
143 
144     /**
145 	 * Enable Avalon Logging
146 	 */
147 	@Override
148 	public void enableLogging(Logger logger)
149 	{
150 		this.log = logger.getChildLogger(getClass().getSimpleName());
151 	}
152 
153 	/**
154      * Initializes the default Group using parameters.
155      *
156      * @param pp a <code>ValueParser</code> value
157      * @return this Group
158      * @throws IntakeException if at least one field could not be initialized
159      */
160     public Group init(ValueParser pp) throws IntakeException
161     {
162         return init(NEW, pp);
163     }
164 
165     /**
166      * Initializes the Group with parameters from RunData
167      * corresponding to key.
168      *
169      * @param key the group id
170      * @param pp a <code>ValueParser</code> value
171      * @return this Group
172      * @throws IntakeException if at least one field could not be initialized
173      */
174     public Group init(String key, ValueParser pp) throws IntakeException
175     {
176         this.oid = key;
177         this.pp = pp;
178         for (ListIterator<Field<?>> i = fields.listIterator(fields.size()); i.hasPrevious();)
179         {
180             i.previous().init(pp);
181         }
182         for (ListIterator<Field<?>> i = fields.listIterator(fields.size()); i.hasPrevious();)
183         {
184             Field<?> field = i.previous();
185             if (field.isSet() && !field.isValidated())
186             {
187                 field.validate();
188             }
189         }
190         return this;
191     }
192 
193     /**
194      * Initializes the group with properties from an object.
195      *
196      * @param obj a <code>Persistent</code> value
197      * @return a <code>Group</code> value
198      */
199     public Group init(Retrievable obj)
200     {
201         this.oid = obj.getQueryKey();
202 
203         Class<?> cls = obj.getClass();
204         while (cls != null)
205         {
206             Field<?>[] flds = mapToObjectFields.get(cls.getName());
207             if (flds != null)
208             {
209                 for (int i = flds.length - 1; i >= 0; i--)
210                 {
211                     flds[i].init(obj);
212                 }
213             }
214 
215             // Also check any interfaces
216             Class<?>[] interfaces = cls.getInterfaces();
217             for (int idx = 0; idx < interfaces.length; idx++)
218             {
219                 Field<?>[] interfaceFields =
220                     mapToObjectFields.get(interfaces[idx].getName());
221                 if (interfaceFields != null)
222                 {
223                     for (int i = 0; i < interfaceFields.length; i++)
224                     {
225                         interfaceFields[i].init(obj);
226                     }
227                 }
228             }
229 
230             cls = cls.getSuperclass();
231         }
232 
233         return this;
234     }
235 
236     /**
237      * Gets a list of the names of the fields stored in this object.
238      *
239      * @return A String array containing the list of names.
240      */
241     public String[] getFieldNames()
242     {
243         String nameList[] = new String[fields.size()];
244         int i = 0;
245         for (Field<?> f : fields)
246         {
247             nameList[i++] = f.getName();
248         }
249         return nameList;
250     }
251 
252     /**
253      * Return the name given to this group.  The long name is to
254      * avoid conflicts with the get(String key) method.
255      *
256      * @return a <code>String</code> value
257      */
258     public String getIntakeGroupName()
259     {
260         return name;
261     }
262 
263     /**
264      * Get the number of Group objects that will be pooled.
265      *
266      * @return an <code>int</code> value
267      */
268     public int getPoolCapacity()
269     {
270         return poolCapacity;
271     }
272 
273     /**
274      * Get the part of the key used to specify the group.
275      * This is specified in the key attribute in the xml file.
276      *
277      * @return a <code>String</code> value
278      */
279     public String getGID()
280     {
281         return gid;
282     }
283 
284     /**
285      * Get the part of the key that distinguishes a group
286      * from others of the same name.
287      *
288      * @return a <code>String</code> value
289      */
290     public String getOID()
291     {
292         return oid;
293     }
294 
295     /**
296      * Concatenation of gid and oid.
297      *
298      * @return a <code>String</code> value
299      */
300     public String getObjectKey()
301     {
302         return gid + oid;
303     }
304 
305     /**
306      * Default object to map this group to.
307      *
308      * @return a <code>String</code> value
309      */
310     public String getDefaultMapToObject()
311     {
312         return defaultMapToObject;
313     }
314 
315     /**
316      * Describe <code>getObjects</code> method here.
317      *
318      * @param pp a <code>ValueParser</code> value
319      * @return an <code>ArrayList</code> value
320      * @throws IntakeException if an error occurs
321      */
322     public List<Group> getObjects(ValueParser pp) throws IntakeException
323     {
324         ArrayList<Group> objs = null;
325         String[] oids = pp.getStrings(gid);
326         if (oids != null)
327         {
328             objs = new ArrayList<Group>(oids.length);
329             for (int i = oids.length - 1; i >= 0; i--)
330             {
331                 objs.add(IntakeServiceFacade.getGroup(name).init(oids[i], pp));
332             }
333         }
334         return objs;
335     }
336 
337     /**
338      * Get the Field
339      *
340      * @param fieldName the name of the field
341      * @return the named field
342      * @throws IntakeException indicates the field could not be found.
343      */
344     public Field<?> get(String fieldName)
345             throws IntakeException
346     {
347         if (fieldsByName.containsKey(fieldName))
348         {
349             return fieldsByName.get(fieldName);
350         }
351         else
352         {
353             throw new IntakeException("Intake Field name: " + fieldName +
354                     " not found in Group " + name);
355         }
356     }
357 
358     /**
359      * Get the list of Fields.
360      * @return list of Fields
361      */
362     public List<Field<?>> getFields()
363     {
364         return fields;
365     }
366 
367     /**
368      * Set a collection of fields for this group
369      *
370      * @param inputFields the fields to set
371      */
372     @XmlElement(name="field")
373     @XmlJavaTypeAdapter(FieldAdapter.class)
374     protected void setFields(List<Field<?>> inputFields)
375     {
376         fields = new LinkedList<Field<?>>(inputFields);
377     }
378 
379     /**
380      * Performs an AND between all the fields in this group.
381      *
382      * @return a <code>boolean</code> value
383      */
384     public boolean isAllValid()
385     {
386         boolean valid = true;
387         for (ListIterator<Field<?>> i = fields.listIterator(fields.size()); i.hasPrevious();)
388         {
389             Field<?> field = i.previous();
390             valid &= field.isValid();
391             if (log.isDebugEnabled() && !field.isValid())
392             {
393                 log.debug("Group(" + oid + "): " + name + "; Field: "
394                         + field.getName() + "; value=" +
395                         field.getValue() + " is invalid!");
396             }
397         }
398         return valid;
399     }
400 
401     /**
402      * Calls a setter methods on obj, for fields which have been set.
403      *
404      * @param obj Object to be set with the values from the group.
405      * @throws IntakeException indicates that a failure occurred while
406      * executing the setter methods of the mapped object.
407      */
408     public void setProperties(Object obj) throws IntakeException
409     {
410         Class<?> cls = obj.getClass();
411 
412         while (cls != null)
413         {
414             if (log.isDebugEnabled())
415             {
416                 log.debug("setProperties(" + cls.getName() + ")");
417             }
418 
419             Field<?>[] flds = mapToObjectFields.get(cls.getName());
420             if (flds != null)
421             {
422                 for (int i = flds.length - 1; i >= 0; i--)
423                 {
424                     flds[i].setProperty(obj);
425                 }
426             }
427 
428             // Also check any interfaces
429             Class<?>[] interfaces = cls.getInterfaces();
430             for (int idx = 0; idx < interfaces.length; idx++)
431             {
432                 Field<?>[] interfaceFields =
433                     mapToObjectFields.get(interfaces[idx].getName());
434                 if (interfaceFields != null)
435                 {
436                     for (int i = 0; i < interfaceFields.length; i++)
437                     {
438                         interfaceFields[i].setProperty(obj);
439                     }
440                 }
441             }
442 
443             cls = cls.getSuperclass();
444         }
445 
446         log.debug("setProperties() finished");
447     }
448 
449     /**
450      * Calls a setter methods on obj, for fields which pass validity tests.
451      * In most cases one should call Intake.isAllValid() and then if that
452      * test passes call setProperties.  Use this method when some data is
453      * known to be invalid, but you still want to set the object properties
454      * that are valid.
455      *
456      * @param obj the object to set the properties for
457      */
458     public void setValidProperties(Object obj)
459     {
460         Class<?> cls = obj.getClass();
461         while (cls != null)
462         {
463             Field<?>[] flds = mapToObjectFields.get(cls.getName());
464             if (flds != null)
465             {
466                 for (int i = flds.length - 1; i >= 0; i--)
467                 {
468                     try
469                     {
470                         flds[i].setProperty(obj);
471                     }
472                     catch (IntakeException e)
473                     {
474                         // just move on to next field
475                     }
476                 }
477             }
478 
479             // Also check any interfaces
480             Class<?>[] interfaces = cls.getInterfaces();
481             for (int idx = 0; idx < interfaces.length; idx++)
482             {
483                 Field<?>[] interfaceFields =
484                     mapToObjectFields.get(interfaces[idx].getName());
485                 if (interfaceFields != null)
486                 {
487                     for (int i = 0; i < interfaceFields.length; i++)
488                     {
489                         try
490                         {
491                             interfaceFields[i].setProperty(obj);
492                         }
493                         catch(IntakeException e)
494                         {
495                             // just move on to next field
496                         }
497                     }
498                 }
499             }
500 
501             cls = cls.getSuperclass();
502         }
503     }
504 
505     /**
506      * Calls getter methods on objects that are known to Intake
507      * so that field values in forms can be initialized from
508      * the values contained in the intake tool.
509      *
510      * @param obj Object that will be used to as a source of data for
511      * setting the values of the fields within the group.
512      * @throws IntakeException indicates that a failure occurred while
513      * executing the setter methods of the mapped object.
514      */
515     public void getProperties(Object obj) throws IntakeException
516     {
517         Class<?> cls = obj.getClass();
518 
519         while (cls != null)
520         {
521             Field<?>[] flds = mapToObjectFields.get(cls.getName());
522             if (flds != null)
523             {
524                 for (int i = flds.length - 1; i >= 0; i--)
525                 {
526                     flds[i].getProperty(obj);
527                 }
528             }
529 
530             // Also check any interfaces
531             Class<?>[] interfaces = cls.getInterfaces();
532             for (int idx = 0; idx < interfaces.length; idx++)
533             {
534                 Field<?>[] interfaceFields =
535                     mapToObjectFields.get(interfaces[idx].getName());
536                 if (interfaceFields != null)
537                 {
538                     for (int i = 0; i < interfaceFields.length; i++)
539                     {
540                         interfaceFields[i].getProperty(obj);
541                     }
542                 }
543             }
544 
545             cls = cls.getSuperclass();
546         }
547     }
548 
549     /**
550      * Removes references to this group and its fields from the
551      * query parameters
552      */
553     public void removeFromRequest()
554     {
555         if (pp != null)
556         {
557             String[] groups = pp.getStrings(gid);
558             if (groups != null)
559             {
560                 pp.remove(gid);
561                 for (int i = 0; i < groups.length; i++)
562                 {
563                     if (groups[i] != null && !groups[i].equals(oid))
564                     {
565                         pp.add(gid, groups[i]);
566                     }
567                 }
568                 for (ListIterator<Field<?>> i = fields.listIterator(fields.size()); i.hasPrevious();)
569                 {
570                     i.previous().removeFromRequest();
571                 }
572             }
573         }
574     }
575 
576     /**
577      * To be used in the event this group is used within multiple
578      * forms within the same template.
579      */
580     public void resetDeclared()
581     {
582         isDeclared = false;
583     }
584 
585     /**
586      * A xhtml valid hidden input field that notifies intake of the
587      * group's presence.
588      *
589      * @return a <code>String</code> value
590      */
591     public String getHtmlFormInput()
592     {
593         StringBuilder sb = new StringBuilder(64);
594         appendHtmlFormInput(sb);
595         return sb.toString();
596     }
597 
598     /**
599      * A xhtml valid hidden input field that notifies intake of the
600      * group's presence.
601      *
602      * @param sb the string builder to append the HTML to
603      */
604     public void appendHtmlFormInput(StringBuilder sb)
605     {
606         if (!isDeclared)
607         {
608             isDeclared = true;
609             sb.append("<input type=\"hidden\" name=\"")
610                     .append(gid)
611                     .append("\" value=\"")
612                     .append(oid)
613                     .append("\"/>\n");
614         }
615     }
616 
617     /**
618      * Creates a string representation of this input group. This
619      * is an xml representation.
620      */
621     @Override
622     public String toString()
623     {
624         StringBuilder result = new StringBuilder();
625 
626         result.append("<group name=\"").append(getIntakeGroupName()).append("\"");
627         result.append(" key=\"").append(getGID()).append("\"");
628         result.append(">\n");
629 
630         if (fields != null)
631         {
632             for (Field<?> field : fields)
633             {
634                 result.append(field);
635             }
636         }
637 
638         result.append("</group>\n");
639 
640         return result.toString();
641     }
642 
643     /**
644      * Get the parent AppData for this group
645      *
646      * @return the parent
647      */
648     public AppData getAppData()
649     {
650         return parent;
651     }
652 
653     /**
654      * JAXB callback to set the parent object
655      *
656      * @param um the Unmarshaller
657      * @param parent the parent object (an AppData object)
658      */
659     public void afterUnmarshal(Unmarshaller um, Object parent)
660     {
661         this.parent = (AppData)parent;
662 
663         // Build map
664         fieldsByName = new HashMap<String, Field<?>>((int) (1.25 * fields.size() + 1));
665 
666         for (Field<?> field : fields)
667         {
668             fieldsByName.put(field.getName(), field);
669         }
670 
671         Map<String, List<Field<?>>> mapToObjectFieldLists =
672                 new HashMap<String, List<Field<?>>>((int) (1.25 * fields.size() + 1));
673 
674         // Fix fields
675         for (Field<?> field : fields)
676         {
677             if (StringUtils.isNotEmpty(field.mapToObject))
678             {
679                 field.mapToObject = this.parent.getBasePackage() + field.mapToObject;
680             }
681 
682             // map fields by their mapToObject
683             List<Field<?>> tmpFields = mapToObjectFieldLists.get(field.getMapToObject());
684             if (tmpFields == null)
685             {
686                 tmpFields = new ArrayList<Field<?>>(fields.size());
687                 mapToObjectFieldLists.put(field.getMapToObject(), tmpFields);
688             }
689 
690             tmpFields.add(field);
691         }
692 
693         // Change the mapToObjectFields values to Field[]
694         mapToObjectFields = new HashMap<String, Field<?>[]>((int) (1.25 * fields.size() + 1));
695 
696         for (Map.Entry<String, List<Field<?>>> entry : mapToObjectFieldLists.entrySet())
697         {
698             mapToObjectFields.put(entry.getKey(),
699                 entry.getValue().toArray(new Field[entry.getValue().size()]));
700         }
701     }
702 
703     // ********** PoolableObjectFactory implementation ******************
704 
705     public static class GroupFactory
706             extends BaseKeyedPooledObjectFactory<String, Group>
707     {
708         private final AppData appData;
709 
710         public GroupFactory(AppData appData)
711         {
712             this.appData = appData;
713         }
714 
715         /**
716          * Creates an instance that can be returned by the pool.
717          * @param key the name of the group
718          * @return an instance that can be returned by the pool.
719          * @throws IntakeException indicates that the group could not be retrieved
720          */
721         @Override
722         public Group create(String key) throws IntakeException
723         {
724             return appData.getGroup(key);
725         }
726 
727         /**
728          * @see org.apache.commons.pool2.BaseKeyedPooledObjectFactory#wrap(java.lang.Object)
729          */
730         @Override
731         public PooledObject<Group> wrap(Group group)
732         {
733             return new DefaultPooledObject<Group>(group);
734         }
735 
736         /**
737          * Uninitialize an instance to be returned to the pool.
738          * @param key the name of the group
739          * @param pooledGroup the instance to be passivated
740          */
741         @Override
742         public void passivateObject(String key, PooledObject<Group> pooledGroup)
743         {
744             Group group = pooledGroup.getObject();
745             group.oid = null;
746             group.pp = null;
747             for (ListIterator<Field<?>> i = group.fields.listIterator(group.fields.size());
748                     i.hasPrevious();)
749             {
750                 i.previous().dispose();
751             }
752             group.isDeclared = false;
753         }
754     }
755 }
756 
757