001    package org.apache.fulcrum.intake.model;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one
005     * or more contributor license agreements.  See the NOTICE file
006     * distributed with this work for additional information
007     * regarding copyright ownership.  The ASF licenses this file
008     * to you under the Apache License, Version 2.0 (the
009     * "License"); you may not use this file except in compliance
010     * with the License.  You may obtain a copy of the License at
011     *
012     *   http://www.apache.org/licenses/LICENSE-2.0
013     *
014     * Unless required by applicable law or agreed to in writing,
015     * software distributed under the License is distributed on an
016     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017     * KIND, either express or implied.  See the License for the
018     * specific language governing permissions and limitations
019     * under the License.
020     */
021    
022    import java.util.ArrayList;
023    import java.util.HashMap;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.Map;
027    
028    import org.apache.commons.logging.Log;
029    import org.apache.commons.logging.LogFactory;
030    import org.apache.commons.pool.BaseKeyedPoolableObjectFactory;
031    import org.apache.fulcrum.intake.IntakeException;
032    import org.apache.fulcrum.intake.IntakeServiceFacade;
033    import org.apache.fulcrum.intake.Retrievable;
034    import org.apache.fulcrum.intake.xmlmodel.AppData;
035    import org.apache.fulcrum.intake.xmlmodel.XmlField;
036    import org.apache.fulcrum.intake.xmlmodel.XmlGroup;
037    import org.apache.fulcrum.parser.ValueParser;
038    
039    /**
040     * Holds a group of Fields
041     *
042     * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
043     * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
044     * @author <a href="mailto:quintonm@bellsouth.net">Quinton McCombs</a>
045     * @version $Id: Group.java 832048 2009-11-02 18:55:08Z tv $
046     */
047    public class Group
048    {
049        public static final String EMPTY = "";
050    
051        /*
052         * An id representing a new object.
053         */
054        public static final String NEW = "_0";
055    
056        private static final Log log;
057        private static final boolean isDebugEnabled;
058    
059        static
060        {
061            log = LogFactory.getLog(Group.class);
062            isDebugEnabled = log.isDebugEnabled();
063        }
064    
065        /**
066         * The key used to represent this group in a parameter.
067         * This key is usually a prefix as part of a field key.
068         */
069        protected final String gid;
070    
071        /**
072         * The name used in templates and java code to refer to this group.
073         */
074        protected final String name;
075    
076        /**
077         * The number of Groups with the same name that will be pooled.
078         */
079        private final int poolCapacity;
080    
081        /**
082         * A map of the fields in this group mapped by field name.
083         */
084        protected Map fields;
085    
086        /**
087         * Map of the fields by mapToObject
088         */
089        protected Map mapToObjectFields;
090    
091        /**
092         * An array of fields in this group.
093         */
094        protected Field[] fieldsArray;
095    
096        /**
097         * The object id used to associate this group to a bean
098         * for one request cycle
099         */
100        protected String oid;
101    
102        /**
103         * The object containing the request data
104         */
105        protected ValueParser pp;
106    
107        /**
108         * A flag to help prevent duplicate hidden fields declaring this group.
109         */
110        protected boolean isDeclared;
111    
112        /**
113         * Constructs a new Group based on the xml specification.  Groups are
114         * instantiated and pooled by the IntakeService and should not
115         * be instantiated otherwise.
116         *
117         * @param group a <code>XmlGroup</code> value
118         * @exception IntakeException if an error occurs in other classes
119         */
120        public Group(XmlGroup group) throws IntakeException
121        {
122            gid = group.getKey();
123            name = group.getName();
124            poolCapacity = Integer.parseInt(group.getPoolCapacity());
125    
126            List inputFields = group.getFields();
127            int size = inputFields.size();
128            fields = new HashMap((int) (1.25 * size + 1));
129            mapToObjectFields = new HashMap((int) (1.25 * size + 1));
130            fieldsArray = new Field[size];
131            for (int i = size - 1; i >= 0; i--)
132            {
133                XmlField f = (XmlField) inputFields.get(i);
134                Field field = FieldFactory.getInstance(f, this);
135                fieldsArray[i] = field;
136                fields.put(f.getName(), field);
137    
138                // map fields by their mapToObject
139                List tmpFields = (List) mapToObjectFields.get(f.getMapToObject());
140                if (tmpFields == null)
141                {
142                    tmpFields = new ArrayList(size);
143                    mapToObjectFields.put(f.getMapToObject(), tmpFields);
144                }
145                tmpFields.add(field);
146            }
147    
148            // Change the mapToObjectFields values to Field[]
149            for (Iterator keys = mapToObjectFields.keySet().iterator(); keys.hasNext();)
150            {
151                Object key = keys.next();
152                List tmpFields = (List) mapToObjectFields.get(key);
153                mapToObjectFields.put(key,
154                        tmpFields.toArray(new Field[tmpFields.size()]));
155            }
156        }
157    
158        /**
159         * Initializes the default Group using parameters.
160         *
161         * @param pp a <code>ValueParser</code> value
162         * @return this Group
163         */
164        public Group init(ValueParser pp) throws IntakeException
165        {
166            return init(NEW, pp);
167        }
168    
169        /**
170         * Initializes the Group with parameters from RunData
171         * corresponding to key.
172         *
173         * @param pp a <code>ValueParser</code> value
174         * @return this Group
175         */
176        public Group init(String key, ValueParser pp) throws IntakeException
177        {
178            this.oid = key;
179            this.pp = pp;
180            for (int i = fieldsArray.length - 1; i >= 0; i--)
181            {
182                fieldsArray[i].init(pp);
183            }
184            return this;
185        }
186    
187        /**
188         * Initializes the group with properties from an object.
189         *
190         * @param obj a <code>Persistent</code> value
191         * @return a <code>Group</code> value
192         */
193        public Group init(Retrievable obj)
194        {
195            this.oid = obj.getQueryKey();
196    
197            Class cls = obj.getClass();
198            while (cls != null)
199            {
200                Field[] flds = (Field[]) mapToObjectFields.get(cls.getName());
201                if (flds != null)
202                {
203                    for (int i = flds.length - 1; i >= 0; i--)
204                    {
205                        flds[i].init(obj);
206                    }
207                }
208    
209                // Also check any interfaces
210                Class[] interfaces = cls.getInterfaces();
211                for (int idx = 0; idx < interfaces.length; idx++)
212                {
213                    Field[] interfaceFields =
214                        (Field[]) mapToObjectFields.get(interfaces[idx].getName());
215                    if (interfaceFields != null)
216                    {
217                        for (int i = 0; i < interfaceFields.length; i++)
218                        {
219                            interfaceFields[i].init(obj);
220                        }
221                    }
222                }
223                
224                cls = cls.getSuperclass();
225            }
226    
227            return this;
228        }
229    
230        /**
231         * Gets a list of the names of the fields stored in this object.
232         *
233         * @return A String array containing the list of names.
234         */
235        public String[] getFieldNames()
236        {
237            String nameList[] = new String[fieldsArray.length];
238            for (int i = 0; i < nameList.length; i++)
239            {
240                nameList[i] = fieldsArray[i].name;
241            }
242            return nameList;
243        }
244    
245        /**
246         * Return the name given to this group.  The long name is to
247         * avoid conflicts with the get(String key) method.
248         *
249         * @return a <code>String</code> value
250         */
251        public String getIntakeGroupName()
252        {
253            return name;
254        }
255    
256        /**
257         * Get the number of Group objects that will be pooled.
258         *
259         * @return an <code>int</code> value
260         */
261        public int getPoolCapacity()
262        {
263            return poolCapacity;
264        }
265    
266        /**
267         * Get the part of the key used to specify the group.
268         * This is specified in the key attribute in the xml file.
269         *
270         * @return a <code>String</code> value
271         */
272        public String getGID()
273        {
274            return gid;
275        }
276    
277        /**
278         * Get the part of the key that distinguishes a group
279         * from others of the same name.
280         *
281         * @return a <code>String</code> value
282         */
283        public String getOID()
284        {
285            return oid;
286        }
287    
288        /**
289         * Concatenation of gid and oid.
290         *
291         * @return a <code>String</code> value
292         */
293        public String getObjectKey()
294        {
295            return gid + oid;
296        }
297    
298        /**
299         * Describe <code>getObjects</code> method here.
300         *
301         * @param pp a <code>ValueParser</code> value
302         * @return an <code>ArrayList</code> value
303         * @exception IntakeException if an error occurs
304         */
305        public ArrayList getObjects(ValueParser pp) throws IntakeException
306        {
307            ArrayList objs = null;
308            String[] oids = pp.getStrings(gid);
309            if (oids != null)
310            {
311                objs = new ArrayList(oids.length);
312                for (int i = oids.length - 1; i >= 0; i--)
313                {
314                    objs.add(IntakeServiceFacade.getGroup(name).init(oids[i], pp));
315                }
316            }
317            return objs;
318        }
319    
320        /**
321         * Get the Field .
322         * @return Field.
323         * @throws IntakeException indicates the field could not be found.
324         */
325        public Field get(String fieldName)
326                throws IntakeException
327        {
328            if (fields.containsKey(fieldName))
329            {
330                return (Field) fields.get(fieldName);
331            }
332            else
333            {
334                throw new IntakeException("Intake Field name: " + fieldName +
335                        " not found in Group " + name);
336            }
337        }
338    
339        /**
340         * Performs an AND between all the fields in this group.
341         *
342         * @return a <code>boolean</code> value
343         */
344        public boolean isAllValid()
345        {
346            boolean valid = true;
347            for (int i = fieldsArray.length - 1; i >= 0; i--)
348            {
349                valid &= fieldsArray[i].isValid();
350                if (isDebugEnabled && !fieldsArray[i].isValid())
351                {
352                    log.debug("Group(" + oid + "): " + name + "; Field: "
353                            + fieldsArray[i].name + "; value=" +
354                            fieldsArray[i].getValue() + " is invalid!");
355                }
356            }
357            return valid;
358        }
359    
360        /**
361         * Calls a setter methods on obj, for fields which have been set.
362         *
363         * @param obj Object to be set with the values from the group.
364         * @throws IntakeException indicates that a failure occurred while
365         * executing the setter methods of the mapped object.
366         */
367        public void setProperties(Object obj) throws IntakeException
368        {
369            Class cls = obj.getClass();
370    
371            while (cls != null)
372            {
373                if (isDebugEnabled)
374                {
375                    log.debug("setProperties(" + cls.getName() + ")");
376                }
377    
378                Field[] flds = (Field[]) mapToObjectFields.get(cls.getName());
379                if (flds != null)
380                {
381                    for (int i = flds.length - 1; i >= 0; i--)
382                    {
383                        flds[i].setProperty(obj);
384                    }
385                }
386    
387                // Also check any interfaces
388                Class[] interfaces = cls.getInterfaces();
389                for (int idx = 0; idx < interfaces.length; idx++)
390                {
391                    Field[] interfaceFields =
392                        (Field[]) mapToObjectFields.get(interfaces[idx].getName());
393                    if (interfaceFields != null)
394                    {
395                        for (int i = 0; i < interfaceFields.length; i++)
396                        {
397                            interfaceFields[i].setProperty(obj);
398                        }
399                    }
400                }            
401    
402                cls = cls.getSuperclass();
403            }
404            
405            log.debug("setProperties() finished");
406        }
407    
408        /**
409         * Calls a setter methods on obj, for fields which pass validity tests.
410         * In most cases one should call Intake.isAllValid() and then if that
411         * test passes call setProperties.  Use this method when some data is
412         * known to be invalid, but you still want to set the object properties
413         * that are valid.
414         */
415        public void setValidProperties(Object obj)
416        {
417            Class cls = obj.getClass();
418            while (cls != null)
419            {
420                Field[] flds = (Field[]) mapToObjectFields.get(cls.getName());
421                if (flds != null)
422                {
423                    for (int i = flds.length - 1; i >= 0; i--)
424                    {
425                        try
426                        {
427                            flds[i].setProperty(obj);
428                        }
429                        catch (IntakeException e)
430                        {
431                            // just move on to next field
432                        }
433                    }
434                }
435    
436                // Also check any interfaces
437                Class[] interfaces = cls.getInterfaces();
438                for (int idx = 0; idx < interfaces.length; idx++)
439                {
440                    Field[] interfaceFields =
441                        (Field[]) mapToObjectFields.get(interfaces[idx].getName());
442                    if (interfaceFields != null)
443                    {
444                        for (int i = 0; i < interfaceFields.length; i++)
445                        {
446                            try
447                            {
448                                interfaceFields[i].setProperty(obj);
449                            }
450                            catch(IntakeException e)
451                            {
452                                // just move on to next field
453                            }
454                        }
455                    }
456                }            
457    
458                cls = cls.getSuperclass();
459            }
460        }
461    
462        /**
463         * Calls getter methods on objects that are known to Intake
464         * so that field values in forms can be initialized from
465         * the values contained in the intake tool.
466         *
467         * @param obj Object that will be used to as a source of data for
468         * setting the values of the fields within the group.
469         * @throws IntakeException indicates that a failure occurred while
470         * executing the setter methods of the mapped object.
471         */
472        public void getProperties(Object obj) throws IntakeException
473        {
474            Class cls = obj.getClass();
475            while (cls != null)
476            {
477                Field[] flds = (Field[]) mapToObjectFields.get(cls.getName());
478                if (flds != null)
479                {
480                    for (int i = flds.length - 1; i >= 0; i--)
481                    {
482                        flds[i].getProperty(obj);
483                    }
484                }
485    
486                // Also check any interfaces
487                Class[] interfaces = cls.getInterfaces();
488                for (int idx = 0; idx < interfaces.length; idx++)
489                {
490                    Field[] interfaceFields =
491                        (Field[]) mapToObjectFields.get(interfaces[idx].getName());
492                    if (interfaceFields != null)
493                    {
494                        for (int i = 0; i < interfaceFields.length; i++)
495                        {
496                            interfaceFields[i].getProperty(obj);
497                        }
498                    }
499                }
500    
501                cls = cls.getSuperclass();
502            }
503        }
504    
505        /**
506         * Removes references to this group and its fields from the
507         * query parameters
508         */
509        public void removeFromRequest()
510        {
511            if (pp != null)
512            {
513                String[] groups = pp.getStrings(gid);
514                if (groups != null)
515                {
516                    pp.remove(gid);
517                    for (int i = 0; i < groups.length; i++)
518                    {
519                        if (groups[i] != null && !groups[i].equals(oid))
520                        {
521                            pp.add(gid, groups[i]);
522                        }
523                    }
524                    for (int i = fieldsArray.length - 1; i >= 0; i--)
525                    {
526                        fieldsArray[i].removeFromRequest();
527                    }
528                }
529            }
530        }
531    
532        /**
533         * To be used in the event this group is used within multiple
534         * forms within the same template.
535         */
536        public void resetDeclared()
537        {
538            isDeclared = false;
539        }
540    
541        /**
542         * A xhtml valid hidden input field that notifies intake of the
543         * group's presence.
544         *
545         * @return a <code>String</code> value
546         */
547        public String getHtmlFormInput()
548        {
549            StringBuffer sb = new StringBuffer(64);
550            appendHtmlFormInput(sb);
551            return sb.toString();
552        }
553    
554        /**
555         * A xhtml valid hidden input field that notifies intake of the
556         * group's presence.
557         */
558        public void appendHtmlFormInput(StringBuffer sb)
559        {
560            if (!isDeclared)
561            {
562                isDeclared = true;
563                sb.append("<input type=\"hidden\" name=\"")
564                        .append(gid)
565                        .append("\" value=\"")
566                        .append(oid)
567                        .append("\"/>\n");
568            }
569        }
570    
571        // ********** PoolableObjectFactory implementation ******************
572    
573        public static class GroupFactory
574                extends BaseKeyedPoolableObjectFactory
575        {
576            private AppData appData;
577    
578            public GroupFactory(AppData appData)
579            {
580                this.appData = appData;
581            }
582    
583            /**
584             * Creates an instance that can be returned by the pool.
585             * @return an instance that can be returned by the pool.
586             * @throws IntakeException indicates that the group could not be retreived
587             */
588            public Object makeObject(Object key) throws IntakeException
589            {
590                return new Group(appData.getGroup((String) key));
591            }
592    
593            /**
594             * Uninitialize an instance to be returned to the pool.
595             * @param obj the instance to be passivated
596             */
597            public void passivateObject(Object key, Object obj)
598            {
599                Group group = (Group) obj;
600                group.oid = null;
601                group.pp = null;
602                for (int i = group.fieldsArray.length - 1; i >= 0; i--)
603                {
604                    group.fieldsArray[i].dispose();
605                }
606                group.isDeclared = false;
607            }
608        }
609    }
610    
611