View Javadoc
1   /*
2    * #%L
3    * Nuiton Decorator
4    * %%
5    * Copyright (C) 2011 CodeLutin, Tony Chemit
6    * %%
7    * This program is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU Lesser General Public License as 
9    * published by the Free Software Foundation, either version 3 of the 
10   * License, or (at your option) any later version.
11   * 
12   * This program is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Lesser Public License for more details.
16   * 
17   * You should have received a copy of the GNU General Lesser Public 
18   * License along with this program.  If not, see
19   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
20   * #L%
21   */
22  package org.nuiton.decorator;
23  
24  import org.apache.commons.beanutils.MethodUtils;
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  import org.nuiton.decorator.JXPathDecorator.Context;
28  import org.nuiton.decorator.JXPathDecorator.JXPathComparator;
29  
30  import java.util.ArrayList;
31  import java.util.Collections;
32  import java.util.Comparator;
33  import java.util.Date;
34  import java.util.List;
35  import java.util.StringTokenizer;
36  import java.util.regex.Matcher;
37  import java.util.regex.Pattern;
38  
39  /**
40   * Some usefull methods on {@link Decorator} to create, and sort data with
41   * decorators.
42   * <p/>
43   * To create a new decorator, use one of the methods : <ul> <li>{@link
44   * #newPropertyDecorator(Class, String)}</li> <li>{@link
45   * #newJXPathDecorator(Class, String)}</li> <li>
46   * {@link #newMultiJXPathDecorator(Class, String, String)})</li>
47   * <li>{@link #newMultiJXPathDecorator(Class, String, String, String)})</li>
48   * </ul>
49   * <p/>
50   * To sort a list of data, using a {@link JXPathDecorator}, use the method
51   * {@link #sort(JXPathDecorator, List, int)}.
52   * <p/>
53   *
54   * @author tchemit <chemit@codelutiln.com>
55   * @since 2.3
56   */
57  public class DecoratorUtil {
58  
59      /** Logger */
60      private static final Log log = LogFactory.getLog(DecoratorUtil.class);
61  
62  
63      /**
64       * Factory method to instanciate a new {@link PropertyDecorator} for the
65       * given class {@code internlaClass} and a readable property name.
66       *
67       * @param type     the class of the objects decorated by the new
68       *                 decorator
69       * @param property the property
70       * @param <O>      the generic type of class to be decorated by the new
71       *                 decorator
72       * @return the new instanciated decorator
73       * @throws IllegalArgumentException if the expression is not valid, says:
74       *                                  <p/>
75       *                                  - a missing right brace was detected.
76       *                                  <p/>
77       *                                  - a ${ was found in a jxpath token.
78       * @throws NullPointerException     if type parameter is null.
79       */
80      public static <O> PropertyDecorator<O> newPropertyDecorator(
81              Class<O> type,
82              String property)
83              throws IllegalArgumentException, NullPointerException {
84          return new PropertyDecorator<O>(type, property);
85      }
86  
87      /**
88       * Factory method to instanciate a new {@link JXPathDecorator} for the given
89       * class {@code internalClass} and expression.
90       *
91       * @param type       the class of the objects decorated by the new
92       *                   decorator
93       * @param expression the expression to use to decorated objects
94       * @param <O>        the generic type of class to be decorated by the new
95       *                   decorator
96       * @return the new instanciated decorator
97       * @throws IllegalArgumentException if the expression is not valid, says:
98       *                                  <p/>
99       *                                  - a missing right brace was detected.
100      *                                  <p/>
101      *                                  - a ${ was found in a jxpath token.
102      * @throws NullPointerException     if type parameter is null.
103      */
104     public static <O> JXPathDecorator<O> newJXPathDecorator(
105             Class<O> type,
106             String expression)
107             throws IllegalArgumentException, NullPointerException {
108 
109         Context<O> context = createJXPathContext(expression);
110         return new JXPathDecorator<O>(type, expression, context);
111     }
112 
113     public static <O> MultiJXPathDecorator<O> newMultiJXPathDecorator(
114             Class<O> type,
115             String expression,
116             String separator)
117             throws IllegalArgumentException, NullPointerException {
118 
119         MultiJXPathDecorator<O> decorator = newMultiJXPathDecorator(
120                 type,
121                 expression,
122                 separator,
123                 separator);
124         return decorator;
125     }
126 
127     /**
128      * Build a new multi jx path decorator which uise a cycling order while changing the context index.
129      * <p/>
130      * For example, having a initial expression:
131      * <pre>A - B - C</pre>
132      * <p/>
133      * When setting conext index to 1, then we will us this expression:
134      * <pre>B - C - A</pre>
135      *
136      * @param type                 type of bean
137      * @param expression           initial expression
138      * @param separator            expression context separator
139      * @param separatorReplacement context separator replacement in decorator
140      * @param <O>                  type of bean
141      * @return the new decorator
142      * @throws IllegalArgumentException ...
143      * @throws NullPointerException     ...
144      * @since 3.0
145      */
146     public static <O> MultiJXPathDecorator<O> newMultiJXPathDecorator(
147             Class<O> type,
148             String expression,
149             String separator,
150             String separatorReplacement)
151             throws IllegalArgumentException, NullPointerException {
152 
153         Context<O>[] contexts = createMultiJXPathContext(
154                 expression,
155                 separator,
156                 separatorReplacement);
157 
158         return new MultiJXPathDecorator<O>(
159                 type,
160                 expression,
161                 separator,
162                 separatorReplacement,
163                 contexts);
164     }
165 
166     /**
167      * Build a new multi jx path decorator which keep order of incoming expression while changing the context index.
168      * <p/>
169      * For example, having a initial expression:
170      * <pre>A - B - C</pre>
171      * <p/>
172      * When setting conext index to 1, then we will us this expression:
173      * <pre>B - A - C</pre>
174      *
175      * @param type                 type of bean
176      * @param expression           initial expression
177      * @param separator            expression context separator
178      * @param separatorReplacement context separator replacement in decorator
179      * @param <O>                  type of bean
180      * @return the new decorator
181      * @throws IllegalArgumentException ...
182      * @throws NullPointerException     ...
183      * @since 3.0
184      */
185     public static <O> MultiJXPathDecorator<O> newMultiJXPathDecoratorKeepingOrder(
186             Class<O> type,
187             String expression,
188             String separator,
189             String separatorReplacement)
190             throws IllegalArgumentException, NullPointerException {
191 
192         Context<O>[] contexts = createMultiJXPathContextKeepingOrder(
193                 expression,
194                 separator,
195                 separatorReplacement);
196 
197         return new MultiJXPathDecorator<O>(
198                 type,
199                 expression,
200                 separator,
201                 separatorReplacement,
202                 contexts);
203     }
204 
205     private static final Object[] EMPTY_CLASS_ARRAY = new Object[0];
206 
207     /**
208      * Creates a new multijxpath decorator from a given jxpath decorator.
209      *
210      * If decorator is clonable, just clone it, otherwise creates a new using his definition.
211      *
212      * @param decorator the decorator to clone
213      * @param <O>       generic type of decorator to clone
214      * @return cloned decorator
215      * @since 3.0
216      */
217     public static <O> MultiJXPathDecorator<O> cloneDecorator(JXPathDecorator<O> decorator) {
218         if (decorator == null) {
219             throw new NullPointerException(
220                     "can not have a null decorator as parameter");
221         }
222         String separator;
223         String separatorReplacement;
224 
225         if (decorator instanceof Cloneable) {
226             Cloneable cloneable = (Cloneable) decorator;
227 
228             try {
229                 Object clone = MethodUtils.invokeExactMethod(cloneable,
230                                                              "clone",
231                                                              EMPTY_CLASS_ARRAY);
232                 return (MultiJXPathDecorator<O>) clone;
233             } catch (Exception e) {
234                 throw new IllegalStateException("Could not clone decorator " + decorator, e);
235             }
236 
237         }
238         if (decorator instanceof MultiJXPathDecorator<?>) {
239 
240             separator = ((MultiJXPathDecorator<?>) decorator).getSeparator();
241             separatorReplacement = ((MultiJXPathDecorator<?>) decorator).getSeparatorReplacement();
242 
243         } else {
244 
245             separator = "??" + new Date().getTime(); // trick to avoid collisions
246             separatorReplacement = " - ";
247         }
248 
249         return newMultiJXPathDecorator(decorator.getType(),
250                                        decorator.getInitialExpression(),
251                                        separator,
252                                        separatorReplacement);
253 
254     }
255 
256     /**
257      * Sort a list of data based on the first token property of a given context
258      * in a given decorator.
259      *
260      * @param <O>       type of data to sort
261      * @param decorator the decorator to use to sort
262      * @param datas     the list of data to sort
263      * @param pos       the index of context to used in decorator to obtain
264      *                  sorted property.
265      */
266     public static <O> void sort(JXPathDecorator<O> decorator,
267                                 List<O> datas,
268                                 int pos) {
269         sort(decorator, datas, pos, false);
270     }
271 
272     /**
273      * Sort a list of data based on the first token property of a given context
274      * in a given decorator.
275      *
276      * @param <O>       type of data to sort
277      * @param decorator the decorator to use to sort
278      * @param datas     the list of data to sort
279      * @param pos       the index of context to used in decorator to obtain
280      *                  sorted property.
281      * @param reverse   flag to sort in reverse order if sets to {@code true}
282      * @since 2.2
283      */
284     public static <O> void sort(JXPathDecorator<O> decorator,
285                                 List<O> datas,
286                                 int pos,
287                                 boolean reverse) {
288         Comparator<O> c = null;
289         boolean cachedComparator = false;
290         try {
291             c = decorator.getComparator(pos);
292             cachedComparator = c instanceof JXPathComparator<?>;
293 
294             if (cachedComparator) {
295                 ((JXPathComparator<O>) c).init(decorator, datas);
296             }
297             Collections.sort(datas, c);
298             if (reverse) {
299 
300                 // reverse order
301                 Collections.reverse(datas);
302             }
303         } finally {
304             if (cachedComparator) {
305                 ((JXPathComparator<?>) c).clear();
306             }
307         }
308     }
309 
310     public static <O> Context<O> createJXPathContext(String expression) {
311         List<String> lTokens = new ArrayList<String>();
312         StringBuilder buffer = new StringBuilder();
313         int size = expression.length();
314         int end = -1;
315         int start;
316         while ((start = expression.indexOf("${", end + 1)) > -1) {
317             if (start > end + 1) {
318 
319                 // prefix of next jxpath token
320                 buffer.append(expression.substring(end + 1, start));
321             }
322 
323             // seek end of jxpath
324             end = expression.indexOf("}", start + 1);
325             if (end == -1) {
326                 throw new IllegalArgumentException(
327                         "could not find the rigth brace starting at car " +
328                         start + " : " + expression.substring(start + 2));
329             }
330             String jxpath = expression.substring(start + 2, end);
331 
332             // not allowed ${ inside a jxpath token
333             if (jxpath.contains("${")) {
334                 throw new IllegalArgumentException(
335                         "could not find a ${ inside a jxpath expression at " +
336                         "car " + (start + 2) + " : " + jxpath);
337             }
338 
339             // save the jxpath token
340             lTokens.add(jxpath);
341 
342             // replace jxpath token in expresion with a string format variable
343             buffer.append('%').append(lTokens.size());
344         }
345         if (size > end + 1) {
346 
347             // suffix after end jxpath (or all expression if no jxpath)
348             buffer.append(expression.substring(end + 1));
349         }
350         String[] tokens = lTokens.toArray(new String[lTokens.size()]);
351         return new Context<O>(buffer.toString(), tokens);
352     }
353 
354     public static <O> Context<O>[] createMultiJXPathContext(
355             String expression,
356             String separator,
357             String separatorReplacement) {
358         int sep = expression.indexOf(separator);
359         if (sep == -1) {
360             Context<O>[] result = newInstance(1);
361             result[0] = createJXPathContext(expression);
362             return result;
363         }
364 
365         List<String> tokens = new ArrayList<String>();
366         StringTokenizer stk = new StringTokenizer(expression, separator);
367         while (stk.hasMoreTokens()) {
368             tokens.add(stk.nextToken());
369         }
370 
371         int nbTokens = tokens.size();
372         Context<O>[] contexts = newInstance(nbTokens);
373         if (log.isDebugEnabled()) {
374             log.debug("Will prepare " + nbTokens + " contexts from [" + expression + "]");
375         }
376         for (int i = 0; i < nbTokens; i++) {
377             StringBuilder buffer = new StringBuilder(expression.length());
378             for (int j = 0; j < nbTokens; j++) {
379                 int index = (i + j) % nbTokens;
380                 String str = tokens.get(index);
381 
382                 //replace all '%(index+1)$' pattern with '%(j+1)$'
383                 Pattern p = Pattern.compile("\\%(" + (index + 1) + ")\\$");
384                 Matcher matcher = p.matcher(str);
385                 String safeStr = matcher.replaceAll("\\%" + (j + 1) + "\\$");
386 
387                 if (log.isDebugEnabled()) {
388                     log.debug("[" + (index + 1) + "-->" + (j + 1) + "] " + str +
389                               " transformed to " + safeStr);
390                 }
391                 buffer.append(separatorReplacement).append(safeStr);
392             }
393             String expr = buffer.substring(separatorReplacement.length());
394             if (log.isDebugEnabled()) {
395                 log.debug("context [" + i + "] : " + expr);
396             }
397             contexts[i] = createJXPathContext(
398                     expr);
399         }
400         return contexts;
401     }
402 
403     public static <O> Context<O>[] createMultiJXPathContextKeepingOrder(
404             String expression,
405             String separator,
406             String separatorReplacement) {
407         int sep = expression.indexOf(separator);
408         if (sep == -1) {
409             Context<O>[] result = newInstance(1);
410             result[0] = createJXPathContext(expression);
411             return result;
412         }
413 
414         List<String> tokens = new ArrayList<String>();
415         StringTokenizer stk = new StringTokenizer(expression, separator);
416         while (stk.hasMoreTokens()) {
417             tokens.add(stk.nextToken());
418         }
419 
420         int nbTokens = tokens.size();
421         Context<O>[] contexts = newInstance(nbTokens);
422         if (log.isDebugEnabled()) {
423             log.debug("Will prepare " + nbTokens + " contexts from [" + expression + "]");
424         }
425         for (int i = 0; i < nbTokens; i++) {
426             StringBuilder buffer = new StringBuilder(expression.length());
427 
428             {
429                 // first set the i context, then keep context in order
430                 int index = i;
431                 String str = tokens.get(index);
432 
433                 //replace all '%(index+1)$' pattern with '%(1)$'
434                 Pattern p = Pattern.compile("\\%(" + (index + 1) + ")\\$");
435                 Matcher matcher = p.matcher(str);
436                 String safeStr = matcher.replaceAll("\\%" + (1) + "\\$");
437 
438                 if (log.isDebugEnabled()) {
439                     log.debug("[" + (index + 1) + "-->" + (1) + "] " + str +
440                               " transformed to " + safeStr);
441                 }
442                 buffer.append(separatorReplacement).append(safeStr);
443             }
444             for (int j = 0; j < nbTokens; j++) {
445                 if (j == i) {
446 
447                     // already done
448                     continue;
449                 }
450 
451                 int index = j;
452 
453                 String str = tokens.get(index);
454 
455                 int againstIndex = j < i ? j + 1 : j;
456 
457                 //replace all '%(index+1)$' pattern with '%(againstIndex+1)$'
458                 Pattern p = Pattern.compile("\\%(" + (index + 1) + ")\\$");
459                 Matcher matcher = p.matcher(str);
460                 String safeStr = matcher.replaceAll("\\%" + (againstIndex + 1) + "\\$");
461 
462                 if (log.isDebugEnabled()) {
463                     log.debug("[" + (index + 1) + "-->" + (againstIndex + 1) + "] " + str +
464                               " transformed to " + safeStr);
465                 }
466                 buffer.append(separatorReplacement).append(safeStr);
467             }
468             String expr = buffer.substring(separatorReplacement.length());
469             if (log.isDebugEnabled()) {
470                 log.debug("context [" + i + "] : " + expr);
471             }
472             contexts[i] = createJXPathContext(
473                     expr);
474         }
475         return contexts;
476     }
477 
478     @SuppressWarnings("unchecked")
479     protected static <O> Context<O>[] newInstance(int size) {
480         // fixme how to instanciate a typed array with no checking warning ?
481         return new Context[size];
482     }
483 }