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