View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.internal.component;
21  
22  import org.apache.myfaces.tobago.component.Attributes;
23  import org.apache.myfaces.tobago.component.Sorter;
24  import org.apache.myfaces.tobago.component.Visual;
25  import org.apache.myfaces.tobago.event.PageActionEvent;
26  import org.apache.myfaces.tobago.event.SheetStateChangeEvent;
27  import org.apache.myfaces.tobago.event.SheetStateChangeListener;
28  import org.apache.myfaces.tobago.event.SheetStateChangeSource;
29  import org.apache.myfaces.tobago.event.SortActionEvent;
30  import org.apache.myfaces.tobago.event.SortActionSource;
31  import org.apache.myfaces.tobago.internal.layout.Grid;
32  import org.apache.myfaces.tobago.internal.layout.OriginCell;
33  import org.apache.myfaces.tobago.layout.Measure;
34  import org.apache.myfaces.tobago.layout.MeasureList;
35  import org.apache.myfaces.tobago.model.ExpandedState;
36  import org.apache.myfaces.tobago.model.SelectedState;
37  import org.apache.myfaces.tobago.model.SheetState;
38  import org.apache.myfaces.tobago.util.ComponentUtils;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import javax.el.ELContext;
43  import javax.el.MethodExpression;
44  import javax.el.ValueExpression;
45  import javax.faces.component.UIColumn;
46  import javax.faces.component.UIComponent;
47  import javax.faces.component.UINamingContainer;
48  import javax.faces.component.behavior.ClientBehaviorHolder;
49  import javax.faces.context.FacesContext;
50  import javax.faces.event.AbortProcessingException;
51  import javax.faces.event.ComponentSystemEvent;
52  import javax.faces.event.ComponentSystemEventListener;
53  import javax.faces.event.FacesEvent;
54  import javax.faces.event.ListenerFor;
55  import javax.faces.event.PhaseId;
56  import javax.faces.event.PreRenderComponentEvent;
57  import java.io.IOException;
58  import java.util.ArrayList;
59  import java.util.Collections;
60  import java.util.List;
61  
62  /**
63   * {@link org.apache.myfaces.tobago.internal.taglib.component.SheetTagDeclaration}
64   */
65  @ListenerFor(systemEventClass = PreRenderComponentEvent.class)
66  public abstract class AbstractUISheet extends AbstractUIData
67      implements SheetStateChangeSource, SortActionSource, ClientBehaviorHolder, Visual,
68      ComponentSystemEventListener {
69  
70    private static final Logger LOG = LoggerFactory.getLogger(AbstractUISheet.class);
71  
72    public static final String COMPONENT_TYPE = "org.apache.myfaces.tobago.Data";
73  
74    public static final String SORTER_ID = "sorter";
75    public static final String NOT_SORTABLE_MESSAGE_ID = "org.apache.myfaces.tobago.Sheet.SORTING_ERROR";
76  
77    private SheetState state;
78    private transient MeasureList columnLayout;
79    private transient boolean autoLayout;
80  
81    private transient Grid headerGrid;
82  
83    @Override
84    public void encodeBegin(final FacesContext facesContext) throws IOException {
85      final SheetState theState = getSheetState(facesContext);
86      final int first = theState.getFirst();
87      if (first > -1 && (!hasRowCount() || first < getRowCount())) {
88        final ValueExpression expression = getValueExpression(Attributes.first.getName());
89        if (expression != null) {
90          expression.setValue(facesContext.getELContext(), first);
91        } else {
92          setFirst(first);
93        }
94      }
95  
96      super.encodeBegin(facesContext);
97    }
98  
99    public void setState(final SheetState state) {
100     this.state = state;
101   }
102 
103   public SheetState getState() {
104     return getSheetState(FacesContext.getCurrentInstance());
105   }
106 
107   public SheetState getSheetState(final FacesContext facesContext) {
108     if (state != null) {
109       return state;
110     }
111 
112     final ValueExpression expression = getValueExpression(Attributes.state.getName());
113     if (expression != null) {
114       final ELContext elContext = facesContext.getELContext();
115       SheetState sheetState = (SheetState) expression.getValue(elContext);
116       if (sheetState == null) {
117         sheetState = new SheetState();
118         expression.setValue(elContext, sheetState);
119       }
120       return sheetState;
121     }
122 
123     state = new SheetState();
124     return state;
125   }
126 
127   public abstract String getColumns();
128 
129   @Override
130   public void processEvent(final ComponentSystemEvent event) throws AbortProcessingException {
131 
132     super.processEvent(event);
133 
134     if (event instanceof PreRenderComponentEvent) {
135       final String columns = getColumns();
136       if (columns != null) {
137         columnLayout = MeasureList.parse(columns);
138       }
139 
140       autoLayout = true;
141       if (columnLayout != null) {
142         for (final Measure token : columnLayout) {
143           if (token != Measure.AUTO) {
144             autoLayout = false;
145             break;
146           }
147         }
148       }
149 
150       LOG.debug("autoLayout={}", autoLayout);
151     }
152   }
153 
154   public MeasureList getColumnLayout() {
155     return columnLayout;
156   }
157 
158   public boolean isAutoLayout() {
159     return autoLayout;
160   }
161 
162   /**
163    * @deprecated since 1.5.5, the name of this method is ambiguous. You may use {@link #getLastRowIndexOfCurrentPage()}.
164    */
165   @Deprecated
166   public int getLast() {
167     final int last = getFirst() + getRows();
168     return last < getRowCount() ? last : getRowCount();
169   }
170 
171   /**
172    * The rowIndex of the last row on the current page plus one (because of zero based iterating).
173    *
174    * @throws IllegalArgumentException If the number of rows in the model returned
175    *                                  by {@link #getRowCount()} is -1 (undefined).
176    */
177   public int getLastRowIndexOfCurrentPage() {
178     if (!hasRowCount()) {
179       throw new IllegalArgumentException(
180           "Can't determine the last row, because the row count of the model is unknown.");
181     }
182     if (isRowsUnlimited()) {
183       return getRowCount();
184     }
185     final int last = getFirst() + getRows();
186     return last < getRowCount() ? last : getRowCount();
187   }
188 
189   /**
190    * @return returns the current page (based by 0).
191    */
192   public int getCurrentPage() {
193     final int rows = getRows();
194     if (rows == 0) {
195       // if the rows are unlimited, there is only one page
196       return 0;
197     }
198     final int first = getFirst();
199     if (hasRowCount() && first >= getRowCount()) {
200       return getPages() - 1; // last page
201     } else {
202       return first / rows;
203     }
204   }
205 
206   /**
207    * @return returns the current page (based by 1).
208    * @deprecated since 1.5.5, please use {@link #getCurrentPage()} which returns the value zero-based.
209    */
210   @Deprecated
211   public int getPage() {
212     return getCurrentPage() + 1;
213   }
214 
215   /**
216    * The number of pages to render.
217    *
218    * @throws IllegalArgumentException If the number of rows in the model returned
219    *                                  by {@link #getRowCount()} is -1 (undefined).
220    */
221   public int getPages() {
222     if (isRowsUnlimited()) {
223       return 1;
224     }
225     if (!hasRowCount()) {
226       throw new IllegalArgumentException(
227           "Can't determine the number of pages, because the row count of the model is unknown.");
228     }
229     return (getRowCount() - 1) / getRows() + 1;
230   }
231 
232   public List<UIComponent> getRenderedChildrenOf(final UIColumn column) {
233     final List<UIComponent> children = new ArrayList<>();
234     for (final UIComponent kid : column.getChildren()) {
235       if (kid.isRendered()) {
236         children.add(kid);
237       }
238     }
239     return children;
240   }
241 
242   /**
243    * @return Is the interval to display starting with the first row?
244    */
245   public boolean isAtBeginning() {
246     return getFirst() == 0;
247   }
248 
249   /**
250    * @return Does the data model knows the number of rows?
251    */
252   public boolean hasRowCount() {
253     return getRowCount() != -1;
254   }
255 
256   /**
257    * @return Should the paging controls be rendered? Either because of the need of paging or because
258    * the show is enforced by {@link #isShowPagingAlways()}
259    */
260   public boolean isPagingVisible() {
261     return isShowPagingAlways() || needMoreThanOnePage();
262   }
263 
264   /**
265    * @return Is panging needed to display all rows? If the number of rows is unknown this method returns true.
266    */
267   public boolean needMoreThanOnePage() {
268     if (isRowsUnlimited()) {
269       return false;
270     } else if (!hasRowCount()) {
271       return true;
272     } else {
273       return getRowCount() > getRows();
274     }
275   }
276 
277   public abstract boolean isShowPagingAlways();
278 
279   public boolean isAtEnd() {
280     if (!hasRowCount()) {
281       final int old = getRowIndex();
282       setRowIndex(getFirst() + getRows() + 1);
283       final boolean atEnd = !isRowAvailable();
284       setRowIndex(old);
285       return atEnd;
286     } else {
287       return getFirst() >= getFirstRowIndexOfLastPage();
288     }
289   }
290 
291   /**
292    * Determines the beginning of the last page in the model.
293    * If the number of rows to display on one page is unlimited, the value is 0 (there is only one page).
294    *
295    * @return The index of the first row of the last paging page.
296    * @throws IllegalArgumentException If the number of rows in the model returned
297    *                                  by {@link #getRowCount()} is -1 (undefined).
298    */
299   public int getFirstRowIndexOfLastPage() {
300     if (isRowsUnlimited()) {
301       return 0;
302     } else if (!hasRowCount()) {
303       throw new IllegalArgumentException(
304           "Can't determine the last page, because the row count of the model is unknown.");
305     } else {
306       final int rows = getRows();
307       final int rowCount = getRowCount();
308       final int tail = rowCount % rows;
309       return rowCount - (tail != 0 ? tail : rows);
310     }
311   }
312 
313   @Override
314   public void processUpdates(final FacesContext context) {
315     super.processUpdates(context);
316 
317     final SheetState sheetState = getSheetState(context);
318     if (sheetState != null) {
319       final List<Integer> list = (List<Integer>) ComponentUtils.getAttribute(this, Attributes.selectedListString);
320       sheetState.setSelectedRows(list != null ? list : Collections.emptyList());
321       ComponentUtils.removeAttribute(this, Attributes.selectedListString);
322       ComponentUtils.removeAttribute(this, Attributes.scrollPosition);
323     }
324   }
325 
326   @Override
327   public Object saveState(final FacesContext context) {
328     final Object[] saveState = new Object[2];
329     saveState[0] = super.saveState(context);
330     saveState[1] = state;
331     return saveState;
332   }
333 
334   @Override
335   public void restoreState(final FacesContext context, final Object savedState) {
336     final Object[] values = (Object[]) savedState;
337     super.restoreState(context, values[0]);
338     state = (SheetState) values[1];
339   }
340 
341   public List<AbstractUIColumnBase> getAllColumns() {
342     final ArrayList<AbstractUIColumnBase> result = new ArrayList<>();
343     findColumns(this, result, true);
344     return result;
345   }
346 
347   private void findColumns(final UIComponent component, final List<AbstractUIColumnBase> result, final boolean all) {
348     for (final UIComponent child : component.getChildren()) {
349       if (all || child.isRendered()) {
350         if (child instanceof AbstractUIColumnBase) {
351           result.add((AbstractUIColumnBase) child);
352         } else if (child instanceof AbstractUIData) {
353           // ignore columns of nested sheets
354         } else {
355           findColumns(child, result, all);
356         }
357       }
358     }
359   }
360 
361   @Override
362   public void queueEvent(final FacesEvent facesEvent) {
363     final UIComponent parent = getParent();
364     if (parent == null) {
365       throw new IllegalStateException("Component is not a descendant of a UIViewRoot");
366     }
367 
368     if (facesEvent.getComponent() == this
369         && (facesEvent instanceof SheetStateChangeEvent
370         || facesEvent instanceof PageActionEvent)) {
371       facesEvent.setPhaseId(PhaseId.INVOKE_APPLICATION);
372       parent.queueEvent(facesEvent);
373     } else {
374       super.queueEvent(facesEvent);
375     }
376   }
377 
378   @Override
379   public void broadcast(final FacesEvent facesEvent) throws AbortProcessingException {
380     super.broadcast(facesEvent);
381     if (facesEvent instanceof SheetStateChangeEvent) {
382       final MethodExpression listener = getStateChangeListenerExpression();
383       listener.invoke(getFacesContext().getELContext(), new Object[]{facesEvent});
384     } else if (facesEvent instanceof PageActionEvent) {
385       if (facesEvent.getComponent() == this) {
386         final MethodExpression listener = getStateChangeListenerExpression();
387         if (listener != null) {
388           listener.invoke(getFacesContext().getELContext(), new Object[]{facesEvent});
389         }
390         performPaging((PageActionEvent) facesEvent);
391       }
392     } else if (facesEvent instanceof SortActionEvent) {
393       getSheetState(getFacesContext()).updateSortState(((SortActionEvent) facesEvent).getColumn().getId());
394       sort(getFacesContext(), (SortActionEvent) facesEvent);
395     }
396   }
397 
398   public void init(final FacesContext facesContext) {
399     sort(facesContext, null);
400     layoutHeader();
401   }
402 
403   private void layoutHeader() {
404     final UIComponent header = getHeader();
405     if (header == null) {
406       LOG.warn("This should not happen. Please file a bug in the issue tracker to reproduce this case.");
407       return;
408     }
409     final MeasureList tokens = new MeasureList();
410     final List<AbstractUIColumnBase> columns = getAllColumns();
411     for (final UIColumn column : columns) {
412       if (!(column instanceof AbstractUIRow)) {
413         tokens.add(Measure.FRACTION1);
414       }
415     }
416     final MeasureList rows = new MeasureList();
417     rows.add(Measure.AUTO);
418     final Grid grid = new Grid(tokens, rows);
419 
420     for (final UIComponent child : header.getChildren()) {
421       if (child.isRendered()) {
422         final int columnSpan = ComponentUtils.getIntAttribute(child, Attributes.columnSpan, 1);
423         final int rowSpan = ComponentUtils.getIntAttribute(child, Attributes.rowSpan, 1);
424         grid.add(new OriginCell(child), columnSpan, rowSpan);
425       }
426     }
427     setHeaderGrid(grid);
428   }
429 
430   protected void sort(final FacesContext facesContext, final SortActionEvent event) {
431     final SheetState sheetState = getSheetState(getFacesContext());
432     if (sheetState.isToBeSorted()) {
433       final MethodExpression expression = getSortActionListenerExpression();
434       if (expression != null) {
435         try {
436           expression.invoke(facesContext.getELContext(),
437               new Object[]{
438                   event != null
439                       ? event
440                       : new SortActionEvent(this,
441                       (UIColumn) findComponent(getSheetState(facesContext).getSortedColumnId()))});
442         } catch (final Exception e) {
443           LOG.warn("Sorting not possible!", e);
444         }
445       } else {
446         new Sorter().perform(this);
447       }
448       sheetState.setToBeSorted(false);
449     }
450   }
451 
452   @Override
453   public void addStateChangeListener(final SheetStateChangeListener listener) {
454     addFacesListener(listener);
455   }
456 
457   @Override
458   public SheetStateChangeListener[] getStateChangeListeners() {
459     return (SheetStateChangeListener[]) getFacesListeners(SheetStateChangeListener.class);
460   }
461 
462   @Override
463   public void removeStateChangeListener(final SheetStateChangeListener listener) {
464     removeFacesListener(listener);
465   }
466 
467   @Override
468   public UIComponent findComponent(final String searchId) {
469     return super.findComponent(stripRowIndex(searchId));
470   }
471 
472   public String stripRowIndex(final String initialSearchId) {
473     String searchId = initialSearchId;
474     if (searchId.length() > 0 && Character.isDigit(searchId.charAt(0))) {
475       for (int i = 1; i < searchId.length(); ++i) {
476         final char c = searchId.charAt(i);
477         if (c == UINamingContainer.getSeparatorChar(getFacesContext())) {
478           searchId = searchId.substring(i + 1);
479           break;
480         }
481         if (!Character.isDigit(c)) {
482           break;
483         }
484       }
485     }
486     return searchId;
487   }
488 
489   public void performPaging(final PageActionEvent pageEvent) {
490 
491     int first;
492 
493     if (LOG.isDebugEnabled()) {
494       LOG.debug("action = '" + pageEvent.getAction().name() + "'");
495     }
496 
497     switch (pageEvent.getAction()) {
498       case first:
499         first = 0;
500         break;
501       case prev:
502         first = getFirst() - getRows();
503         first = first < 0 ? 0 : first;
504         break;
505       case next:
506         if (hasRowCount()) {
507           first = getFirst() + getRows();
508           first = first > getRowCount() ? getFirstRowIndexOfLastPage() : first;
509         } else {
510           if (isAtEnd()) {
511             first = getFirst();
512           } else {
513             first = getFirst() + getRows();
514           }
515         }
516         break;
517       case last:
518         first = getFirstRowIndexOfLastPage();
519         break;
520       case toRow:
521         first = pageEvent.getValue() - 1;
522         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
523           first = getFirstRowIndexOfLastPage();
524         } else if (first < 0) {
525           first = 0;
526         }
527         break;
528       case toPage:
529         final int pageIndex = pageEvent.getValue() - 1;
530         first = pageIndex * getRows();
531         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
532           first = getFirstRowIndexOfLastPage();
533         } else if (first < 0) {
534           first = 0;
535         }
536         break;
537       default:
538         // may not happen
539         first = -1;
540     }
541 
542     final ValueExpression expression = getValueExpression(Attributes.first.getName());
543     if (expression != null) {
544       expression.setValue(getFacesContext().getELContext(), first);
545     } else {
546       setFirst(first);
547     }
548 
549     getState().setFirst(first);
550   }
551 
552   @Override
553   public boolean isRendersRowContainer() {
554     return true;
555   }
556 
557   public abstract boolean isShowHeader();
558 
559   @Override
560   public ExpandedState getExpandedState() {
561     return getState().getExpandedState();
562   }
563 
564   @Override
565   public SelectedState getSelectedState() {
566     return getState().getSelectedState();
567   }
568 
569   public Grid getHeaderGrid() {
570     return headerGrid;
571   }
572 
573   public void setHeaderGrid(final Grid headerGrid) {
574     this.headerGrid = headerGrid;
575   }
576 }