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