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.component;
21  
22  import org.apache.myfaces.tobago.event.SortActionEvent;
23  import org.apache.myfaces.tobago.internal.component.AbstractUICommand;
24  import org.apache.myfaces.tobago.internal.component.AbstractUISheet;
25  import org.apache.myfaces.tobago.internal.util.StringUtils;
26  import org.apache.myfaces.tobago.model.SheetState;
27  import org.apache.myfaces.tobago.util.BeanComparator;
28  import org.apache.myfaces.tobago.util.MessageUtils;
29  import org.apache.myfaces.tobago.util.ValueExpressionComparator;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  import javax.el.ValueExpression;
34  import javax.faces.application.FacesMessage;
35  import javax.faces.component.UIColumn;
36  import javax.faces.component.UICommand;
37  import javax.faces.component.UIComponent;
38  import javax.faces.component.UIInput;
39  import javax.faces.component.UIOutput;
40  import javax.faces.component.UISelectBoolean;
41  import javax.faces.component.UISelectMany;
42  import javax.faces.component.UISelectOne;
43  import javax.faces.context.FacesContext;
44  import javax.faces.model.DataModel;
45  import java.util.ArrayList;
46  import java.util.Arrays;
47  import java.util.Collections;
48  import java.util.Comparator;
49  import java.util.List;
50  
51  public class Sorter {
52  
53    private static final Logger LOG = LoggerFactory.getLogger(Sorter.class);
54  
55    private Comparator comparator;
56  
57    /**
58     * @deprecated since 2.0.7, please use {@link #perform(org.apache.myfaces.tobago.internal.component.AbstractUISheet)}
59     */
60    @Deprecated
61    public void perform(final SortActionEvent sortEvent) {
62      final AbstractUISheet data = (AbstractUISheet) sortEvent.getComponent();
63      perform(data);
64    }
65  
66    public void perform(final AbstractUISheet sheet) {
67      final FacesContext facesContext = FacesContext.getCurrentInstance();
68      Object data = sheet.getValue();
69      if (data instanceof DataModel) {
70        data = ((DataModel) data).getWrappedData();
71      }
72      final SheetState sheetState = sheet.getSheetState(facesContext);
73  
74      final String sortedColumnId = sheetState.getSortedColumnId();
75      if (LOG.isDebugEnabled()) {
76        LOG.debug("sorterId = '{}'", sortedColumnId);
77      }
78  
79      boolean success = false;
80      if (sortedColumnId != null) {
81        final UIColumn column = (UIColumn) sheet.findComponent(sortedColumnId);
82        if (column != null) {
83          success = perform(facesContext, sheet, data, column, sheetState);
84        } else {
85          LOG.error("No column to sort found, sorterId = '{}'!", sortedColumnId);
86          addNotSortableMessage(facesContext, null);
87        }
88      } else {
89        LOG.error("No sorterId!");
90        addNotSortableMessage(facesContext, null);
91      }
92  
93      if (!success) {
94        sheetState.resetSortState();
95      }
96    }
97  
98    private boolean perform(
99        final FacesContext facesContext, final AbstractUISheet sheet, final Object data, final UIColumn column,
100       final SheetState sheetState) {
101 
102     final Comparator actualComparator;
103 
104     if (data instanceof List || data instanceof Object[]) {
105       final String sortProperty;
106 
107       try {
108         final UIComponent child = getFirstSortableChild(column.getChildren());
109         if (child != null) {
110 
111           final Attributes attribute = child instanceof AbstractUICommand ? Attributes.label : Attributes.value;
112           if (child.getValueExpression(attribute.getName()) != null) {
113             final String var = sheet.getVar();
114             if (var == null) {
115                 LOG.error("No sorting performed. Property var of sheet is not set!");
116                 addNotSortableMessage(facesContext, column);
117                 return false;
118             }
119             String expressionString = child.getValueExpression(attribute.getName()).getExpressionString();
120             if (isSimpleProperty(expressionString)) {
121               if (expressionString.startsWith("#{")
122                   && expressionString.endsWith("}")) {
123                 expressionString =
124                     expressionString.substring(2,
125                         expressionString.length() - 1);
126               }
127               sortProperty = expressionString.substring(var.length() + 1);
128 
129               actualComparator = new BeanComparator(
130                   sortProperty, comparator, !sheetState.isAscending());
131 
132               if (LOG.isDebugEnabled()) {
133                 LOG.debug("Sort property is {}", sortProperty);
134               }
135             } else {
136 
137               final boolean descending = !sheetState.isAscending();
138               final ValueExpression expression = child.getValueExpression("value");
139               actualComparator = new ValueExpressionComparator(facesContext, var, expression, descending, comparator);
140             }
141           } else {
142             LOG.error("No sorting performed, because no expression found for "
143                     + "attribute '{}' in component '{}' with id='{}'! You may check the type of the component!",
144                 attribute.getName(), child.getClass().getName(), child.getClientId());
145             addNotSortableMessage(facesContext, column);
146             return false;
147           }
148         } else {
149           LOG.error("No sorting performed. Value is not instanceof List or Object[]!");
150           addNotSortableMessage(facesContext, column);
151           return false;
152         }
153       } catch (final Exception e) {
154         LOG.error("Error while extracting sortMethod :" + e.getMessage(), e);
155         addNotSortableMessage(facesContext, column);
156         return false;
157       }
158 
159       // TODO: locale / comparator parameter?
160       // don't compare numbers with Collator.getInstance() comparator
161 //        Comparator comparator = Collator.getInstance();
162 //          comparator = new RowComparator(ascending, method);
163 
164       // memorize selected rows
165       List<Object> selectedDataRows = null;
166       if (sheetState.getSelectedRows().size() > 0) {
167         selectedDataRows = new ArrayList<>(sheetState.getSelectedRows().size());
168         Object dataRow;
169         for (final Integer index : sheetState.getSelectedRows()) {
170           if (data instanceof List) {
171             dataRow = ((List) data).get(index);
172           } else {
173             dataRow = ((Object[]) data)[index];
174           }
175           selectedDataRows.add(dataRow);
176         }
177       }
178 
179       // do sorting
180       if (data instanceof List) {
181         Collections.sort((List) data, actualComparator);
182       } else { // value is instanceof Object[]
183         Arrays.sort((Object[]) data, actualComparator);
184       }
185 
186       // restore selected rows
187       if (selectedDataRows != null) {
188         sheetState.getSelectedRows().clear();
189         for (final Object dataRow : selectedDataRows) {
190           int index = -1;
191           if (data instanceof List) {
192             for (int i = 0; i < ((List) data).size() && index < 0; i++) {
193               if (dataRow == ((List) data).get(i)) {
194                 index = i;
195               }
196             }
197           } else {
198             for (int i = 0; i < ((Object[]) data).length && index < 0; i++) {
199               if (dataRow == ((Object[]) data)[i]) {
200                 index = i;
201               }
202             }
203           }
204           if (index >= 0) {
205             sheetState.getSelectedRows().add(index);
206           }
207         }
208       }
209 
210     } else {  // DataModel?, ResultSet, Result or Object
211       LOG.warn("Sorting not supported for type "
212           + (data != null ? data.getClass().toString() : "null"));
213     }
214     return true;
215   }
216 
217   // XXX needs to be tested
218   // XXX was based on ^#\{(\w+(\.\w)*)\}$ which is wrong, because there is a + missing after the last \w
219   boolean isSimpleProperty(final String expressionString) {
220     if (expressionString.startsWith("#{") && expressionString.endsWith("}")) {
221       final String inner = expressionString.substring(2, expressionString.length() - 1);
222       final String[] parts = StringUtils.split(inner, '.');
223       for (final String part : parts) {
224         if (!StringUtils.isAlpha(part)) {
225           return false;
226         }
227       }
228       return true;
229     }
230     return false;
231   }
232 
233   private void addNotSortableMessage(final FacesContext facesContext, final UIColumn column) {
234     MessageUtils.addMessage(facesContext, column, FacesMessage.SEVERITY_WARN,
235         AbstractUISheet.NOT_SORTABLE_MESSAGE_ID, new Object[]{MessageUtils.getLabel(facesContext, column)});
236   }
237 
238   private UIComponent getFirstSortableChild(final List<UIComponent> children) {
239     UIComponent result = null;
240 
241     for (UIComponent child : children) {
242       result = child;
243       if (child instanceof UISelectMany
244           || child instanceof UISelectOne
245           || child instanceof UISelectBoolean
246           || (child instanceof AbstractUICommand && child.getChildren().isEmpty())
247           || (child instanceof UIInput && RendererTypes.HIDDEN.equals(child.getRendererType()))) {
248         continue;
249         // look for a better component if any
250       }
251       if (child instanceof UIOutput) {
252         break;
253       }
254       if (child instanceof UICommand
255           || child instanceof javax.faces.component.UIPanel) {
256         child = getFirstSortableChild(child.getChildren());
257         if (child instanceof UIOutput) {
258           break;
259         }
260       }
261     }
262     return result;
263   }
264 
265   public Comparator getComparator() {
266     return comparator;
267   }
268 
269   public void setComparator(final Comparator comparator) {
270     this.comparator = comparator;
271   }
272 }
273