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