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.contrib.table.components;
016    
017    import java.io.Serializable;
018    import java.util.ArrayList;
019    import java.util.Collection;
020    import java.util.Iterator;
021    import java.util.List;
022    
023    import org.apache.hivemind.ApplicationRuntimeException;
024    import org.apache.tapestry.BaseComponent;
025    import org.apache.tapestry.IComponent;
026    import org.apache.tapestry.IMarkupWriter;
027    import org.apache.tapestry.IRequestCycle;
028    import org.apache.tapestry.contrib.table.model.IAdvancedTableColumnSource;
029    import org.apache.tapestry.contrib.table.model.IBasicTableModel;
030    import org.apache.tapestry.contrib.table.model.ITableColumn;
031    import org.apache.tapestry.contrib.table.model.ITableColumnModel;
032    import org.apache.tapestry.contrib.table.model.ITableDataModel;
033    import org.apache.tapestry.contrib.table.model.ITableModel;
034    import org.apache.tapestry.contrib.table.model.ITableModelSource;
035    import org.apache.tapestry.contrib.table.model.ITablePagingState;
036    import org.apache.tapestry.contrib.table.model.ITableSessionStateManager;
037    import org.apache.tapestry.contrib.table.model.ITableSessionStoreManager;
038    import org.apache.tapestry.contrib.table.model.common.BasicTableModelWrap;
039    import org.apache.tapestry.contrib.table.model.simple.SimpleListTableDataModel;
040    import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumnModel;
041    import org.apache.tapestry.contrib.table.model.simple.SimpleTableModel;
042    import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
043    import org.apache.tapestry.event.PageBeginRenderListener;
044    import org.apache.tapestry.event.PageDetachListener;
045    import 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     */
097    public 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    }