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.OnComponentPopulated;
24  import org.apache.myfaces.tobago.component.Sorter;
25  import org.apache.myfaces.tobago.component.Visual;
26  import org.apache.myfaces.tobago.event.PageActionEvent;
27  import org.apache.myfaces.tobago.event.SheetStateChangeEvent;
28  import org.apache.myfaces.tobago.event.SheetStateChangeListener;
29  import org.apache.myfaces.tobago.event.SheetStateChangeSource2;
30  import org.apache.myfaces.tobago.event.SortActionEvent;
31  import org.apache.myfaces.tobago.event.SortActionSource2;
32  import org.apache.myfaces.tobago.internal.layout.Grid;
33  import org.apache.myfaces.tobago.internal.layout.OriginCell;
34  import org.apache.myfaces.tobago.layout.AutoLayoutToken;
35  import org.apache.myfaces.tobago.layout.LayoutToken;
36  import org.apache.myfaces.tobago.layout.LayoutTokens;
37  import org.apache.myfaces.tobago.layout.RelativeLayoutToken;
38  import org.apache.myfaces.tobago.model.ExpandedState;
39  import org.apache.myfaces.tobago.model.SelectedState;
40  import org.apache.myfaces.tobago.model.SheetState;
41  import org.apache.myfaces.tobago.util.ComponentUtils;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  
45  import javax.el.ELContext;
46  import javax.el.MethodExpression;
47  import javax.el.ValueExpression;
48  import javax.faces.component.UIColumn;
49  import javax.faces.component.UIComponent;
50  import javax.faces.component.UINamingContainer;
51  import javax.faces.component.behavior.ClientBehaviorHolder;
52  import javax.faces.context.FacesContext;
53  import javax.faces.event.AbortProcessingException;
54  import javax.faces.event.ComponentSystemEvent;
55  import javax.faces.event.ComponentSystemEventListener;
56  import javax.faces.event.FacesEvent;
57  import javax.faces.event.ListenerFor;
58  import javax.faces.event.PhaseId;
59  import javax.faces.event.PreRenderComponentEvent;
60  import java.io.IOException;
61  import java.util.ArrayList;
62  import java.util.Collection;
63  import java.util.Collections;
64  import java.util.List;
65  
66  @ListenerFor(systemEventClass = PreRenderComponentEvent.class)
67  public abstract class AbstractUISheet extends AbstractUIData
68      implements SheetStateChangeSource2, SortActionSource2, OnComponentPopulated, ClientBehaviorHolder, Visual,
69                 ComponentSystemEventListener {
70  
71    private static final Logger LOG = LoggerFactory.getLogger(AbstractUISheet.class);
72  
73    public static final String COMPONENT_TYPE = "org.apache.myfaces.tobago.Data";
74  
75    public static final String SORTER_ID = "sorter";
76  
77    // todo generate
78    private static final Collection<String> EVENT_NAMES = Collections.singletonList("reload");
79  
80    private SheetState state;
81    private transient LayoutTokens columnLayout;
82    private transient boolean autoLayout;
83  
84    private transient Grid headerGrid;
85  
86    @Override
87    public void encodeBegin(final FacesContext facesContext) throws IOException {
88      final SheetState state = getSheetState(facesContext);
89      final int first = state.getFirst();
90      if (first > -1 && (!hasRowCount() || first < getRowCount())) {
91        final ValueExpression expression = getValueExpression(Attributes.first.getName());
92        if (expression != null) {
93          expression.setValue(facesContext.getELContext(), first);
94        } else {
95          setFirst(first);
96        }
97      }
98  
99      super.encodeBegin(facesContext);
100   }
101 
102   public void setState(final SheetState state) {
103     this.state = state;
104   }
105 
106   public SheetState getState() {
107     return getSheetState(FacesContext.getCurrentInstance());
108   }
109 
110   public SheetState getSheetState(final FacesContext facesContext) {
111     if (state != null) {
112       return state;
113     }
114 
115     final ValueExpression expression = getValueExpression(Attributes.state.getName());
116     if (expression != null) {
117       final ELContext elContext = facesContext.getELContext();
118       SheetState sheetState = (SheetState) expression.getValue(elContext);
119       if (sheetState == null) {
120         sheetState = new SheetState();
121         expression.setValue(elContext, sheetState);
122       }
123       return sheetState;
124     }
125 
126     state = new SheetState();
127     return state;
128   }
129 
130   public abstract String getColumns();
131 
132   @Override
133   public void processEvent(ComponentSystemEvent event) throws AbortProcessingException {
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       final UIComponent source = facesEvent.getComponent();
373       final UIComponent sourceParent = source.getParent();
374       if (sourceParent.getParent() == this
375           && source.getId() != null && source.getId().endsWith(SORTER_ID)) {
376         facesEvent.setPhaseId(PhaseId.INVOKE_APPLICATION);
377         parent.queueEvent(new SortActionEvent(this, (UIColumn) sourceParent));
378       } else {
379         super.queueEvent(facesEvent);
380       }
381     }
382   }
383 
384   @Override
385   public void broadcast(final FacesEvent facesEvent) throws AbortProcessingException {
386     super.broadcast(facesEvent);
387     if (facesEvent instanceof SheetStateChangeEvent) {
388       final MethodExpression listener = getStateChangeListenerExpression();
389       listener.invoke(getFacesContext().getELContext(), new Object[]{facesEvent});
390     } else if (facesEvent instanceof PageActionEvent) {
391       if (facesEvent.getComponent() == this) {
392         final MethodExpression listener = getStateChangeListenerExpression();
393         if (listener != null) {
394           listener.invoke(getFacesContext().getELContext(), new Object[]{facesEvent});
395         }
396         performPaging((PageActionEvent) facesEvent);
397       }
398     } else if (facesEvent instanceof SortActionEvent) {
399       getSheetState(getFacesContext()).updateSortState((SortActionEvent) facesEvent);
400       sort(getFacesContext(), (SortActionEvent) facesEvent);
401     }
402   }
403 
404   public void init(FacesContext facesContext) {
405     sort(facesContext, null);
406     layoutHeader();
407   }
408 
409   private void layoutHeader() {
410     final UIComponent header = getHeader();
411     if (header == null) {
412       LOG.warn("This should not happen. Please file a bug in the issue tracker to reproduce this case.");
413       return;
414     }
415     final LayoutTokens tokens = new LayoutTokens();
416     final List<AbstractUIColumnBase> columns = getAllColumns();
417     for (final UIColumn column : columns) {
418       if (!(column instanceof AbstractUIColumnEvent)) {
419         tokens.addToken(RelativeLayoutToken.DEFAULT_INSTANCE);
420       }
421     }
422     final LayoutTokens rows = new LayoutTokens();
423     rows.addToken(AutoLayoutToken.INSTANCE);
424     final Grid grid = new Grid(tokens, rows);
425 
426     for (final UIComponent child : header.getChildren()) {
427       if (child.isRendered()) {
428         int columnSpan = ComponentUtils.getIntAttribute(child, Attributes.columnSpan, 1);
429         int rowSpan = ComponentUtils.getIntAttribute(child, Attributes.rowSpan, 1);
430         grid.add(new OriginCell(child), columnSpan, rowSpan);
431       }
432     }
433     setHeaderGrid(grid);
434   }
435 
436   protected void sort(FacesContext facesContext, SortActionEvent event) {
437     final SheetState sheetState = getSheetState(getFacesContext());
438     if (sheetState.isToBeSorted()) {
439       final MethodExpression expression = getSortActionListenerExpression();
440       if (expression != null) {
441         try {
442           if (event == null) {
443             event =
444                 new SortActionEvent(this, (UIColumn) findComponent(getSheetState(facesContext).getSortedColumnId()));
445           }
446           expression.invoke(facesContext.getELContext(), new Object[]{event});
447         } catch (Exception e) {
448           LOG.warn("Sorting not possible!", e);
449         }
450       } else {
451         new Sorter().perform(this);
452       }
453       sheetState.setToBeSorted(false);
454     }
455   }
456 
457   @Override
458   public void addStateChangeListener(final SheetStateChangeListener listener) {
459     addFacesListener(listener);
460   }
461 
462   @Override
463   public SheetStateChangeListener[] getStateChangeListeners() {
464     return (SheetStateChangeListener[]) getFacesListeners(SheetStateChangeListener.class);
465   }
466 
467   @Override
468   public void removeStateChangeListener(final SheetStateChangeListener listener) {
469     removeFacesListener(listener);
470   }
471 
472   @Override
473   public UIComponent findComponent(final String searchId) {
474     return super.findComponent(stripRowIndex(searchId));
475   }
476 
477   public String stripRowIndex(String searchId) {
478     if (searchId.length() > 0 && Character.isDigit(searchId.charAt(0))) {
479       for (int i = 1; i < searchId.length(); ++i) {
480         final char c = searchId.charAt(i);
481         if (c == UINamingContainer.getSeparatorChar(getFacesContext())) {
482           searchId = searchId.substring(i + 1);
483           break;
484         }
485         if (!Character.isDigit(c)) {
486           break;
487         }
488       }
489     }
490     return searchId;
491   }
492 
493   public void performPaging(final PageActionEvent pageEvent) {
494 
495     int first;
496 
497     if (LOG.isDebugEnabled()) {
498       LOG.debug("action = '" + pageEvent.getAction().name() + "'");
499     }
500 
501     switch (pageEvent.getAction()) {
502       case FIRST:
503         first = 0;
504         break;
505       case PREV:
506         first = getFirst() - getRows();
507         first = first < 0 ? 0 : first;
508         break;
509       case NEXT:
510         if (hasRowCount()) {
511           first = getFirst() + getRows();
512           first = first > getRowCount() ? getFirstRowIndexOfLastPage() : first;
513         } else {
514           if (isAtEnd()) {
515             first = getFirst();
516           } else {
517             first = getFirst() + getRows();
518           }
519         }
520         break;
521       case LAST:
522         first = getFirstRowIndexOfLastPage();
523         break;
524       case TO_ROW:
525         first = pageEvent.getValue() - 1;
526         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
527           first = getFirstRowIndexOfLastPage();
528         } else if (first < 0) {
529           first = 0;
530         }
531         break;
532       case TO_PAGE:
533         final int pageIndex = pageEvent.getValue() - 1;
534         first = pageIndex * getRows();
535         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
536           first = getFirstRowIndexOfLastPage();
537         } else if (first < 0) {
538           first = 0;
539         }
540         break;
541       default:
542         // may not happen
543         first = -1;
544     }
545 
546     final ValueExpression expression = getValueExpression(Attributes.first.getName());
547     if (expression != null) {
548       expression.setValue(getFacesContext().getELContext(), first);
549     } else {
550       setFirst(first);
551     }
552 
553     getState().setFirst(first);
554   }
555 
556   @Override
557   public void onComponentPopulated(final FacesContext facesContext, final UIComponent parent) {
558   }
559 
560   @Override
561   public boolean isRendersRowContainer() {
562     return true;
563   }
564 
565   public abstract boolean isShowHeader();
566 
567   @Override
568   public ExpandedState getExpandedState() {
569     return getState().getExpandedState();
570   }
571 
572   @Override
573   public SelectedState getSelectedState() {
574     return getState().getSelectedState();
575   }
576 
577   public Grid getHeaderGrid() {
578     return headerGrid;
579   }
580 
581   public void setHeaderGrid(final Grid headerGrid) {
582     this.headerGrid = headerGrid;
583   }
584 
585   @Override
586   public String getDefaultEventName() {
587     return "reload";
588   }
589 
590   @Override
591   public Collection<String> getEventNames() {
592     return EVENT_NAMES;
593   }
594 }