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