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 }