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.compat.FacesUtils;
23  import org.apache.myfaces.tobago.component.Attributes;
24  import org.apache.myfaces.tobago.component.ColumnEvent;
25  import org.apache.myfaces.tobago.component.ComponentTypes;
26  import org.apache.myfaces.tobago.component.Facets;
27  import org.apache.myfaces.tobago.component.OnComponentPopulated;
28  import org.apache.myfaces.tobago.component.RendererTypes;
29  import org.apache.myfaces.tobago.component.Sorter;
30  import org.apache.myfaces.tobago.component.SupportsRenderedPartially;
31  import org.apache.myfaces.tobago.event.PageActionEvent;
32  import org.apache.myfaces.tobago.event.SheetStateChangeEvent;
33  import org.apache.myfaces.tobago.event.SheetStateChangeListener;
34  import org.apache.myfaces.tobago.event.SheetStateChangeSource;
35  import org.apache.myfaces.tobago.event.SortActionEvent;
36  import org.apache.myfaces.tobago.event.SortActionSource;
37  import org.apache.myfaces.tobago.internal.layout.Grid;
38  import org.apache.myfaces.tobago.layout.LayoutComponent;
39  import org.apache.myfaces.tobago.layout.LayoutContainer;
40  import org.apache.myfaces.tobago.layout.LayoutManager;
41  import org.apache.myfaces.tobago.layout.LayoutTokens;
42  import org.apache.myfaces.tobago.model.ExpandedState;
43  import org.apache.myfaces.tobago.model.SheetState;
44  import org.apache.myfaces.tobago.renderkit.LayoutComponentRenderer;
45  import org.apache.myfaces.tobago.util.ComponentUtils;
46  import org.apache.myfaces.tobago.util.CreateComponentUtils;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  import javax.el.ELContext;
51  import javax.el.ValueExpression;
52  import javax.faces.component.UIColumn;
53  import javax.faces.component.UIComponent;
54  import javax.faces.context.FacesContext;
55  import javax.faces.el.MethodBinding;
56  import javax.faces.event.AbortProcessingException;
57  import javax.faces.event.FacesEvent;
58  import javax.faces.event.PhaseId;
59  import java.io.IOException;
60  import java.util.ArrayList;
61  import java.util.Collections;
62  import java.util.List;
63  import java.util.Map;
64  
65  public abstract class AbstractUISheet extends AbstractUIData
66      implements SheetStateChangeSource, SortActionSource, OnComponentPopulated,
67      LayoutContainer, LayoutComponent, SupportsRenderedPartially {
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    /**
74     * @see org.apache.myfaces.tobago.component.Facets
75     * @deprecated Please use Facets instead. Will be removed after Tobago 1.5.0 */
76    @Deprecated
77    public static final String FACET_SORTER = "sorter";
78    public static final String SORTER_ID = "sorter";
79    /**
80     * @see org.apache.myfaces.tobago.component.Attributes
81     * @deprecated Please use Attributes instead. Will be removed after Tobago 1.5.0 */
82    @Deprecated
83    public static final String ATTR_SCROLL_POSITION = "attrScrollPosition";
84  
85    public static final String NONE = "none";
86    public static final String SINGLE = "single";
87    public static final String MULTI = "multi";
88  
89    private SheetState state;
90    private List<Integer> widthList;
91    private transient LayoutTokens columnLayout;
92  
93    private transient int ajaxResponseCode;
94  
95    private transient List<LayoutComponent> layoutComponents;
96  
97    private transient Boolean needVerticalScrollbar;
98  
99    private transient Grid headerGrid;
100 
101   public LayoutComponentRenderer getLayoutComponentRenderer(FacesContext context) {
102     return (LayoutComponentRenderer) getRenderer(context);
103   }
104 
105   @Override
106   public void encodeBegin(FacesContext facesContext) throws IOException {
107     SheetState state = getSheetState(facesContext);
108     final int first = state.getFirst();
109     if (first > -1 && (!hasRowCount() || first < getRowCount())) {
110       final ValueExpression expression = getValueExpression(Attributes.FIRST);
111       if (expression != null) {
112         expression.setValue(facesContext.getELContext(), first);
113       } else {
114         setFirst(first);
115       }
116     }
117     super.encodeBegin(facesContext);
118   }
119 
120   public void setState(SheetState state) {
121     this.state = state;
122   }
123 
124   public SheetState getState() {
125     return getSheetState(FacesContext.getCurrentInstance());
126   }
127 
128   public SheetState getSheetState(FacesContext facesContext) {
129     if (state != null) {
130       return state;
131     }
132 
133     final ValueExpression expression = getValueExpression(Attributes.STATE);
134     if (expression != null) {
135       final ELContext elContext = facesContext.getELContext();
136       SheetState sheetState = (SheetState) expression.getValue(elContext);
137       if (sheetState == null) {
138         sheetState = new SheetState();
139         expression.setValue(elContext, sheetState);
140       }
141       return sheetState;
142     }
143 
144     state = new SheetState();
145     return state;
146   }
147 
148   public abstract String getColumns();
149 
150   public LayoutTokens getColumnLayout() {
151     if (columnLayout == null) {
152       String columns = getColumns();
153       if (columns != null) {
154         columnLayout = LayoutTokens.parse(columns);
155       }
156     }
157     return columnLayout;
158   }
159 
160   /**
161    * Remove the (by user) resized column widths. An application may provide a button to access it.
162    * Since 1.0.26.
163    */
164   public void resetColumnWidths() {
165     SheetState state = getState();
166     if (state != null) {
167       state.setColumnWidths(null);
168     }
169     getAttributes().remove(Attributes.WIDTH_LIST_STRING);
170   }
171 
172   /**
173    * @deprecated The name of this method is ambiguous.
174    * You may use {@link #getLastRowIndexOfCurrentPage()}. Deprecated since 1.5.5.
175    */
176   public int getLast() {
177     int last = getFirst() + getRows();
178     return last < getRowCount() ? last : getRowCount();
179   }
180 
181   /**
182    * The rowIndex of the last row on the current page plus one (because of zero based iterating).
183    * @throws IllegalArgumentException If the number of rows in the model returned
184    * by {@link #getRowCount()} is -1 (undefined).
185    */
186   public int getLastRowIndexOfCurrentPage() {
187     if (!hasRowCount()) {
188       throw new IllegalArgumentException(
189           "Can't determine the last row, because the row count of the model is unknown.");
190     }
191     if (isRowsUnlimited()) {
192       return getRowCount();
193     }
194     int last = getFirst() + getRows();
195     return last < getRowCount() ? last : getRowCount();
196   }
197 
198   /**
199    * @return returns the current page (based by 0).
200    */
201   public int getCurrentPage() {
202     int rows = getRows();
203     if (rows == 0) {
204       // if the rows are unlimited, there is only one page
205       return 0;
206     }
207     int first = getFirst();
208     if (hasRowCount() && first >= getRowCount()) {
209       return getPages() - 1; // last page
210     } else {
211       return (first / rows);
212     }
213   }
214   
215   /**
216    * @return returns the current page (based by 1).
217    * @deprecated Please use {@link #getCurrentPage()} which returns the value zero-based. Deprecated since 1.5.5.
218    */
219   @Deprecated
220   public int getPage() {
221     return getCurrentPage() + 1;
222   }
223 
224   /**
225    * The number of pages to render.
226    * @throws IllegalArgumentException If the number of rows in the model returned
227    * by {@link #getRowCount()} is -1 (undefined).
228    */
229   public int getPages() {
230     if (isRowsUnlimited()) {
231       return 1;
232     }
233     if (!hasRowCount()) {
234       throw new IllegalArgumentException(
235           "Can't determine the number of pages, because the row count of the model is unknown.");
236     }
237     return (getRowCount() - 1) / getRows() + 1;
238   }
239 
240   public List<UIComponent> getRenderedChildrenOf(UIColumn column) {
241     List<UIComponent> children = new ArrayList<UIComponent>();
242     for (UIComponent kid : column.getChildren()) {
243       if (kid.isRendered()) {
244         children.add(kid);
245       }
246     }
247     return children;
248   }
249 
250   /**
251    * @return Is the interval to display starting with the first row?
252    */
253   public boolean isAtBeginning() {
254     return getFirst() == 0;
255   }
256 
257   /**
258    * @return Does the data model knows the number of rows?
259    */
260   public boolean hasRowCount() {
261     return getRowCount() != -1;
262   }
263 
264   /**
265    * @return Should the paging controls be rendered? Either because of the need of paging or because
266    * the show is enforced by {@link #isShowPagingAlways()}
267    */
268   public boolean isPagingVisible() {
269     return isShowPagingAlways() || needMoreThanOnePage();
270   }
271 
272   /**
273    * @return Is panging needed to display all rows? If the number of rows is unknown this method returns true.
274    */
275   public boolean needMoreThanOnePage() {
276     if (isRowsUnlimited()) {
277       return false;
278     } else if (!hasRowCount()) {
279       return true;
280     } else {
281       return getRowCount() > getRows();
282     }
283   }
284 
285   public abstract boolean isShowPagingAlways();
286 
287   public boolean isAtEnd() {
288     if (!hasRowCount()) {
289       final int old = getRowIndex();
290       setRowIndex(getFirst() + getRows() + 1);
291       final boolean atEnd = !isRowAvailable();
292       setRowIndex(old);
293       return atEnd;
294     } else {
295       return getFirst() >= getFirstRowIndexOfLastPage();
296     }
297   }
298 
299   /**
300    * @deprecated The name of this method is ambiguous.
301    * You may use {@link #getFirstRowIndexOfLastPage()}. Deprecated since 1.5.5.
302    */
303   @Deprecated
304   public int getLastPageIndex() {
305     if (hasRowCount()) {
306       return getFirstRowIndexOfLastPage();
307     } else {
308       return 0;
309     }
310   }
311 
312   /**
313    * Determines the beginning of the last page in the model.
314    * If the number of rows to display on one page is unlimited, the value is 0 (there is only one page).
315    * @return The index of the first row of the last paging page.
316    * @throws IllegalArgumentException If the number of rows in the model returned
317    * by {@link #getRowCount()} is -1 (undefined).
318    */
319   public int getFirstRowIndexOfLastPage() {
320     if (isRowsUnlimited()) {
321       return 0;
322     } else if (!hasRowCount()) {
323       throw new IllegalArgumentException(
324           "Can't determine the last page, because the row count of the model is unknown.");
325     } else {
326       int rows = getRows();
327       int rowCount = getRowCount();
328       int tail = rowCount % rows;
329       return rowCount - (tail != 0 ? tail : rows);
330     }
331   }
332 
333   @Override
334   public void processUpdates(FacesContext context) {
335     super.processUpdates(context);
336     updateSheetState(context);
337   }
338 
339   private void updateSheetState(FacesContext facesContext) {
340     SheetState state = getSheetState(facesContext);
341     if (state != null) {
342       // ensure sortActionListener
343 //      getSortActionListener();
344 //      state.setSortedColumn(sortActionListener != null ? sortActionListener.getColumn() : -1);
345 //      state.setAscending(sortActionListener != null && sortActionListener.isAscending());
346       Map attributes = getAttributes();
347       //noinspection unchecked
348       final List<Integer> list = (List<Integer>) attributes.get(Attributes.SELECTED_LIST_STRING);
349       state.setSelectedRows(list != null ? list : Collections.<Integer>emptyList());
350       state.setColumnWidths((String) attributes.get(Attributes.WIDTH_LIST_STRING));
351       state.setScrollPosition((Integer[]) attributes.get(Attributes.SCROLL_POSITION));
352       attributes.remove(Attributes.SELECTED_LIST_STRING);
353       attributes.remove(Attributes.SCROLL_POSITION);
354     }
355   }
356 
357 
358   @Override
359   public Object saveState(FacesContext context) {
360     Object[] saveState = new Object[2];
361     saveState[0] = super.saveState(context);
362     saveState[1] = state;
363     return saveState;
364   }
365 
366   @Override
367   public void restoreState(FacesContext context, Object savedState) {
368     Object[] values = (Object[]) savedState;
369     super.restoreState(context, values[0]);
370     state = (SheetState) values[1];
371   }
372 
373   public List<AbstractUIColumn> getAllColumns() {
374     List<AbstractUIColumn> columns = new ArrayList<AbstractUIColumn>();
375     for (AbstractUIColumn kid : ComponentUtils.findDescendantList(this, AbstractUIColumn.class)) {
376       columns.add(kid);
377     }
378     return columns;
379   }
380 
381   public List<AbstractUIColumn> getRenderedColumns() {
382     List<AbstractUIColumn> columns = new ArrayList<AbstractUIColumn>();
383     for (AbstractUIColumn kid : ComponentUtils.findDescendantList(this, AbstractUIColumn.class)) {
384       if (kid.isRendered()) {
385         columns.add(kid);
386       }
387     }
388     return columns;
389   }
390 
391 /*  public MethodBinding getSortActionListener() {
392     if (sortActionListener != null) {
393       return sortActionListener;
394     } else {
395       return new Sorter();
396     }
397   }*/
398 
399   @Override
400   public void queueEvent(FacesEvent facesEvent) {
401     UIComponent parent = getParent();
402     if (parent == null) {
403       throw new IllegalStateException("Component is not a descendant of a UIViewRoot");
404     }
405 
406     if (facesEvent.getComponent() == this
407         && (facesEvent instanceof SheetStateChangeEvent
408         || facesEvent instanceof PageActionEvent)) {
409       facesEvent.setPhaseId(PhaseId.INVOKE_APPLICATION);
410       parent.queueEvent(facesEvent);
411     } else {
412       UIComponent source = facesEvent.getComponent();
413       UIComponent sourceParent = source.getParent();
414       if (sourceParent.getParent() == this
415           && source.getId() != null && source.getId().endsWith(SORTER_ID)) {
416         facesEvent.setPhaseId(PhaseId.INVOKE_APPLICATION);
417         parent.queueEvent(new SortActionEvent(this, (UIColumn) sourceParent));
418       } else {
419         super.queueEvent(facesEvent);
420       }
421     }
422   }
423 
424   @Override
425   public void broadcast(FacesEvent facesEvent) throws AbortProcessingException {
426     super.broadcast(facesEvent);
427     if (facesEvent instanceof SheetStateChangeEvent) {
428       FacesUtils.invokeMethodBinding(getFacesContext(), getStateChangeListener(), facesEvent);
429     } else if (facesEvent instanceof PageActionEvent) {
430       if (facesEvent.getComponent() == this) {
431         FacesUtils.invokeMethodBinding(
432             getFacesContext(), getStateChangeListener(), new SheetStateChangeEvent(this));
433         performPaging((PageActionEvent) facesEvent);
434       }
435     } else if (facesEvent instanceof SortActionEvent) {
436       MethodBinding methodBinding = getSortActionListener();
437       if (methodBinding!= null) {
438         // TODO should be first invokeMethodBinding and the update state
439         getSheetState(getFacesContext()).updateSortState((SortActionEvent) facesEvent);
440         FacesUtils.invokeMethodBinding(getFacesContext(), methodBinding, facesEvent);
441       } else {
442         getSheetState(getFacesContext()).updateSortState((SortActionEvent) facesEvent);
443         new Sorter().perform((SortActionEvent) facesEvent);
444       }
445     }
446   }
447 
448   public void addStateChangeListener(SheetStateChangeListener listener) {
449     addFacesListener(listener);
450   }
451 
452   public SheetStateChangeListener[] getStateChangeListeners() {
453     return (SheetStateChangeListener[]) getFacesListeners(SheetStateChangeListener.class);
454   }
455 
456   public void removeStateChangeListener(SheetStateChangeListener listener) {
457     removeFacesListener(listener);
458   }
459 
460   public List<Integer> getWidthList() {
461     return widthList;
462   }
463 
464   public void setWidthList(List<Integer> widthList) {
465     this.widthList = widthList;
466   }
467 
468 
469 
470   public Integer[] getScrollPosition() {
471     Integer[] scrollPosition = (Integer[]) getAttributes().get(Attributes.SCROLL_POSITION);
472     if (scrollPosition == null) {
473       scrollPosition = getSheetState(FacesContext.getCurrentInstance()).getScrollPosition();
474     }
475     return scrollPosition;
476   }
477 
478 
479   @Override
480   public UIComponent findComponent(String searchId) {
481     return super.findComponent(stripRowIndex(searchId));
482   }
483 
484   public String stripRowIndex(String searchId) {
485     if (searchId.length() > 0 && Character.isDigit(searchId.charAt(0))) {
486       for (int i = 1; i < searchId.length(); ++i) {
487         char c = searchId.charAt(i);
488         if (c == SEPARATOR_CHAR) {
489           searchId = searchId.substring(i + 1);
490           break;
491         }
492         if (!Character.isDigit(c)) {
493           break;
494         }
495       }
496     }
497     return searchId;
498   }
499 
500   public void performPaging(PageActionEvent pageEvent) {
501 
502     int first;
503 
504     if (LOG.isDebugEnabled()) {
505       LOG.debug("action = '" + pageEvent.getAction().name() + "'");
506     }
507 
508     switch (pageEvent.getAction()) {
509       case FIRST:
510         first = 0;
511         break;
512       case PREV:
513         first = getFirst() - getRows();
514         first = first < 0 ? 0 : first;
515         break;
516       case NEXT:
517         if (hasRowCount()) {
518           first = getFirst() + getRows();
519           first = first > getRowCount() ? getFirstRowIndexOfLastPage() : first;
520         } else {
521           if (isAtEnd()) {
522             first = getFirst();
523           } else {
524             first = getFirst() + getRows();
525           }
526         }
527         break;
528       case LAST:
529         first = getFirstRowIndexOfLastPage();
530         break;
531       case TO_ROW:
532         first = pageEvent.getValue() - 1;
533         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
534           first = getFirstRowIndexOfLastPage();
535         } else if (first < 0) {
536           first = 0;
537         }
538         break;
539       case TO_PAGE:
540         int pageIndex = pageEvent.getValue() - 1;
541         first = pageIndex * getRows();
542         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
543           first = getFirstRowIndexOfLastPage();
544         } else if (first < 0) {
545           first = 0;
546         }
547         break;
548       default:
549         // may not happen
550         first = -1;
551     }
552 
553     final ValueExpression expression = getValueExpression(Attributes.FIRST);
554     if (expression != null) {
555       expression.setValue(getFacesContext().getELContext(), first);
556     } else {
557       setFirst(first);
558     }
559 
560     getState().setFirst(first);
561 //      sheet.queueEvent(new SheetStateChangeEvent(sheet));
562   }
563 
564   public List<LayoutComponent> getComponents() {
565     if (layoutComponents != null) {
566       return layoutComponents;
567     }
568     layoutComponents = new ArrayList<LayoutComponent>();
569     for (UIComponent column : getChildren()) {
570       if (column instanceof AbstractUIColumnSelector) {
571         layoutComponents.add(null); // XXX UIColumnSelector is currently not an instance of LayoutComponent
572       } else if (column instanceof ColumnEvent) {
573         // ignore
574       } else if (column instanceof AbstractUIColumnNode) {
575         layoutComponents.add((AbstractUIColumnNode) column);
576       } else if (column instanceof UIColumn) {
577         LayoutComponent layoutComponent = null;
578         for (UIComponent component : column.getChildren()) {
579           if (component instanceof LayoutComponent) {
580             if (layoutComponent == null) {
581               layoutComponent = (LayoutComponent) component;
582             } else {
583               LOG.warn(
584                   "Found more than one layout components inside of a UIColumn: column id='{}' renderer-type='{}'",
585                   column.getClientId(FacesContext.getCurrentInstance()),
586                   component.getRendererType());
587             }
588           }
589         }
590         if (layoutComponent != null) {
591           layoutComponents.add(layoutComponent);
592         } else {
593           final FacesContext facesContext = FacesContext.getCurrentInstance();
594           final AbstractUIOut dummy = (AbstractUIOut) CreateComponentUtils.createComponent(
595               facesContext, ComponentTypes.OUT, RendererTypes.OUT, facesContext.getViewRoot().createUniqueId());
596           dummy.setTransient(true);
597           column.getChildren().add(dummy);
598           layoutComponents.add(dummy);
599           LOG.warn(
600               "Found no component inside of a UIColumn: column id='{}'. Creating a dummy with id='{}'!",
601               column.getClientId(facesContext), dummy.getClientId(facesContext));
602         }
603       }
604     }
605     return layoutComponents;
606   }
607 
608   public void onComponentPopulated(FacesContext facesContext, UIComponent parent) {
609     if (getLayoutManager() == null) {
610       setLayoutManager(CreateComponentUtils.createAndInitLayout(
611           facesContext, ComponentTypes.SHEET_LAYOUT, RendererTypes.SHEET_LAYOUT, parent));
612     }
613   }
614 
615   public LayoutManager getLayoutManager() {
616     return (LayoutManager) getFacet(Facets.LAYOUT);
617   }
618 
619   public void setLayoutManager(LayoutManager layoutManager) {
620     getFacets().put(Facets.LAYOUT, (AbstractUILayoutBase) layoutManager);
621   }
622 
623   public boolean isLayoutChildren() {
624     return isRendered();
625   }
626 
627   public boolean isRendersRowContainer() {
628     return true;
629   }
630 
631   public abstract boolean isShowHeader();
632 
633   public Boolean getNeedVerticalScrollbar() {
634     return needVerticalScrollbar;
635   }
636 
637   public void setNeedVerticalScrollbar(Boolean needVerticalScrollbar) {
638     this.needVerticalScrollbar = needVerticalScrollbar;
639   }
640 
641   @Override
642   public ExpandedState getExpandedState() {
643     return getState().getExpandedState();
644   }
645 
646   public Grid getHeaderGrid() {
647     return headerGrid;
648   }
649 
650   public void setHeaderGrid(Grid headerGrid) {
651     this.headerGrid = headerGrid;
652   }
653 }