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