001    /**
002     * Copyright 2005-2013 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.web.controller.helper;
017    
018    import java.io.StringReader;
019    import java.util.ArrayList;
020    import java.util.Arrays;
021    import java.util.List;
022    
023    import javax.json.Json;
024    import javax.json.JsonArray;
025    import javax.json.JsonObject;
026    import javax.json.JsonReader;
027    import javax.servlet.http.HttpServletRequest;
028    import javax.servlet.http.HttpServletResponse;
029    
030    import org.apache.commons.collections.CollectionUtils;
031    import org.apache.commons.lang.StringUtils;
032    import org.kuali.rice.krad.uif.UifConstants;
033    import org.kuali.rice.krad.uif.container.CollectionGroup;
034    import org.kuali.rice.krad.uif.layout.TableLayoutManager;
035    import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
036    import org.kuali.rice.krad.uif.util.ColumnSort;
037    import org.kuali.rice.krad.uif.util.ComponentFactory;
038    import org.kuali.rice.krad.uif.util.MultiColumnComparator;
039    import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
040    import org.kuali.rice.krad.uif.view.View;
041    import org.kuali.rice.krad.web.form.UifFormBase;
042    
043    /**
044     * @author Kuali Rice Team (rice.collab@kuali.org)
045     */
046    public class DataTablesPagingHelper {
047    
048        private int totalCollectionSize;
049        private Integer filteredCollectionSize;
050    
051        private TableLayoutManager tableLayoutManager;
052    
053        public void processPagingRequest(String tableId, UifFormBase form,
054                HttpServletRequest request, HttpServletResponse response,
055                DataTablesInputs dataTablesInputs) {
056            // Set property to trigger special JSON rendering logic in uifRender.ftl
057            form.setRequestJsonTemplate(UifConstants.TableToolsValues.JSON_TEMPLATE);
058            
059            View view = form.getPostedView();
060    
061            if (view != null) { // avoid blowing the stack if the session expired
062                // don't set the component to update unless we have a postedView, otherwise we'll get an NPE later
063                form.setUpdateComponentId(tableId);
064    
065                @SuppressWarnings("unchecked") List<ColumnSort> oldColumnSorts =
066                        (List<ColumnSort>) form.getExtensionData().get(tableId + UifConstants.IdSuffixes.COLUMN_SORTS);
067    
068                // Create references that we'll need beyond the synchronized block here.
069                CollectionGroup newCollectionGroup = null;
070                List<Object> modelCollection = null;
071                List<ColumnSort> newColumnSorts = null;
072    
073                synchronized (view) { // only one concurrent request per view please
074    
075                    CollectionGroup oldCollectionGroup = (CollectionGroup) view.getViewIndex().getComponentById(tableId);
076                    newColumnSorts = buildColumnSorts(view, dataTablesInputs, oldCollectionGroup);
077    
078                    // get the collection for this group from the model
079                    modelCollection = ObjectPropertyUtils.getPropertyValue(form,
080                            oldCollectionGroup.getBindingInfo().getBindingPath());
081    
082                    applyTableJsonSort(modelCollection, oldColumnSorts, newColumnSorts, oldCollectionGroup, view);
083    
084                    // get a new instance of the collection group component that we'll run the lifecycle on
085                    newCollectionGroup = (CollectionGroup) ComponentFactory.getNewInstanceForRefresh(form.getPostedView(),
086                            tableId);
087    
088                    // set up the collection group properties related to paging in the collection group to set the bounds for
089                    // what needs to be rendered
090                    newCollectionGroup.setUseServerPaging(true);
091                    newCollectionGroup.setDisplayStart(dataTablesInputs.iDisplayStart);
092                    newCollectionGroup.setDisplayLength(dataTablesInputs.iDisplayLength);
093    
094                    // run lifecycle on the table component and update in view
095                    ViewLifecycle.performComponentLifecycle(view, form, request, response,
096                                    newCollectionGroup, oldCollectionGroup.getId());
097                }
098    
099                this.tableLayoutManager = (TableLayoutManager) newCollectionGroup.getLayoutManager();
100                this.filteredCollectionSize = newCollectionGroup.getFilteredCollectionSize();
101                this.totalCollectionSize = modelCollection.size();
102    
103                // these other params above don't need to stay in the form after this request, but <tableId>_columnSorts
104                // does so that we avoid re-sorting on each request.
105                form.getExtensionData().put(tableId + "_columnSorts", newColumnSorts);
106            }
107        }
108    
109        /**
110         * Extract the sorting information from the DataTablesInputs into a more generic form.
111         *
112         * @param view posted view containing the collection
113         * @param dataTablesInputs the parsed request data from dataTables
114         * @return the List of ColumnSort elements representing the requested sort columns, types, and directions
115         */
116        private List<ColumnSort> buildColumnSorts(View view, DataTablesInputs dataTablesInputs, CollectionGroup collectionGroup) {
117            int[] sortCols = dataTablesInputs.iSortCol_; // cols being sorted on (for multi-col sort)
118            boolean[] sortable = dataTablesInputs.bSortable_; // which columns are sortable
119            String[] sortDir = dataTablesInputs.sSortDir_; // direction to sort
120    
121            // parse table options to gather the sort types
122            String aoColumnDefsValue = (String) view.getViewIndex().getPostContextEntry(collectionGroup.getId(),
123                    UifConstants.TableToolsKeys.AO_COLUMN_DEFS);
124            JsonArray jsonColumnDefs = null;
125    
126            if (!StringUtils.isEmpty(aoColumnDefsValue)) { // we'll parse this using a JSON library to make things simpler
127                // function definitions are not allowed in JSON
128                aoColumnDefsValue = aoColumnDefsValue.replaceAll("function\\([^)]*\\)\\s*\\{[^}]*\\}", "\"REDACTED\"");
129                JsonReader jsonReader = Json.createReader(new StringReader(aoColumnDefsValue));
130                jsonColumnDefs = jsonReader.readArray();
131            }
132    
133            List<ColumnSort> columnSorts = new ArrayList<ColumnSort>(sortCols.length);
134    
135            for (int sortColsIndex = 0; sortColsIndex < sortCols.length; sortColsIndex++) {
136                int sortCol = sortCols[sortColsIndex]; // get the index of the column being sorted on
137    
138                if (sortable[sortCol]) {
139                    String sortType = getSortType(jsonColumnDefs, sortCol);
140                    ColumnSort.Direction sortDirection = ColumnSort.Direction.valueOf(sortDir[sortColsIndex].toUpperCase());
141                    columnSorts.add(new ColumnSort(sortCol, sortDirection, sortType));
142                }
143            }
144    
145            return columnSorts;
146        }
147    
148        /**
149         * Get the sort type string from the parsed column definitions object.
150         *
151         * @param jsonColumnDefs the JsonArray representation of the aoColumnDefs property from the RichTable template
152         * options
153         * @param sortCol the index of the column to get the sort type for
154         * @return the name of the sort type specified in the template options, or the default of "string" if none is
155         *         found.
156         */
157        private String getSortType(JsonArray jsonColumnDefs, int sortCol) {
158            String sortType = "string"; // default to string if nothing is spec'd
159    
160            if (jsonColumnDefs != null) {
161                JsonObject column = jsonColumnDefs.getJsonObject(sortCol);
162    
163                if (column.containsKey("sType")) {
164                    sortType = column.getString("sType");
165                }
166            }
167            return sortType;
168        }
169    
170        /**
171         * Sort the given modelCollection (in place) according to the specified columnSorts.
172         *
173         * <p>Not all columns will necessarily be directly mapped to the modelCollection, so the collectionGroup and view
174         * are available as well for use in calculating those other column values.  However, if all the columns are in fact
175         * mapped to the elements of the modelCollection, subclasses should be able to easily override this method to
176         * provide custom sorting logic.</p>
177         *
178         * <p>
179         * Create an index array and sort that. The array slots represents the slots in the modelCollection, and
180         * the values are indices to the elements in the modelCollection.  At the end, we'll re-order the
181         * modelCollection so that the elements are in the collection slots that correspond to the array locations.
182         *
183         * A small example may be in order.  Here's the incoming modelCollection:
184         *
185         * modelCollection = { "Washington, George", "Adams, John", "Jefferson, Thomas", "Madison, James" }
186         *
187         * Initialize the array with its element references all matching initial positions in the modelCollection:
188         *
189         * reSortIndices = { 0, 1, 2, 3 }
190         *
191         * After doing our sort in the array (where we sort indices based on the values in the modelCollection):
192         *
193         * reSortIndices = { 1, 2, 3, 0 }
194         *
195         * Then, we go back and apply that ordering to the modelCollection:
196         *
197         * modelCollection = { "Adams, John", "Jefferson, Thomas", "Madison, James", "Washington, George" }
198         *
199         * Why do it this way instead of just sorting the modelCollection directly?  Because we may need to know
200         * the original index of the element e.g. for the auto sequence column.
201         * </p>
202         *
203         * @param modelCollection the collection to sort
204         * @param oldColumnSorts the sorting that reflects the current state of the collection
205         * @param newColumnSorts the sorting to apply to the collection
206         * @param collectionGroup the CollectionGroup that is being rendered
207         * @param view the view
208         */
209        protected void applyTableJsonSort(final List<Object> modelCollection, List<ColumnSort> oldColumnSorts,
210                final List<ColumnSort> newColumnSorts, final CollectionGroup collectionGroup, final View view) {
211    
212            boolean isCollectionEmpty = CollectionUtils.isEmpty(modelCollection);
213            boolean isSortingSpecified = !CollectionUtils.isEmpty(newColumnSorts);
214            boolean isSortOrderChanged = newColumnSorts != oldColumnSorts && !newColumnSorts.equals(oldColumnSorts);
215    
216            if (!isCollectionEmpty && isSortingSpecified && isSortOrderChanged) {
217                Integer[] sortIndices = new Integer[modelCollection.size()];
218                for (int i = 0; i < sortIndices.length; i++) {
219                    sortIndices[i] = i;
220                }
221    
222                MultiColumnComparator comparator =
223                        new MultiColumnComparator(modelCollection, collectionGroup, newColumnSorts, view);
224                Arrays.sort(sortIndices, comparator);
225    
226                // apply the sort to the modelCollection
227                Object[] sorted = new Object[sortIndices.length];
228                for (int i = 0; i < sortIndices.length; i++) {
229                    sorted[i] = modelCollection.get(sortIndices[i]);
230                }
231                for (int i = 0; i < sorted.length; i++) {
232                    modelCollection.set(i, sorted[i]);
233                }
234            }
235        }
236    
237        public int getTotalCollectionSize() {
238            return totalCollectionSize;
239        }
240    
241        public Integer getFilteredCollectionSize() {
242            return filteredCollectionSize;
243        }
244    
245        public TableLayoutManager getTableLayoutManager() {
246            return tableLayoutManager;
247        }
248    
249        /**
250         * Input command processor for supporting DataTables server-side processing.
251         *
252         * @see <a href="http://datatables.net/usage/server-side">http://datatables.net/usage/server-side</a>
253         */
254        public static class DataTablesInputs {
255            private static final String DISPLAY_START = "iDisplayStart";
256            private static final String DISPLAY_LENGTH = "iDisplayLength";
257            private static final String COLUMNS = "iColumns";
258            private static final String REGEX = "bRegex";
259            private static final String REGEX_PREFIX = "bRegex_";
260            private static final String SORTABLE_PREFIX = "bSortable_";
261            private static final String SORTING_COLS = "iSortingCols";
262            private static final String SORT_COL_PREFIX = "iSortCol_";
263            private static final String SORT_DIR_PREFIX = "sSortDir_";
264            private static final String DATA_PROP_PREFIX = "mDataProp_";
265            private static final String ECHO = "sEcho";
266    
267            private final int iDisplayStart, iDisplayLength, iColumns, iSortingCols, sEcho;
268    
269            // TODO: All search related options are commented out of this class.
270            // If we implement search for datatables we'll want to re-activate that code to capture the configuration
271            // values from the request
272    
273            //        private final String sSearch;
274            //        private final Pattern patSearch;
275    
276            private final boolean bRegex;
277            private final boolean[] /*bSearchable_,*/ bRegex_, bSortable_;
278            private final String[] /*sSearch_,*/ sSortDir_, mDataProp_;
279    
280            //        private final Pattern[] patSearch_;
281    
282            private final int[] iSortCol_;
283    
284            public DataTablesInputs(HttpServletRequest request) {
285                String s;
286                iDisplayStart = (s = request.getParameter(DISPLAY_START)) == null ? 0 : Integer.parseInt(s);
287                iDisplayLength = (s = request.getParameter(DISPLAY_LENGTH)) == null ? 0 : Integer.parseInt(s);
288                iColumns = (s = request.getParameter(COLUMNS)) == null ? 0 : Integer.parseInt(s);
289                bRegex = (s = request.getParameter(REGEX)) == null ? false : new Boolean(s);
290    
291                //            patSearch = (sSearch = request.getParameter("sSearch")) == null
292                //                    || !bRegex ? null : Pattern.compile(sSearch);
293                //            bSearchable_ = new boolean[iColumns];
294                //            sSearch_ = new String[iColumns];
295                //            patSearch_ = new Pattern[iColumns];
296    
297                bRegex_ = new boolean[iColumns];
298                bSortable_ = new boolean[iColumns];
299    
300                for (int i = 0; i < iColumns; i++) {
301    
302                    //                bSearchable_[i] = (s = request.getParameter("bSearchable_" + i)) == null ? false
303                    //                        : new Boolean(s);
304    
305                    bRegex_[i] = (s = request.getParameter(REGEX_PREFIX + i)) == null ? false : new Boolean(s);
306    
307                    //                patSearch_[i] = (sSearch_[i] = request.getParameter("sSearch_"
308                    //                        + i)) == null
309                    //                        || !bRegex_[i] ? null : Pattern.compile(sSearch_[i]);
310    
311                    bSortable_[i] = (s = request.getParameter(SORTABLE_PREFIX + i)) == null ? false : new Boolean(s);
312                }
313    
314                iSortingCols = (s = request.getParameter(SORTING_COLS)) == null ? 0 : Integer.parseInt(s);
315                iSortCol_ = new int[iSortingCols];
316                sSortDir_ = new String[iSortingCols];
317    
318                for (int i = 0; i < iSortingCols; i++) {
319                    iSortCol_[i] = (s = request.getParameter(SORT_COL_PREFIX + i)) == null ? 0 : Integer.parseInt(s);
320                    sSortDir_[i] = request.getParameter(SORT_DIR_PREFIX + i);
321                }
322    
323                mDataProp_ = new String[iColumns];
324    
325                for (int i = 0; i < iColumns; i++) {
326                    mDataProp_[i] = request.getParameter(DATA_PROP_PREFIX + i);
327                }
328    
329                sEcho = (s = request.getParameter(ECHO)) == null ? 0 : Integer.parseInt(s);
330            }
331    
332            @Override
333            public String toString() {
334                StringBuilder sb = new StringBuilder(super.toString());
335                sb.append("\n\t" + DISPLAY_START + " = ");
336                sb.append(iDisplayStart);
337                sb.append("\n\t" + DISPLAY_LENGTH + " = ");
338                sb.append(iDisplayLength);
339                sb.append("\n\t" + COLUMNS + " = ");
340                sb.append(iColumns);
341    
342                //            sb.append("\n\tsSearch = ");
343                //            sb.append(sSearch);
344    
345                sb.append("\n\t" + REGEX + " = ");
346                sb.append(bRegex);
347    
348                for (int i = 0; i < iColumns; i++) {
349    
350                    //                sb.append("\n\tbSearchable_").append(i).append(" = ");
351                    //                sb.append(bSearchable_[i]);
352    
353                    //                sb.append("\n\tsSearch_").append(i).append(" = ");
354                    //                sb.append(sSearch_[i]);
355    
356                    sb.append("\n\t").append(REGEX_PREFIX).append(i).append(" = ");
357                    sb.append(bRegex_[i]);
358                    sb.append("\n\t").append(SORTABLE_PREFIX).append(i).append(" = ");
359                    sb.append(bSortable_[i]);
360                }
361    
362                sb.append("\n\t").append(SORTING_COLS);
363                sb.append(iSortingCols);
364    
365                for (int i = 0; i < iSortingCols; i++) {
366                    sb.append("\n\t").append(SORT_COL_PREFIX).append(i).append(" = ");
367                    sb.append(iSortCol_[i]);
368                    sb.append("\n\t").append(SORT_DIR_PREFIX).append(i).append(" = ");
369                    sb.append(sSortDir_[i]);
370                }
371    
372                for (int i = 0; i < iColumns; i++) {
373                    sb.append("\n\t").append(DATA_PROP_PREFIX).append(i).append(" = ");
374                    sb.append(mDataProp_[i]);
375                }
376    
377                sb.append("\n\t" + ECHO + " = ");
378                sb.append(sEcho);
379    
380                return sb.toString();
381            }
382        }
383    }