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  /**
65   * {@link org.apache.myfaces.tobago.internal.taglib.component.SheetTagDeclaration}
66   */
67  @ListenerFor(systemEventClass = PreRenderComponentEvent.class)
68  public abstract class AbstractUISheet extends AbstractUIData
69      implements SheetStateChangeSource2, SortActionSource2, ClientBehaviorHolder, Visual,
70                 ComponentSystemEventListener {
71  
72    private static final Logger LOG = LoggerFactory.getLogger(AbstractUISheet.class);
73  
74    public static final String COMPONENT_TYPE = "org.apache.myfaces.tobago.Data";
75  
76    public static final String SORTER_ID = "sorter";
77  
78    private SheetState state;
79    private transient LayoutTokens columnLayout;
80    private transient boolean autoLayout;
81  
82    private transient Grid headerGrid;
83  
84    @Override
85    public void encodeBegin(final FacesContext facesContext) throws IOException {
86      final SheetState state = getSheetState(facesContext);
87      final int first = state.getFirst();
88      if (first > -1 && (!hasRowCount() || first < getRowCount())) {
89        final ValueExpression expression = getValueExpression(Attributes.first.getName());
90        if (expression != null) {
91          expression.setValue(facesContext.getELContext(), first);
92        } else {
93          setFirst(first);
94        }
95      }
96  
97      super.encodeBegin(facesContext);
98    }
99  
100   public void setState(final SheetState state) {
101     this.state = state;
102   }
103 
104   public SheetState getState() {
105     return getSheetState(FacesContext.getCurrentInstance());
106   }
107 
108   public SheetState getSheetState(final FacesContext facesContext) {
109     if (state != null) {
110       return state;
111     }
112 
113     final ValueExpression expression = getValueExpression(Attributes.state.getName());
114     if (expression != null) {
115       final ELContext elContext = facesContext.getELContext();
116       SheetState sheetState = (SheetState) expression.getValue(elContext);
117       if (sheetState == null) {
118         sheetState = new SheetState();
119         expression.setValue(elContext, sheetState);
120       }
121       return sheetState;
122     }
123 
124     state = new SheetState();
125     return state;
126   }
127 
128   public abstract String getColumns();
129 
130   @Override
131   public void processEvent(ComponentSystemEvent event) throws AbortProcessingException {
132 
133     super.processEvent(event);
134 
135     if (event instanceof PreRenderComponentEvent) {
136       final String columns = getColumns();
137       if (columns != null) {
138         columnLayout = LayoutTokens.parse(columns);
139       }
140 
141       autoLayout = true;
142       if (columnLayout != null) {
143         for (LayoutToken layoutToken : columnLayout.getTokens()) {
144           if (!(layoutToken instanceof AutoLayoutToken)) {
145             autoLayout = false;
146             break;
147           }
148         }
149       }
150 
151       LOG.debug("autoLayout={}", autoLayout);
152     }
153   }
154 
155   public LayoutTokens getColumnLayout() {
156     return columnLayout;
157   }
158 
159   public boolean isAutoLayout() {
160     return autoLayout;
161   }
162 
163   /**
164    * @deprecated The name of this method is ambiguous.
165    * You may use {@link #getLastRowIndexOfCurrentPage()}. Deprecated since 1.5.5.
166    */
167   public int getLast() {
168     final int last = getFirst() + getRows();
169     return last < getRowCount() ? last : getRowCount();
170   }
171 
172   /**
173    * The rowIndex of the last row on the current page plus one (because of zero based iterating).
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 Please use {@link #getCurrentPage()} which returns the value zero-based. Deprecated since 1.5.5.
209    */
210   @Deprecated
211   public int getPage() {
212     return getCurrentPage() + 1;
213   }
214 
215   /**
216    * The number of pages to render.
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<UIComponent>();
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    * @return The index of the first row of the last paging page.
294    * @throws IllegalArgumentException If the number of rows in the model returned
295    * by {@link #getRowCount()} is -1 (undefined).
296    */
297   public int getFirstRowIndexOfLastPage() {
298     if (isRowsUnlimited()) {
299       return 0;
300     } else if (!hasRowCount()) {
301       throw new IllegalArgumentException(
302           "Can't determine the last page, because the row count of the model is unknown.");
303     } else {
304       final int rows = getRows();
305       final int rowCount = getRowCount();
306       final int tail = rowCount % rows;
307       return rowCount - (tail != 0 ? tail : rows);
308     }
309   }
310 
311   @Override
312   public void processUpdates(final FacesContext context) {
313     super.processUpdates(context);
314 
315     final SheetState state = getSheetState(context);
316     if (state != null) {
317       final List<Integer> list = (List<Integer>) ComponentUtils.getAttribute(this, Attributes.selectedListString);
318       state.setSelectedRows(list != null ? list : Collections.<Integer>emptyList());
319       ComponentUtils.removeAttribute(this, Attributes.selectedListString);
320       ComponentUtils.removeAttribute(this, Attributes.scrollPosition);
321     }
322   }
323 
324   @Override
325   public Object saveState(final FacesContext context) {
326     final Object[] saveState = new Object[2];
327     saveState[0] = super.saveState(context);
328     saveState[1] = state;
329     return saveState;
330   }
331 
332   @Override
333   public void restoreState(final FacesContext context, final Object savedState) {
334     final Object[] values = (Object[]) savedState;
335     super.restoreState(context, values[0]);
336     state = (SheetState) values[1];
337   }
338 
339   public List<AbstractUIColumnBase> getAllColumns() {
340     ArrayList<AbstractUIColumnBase> result = new ArrayList<AbstractUIColumnBase>();
341     findColumns(this, result, true);
342     return result;
343   }
344 
345   private void findColumns(final UIComponent component, final List<AbstractUIColumnBase> result, final boolean all) {
346     for (final UIComponent child : component.getChildren()) {
347       if (all || child.isRendered()) {
348         if (child instanceof AbstractUIColumnBase) {
349           result.add((AbstractUIColumnBase) child);
350         } else if (child instanceof AbstractUIData) {
351           // ignore columns of nested sheets
352         } else {
353           findColumns(child, result, all);
354         }
355       }
356     }
357   }
358 
359   @Override
360   public void queueEvent(final FacesEvent facesEvent) {
361     final UIComponent parent = getParent();
362     if (parent == null) {
363       throw new IllegalStateException("Component is not a descendant of a UIViewRoot");
364     }
365 
366     if (facesEvent.getComponent() == this
367         && (facesEvent instanceof SheetStateChangeEvent
368         || facesEvent instanceof PageActionEvent)) {
369       facesEvent.setPhaseId(PhaseId.INVOKE_APPLICATION);
370       parent.queueEvent(facesEvent);
371     } else {
372       super.queueEvent(facesEvent);
373     }
374   }
375 
376   @Override
377   public void broadcast(final FacesEvent facesEvent) throws AbortProcessingException {
378     super.broadcast(facesEvent);
379     if (facesEvent instanceof SheetStateChangeEvent) {
380       final MethodExpression listener = getStateChangeListenerExpression();
381       listener.invoke(getFacesContext().getELContext(), new Object[]{facesEvent});
382     } else if (facesEvent instanceof PageActionEvent) {
383       if (facesEvent.getComponent() == this) {
384         final MethodExpression listener = getStateChangeListenerExpression();
385         if (listener != null) {
386           listener.invoke(getFacesContext().getELContext(), new Object[]{facesEvent});
387         }
388         performPaging((PageActionEvent) facesEvent);
389       }
390     } else if (facesEvent instanceof SortActionEvent) {
391       getSheetState(getFacesContext()).updateSortState((SortActionEvent) facesEvent);
392       sort(getFacesContext(), (SortActionEvent) facesEvent);
393     }
394   }
395 
396   public void init(FacesContext facesContext) {
397     sort(facesContext, null);
398     layoutHeader();
399   }
400 
401   private void layoutHeader() {
402     final UIComponent header = getHeader();
403     if (header == null) {
404       LOG.warn("This should not happen. Please file a bug in the issue tracker to reproduce this case.");
405       return;
406     }
407     final LayoutTokens tokens = new LayoutTokens();
408     final List<AbstractUIColumnBase> columns = getAllColumns();
409     for (final UIColumn column : columns) {
410       if (!(column instanceof AbstractUIRow)) {
411         tokens.addToken(RelativeLayoutToken.DEFAULT_INSTANCE);
412       }
413     }
414     final LayoutTokens rows = new LayoutTokens();
415     rows.addToken(AutoLayoutToken.INSTANCE);
416     final Grid grid = new Grid(tokens, rows);
417 
418     for (final UIComponent child : header.getChildren()) {
419       if (child.isRendered()) {
420         int columnSpan = ComponentUtils.getIntAttribute(child, Attributes.column, 1);
421         int rowSpan = ComponentUtils.getIntAttribute(child, Attributes.row, 1);
422         grid.add(new OriginCell(child), columnSpan, rowSpan);
423       }
424     }
425     setHeaderGrid(grid);
426   }
427 
428   protected void sort(final FacesContext facesContext, final SortActionEvent event) {
429     final SheetState sheetState = getSheetState(getFacesContext());
430     if (sheetState.isToBeSorted()) {
431       final MethodExpression expression = getSortActionListenerExpression();
432       if (expression != null) {
433         try {
434           expression.invoke(facesContext.getELContext(),
435               new Object[]{
436                   event != null
437                       ? event
438                       : new SortActionEvent(this,
439                       (UIColumn) findComponent(getSheetState(facesContext).getSortedColumnId()))});
440         } catch (Exception e) {
441           LOG.warn("Sorting not possible!", e);
442         }
443       } else {
444         new Sorter().perform(this);
445       }
446       sheetState.setToBeSorted(false);
447     }
448   }
449 
450   @Override
451   public void addStateChangeListener(final SheetStateChangeListener listener) {
452     addFacesListener(listener);
453   }
454 
455   @Override
456   public SheetStateChangeListener[] getStateChangeListeners() {
457     return (SheetStateChangeListener[]) getFacesListeners(SheetStateChangeListener.class);
458   }
459 
460   @Override
461   public void removeStateChangeListener(final SheetStateChangeListener listener) {
462     removeFacesListener(listener);
463   }
464 
465   @Override
466   public UIComponent findComponent(final String searchId) {
467     return super.findComponent(stripRowIndex(searchId));
468   }
469 
470   public String stripRowIndex(final String initialSearchId) {
471     String searchId = initialSearchId;
472     if (searchId.length() > 0 && Character.isDigit(searchId.charAt(0))) {
473       for (int i = 1; i < searchId.length(); ++i) {
474         final char c = searchId.charAt(i);
475         if (c == UINamingContainer.getSeparatorChar(getFacesContext())) {
476           searchId = searchId.substring(i + 1);
477           break;
478         }
479         if (!Character.isDigit(c)) {
480           break;
481         }
482       }
483     }
484     return searchId;
485   }
486 
487   public void performPaging(final PageActionEvent pageEvent) {
488 
489     int first;
490 
491     if (LOG.isDebugEnabled()) {
492       LOG.debug("action = '" + pageEvent.getAction().name() + "'");
493     }
494 
495     switch (pageEvent.getAction()) {
496       case first:
497         first = 0;
498         break;
499       case prev:
500         first = getFirst() - getRows();
501         first = first < 0 ? 0 : first;
502         break;
503       case next:
504         if (hasRowCount()) {
505           first = getFirst() + getRows();
506           first = first > getRowCount() ? getFirstRowIndexOfLastPage() : first;
507         } else {
508           if (isAtEnd()) {
509             first = getFirst();
510           } else {
511             first = getFirst() + getRows();
512           }
513         }
514         break;
515       case last:
516         first = getFirstRowIndexOfLastPage();
517         break;
518       case toRow:
519         first = pageEvent.getValue() - 1;
520         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
521           first = getFirstRowIndexOfLastPage();
522         } else if (first < 0) {
523           first = 0;
524         }
525         break;
526       case toPage:
527         final int pageIndex = pageEvent.getValue() - 1;
528         first = pageIndex * getRows();
529         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
530           first = getFirstRowIndexOfLastPage();
531         } else if (first < 0) {
532           first = 0;
533         }
534         break;
535       default:
536         // may not happen
537         first = -1;
538     }
539 
540     final ValueExpression expression = getValueExpression(Attributes.first.getName());
541     if (expression != null) {
542       expression.setValue(getFacesContext().getELContext(), first);
543     } else {
544       setFirst(first);
545     }
546 
547     getState().setFirst(first);
548   }
549 
550   @Override
551   public boolean isRendersRowContainer() {
552     return true;
553   }
554 
555   public abstract boolean isShowHeader();
556 
557   @Override
558   public ExpandedState getExpandedState() {
559     return getState().getExpandedState();
560   }
561 
562   @Override
563   public SelectedState getSelectedState() {
564     return getState().getSelectedState();
565   }
566 
567   public Grid getHeaderGrid() {
568     return headerGrid;
569   }
570 
571   public void setHeaderGrid(final Grid headerGrid) {
572     this.headerGrid = headerGrid;
573   }
574 }