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.contrib.table.components;
016
017import java.io.Serializable;
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Iterator;
021import java.util.List;
022
023import org.apache.hivemind.ApplicationRuntimeException;
024import org.apache.tapestry.BaseComponent;
025import org.apache.tapestry.IComponent;
026import org.apache.tapestry.IMarkupWriter;
027import org.apache.tapestry.IRequestCycle;
028import org.apache.tapestry.contrib.table.model.IAdvancedTableColumnSource;
029import org.apache.tapestry.contrib.table.model.IBasicTableModel;
030import org.apache.tapestry.contrib.table.model.ITableColumn;
031import org.apache.tapestry.contrib.table.model.ITableColumnModel;
032import org.apache.tapestry.contrib.table.model.ITableDataModel;
033import org.apache.tapestry.contrib.table.model.ITableModel;
034import org.apache.tapestry.contrib.table.model.ITableModelSource;
035import org.apache.tapestry.contrib.table.model.ITablePagingState;
036import org.apache.tapestry.contrib.table.model.ITableSessionStateManager;
037import org.apache.tapestry.contrib.table.model.ITableSessionStoreManager;
038import org.apache.tapestry.contrib.table.model.common.BasicTableModelWrap;
039import org.apache.tapestry.contrib.table.model.simple.SimpleListTableDataModel;
040import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumnModel;
041import org.apache.tapestry.contrib.table.model.simple.SimpleTableModel;
042import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
043import org.apache.tapestry.event.PageBeginRenderListener;
044import org.apache.tapestry.event.PageDetachListener;
045import org.apache.tapestry.event.PageEvent;
046
047/**
048 * A low level Table component that wraps all other low level Table components. This component
049 * carries the {@link org.apache.tapestry.contrib.table.model.ITableModel}that is used by the other
050 * Table components. Please see the documentation of
051 * {@link org.apache.tapestry.contrib.table.model.ITableModel}if you need to know more about how a
052 * table is represented.
053 * <p>
054 * This component also handles the saving of the state of the model using an
055 * {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}to determine what part
056 * of the model is to be saved and an
057 * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager}to determine how to
058 * save it.
059 * <p>
060 * Upon the beginning of a new request cycle when the table model is first needed, the model is
061 * obtained using the following process:
062 * <ul>
063 * <li>The persistent state of the table is loaded. If the tableSessionStoreManager binding has not
064 * been bound, the state is loaded from a persistent property within the component (it is null at
065 * the beginning). Otherwise the supplied
066 * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager}is used to load the
067 * persistent state.
068 * <li>The table model is recreated using the
069 * {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}that could be supplied
070 * using the tableSessionStateManager binding (but has a default value and is therefore not
071 * required).
072 * <li>If the {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}returns
073 * null, then a table model is taken from the tableModel binding. Thus, if the
074 * {@link org.apache.tapestry.contrib.table.model.common.NullTableSessionStateManager}is used, the
075 * table model would be taken from the tableModel binding every time.
076 * </ul>
077 * Just before the rendering phase the persistent state of the model is saved in the session. This
078 * process occurs in reverse:
079 * <ul>
080 * <li>The persistent state of the model is taken via the
081 * {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}.
082 * <li>If the tableSessionStoreManager binding has not been bound, the persistent state is saved as
083 * a persistent page property. Otherwise the supplied
084 * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager}is used to save the
085 * persistent state. Use of the
086 * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager}is usually necessary
087 * when tables with the same model have to be used across multiple pages, and hence the state has to
088 * be saved in the Visit, rather than in a persistent component property.
089 * </ul>
090 * <p>
091 * <p>
092 * Please see the Component Reference for details on how to use this component. [ <a
093 * href="../../../../../../../ComponentReference/contrib.TableView.html">Component Reference </a>]
094 * 
095 * @author mindbridge
096 */
097public abstract class TableView extends BaseComponent implements PageDetachListener,
098        PageBeginRenderListener, ITableModelSource
099{
100    /** @since 4.0 */
101    public abstract TableColumnModelSource getModelSource();
102
103    /** @since 4.0 */
104    public abstract IAdvancedTableColumnSource getColumnSource();
105    
106    // Component properties
107    private ITableSessionStateManager m_objDefaultSessionStateManager = null;
108
109    private ITableColumnModel m_objColumnModel = null;
110
111    // Transient objects
112    private ITableModel m_objTableModel;
113
114    private ITableModel m_objCachedTableModelValue;
115
116    // enhanced parameter methods
117    public abstract ITableModel getTableModelValue();
118
119    public abstract Object getSource();
120
121    public abstract Object getColumns();
122
123    public abstract int getInitialPage();
124
125    public abstract String getInitialSortColumn();
126
127    public abstract boolean getInitialSortOrder();
128
129    public abstract ITableSessionStateManager getTableSessionStateManager();
130
131    public abstract ITableSessionStoreManager getTableSessionStoreManager();
132
133    public abstract IComponent getColumnSettingsContainer();
134
135    public abstract int getPageSize();
136
137    public abstract String getPersist();
138
139    // enhanced property methods
140    public abstract Serializable getSessionState();
141
142    public abstract void setSessionState(Serializable sessionState);
143
144    public abstract Serializable getClientState();
145
146    public abstract void setClientState(Serializable sessionState);
147
148    public abstract Serializable getClientAppState();
149
150    public abstract void setClientAppState(Serializable sessionState);
151
152    /**
153     * The component constructor. Invokes the component member initializations.
154     */
155    public TableView()
156    {
157        initialize();
158    }
159
160    /**
161     * Invokes the component member initializations.
162     * 
163     * @see org.apache.tapestry.event.PageDetachListener#pageDetached(PageEvent)
164     */
165    public void pageDetached(PageEvent objEvent)
166    {
167        initialize();
168    }
169
170    /**
171     * Initialize the component member variables.
172     */
173    private void initialize()
174    {
175        m_objTableModel = null;
176        m_objCachedTableModelValue = null;
177    }
178
179    /**
180     * Resets the table by removing any stored table state. This means that the current column to
181     * sort on and the current page will be forgotten and all data will be reloaded.
182     */
183    public void reset()
184    {
185        initialize();
186        storeSessionState(null);
187    }
188
189    public ITableModel getCachedTableModelValue()
190    {
191        if (m_objCachedTableModelValue == null)
192            m_objCachedTableModelValue = getTableModelValue();
193        return m_objCachedTableModelValue;
194    }
195
196    /**
197     * Returns the tableModel.
198     * 
199     * @return ITableModel the table model used by the table components
200     */
201    public ITableModel getTableModel()
202    {
203        // if null, first try to recreate the model from the session state
204        if (m_objTableModel == null)
205        {
206            Serializable objState = loadSessionState();
207            ITableSessionStateManager objStateManager = getTableSessionStateManager();
208            m_objTableModel = objStateManager.recreateTableModel(objState);
209        }
210
211        // if the session state does not help, get the model from the binding
212        if (m_objTableModel == null)
213            m_objTableModel = getCachedTableModelValue();
214
215        // if the model from the binding is null, build a model from source and columns
216        if (m_objTableModel == null)
217            m_objTableModel = generateTableModel(null);
218
219        if (m_objTableModel == null)
220            throw new ApplicationRuntimeException(TableMessages.missingTableModel(this));
221
222        return m_objTableModel;
223    }
224
225    /**
226     * Generate a table model using the 'source' and 'columns' parameters.
227     * 
228     * @return the newly generated table model
229     */
230    protected ITableModel generateTableModel(SimpleTableState objState)
231    {
232        // create a new table state if none is passed
233        if (objState == null)
234        {
235            objState = new SimpleTableState();
236            objState.getSortingState().setSortColumn(getInitialSortColumn(), getInitialSortOrder());
237            objState.getPagingState().setCurrentPage(getInitialPage());
238        }
239
240        // update the page size if set in the parameter
241        if (isParameterBound("pageSize"))
242            objState.getPagingState().setPageSize(getPageSize());
243
244        // get the column model. if not possible, return null.
245        ITableColumnModel objColumnModel = getTableColumnModel();
246        if (objColumnModel == null)
247            return null;
248
249        Object objSourceValue = getSource();
250        if (objSourceValue == null)
251            return null;
252
253        // if the source parameter is of type {@link IBasicTableModel},
254        // create and return an appropriate wrapper
255        if (objSourceValue instanceof IBasicTableModel)
256            return new BasicTableModelWrap((IBasicTableModel) objSourceValue, objColumnModel,
257                    objState);
258
259        // otherwise, the source parameter must contain the data to be displayed
260        ITableDataModel objDataModel = null;
261        if (objSourceValue instanceof Object[])
262            objDataModel = new SimpleListTableDataModel((Object[]) objSourceValue);
263        else if (objSourceValue instanceof List)
264            objDataModel = new SimpleListTableDataModel((List) objSourceValue);
265        else if (objSourceValue instanceof Collection)
266            objDataModel = new SimpleListTableDataModel((Collection) objSourceValue);
267        else if (objSourceValue instanceof Iterator)
268            objDataModel = new SimpleListTableDataModel((Iterator) objSourceValue);
269
270        if (objDataModel == null)
271            throw new ApplicationRuntimeException(TableMessages.invalidTableSource(
272                    this,
273                    objSourceValue));
274
275        return new SimpleTableModel(objDataModel, objColumnModel, objState);
276    }
277
278    /**
279     * Returns the table column model as specified by the 'columns' binding. If the value of the
280     * 'columns' binding is of a type different than ITableColumnModel, this method makes the
281     * appropriate conversion.
282     * 
283     * @return The table column model as specified by the 'columns' binding
284     */
285    protected ITableColumnModel getTableColumnModel()
286    {
287        Object objColumns = getColumns();
288
289        if (objColumns == null)
290            return null;
291
292        if (objColumns instanceof ITableColumnModel)
293        {
294            return (ITableColumnModel) objColumns;
295        }
296
297        if (objColumns instanceof Iterator)
298        {
299            // convert to List
300            Iterator objColumnsIterator = (Iterator) objColumns;
301            List arrColumnsList = new ArrayList();
302            addAll(arrColumnsList, objColumnsIterator);
303            objColumns = arrColumnsList;
304        }
305
306        if (objColumns instanceof List)
307        {
308            // validate that the list contains only ITableColumn instances
309            List arrColumnsList = (List) objColumns;
310            int nColumnsNumber = arrColumnsList.size();
311            for (int i = 0; i < nColumnsNumber; i++)
312            {
313                if (!(arrColumnsList.get(i) instanceof ITableColumn))
314                    throw new ApplicationRuntimeException(TableMessages.columnsOnlyPlease(this));
315            }
316            //objColumns = arrColumnsList.toArray(new ITableColumn[nColumnsNumber]);
317            return new SimpleTableColumnModel(arrColumnsList);
318        }
319
320        if (objColumns instanceof ITableColumn[])
321        {
322            return new SimpleTableColumnModel((ITableColumn[]) objColumns);
323        }
324
325        if (objColumns instanceof String)
326        {
327            String strColumns = (String) objColumns;
328            if (getBinding("columns").isInvariant())
329            {
330                // if the binding is invariant, create the columns only once
331                if (m_objColumnModel == null)
332                    m_objColumnModel = generateTableColumnModel(strColumns);
333                return m_objColumnModel;
334            }
335
336            // if the binding is not invariant, create them every time
337            return generateTableColumnModel(strColumns);
338        }
339
340        throw new ApplicationRuntimeException(TableMessages.invalidTableColumns(this, objColumns));
341    }
342
343    private void addAll(List arrColumnsList, Iterator objColumnsIterator)
344    {
345        while (objColumnsIterator.hasNext())
346            arrColumnsList.add(objColumnsIterator.next());
347    }
348
349    /**
350     * Generate a table column model out of the description string provided. Entries in the
351     * description string are separated by commas. Each column entry is of the format name,
352     * name:expression, or name:displayName:expression. An entry prefixed with ! represents a
353     * non-sortable column. If the whole description string is prefixed with *, it represents
354     * columns to be included in a Form.
355     * 
356     * @param strDesc
357     *            the description of the column model to be generated
358     * @return a table column model based on the provided description
359     */
360    protected ITableColumnModel generateTableColumnModel(String strDesc)
361    {
362        IComponent objColumnSettingsContainer = getColumnSettingsContainer();
363        IAdvancedTableColumnSource objColumnSource = getColumnSource();
364        
365        return getModelSource().generateTableColumnModel(objColumnSource, strDesc, this, objColumnSettingsContainer);
366    }
367
368    /**
369     * The default session state manager to be used in case no such manager is provided by the
370     * corresponding parameter.
371     * 
372     * @return the default session state manager
373     */
374    public ITableSessionStateManager getDefaultTableSessionStateManager()
375    {
376        if (m_objDefaultSessionStateManager == null)
377            m_objDefaultSessionStateManager = new TableViewSessionStateManager(this);
378        return m_objDefaultSessionStateManager;
379    }
380
381    /**
382     * Invoked when there is a modification of the table state and it needs to be saved
383     * 
384     * @see org.apache.tapestry.contrib.table.model.ITableModelSource#fireObservedStateChange()
385     */
386    public void fireObservedStateChange()
387    {
388        saveSessionState();
389    }
390
391    /**
392     * Ensures that the table state is saved before the render phase begins in case there are
393     * modifications for which {@link #fireObservedStateChange()}has not been invoked.
394     * 
395     * @see org.apache.tapestry.event.PageBeginRenderListener#pageBeginRender(org.apache.tapestry.event.PageEvent)
396     */
397    public void pageBeginRender(PageEvent event)
398    {
399        // 'suspenders': save the table model if it has been already loaded.
400        // this means that if a change has been made explicitly in a listener,
401        // it will be saved. this is the last place before committing the changes
402        // where a save can occur
403        if (m_objTableModel != null)
404            saveSessionState();
405    }
406
407    /**
408     * Saves the table state using the SessionStateManager to determine what to save and the
409     * SessionStoreManager to determine where to save it.
410     */
411    protected void saveSessionState()
412    {
413        ITableModel objModel = getTableModel();
414        Serializable objState = getTableSessionStateManager().getSessionState(objModel);
415        storeSessionState(objState);
416    }
417
418    /**
419     * Loads the table state using the SessionStoreManager.
420     * 
421     * @return the stored table state
422     */
423    protected Serializable loadSessionState()
424    {
425        ITableSessionStoreManager objManager = getTableSessionStoreManager();
426        if (objManager != null)
427            return objManager.loadState(getPage().getRequestCycle());
428        String strPersist = getPersist();
429        if (strPersist.equals("client") || strPersist.equals("client:page"))
430                return getClientState();
431        else if (strPersist.equals("client:app"))
432                return getClientAppState();
433        else
434                return getSessionState();
435    }
436
437    /**
438     * Stores the table state using the SessionStoreManager.
439     * 
440     * @param objState
441     *            the table state to store
442     */
443    protected void storeSessionState(Serializable objState)
444    {
445        ITableSessionStoreManager objManager = getTableSessionStoreManager();
446        if (objManager != null)
447            objManager.saveState(getPage().getRequestCycle(), objState);
448        else {
449                String strPersist = getPersist();
450                if (strPersist.equals("client") || strPersist.equals("client:page"))
451                        setClientState(objState);
452                else if (strPersist.equals("client:app"))
453                        setClientAppState(objState);
454                else 
455                        setSessionState(objState);
456        }
457    }
458
459    /**
460     * Make sure that the values stored in the model are useable and correct. The changes made here
461     * are not saved.
462     */
463    protected void validateValues()
464    {
465        ITableModel objModel = getTableModel();
466
467        // make sure current page is within the allowed range
468        ITablePagingState objPagingState = objModel.getPagingState();
469        int nCurrentPage = objPagingState.getCurrentPage();
470        int nPageCount = objModel.getPageCount();
471        if (nCurrentPage >= nPageCount)
472        {
473            // the current page is greater than the page count. adjust.
474            nCurrentPage = nPageCount - 1;
475            objPagingState.setCurrentPage(nCurrentPage);
476        }
477        if (nCurrentPage < 0)
478        {
479            // the current page is before the first page. adjust.
480            nCurrentPage = 0;
481            objPagingState.setCurrentPage(nCurrentPage);
482        }
483    }
484
485    /**
486     * Stores a pointer to this component in the Request Cycle while rendering so that wrapped
487     * components have access to it.
488     * 
489     * @see org.apache.tapestry.BaseComponent#renderComponent(IMarkupWriter, IRequestCycle)
490     */
491    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
492    {
493        Object objOldValue = cycle.getAttribute(ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE);
494        cycle.setAttribute(ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE, this);
495
496        initialize();
497        validateValues();
498        super.renderComponent(writer, cycle);
499
500        cycle.setAttribute(ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE, objOldValue);
501    }
502
503}