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.Visual;
24  import org.apache.myfaces.tobago.event.PageActionEvent;
25  import org.apache.myfaces.tobago.event.SheetStateChangeEvent;
26  import org.apache.myfaces.tobago.event.SheetStateChangeListener;
27  import org.apache.myfaces.tobago.event.SheetStateChangeSource;
28  import org.apache.myfaces.tobago.event.SortActionEvent;
29  import org.apache.myfaces.tobago.event.SortActionSource;
30  import org.apache.myfaces.tobago.internal.layout.Grid;
31  import org.apache.myfaces.tobago.internal.layout.OriginCell;
32  import org.apache.myfaces.tobago.internal.util.SortingUtils;
33  import org.apache.myfaces.tobago.layout.Measure;
34  import org.apache.myfaces.tobago.layout.MeasureList;
35  import org.apache.myfaces.tobago.layout.ShowPosition;
36  import org.apache.myfaces.tobago.model.ExpandedState;
37  import org.apache.myfaces.tobago.model.ScrollPosition;
38  import org.apache.myfaces.tobago.model.SelectedState;
39  import org.apache.myfaces.tobago.model.SheetState;
40  import org.apache.myfaces.tobago.renderkit.RendererBase;
41  import org.apache.myfaces.tobago.util.AjaxUtils;
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.component.behavior.ClientBehaviorHolder;
53  import javax.faces.context.FacesContext;
54  import javax.faces.event.AbortProcessingException;
55  import javax.faces.event.ComponentSystemEvent;
56  import javax.faces.event.ComponentSystemEventListener;
57  import javax.faces.event.FacesEvent;
58  import javax.faces.event.ListenerFor;
59  import javax.faces.event.PhaseId;
60  import javax.faces.event.PreRenderComponentEvent;
61  import javax.faces.render.Renderer;
62  import java.io.IOException;
63  import java.lang.invoke.MethodHandles;
64  import java.util.ArrayList;
65  import java.util.Collections;
66  import java.util.List;
67  
68  /**
69   * {@link org.apache.myfaces.tobago.internal.taglib.component.SheetTagDeclaration}
70   */
71  @ListenerFor(systemEventClass = PreRenderComponentEvent.class)
72  public abstract class AbstractUISheet extends AbstractUIData
73      implements SheetStateChangeSource, SortActionSource, ClientBehaviorHolder, Visual,
74      ComponentSystemEventListener {
75  
76    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
77  
78    /**
79     * @deprecated since 4.4.0.
80     */
81    @Deprecated
82    public static final String COMPONENT_TYPE = "org.apache.myfaces.tobago.Data";
83  
84    public static final String SORTER_ID = "sorter";
85    public static final String NOT_SORTABLE_COL_MESSAGE_ID = "org.apache.myfaces.tobago.UISheet.SORTING_COL";
86    public static final String NOT_SORTABLE_MESSAGE_ID = "org.apache.myfaces.tobago.UISheet.SORTING";
87  
88    private SheetState state;
89    private transient MeasureList columnLayout;
90    private transient boolean autoLayout;
91  
92    private transient Grid headerGrid;
93  
94    @Override
95    public void encodeAll(FacesContext facesContext) throws IOException {
96  
97      if (isLazy()) {
98        if (getRows() == 0) {
99          LOG.warn("Sheet id={} has lazy=true set, but not set the rows attribute!", getClientId(facesContext));
100       }
101       if (getShowRowRange() != ShowPosition.none) {
102         LOG.warn("Sheet id={} has lazy=true set, but also set showRowRange!=none!", getClientId(facesContext));
103       }
104       if (getShowPageRange() != ShowPosition.none) {
105         LOG.warn("Sheet id={} has lazy=true set, but also set showPageRange!=none!", getClientId(facesContext));
106       }
107       if (getShowDirectLinks() != ShowPosition.none) {
108         LOG.warn("Sheet id={} has lazy=true set, but also set showDirectLinks!=none!", getClientId(facesContext));
109       }
110     }
111 
112     final AbstractUIReload reload = ComponentUtils.getReloadFacet(this);
113 
114     if (reload != null && AjaxUtils.isAjaxRequest(facesContext) && reload.isRendered() && !reload.isUpdate()) {
115       // do not render content
116       final Renderer renderer = getRenderer(facesContext);
117       if (renderer instanceof RendererBase) {
118         ((RendererBase) renderer).encodeReload(facesContext, reload);
119       } else {
120         LOG.warn("Found reload facet but no renderer support for it id='{}'!", getClientId(facesContext));
121       }
122     } else {
123       super.encodeAll(facesContext);
124     }
125   }
126 
127   @Override
128   public void encodeBegin(final FacesContext facesContext) throws IOException {
129     final SheetState theState = getSheetState(facesContext);
130     final int first = theState.getFirst();
131     if (first > -1 && (!hasRowCount() || first < getRowCount())) {
132       final ValueExpression expression = getValueExpression(Attributes.first.getName());
133       if (expression != null) {
134         expression.setValue(facesContext.getELContext(), first);
135       } else {
136         setFirst(first);
137       }
138     }
139 
140     super.encodeBegin(facesContext);
141   }
142 
143   public void setState(final SheetState state) {
144     this.state = state;
145   }
146 
147   public SheetState getState() {
148     return getSheetState(FacesContext.getCurrentInstance());
149   }
150 
151   public SheetState getSheetState(final FacesContext facesContext) {
152     if (state != null) {
153       return state;
154     }
155 
156     final ValueExpression expression = getValueExpression(Attributes.state.getName());
157     if (expression != null) {
158       final ELContext elContext = facesContext.getELContext();
159       SheetState sheetState = (SheetState) expression.getValue(elContext);
160       if (sheetState == null) {
161         sheetState = new SheetState();
162         expression.setValue(elContext, sheetState);
163       }
164       return sheetState;
165     }
166 
167     state = new SheetState();
168     return state;
169   }
170 
171   public abstract String getColumns();
172 
173   @Override
174   public void processEvent(final ComponentSystemEvent event) throws AbortProcessingException {
175 
176     super.processEvent(event);
177 
178     if (event instanceof PreRenderComponentEvent) {
179       final String columns = getColumns();
180       if (columns != null) {
181         columnLayout = MeasureList.parse(columns);
182       }
183 
184       autoLayout = true;
185       if (columnLayout != null) {
186         for (final Measure token : columnLayout) {
187           if (token != Measure.AUTO) {
188             autoLayout = false;
189             break;
190           }
191         }
192       }
193 
194       LOG.debug("autoLayout={}", autoLayout);
195     }
196   }
197 
198   public MeasureList getColumnLayout() {
199     return columnLayout;
200   }
201 
202   public boolean isAutoLayout() {
203     return autoLayout;
204   }
205 
206   /**
207    * The rowIndex of the last row on the current page plus one (because of zero based iterating).
208    *
209    * @throws IllegalArgumentException If the number of rows in the model returned by {@link #getRowCount()} is -1
210    * (undefined).
211    */
212   public int getLastRowIndexOfCurrentPage() {
213     if (!hasRowCount()) {
214       throw new IllegalArgumentException(
215           "Can't determine the last row, because the row count of the model is unknown.");
216     }
217     if (isRowsUnlimited()) {
218       return getRowCount();
219     }
220     final int last = getFirst() + getRows();
221     return Math.min(last, getRowCount());
222   }
223 
224   /**
225    * @return returns the current page (based by 0).
226    */
227   public int getCurrentPage() {
228     final int rows = getRows();
229     if (rows == 0) {
230       // if the rows are unlimited, there is only one page
231       return 0;
232     }
233     final int first = getFirst();
234     if (hasRowCount() && first >= getRowCount()) {
235       return getPages() - 1; // last page
236     } else {
237       return first / rows;
238     }
239   }
240 
241   /**
242    * The number of pages to render.
243    *
244    * @throws IllegalArgumentException If the number of rows in the model returned by {@link #getRowCount()} is -1
245    * (undefined).
246    */
247   public int getPages() {
248     if (isRowsUnlimited()) {
249       return 1;
250     }
251     if (!hasRowCount()) {
252       throw new IllegalArgumentException(
253           "Can't determine the number of pages, because the row count of the model is unknown.");
254     }
255     return (getRowCount() - 1) / getRows() + 1;
256   }
257 
258   public List<UIComponent> getRenderedChildrenOf(final UIColumn column) {
259     final List<UIComponent> children = new ArrayList<>();
260     for (final UIComponent kid : column.getChildren()) {
261       if (kid.isRendered()) {
262         children.add(kid);
263       }
264     }
265     return children;
266   }
267 
268   /**
269    * @return Is the interval to display starting with the first row?
270    */
271   public boolean isAtBeginning() {
272     return getFirst() == 0;
273   }
274 
275   /**
276    * @return Does the data model knows the number of rows?
277    */
278   public boolean hasRowCount() {
279     return getRowCount() != -1;
280   }
281 
282   /**
283    * @return Should the paging controls be rendered? Either because of the need of paging or because the show is
284    * enforced by {@link #isShowPagingAlways()}
285    */
286   public boolean isPagingVisible() {
287     return isShowPagingAlways() || needMoreThanOnePage();
288   }
289 
290   /**
291    * @return Is panging needed to display all rows? If the number of rows is unknown this method returns true.
292    */
293   public boolean needMoreThanOnePage() {
294     if (isRowsUnlimited()) {
295       return false;
296     } else if (!hasRowCount()) {
297       return true;
298     } else {
299       return getRowCount() > getRows();
300     }
301   }
302 
303   public abstract boolean isShowPagingAlways();
304 
305   public boolean isAtEnd() {
306     if (!hasRowCount()) {
307       final int old = getRowIndex();
308       setRowIndex(getFirst() + getRows() + 1);
309       final boolean atEnd = !isRowAvailable();
310       setRowIndex(old);
311       return atEnd;
312     } else {
313       return getFirst() >= getFirstRowIndexOfLastPage();
314     }
315   }
316 
317   /**
318    * Determines the beginning of the last page in the model. If the number of rows to display on one page is unlimited,
319    * the value is 0 (there is only one page).
320    *
321    * @return The index of the first row of the last paging page.
322    * @throws IllegalArgumentException If the number of rows in the model returned by {@link #getRowCount()} is -1
323    * (undefined).
324    */
325   public int getFirstRowIndexOfLastPage() {
326     if (isRowsUnlimited()) {
327       return 0;
328     } else if (!hasRowCount()) {
329       throw new IllegalArgumentException(
330           "Can't determine the last page, because the row count of the model is unknown.");
331     } else {
332       final int rows = getRows();
333       final int rowCount = getRowCount();
334       final int tail = rowCount % rows;
335       return rowCount - (tail != 0 ? tail : rows);
336     }
337   }
338 
339   @Override
340   public void processUpdates(final FacesContext context) {
341     super.processUpdates(context);
342 
343     final SheetState sheetState = getSheetState(context);
344     if (sheetState != null) {
345       final List<Integer> list = (List<Integer>) ComponentUtils.getAttribute(this, Attributes.selectedListString);
346       sheetState.setSelectedRows(list != null ? list : Collections.emptyList());
347       ComponentUtils.removeAttribute(this, Attributes.selectedListString);
348       ComponentUtils.removeAttribute(this, Attributes.scrollPosition);
349     }
350   }
351 
352   @Override
353   public Object saveState(final FacesContext context) {
354     final Object[] saveState = new Object[2];
355     saveState[0] = super.saveState(context);
356     saveState[1] = state;
357     return saveState;
358   }
359 
360   @Override
361   public void restoreState(final FacesContext context, final Object savedState) {
362     final Object[] values = (Object[]) savedState;
363     super.restoreState(context, values[0]);
364     state = (SheetState) values[1];
365   }
366 
367   public List<AbstractUIColumnBase> getAllColumns() {
368     final ArrayList<AbstractUIColumnBase> result = new ArrayList<>();
369     findColumns(this, result, true);
370     return result;
371   }
372 
373   private void findColumns(final UIComponent component, final List<AbstractUIColumnBase> result, final boolean all) {
374     for (final UIComponent child : component.getChildren()) {
375       if (all || child.isRendered()) {
376         if (child instanceof AbstractUIColumnBase) {
377           result.add((AbstractUIColumnBase) child);
378         } else if (child instanceof AbstractUIData) {
379           // ignore columns of nested sheets
380         } else {
381           findColumns(child, result, all);
382         }
383       }
384     }
385   }
386 
387   @Override
388   public void queueEvent(final FacesEvent facesEvent) {
389     final UIComponent parent = getParent();
390     if (parent == null) {
391       throw new IllegalStateException("Component is not a descendant of a UIViewRoot");
392     }
393 
394     if (facesEvent.getComponent() == this
395         && (facesEvent instanceof SheetStateChangeEvent
396         || facesEvent instanceof PageActionEvent)) {
397       facesEvent.setPhaseId(PhaseId.INVOKE_APPLICATION);
398       parent.queueEvent(facesEvent);
399     } else {
400       super.queueEvent(facesEvent);
401     }
402   }
403 
404   @Override
405   public void broadcast(final FacesEvent facesEvent) throws AbortProcessingException {
406     super.broadcast(facesEvent);
407     if (facesEvent instanceof SheetStateChangeEvent) {
408       final MethodExpression listener = getStateChangeListenerExpression();
409       listener.invoke(getFacesContext().getELContext(), new Object[]{facesEvent});
410     } else if (facesEvent instanceof PageActionEvent) {
411       if (facesEvent.getComponent() == this) {
412         final MethodExpression listener = getStateChangeListenerExpression();
413         if (listener != null) {
414           listener.invoke(getFacesContext().getELContext(), new Object[]{facesEvent});
415         }
416         performPaging((PageActionEvent) facesEvent);
417       }
418     } else if (facesEvent instanceof SortActionEvent) {
419       getSheetState(getFacesContext()).updateSortState(((SortActionEvent) facesEvent).getColumn().getId());
420       sort(getFacesContext(), (SortActionEvent) facesEvent);
421     }
422   }
423 
424   public void init(final FacesContext facesContext) {
425     sort(facesContext, null);
426     layoutHeader();
427   }
428 
429   private void layoutHeader() {
430     final UIComponent header = getHeader();
431     if (header == null) {
432       LOG.warn("This should not happen. Please file a bug in the issue tracker to reproduce this case.");
433       return;
434     }
435     final MeasureList tokens = new MeasureList();
436     final List<AbstractUIColumnBase> columns = getAllColumns();
437     for (final UIColumn column : columns) {
438       if (!(column instanceof AbstractUIRow)) {
439         tokens.add(Measure.FRACTION1);
440       }
441     }
442     final MeasureList rows = new MeasureList();
443     rows.add(Measure.AUTO);
444     final Grid grid = new Grid(tokens, rows);
445 
446     for (final UIComponent child : header.getChildren()) {
447       if (child.isRendered()) {
448         final int columnSpan = ComponentUtils.getIntAttribute(child, Attributes.columnSpan, 1);
449         final int rowSpan = ComponentUtils.getIntAttribute(child, Attributes.rowSpan, 1);
450         grid.add(new OriginCell(child), columnSpan, rowSpan);
451       }
452     }
453     setHeaderGrid(grid);
454   }
455 
456   protected void sort(final FacesContext facesContext, final SortActionEvent event) {
457     final SheetState sheetState = getSheetState(getFacesContext());
458     if (sheetState.isToBeSorted()) {
459       final MethodExpression expression = getSortActionListenerExpression();
460       if (expression != null) {
461         try {
462           expression.invoke(facesContext.getELContext(),
463               new Object[]{
464                   event != null
465                       ? event
466                       : new SortActionEvent(this,
467                       (UIColumn) findComponent(getSheetState(facesContext).getSortedColumnId()))});
468         } catch (final Exception e) {
469           LOG.warn("Sorting not possible!", e);
470         }
471       } else {
472         SortingUtils.sort(this, null);
473       }
474       sheetState.setToBeSorted(false);
475     }
476   }
477 
478   @Override
479   public void addStateChangeListener(final SheetStateChangeListener listener) {
480     addFacesListener(listener);
481   }
482 
483   @Override
484   public SheetStateChangeListener[] getStateChangeListeners() {
485     return (SheetStateChangeListener[]) getFacesListeners(SheetStateChangeListener.class);
486   }
487 
488   @Override
489   public void removeStateChangeListener(final SheetStateChangeListener listener) {
490     removeFacesListener(listener);
491   }
492 
493   @Override
494   public UIComponent findComponent(final String searchId) {
495     return super.findComponent(stripRowIndex(searchId));
496   }
497 
498   public String stripRowIndex(final String initialSearchId) {
499     String searchId = initialSearchId;
500     if (searchId.length() > 0 && Character.isDigit(searchId.charAt(0))) {
501       for (int i = 1; i < searchId.length(); ++i) {
502         final char c = searchId.charAt(i);
503         if (c == UINamingContainer.getSeparatorChar(getFacesContext())) {
504           searchId = searchId.substring(i + 1);
505           break;
506         }
507         if (!Character.isDigit(c)) {
508           break;
509         }
510       }
511     }
512     return searchId;
513   }
514 
515   public void performPaging(final PageActionEvent pageEvent) {
516 
517     int first;
518 
519     if (LOG.isDebugEnabled()) {
520       LOG.debug("action = '" + pageEvent.getAction().name() + "'");
521     }
522 
523     ScrollPosition scrollPosition = getState().getScrollPosition();
524     scrollPosition.setTop(0);
525     switch (pageEvent.getAction()) {
526       case first:
527         first = 0;
528         break;
529       case prev:
530         first = getFirst() - getRows();
531         first = Math.max(first, 0);
532         scrollPosition.setTop(Integer.MAX_VALUE);
533         break;
534       case next:
535         if (hasRowCount()) {
536           first = getFirst() + getRows();
537           first = first > getRowCount() ? getFirstRowIndexOfLastPage() : first;
538         } else {
539           if (isAtEnd()) {
540             first = getFirst();
541           } else {
542             first = getFirst() + getRows();
543           }
544         }
545         break;
546       case last:
547         first = getFirstRowIndexOfLastPage();
548         break;
549       case toRow:
550       case lazy:
551         first = pageEvent.getValue() - 1;
552         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
553           first = getFirstRowIndexOfLastPage();
554         } else if (first < 0) {
555           first = 0;
556         }
557         break;
558       case toPage:
559         final int pageIndex = pageEvent.getValue() - 1;
560         first = pageIndex * getRows();
561         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
562           first = getFirstRowIndexOfLastPage();
563         } else if (first < 0) {
564           first = 0;
565         }
566         break;
567       default:
568         // may not happen
569         first = -1;
570     }
571 
572     final ValueExpression expression = getValueExpression(Attributes.first.getName());
573     if (expression != null) {
574       expression.setValue(getFacesContext().getELContext(), first);
575     } else {
576       setFirst(first);
577     }
578 
579     getState().setFirst(first);
580   }
581 
582   @Override
583   public boolean isRendersRowContainer() {
584     return true;
585   }
586 
587   public abstract boolean isShowHeader();
588 
589   @Override
590   public ExpandedState getExpandedState() {
591     return getState().getExpandedState();
592   }
593 
594   @Override
595   public SelectedState getSelectedState() {
596     return getState().getSelectedState();
597   }
598 
599   public Grid getHeaderGrid() {
600     return headerGrid;
601   }
602 
603   public void setHeaderGrid(final Grid headerGrid) {
604     this.headerGrid = headerGrid;
605   }
606 
607   public abstract boolean isShowDirectLinksArrows();
608 
609   public abstract boolean isShowPageRangeArrows();
610 
611   public abstract ShowPosition getShowRowRange();
612 
613   public abstract ShowPosition getShowPageRange();
614 
615   public abstract ShowPosition getShowDirectLinks();
616 
617   public abstract boolean isLazy();
618 }