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