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