001    // Copyright 2004, 2005 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry.components;
016    
017    import java.util.ArrayList;
018    import java.util.Collections;
019    import java.util.HashMap;
020    import java.util.Iterator;
021    import java.util.List;
022    import java.util.Map;
023    
024    import org.apache.hivemind.ApplicationRuntimeException;
025    import org.apache.hivemind.HiveMind;
026    import org.apache.tapestry.IBinding;
027    import org.apache.tapestry.IForm;
028    import org.apache.tapestry.IMarkupWriter;
029    import org.apache.tapestry.IRequestCycle;
030    import org.apache.tapestry.Tapestry;
031    import org.apache.tapestry.TapestryUtils;
032    import org.apache.tapestry.coerce.ValueConverter;
033    import org.apache.tapestry.engine.NullWriter;
034    import org.apache.tapestry.form.AbstractFormComponent;
035    import org.apache.tapestry.markup.NestedMarkupWriterImpl;
036    import org.apache.tapestry.services.ComponentRenderWorker;
037    import org.apache.tapestry.services.DataSqueezer;
038    import org.apache.tapestry.services.ExpressionEvaluator;
039    import org.apache.tapestry.services.ResponseBuilder;
040    
041    /**
042     * @author mb
043     * @since 4.0
044     * @see org.apache.tapestry.components.IPrimaryKeyConverter
045     * @see org.apache.tapestry.util.DefaultPrimaryKeyConverter
046     */
047    public abstract class ForBean extends AbstractFormComponent
048    {
049        // constants
050        
051        /**
052         * Prefix on the hidden value stored into the field to indicate the the actual value is stored
053         * (this is used when there is no primary key converter). The remainder of the string is a
054         * {@link DataSqueezer squeezed} representation of the value.
055         */
056        private static final char DESC_VALUE = 'V';
057    
058        /**
059         * Prefix on the hidden value stored into the field that indicates the primary key of the
060         * iterated value is stored; the remainder of the string is a {@link DataSqueezer squeezed}
061         * representation of the primary key. The {@link IPrimaryKeyConverter converter} is used to
062         * obtain the value from this key.
063         */
064        private static final char DESC_PRIMARY_KEY = 'P';
065    
066        private final RepSource _completeRepSource = new CompleteRepSource();
067    
068        private final RepSource _keyExpressionRepSource = new KeyExpressionRepSource();
069    
070        // intermediate members
071        private Object _value;
072    
073        private int _index;
074    
075        private boolean _rendering;
076        
077        private boolean _hasNext = false;
078        
079        // parameters
080        public abstract boolean getRenderTag();
081        
082        public abstract String getElement();
083        
084        public abstract String getKeyExpression();
085    
086        public abstract IPrimaryKeyConverter getConverter();
087    
088        public abstract Object getDefaultValue();
089    
090        public abstract boolean getMatch();
091    
092        public abstract boolean getVolatile();
093    
094        // injects
095        public abstract DataSqueezer getDataSqueezer();
096    
097        public abstract ValueConverter getValueConverter();
098    
099        public abstract ExpressionEvaluator getExpressionEvaluator();
100        
101        public abstract ComponentRenderWorker getRenderWorker();
102        
103        public abstract ResponseBuilder getResponseBuilder();
104        
105        public boolean hasNext()
106        {
107            return _hasNext;
108        }
109        
110        /**
111         * Gets the source binding and iterates through its values. For each, it updates the value
112         * binding and render's its wrapped elements.
113         */
114        protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
115        {
116            // form may be null if component is not located in a form
117            IForm form = (IForm) cycle.getAttribute(TapestryUtils.FORM_ATTRIBUTE);
118            
119            // If the cycle is rewinding, but not this particular form,
120            // then do nothing (don't even render the body).
121            
122            boolean cycleRewinding = cycle.isRewinding();
123            if (cycleRewinding && form != null && !form.isRewinding())
124                return;
125            
126            setForm(form);
127            
128            // Get the data to be iterated upon. Store in form if needed.
129            
130            Iterator dataSource = getData(cycle, form);
131    
132            // Do not iterate if dataSource is null.
133            // The dataSource was either not convertable to Iterator, or was empty.
134            
135            if (dataSource == null)
136                return;
137            
138            if (!cycleRewinding && form != null && !NullWriter.class.isInstance(writer))
139                form.setFormFieldUpdating(true);
140            
141            String element = HiveMind.isNonBlank(getElement()) ? getElement() : getTemplateTagName();
142            
143            boolean render = !cycleRewinding && HiveMind.isNonBlank(element) && getRenderTag();
144            
145            IMarkupWriter loopWriter = writer;
146            
147            // Perform the iterations
148            try
149            {
150                _index = 0;
151                _rendering = true;
152                
153                while (dataSource.hasNext())
154                {
155                    _hasNext = true;
156                    
157                    // Get current value
158                    _value = dataSource.next();
159                    
160                    // Update output component parameters
161                    updateOutputParameters();
162                    
163                    // Render component
164                    // swap out writers if necessary
165                    
166                    if (getResponseBuilder().isDynamic()
167                            && getResponseBuilder().contains(this) 
168                            && !NestedMarkupWriterImpl.class.isInstance(writer)) {
169                        
170                        loopWriter = getResponseBuilder().getWriter(getClientId(), ResponseBuilder.ELEMENT_TYPE);
171                    }
172                    
173                    if (render) {
174                        
175                        loopWriter.begin(element);
176                        
177                        renderInformalParameters(loopWriter, cycle);
178                        renderIdAttribute(loopWriter, cycle);
179                    }
180                    
181                    renderBody(loopWriter, cycle);
182                    
183                    if (render) {
184                        
185                        loopWriter.end(element);
186                    }
187                    
188                    _index++;
189                    
190                    _hasNext = dataSource.hasNext();
191                    
192                    // TODO: Fragile / messy
193                    // Cause unique client id to be generated as well as event connection
194                    // works or other after render workers. (basically reproduce what happens
195                    // inside of AbstractComponent.render() . Perhaps this means it's time for
196                    // refactoring of this logic, like deferring rendering to an actual component
197                    // that can have its proper render() method invoked multiple times.
198                    
199                    getRenderWorker().renderComponent(cycle, this);
200                    generateClientId();
201                    
202                    // set loopWriter back to original as the client ids/ etc change on each loop
203                    
204                    loopWriter = writer;
205                }
206            }
207            finally
208            {
209                _rendering = false;
210                _value = null;
211            }
212        }
213        
214        /**
215         * Overriden so that RenderWorker doesn't get run as we've been invoking
216         * it manually already.
217         */
218        protected void cleanupAfterRender(IRequestCycle cycle)
219        {
220        }
221        
222        protected void generateClientId()
223        {
224            String id = getSpecifiedId();
225            
226            if (id != null && getPage() != null && getPage().getRequestCycle() != null)
227                 setClientId(getPage().getRequestCycle().getUniqueId(TapestryUtils.convertTapestryIdToNMToken(id)));
228        }
229        
230        /**
231         * Returns the most recent value extracted from the source parameter.
232         *
233         */
234    
235        public final Object getValue()
236        {
237            if (!_rendering)
238                throw Tapestry.createRenderOnlyPropertyException(this, "value");
239    
240            return _value;
241        }
242    
243        /**
244         * The index number, within the {@link #getStoredData(IRequestCycle, String) }, of the the current value.
245         * 
246         */
247    
248        public int getIndex()
249        {
250            if (!_rendering)
251                throw Tapestry.createRenderOnlyPropertyException(this, "index");
252    
253            return _index;
254        }
255    
256        public boolean isDisabled()
257        {
258            return false;
259        }
260    
261        /**
262         * Updates the index and value output parameters if bound.
263         */
264        protected void updateOutputParameters()
265        {
266            IBinding indexBinding = getBinding("index");
267            if (indexBinding != null)
268                indexBinding.setObject(new Integer(_index));
269    
270            IBinding valueBinding = getBinding("value");
271            if (valueBinding != null)
272                valueBinding.setObject(_value);
273        }
274    
275        /**
276         * Updates the primaryKeys parameter if bound.
277         */
278        protected void updatePrimaryKeysParameter(String[] stringReps)
279        {
280            IBinding primaryKeysBinding = getBinding("primaryKeys");
281            if (primaryKeysBinding == null)
282                return;
283    
284            DataSqueezer squeezer = getDataSqueezer();
285    
286            int repsCount = stringReps.length;
287            List primaryKeys = new ArrayList(repsCount);
288            for (int i = 0; i < stringReps.length; i++)
289            {
290                String rep = stringReps[i];
291                if (rep.length() == 0 || rep.charAt(0) != DESC_PRIMARY_KEY)
292                    continue;
293                Object primaryKey = squeezer.unsqueeze(rep.substring(1));
294                primaryKeys.add(primaryKey);
295            }
296    
297            primaryKeysBinding.setObject(primaryKeys);
298        }
299    
300        // Do nothing in those methods, but make the JVM happy
301        protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle)
302        {
303        }
304    
305        protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle)
306        {
307        }
308    
309        /**
310         * Returns a list with the values to be iterated upon. The list is obtained in different ways: -
311         * If the component is not located in a form or 'volatile' is set to true, then the simply the
312         * values passed to 'source' are returned (same as Foreach) - If the component is in a form, and
313         * the form is rewinding, the values stored in the form are returned -- rewind is then always
314         * the same as render. - If the component is in a form, and the form is being rendered, the
315         * values are stored in the form as Hidden fields.
316         * 
317         * @param cycle
318         *            The current request cycle
319         * @param form
320         *            The form within which the component is located (if any)
321         * @return An iterator with the values to be cycled upon
322         */
323        private Iterator getData(IRequestCycle cycle, IForm form)
324        {
325            if (form == null || getVolatile())
326                return evaluateSourceIterator();
327            
328            String name = form.getElementId(this);
329            
330            if (cycle.isRewinding())
331                return getStoredData(cycle, name);
332            
333            return storeSourceData(form, name);
334        }
335    
336        /**
337         * Returns a list of the values stored as Hidden fields in the form. A conversion is performed
338         * if the primary key of the value is stored.
339         * 
340         * @param cycle
341         *            The current request cycle
342         * @param name
343         *            The name of the HTTP parameter whether the values
344         * @return an iterator with the values stored in the provided Hidden fields
345         */
346        protected Iterator getStoredData(IRequestCycle cycle, String name)
347        {
348            String[] stringReps = cycle.getParameters(name);
349            if (stringReps == null)
350                return null;
351    
352            updatePrimaryKeysParameter(stringReps);
353    
354            return new ReadSourceDataIterator(stringReps);
355        }
356    
357        /**
358         * Pulls data from successive strings (posted by client-side hidden fields); each string
359         * representation may be either a value or a primary key.
360         */
361        private class ReadSourceDataIterator implements Iterator
362        {
363            private final Iterator _sourceIterator = evaluateSourceIterator();
364    
365            private final Iterator _fullSourceIterator = evaluateFullSourceIterator();
366    
367            private final String[] _stringReps;
368    
369            private int _index = 0;
370    
371            private final Map _repToValueMap = new HashMap();
372    
373            ReadSourceDataIterator(String[] stringReps)
374            {
375                _stringReps = stringReps;
376            }
377    
378            public boolean hasNext()
379            {
380                return _index < _stringReps.length;
381            }
382    
383            public Object next()
384            {
385                String rep = _stringReps[_index++];
386    
387                return getValueFromStringRep(_sourceIterator, _fullSourceIterator, _repToValueMap, rep);
388            }
389    
390            public void remove()
391            {
392                throw new UnsupportedOperationException("remove()");
393            }
394    
395        }
396    
397        /**
398         * Stores the provided data in the form and then returns the data as an iterator. If the primary
399         * key of the value can be determined, then that primary key is saved instead.
400         * 
401         * @param form
402         *            The form where the data will be stored
403         * @param name
404         *            The name under which the data will be stored
405         * @return an iterator with the bound values stored in the form
406         */
407        protected Iterator storeSourceData(IForm form, String name)
408        {
409            return new StoreSourceDataIterator(form, name, evaluateSourceIterator());
410        }
411    
412        /**
413         * Iterates over a set of values, using {@link ForBean#getStringRepFromValue(Object)} to obtain
414         * the correct client-side string representation, and working with the form to store each
415         * successive value into the form.
416         */
417        private class StoreSourceDataIterator implements Iterator
418        {
419            private final IForm _form;
420    
421            private final String _name;
422    
423            private final Iterator _delegate;
424    
425            StoreSourceDataIterator(IForm form, String name, Iterator delegate)
426            {
427                _form = form;
428                _name = name;
429                _delegate = delegate;
430            }
431    
432            public boolean hasNext()
433            {
434                return _delegate.hasNext();
435            }
436    
437            public Object next()
438            {
439                Object value = _delegate.next();
440    
441                
442                
443                String rep = getStringRepFromValue(value);
444    
445                _form.addHiddenValue(_name, rep);
446    
447                return value;
448            }
449    
450            public void remove()
451            {
452                throw new UnsupportedOperationException("remove()");
453            }
454        }
455    
456        /**
457         * Returns the string representation of the value. The first letter of the string representation
458         * shows whether a value or a primary key is being described.
459         * 
460         * @param value
461         * @return The string representation of the given value.
462         */
463        protected String getStringRepFromValue(Object value)
464        {
465            String rep;
466            
467            DataSqueezer squeezer = getDataSqueezer();
468            
469            // try to extract the primary key from the value
470            
471            Object pk = getPrimaryKeyFromValue(value);
472            
473            try {
474    
475                if (pk != null) {
476    
477                    // Primary key was extracted successfully.
478                    rep = DESC_PRIMARY_KEY + squeezer.squeeze(pk);
479                } else {
480    
481                    // primary key could not be extracted. squeeze value.
482                    rep = DESC_VALUE + squeezer.squeeze(value);
483                }
484                
485            } catch (Exception e) {
486                throw new ApplicationRuntimeException(ComponentMessages.keySqueezeError(this, value, e), this, getLocation(), e);
487            }
488            
489            return rep;
490        }
491    
492        /**
493         * Returns the primary key of the given value. Uses the 'keyExpression' or the 'converter' (if
494         * either is provided).
495         * 
496         * @param value
497         *            The value from which the primary key should be extracted
498         * @return The primary key of the value, or null if such cannot be extracted.
499         */
500        protected Object getPrimaryKeyFromValue(Object value)
501        {
502            if (value == null)
503                return null;
504            
505            Object primaryKey = getKeyExpressionFromValue(value);
506            
507            if (primaryKey == null)
508                primaryKey = getConverterFromValue(value);
509    
510            return primaryKey;
511        }
512    
513        /**
514         * Uses the 'keyExpression' parameter to determine the primary key of the given value.
515         * 
516         * @param value
517         *            The value from which the primary key should be extracted
518         * @return The primary key of the value as defined by 'keyExpression', or null if such cannot be
519         *         extracted.
520         */
521        protected Object getKeyExpressionFromValue(Object value)
522        {
523            String keyExpression = getKeyExpression();
524            if (keyExpression == null)
525                return null;
526    
527            Object primaryKey = getExpressionEvaluator().read(value, keyExpression);
528            return primaryKey;
529        }
530    
531        /**
532         * Uses the 'converter' parameter to determine the primary key of the given value.
533         * 
534         * @param value
535         *            The value from which the primary key should be extracted
536         * @return The primary key of the value as provided by the converter, or null if such cannot be
537         *         extracted.
538         */
539        protected Object getConverterFromValue(Object value)
540        {
541            IPrimaryKeyConverter converter = getConverter();
542            if (converter == null)
543                return null;
544    
545            Object primaryKey = converter.getPrimaryKey(value);
546            
547            return primaryKey;
548        }
549    
550        /**
551         * Determines the value that corresponds to the given string representation. If the 'match'
552         * parameter is true, attempt to find a value in 'source' or 'fullSource' that generates the
553         * same string representation. Otherwise, create a new value from the string representation.
554         * 
555         * @param rep
556         *            the string representation for which a value should be returned
557         * @return the value that corresponds to the provided string representation
558         */
559        protected Object getValueFromStringRep(Iterator sourceIterator, Iterator fullSourceIterator,
560                Map repToValueMap, String rep)
561        {
562            Object value = null;
563            DataSqueezer squeezer = getDataSqueezer();
564    
565            // Check if the string rep is empty. If so, just return the default value.
566            if (rep == null || rep.length() == 0)
567                return getDefaultValue();
568            
569            // If required, find a value with an equivalent string representation and return it
570            boolean match = getMatch();
571            if (match)
572            {
573                value = findValueWithStringRep( sourceIterator, fullSourceIterator, repToValueMap,
574                        rep, _completeRepSource);
575                
576                if (value != null)
577                    return value;
578            }
579    
580            // Matching of the string representation was not successful or was disabled.
581            // Use the standard approaches to obtain the value from the rep.
582            
583            char desc = rep.charAt(0);
584            String squeezed = rep.substring(1);
585            switch (desc)
586            {
587                case DESC_VALUE:
588                    // If the string rep is just the value itself, unsqueeze it
589                    value = squeezer.unsqueeze(squeezed);
590                    break;
591                    
592                case DESC_PRIMARY_KEY:
593                    // Perform keyExpression match if not already attempted
594                    if (!match && getKeyExpression() != null)
595                        value = findValueWithStringRep(
596                                sourceIterator,
597                                fullSourceIterator,
598                                repToValueMap,
599                                rep,
600                                _keyExpressionRepSource);
601    
602                    // If 'converter' is defined, try to perform conversion from primary key to value
603                    if (value == null)
604                    {
605                        IPrimaryKeyConverter converter = getConverter();
606                        if (converter != null)
607                        {
608                            Object pk = squeezer.unsqueeze(squeezed);
609                            value = converter.getValue(pk);
610                        }
611                    }
612                    break;
613            }
614    
615            if (value == null)
616                value = getDefaultValue();
617    
618            return value;
619        }
620    
621        /**
622         * Attempt to find a value in 'source' or 'fullSource' that generates the provided string
623         * representation. Use the RepSource interface to determine what the string representation of a
624         * particular value is.
625         * 
626         * @param rep
627         *            the string representation for which a value should be returned
628         * @param repSource
629         *            an interface providing the string representation of a given value
630         * @return the value in 'source' or 'fullSource' that corresponds to the provided string
631         *         representation
632         */
633        protected Object findValueWithStringRep(Iterator sourceIterator, Iterator fullSourceIterator,
634                Map repToValueMap, String rep, RepSource repSource)
635        {
636            Object value = repToValueMap.get(rep);
637            if (value != null)
638                return value;
639    
640            value = findValueWithStringRepInIterator(sourceIterator, repToValueMap, rep, repSource);
641            if (value != null)
642                return value;
643            
644            value = findValueWithStringRepInIterator(fullSourceIterator, repToValueMap, rep, repSource);
645            return value;
646        }
647    
648        /**
649         * Attempt to find a value in the provided collection that generates the required string
650         * representation. Use the RepSource interface to determine what the string representation of a
651         * particular value is.
652         * 
653         * @param rep
654         *            the string representation for which a value should be returned
655         * @param repSource
656         *            an interface providing the string representation of a given value
657         * @param it
658         *            the iterator of the collection in which a value should be searched
659         * @return the value in the provided collection that corresponds to the required string
660         *         representation
661         */
662        protected Object findValueWithStringRepInIterator(Iterator it, Map repToValueMap, String rep,
663                RepSource repSource)
664        {
665            while (it.hasNext())
666            {
667                Object sourceValue = it.next();
668                if (sourceValue == null)
669                    continue;
670                
671                String sourceRep = repSource.getStringRep(sourceValue);
672                repToValueMap.put(sourceRep, sourceValue);
673                
674                if (rep.equals(sourceRep))
675                    return sourceValue;
676            }
677    
678            return null;
679        }
680    
681        /**
682         * Returns a new iterator of the values in 'source'.
683         * 
684         * @return the 'source' iterator
685         */
686        protected Iterator evaluateSourceIterator()
687        {
688            Iterator it = null;
689            Object source = null;
690    
691            IBinding sourceBinding = getBinding("source");
692            if (sourceBinding != null)
693                source = sourceBinding.getObject();
694            
695            if (source != null)
696                it = (Iterator) getValueConverter().coerceValue(source, Iterator.class);
697            
698            if (it == null)
699                it = Collections.EMPTY_LIST.iterator();
700    
701            return it;
702        }
703    
704        /**
705         * Returns a new iterator of the values in 'fullSource'.
706         * 
707         * @return the 'fullSource' iterator
708         */
709        protected Iterator evaluateFullSourceIterator()
710        {
711            Iterator it = null;
712            Object fullSource = null;
713    
714            IBinding fullSourceBinding = getBinding("fullSource");
715            if (fullSourceBinding != null)
716                fullSource = fullSourceBinding.getObject();
717    
718            if (fullSource != null)
719                it = (Iterator) getValueConverter().coerceValue(fullSource, Iterator.class);
720    
721            if (it == null)
722                it = Collections.EMPTY_LIST.iterator();
723    
724            return it;
725        }
726    
727        /**
728         * An interface that provides the string representation of a given value.
729         */
730        protected interface RepSource
731        {
732            String getStringRep(Object value);
733        }
734    
735        /**
736         * An implementation of RepSource that provides the string representation of the given value
737         * using all methods.
738         */
739        protected class CompleteRepSource implements RepSource
740        {
741            public String getStringRep(Object value)
742            {
743                return getStringRepFromValue(value);
744            }
745        }
746    
747        /**
748         * An implementation of RepSource that provides the string representation of the given value
749         * using just the 'keyExpression' parameter.
750         */
751        protected class KeyExpressionRepSource implements RepSource
752        {
753            public String getStringRep(Object value)
754            {
755                Object pk = getKeyExpressionFromValue(value);
756                return DESC_PRIMARY_KEY + getDataSqueezer().squeeze(pk);
757            }
758        }
759    
760        /**
761         * For component can not take focus.
762         */
763        protected boolean getCanTakeFocus()
764        {
765            return false;
766        }
767    
768        public String getDisplayName()
769        {
770            return null;
771        }
772    
773    }