001 // Copyright 2006, 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014
015 package org.apache.tapestry5.internal;
016
017 import org.apache.tapestry5.*;
018 import org.apache.tapestry5.func.Mapper;
019 import org.apache.tapestry5.internal.util.Holder;
020 import org.apache.tapestry5.ioc.Messages;
021 import org.apache.tapestry5.ioc.OperationTracker;
022 import org.apache.tapestry5.ioc.Orderable;
023 import org.apache.tapestry5.ioc.Resource;
024 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
025 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
026 import org.apache.tapestry5.services.ComponentEventRequestParameters;
027 import org.apache.tapestry5.services.LinkCreationListener;
028 import org.apache.tapestry5.services.LinkCreationListener2;
029 import org.apache.tapestry5.services.PageRenderRequestParameters;
030 import org.apache.tapestry5.services.javascript.StylesheetLink;
031
032 import java.io.IOException;
033 import java.io.InputStream;
034 import java.io.OutputStream;
035 import java.lang.annotation.Annotation;
036 import java.lang.ref.Reference;
037 import java.util.List;
038 import java.util.Map;
039 import java.util.regex.Pattern;
040
041 /**
042 * Shared utility methods used by various implementation classes.
043 */
044 @SuppressWarnings("all")
045 public class TapestryInternalUtils
046 {
047 private static final String SLASH = "/";
048
049 private static final Pattern SLASH_PATTERN = Pattern.compile(SLASH);
050
051 private static final Pattern NON_WORD_PATTERN = Pattern.compile("[^\\w]");
052
053 private static final Pattern COMMA_PATTERN = Pattern.compile("\\s*,\\s*");
054
055 private static final int BUFFER_SIZE = 5000;
056
057 /**
058 * Capitalizes the string, and inserts a space before each upper case character (or sequence of upper case
059 * characters). Thus "userId" becomes "User Id", etc. Also, converts underscore into space (and capitalizes the
060 * following word), thus "user_id" also becomes "User Id".
061 */
062 public static String toUserPresentable(String id)
063 {
064 StringBuilder builder = new StringBuilder(id.length() * 2);
065
066 char[] chars = id.toCharArray();
067 boolean postSpace = true;
068 boolean upcaseNext = true;
069
070 for (char ch : chars)
071 {
072 if (upcaseNext)
073 {
074 builder.append(Character.toUpperCase(ch));
075 upcaseNext = false;
076
077 continue;
078 }
079
080 if (ch == '_')
081 {
082 builder.append(' ');
083 upcaseNext = true;
084 continue;
085 }
086
087 boolean upperCase = Character.isUpperCase(ch);
088
089 if (upperCase && !postSpace)
090 builder.append(' ');
091
092 builder.append(ch);
093
094 postSpace = upperCase;
095 }
096
097 return builder.toString();
098 }
099
100 public static Map<String, String> mapFromKeysAndValues(String... keysAndValues)
101 {
102 Map<String, String> result = CollectionFactory.newMap();
103
104 int i = 0;
105 while (i < keysAndValues.length)
106 {
107 String key = keysAndValues[i++];
108 String value = keysAndValues[i++];
109
110 result.put(key, value);
111 }
112
113 return result;
114 }
115
116 /**
117 * Converts a string to an {@link OptionModel}. The string is of the form "value=label". If the equals sign is
118 * omitted, then the same value is used for both value and label.
119 */
120 public static OptionModel toOptionModel(String input)
121 {
122 assert input != null;
123 int equalsx = input.indexOf('=');
124
125 if (equalsx < 0)
126 return new OptionModelImpl(input);
127
128 String value = input.substring(0, equalsx);
129 String label = input.substring(equalsx + 1);
130
131 return new OptionModelImpl(label, value);
132 }
133
134 /**
135 * Parses a string input into a series of value=label pairs compatible with {@link #toOptionModel(String)}. Splits
136 * on commas. Ignores whitespace around commas.
137 *
138 * @param input comma seperated list of terms
139 * @return list of option models
140 */
141 public static List<OptionModel> toOptionModels(String input)
142 {
143 assert input != null;
144 List<OptionModel> result = CollectionFactory.newList();
145
146 for (String term : input.split(","))
147 result.add(toOptionModel(term.trim()));
148
149 return result;
150 }
151
152 /**
153 * Wraps the result of {@link #toOptionModels(String)} as a {@link SelectModel} (with no option groups).
154 */
155 public static SelectModel toSelectModel(String input)
156 {
157 List<OptionModel> options = toOptionModels(input);
158
159 return new SelectModelImpl(null, options);
160 }
161
162 /**
163 * Converts a map entry to an {@link OptionModel}.
164 */
165 public static OptionModel toOptionModel(Map.Entry input)
166 {
167 assert input != null;
168 String label = input.getValue() != null ? String.valueOf(input.getValue()) : "";
169
170 return new OptionModelImpl(label, input.getKey());
171 }
172
173 /**
174 * Processes a map input into a series of map entries compatible with {@link #toOptionModel(Map.Entry)}.
175 *
176 * @param input map of elements
177 * @return list of option models
178 */
179 public static <K, V> List<OptionModel> toOptionModels(Map<K, V> input)
180 {
181 assert input != null;
182 List<OptionModel> result = CollectionFactory.newList();
183
184 for (Map.Entry entry : input.entrySet())
185 result.add(toOptionModel(entry));
186
187 return result;
188 }
189
190 /**
191 * Wraps the result of {@link #toOptionModels(Map)} as a {@link SelectModel} (with no option groups).
192 */
193 public static <K, V> SelectModel toSelectModel(Map<K, V> input)
194 {
195 List<OptionModel> options = toOptionModels(input);
196
197 return new SelectModelImpl(null, options);
198 }
199
200 /**
201 * Converts an object to an {@link OptionModel}.
202 */
203 public static OptionModel toOptionModel(Object input)
204 {
205 String label = (input != null ? String.valueOf(input) : "");
206
207 return new OptionModelImpl(label, input);
208 }
209
210 /**
211 * Processes a list input into a series of objects compatible with {@link #toOptionModel(Object)}.
212 *
213 * @param input list of elements
214 * @return list of option models
215 */
216 public static <E> List<OptionModel> toOptionModels(List<E> input)
217 {
218 assert input != null;
219 List<OptionModel> result = CollectionFactory.newList();
220
221 for (E element : input)
222 result.add(toOptionModel(element));
223
224 return result;
225 }
226
227 /**
228 * Wraps the result of {@link #toOptionModels(List)} as a {@link SelectModel} (with no option groups).
229 */
230 public static <E> SelectModel toSelectModel(List<E> input)
231 {
232 List<OptionModel> options = toOptionModels(input);
233
234 return new SelectModelImpl(null, options);
235 }
236
237 /**
238 * Parses a key/value pair where the key and the value are seperated by an equals sign. The key and value are
239 * trimmed of leading and trailing whitespace, and returned as a {@link KeyValue}.
240 */
241 public static KeyValue parseKeyValue(String input)
242 {
243 int pos = input.indexOf('=');
244
245 if (pos < 1)
246 throw new IllegalArgumentException(InternalMessages.badKeyValue(input));
247
248 String key = input.substring(0, pos);
249 String value = input.substring(pos + 1);
250
251 return new KeyValue(key.trim(), value.trim());
252 }
253
254 /**
255 * Used to convert a property expression into a key that can be used to locate various resources (Blocks, messages,
256 * etc.). Strips out any punctuation characters, leaving just words characters (letters, number and the
257 * underscore).
258 *
259 * @param expression a property expression
260 * @return the expression with punctuation removed
261 */
262 public static String extractIdFromPropertyExpression(String expression)
263 {
264 return replace(expression, NON_WORD_PATTERN, "");
265 }
266
267 /**
268 * Looks for a label within the messages based on the id. If found, it is used, otherwise the name is converted to a
269 * user presentable form.
270 */
271 public static String defaultLabel(String id, Messages messages, String propertyExpression)
272 {
273 String key = id + "-label";
274
275 if (messages.contains(key))
276 return messages.get(key);
277
278 return toUserPresentable(extractIdFromPropertyExpression(lastTerm(propertyExpression)));
279 }
280
281 /**
282 * Strips a dotted sequence (such as a property expression, or a qualified class name) down to the last term of that
283 * expression, by locating the last period ('.') in the string.
284 */
285 public static String lastTerm(String input)
286 {
287 int dotx = input.lastIndexOf('.');
288
289 return input.substring(dotx + 1);
290 }
291
292 /**
293 * Converts an list of strings into a space-separated string combining them all, suitable for use as an HTML class
294 * attribute value.
295 *
296 * @param classes classes to combine
297 * @return the joined classes, or null if classes is empty
298 */
299 public static String toClassAttributeValue(List<String> classes)
300 {
301 if (classes.isEmpty())
302 return null;
303
304 return InternalUtils.join(classes, " ");
305 }
306
307 /**
308 * Converts an enum to a label string, allowing for overrides from a message catalog.
309 * <p/>
310 * <ul>
311 * <li>As key <em>prefix</em>.<em>name</em> if present. Ex: "ElementType.LOCAL_VARIABLE"
312 * <li>As key <em>name</em> if present, i.e., "LOCAL_VARIABLE".
313 * <li>As a user-presentable version of the name, i.e., "Local Variable".
314 * </ul>
315 *
316 * @param messages the messages to search for the label
317 * @param prefix prepended to key
318 * @param value to get a label for
319 * @return the label
320 */
321 public static String getLabelForEnum(Messages messages, String prefix, Enum value)
322 {
323 String name = value.name();
324
325 String key = prefix + "." + name;
326
327 if (messages.contains(key))
328 return messages.get(key);
329
330 if (messages.contains(name))
331 return messages.get(name);
332
333 return toUserPresentable(name.toLowerCase());
334 }
335
336 public static String getLabelForEnum(Messages messages, Enum value)
337 {
338 String prefix = lastTerm(value.getClass().getName());
339
340 return getLabelForEnum(messages, prefix, value);
341 }
342
343 private static String replace(String input, Pattern pattern, String replacement)
344 {
345 return pattern.matcher(input).replaceAll(replacement);
346 }
347
348 /**
349 * Determines if the two values are equal. They are equal if they are the exact same value (including if they are
350 * both null). Otherwise standard equals() comparison is used.
351 *
352 * @param left value to compare, possibly null
353 * @param right value to compare, possibly null
354 * @return true if same value, both null, or equal
355 */
356 public static <T> boolean isEqual(T left, T right)
357 {
358 if (left == right)
359 return true;
360
361 if (left == null)
362 return false;
363
364 return left.equals(right);
365 }
366
367 /**
368 * Splits a path at each slash.
369 */
370 public static String[] splitPath(String path)
371 {
372 return SLASH_PATTERN.split(path);
373 }
374
375 /**
376 * Splits a value around commas. Whitespace around the commas is removed, as is leading and trailing whitespace.
377 *
378 * @since 5.1.0.0
379 */
380 public static String[] splitAtCommas(String value)
381 {
382 if (InternalUtils.isBlank(value))
383 return InternalConstants.EMPTY_STRING_ARRAY;
384
385 return COMMA_PATTERN.split(value.trim());
386 }
387
388 /**
389 * Copies some content from an input stream to an output stream. It is the caller's responsibility to close the
390 * streams.
391 *
392 * @param in source of data
393 * @param out sink of data
394 * @throws IOException
395 * @since 5.1.0.0
396 */
397 public static void copy(InputStream in, OutputStream out) throws IOException
398 {
399 byte[] buffer = new byte[BUFFER_SIZE];
400
401 while (true)
402 {
403 int length = in.read(buffer);
404
405 if (length < 0)
406 break;
407
408 out.write(buffer, 0, length);
409 }
410
411 // TAPESTRY-2415: WebLogic needs this flush() call.
412 out.flush();
413 }
414
415 public static boolean isEqual(EventContext left, EventContext right)
416 {
417 if (left == right)
418 return true;
419
420 int count = left.getCount();
421
422 if (count != right.getCount())
423 return false;
424
425 for (int i = 0; i < count; i++)
426 {
427 if (!left.get(Object.class, i).equals(right.get(Object.class, i)))
428 return false;
429 }
430
431 return true;
432 }
433
434 /**
435 * Converts an Asset to an Asset2 if necessary. When actually wrapping an Asset as an Asset2, the asset is assumed
436 * to be variant (i.e., not cacheable).
437 *
438 * @since 5.1.0.0
439 */
440 public static Asset2 toAsset2(final Asset asset)
441 {
442 if (asset instanceof Asset2)
443 return (Asset2) asset;
444
445 return new Asset2()
446 {
447 /** Returns false. */
448 public boolean isInvariant()
449 {
450 return false;
451 }
452
453 public Resource getResource()
454 {
455 return asset.getResource();
456 }
457
458 public String toClientURL()
459 {
460 return asset.toClientURL();
461 }
462
463 @Override
464 public String toString()
465 {
466 return asset.toString();
467 }
468 };
469 }
470
471 public static InternalPropertyConduit toInternalPropertyConduit(final PropertyConduit conduit)
472 {
473 if (conduit instanceof InternalPropertyConduit)
474 return (InternalPropertyConduit) conduit;
475
476 return new InternalPropertyConduit()
477 {
478
479 public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
480 {
481 return conduit.getAnnotation(annotationClass);
482 }
483
484 public void set(Object instance, Object value)
485 {
486 conduit.set(instance, value);
487 }
488
489 public Class getPropertyType()
490 {
491 return conduit.getPropertyType();
492 }
493
494 public Object get(Object instance)
495 {
496 return conduit.get(instance);
497 }
498
499 public String getPropertyName()
500 {
501 return null;
502 }
503 };
504 }
505
506 /**
507 * @param mixinDef the original mixin definition.
508 * @return an Orderable whose id is the mixin name.
509 */
510 public static Orderable<String> mixinTypeAndOrder(String mixinDef)
511 {
512 int idx = mixinDef.indexOf("::");
513 if (idx == -1)
514 {
515 return new Orderable<String>(mixinDef, mixinDef);
516 }
517 String type = mixinDef.substring(0, idx);
518 String[] constraints = splitMixinConstraints(mixinDef.substring(idx + 2));
519
520 return new Orderable<String>(type, type, constraints);
521 }
522
523 public static String[] splitMixinConstraints(String s)
524 {
525 return InternalUtils.isBlank(s) ? null : s.split(";");
526 }
527
528 /**
529 * Common mapper, used primarily with {@link org.apache.tapestry5.func.Flow#map(org.apache.tapestry5.func.Mapper)}
530 *
531 * @since 5.2.0
532 */
533 public static Mapper<Asset, StylesheetLink> assetToStylesheetLink = new Mapper<Asset, StylesheetLink>()
534 {
535 public StylesheetLink map(Asset input)
536 {
537 return new StylesheetLink(input);
538 }
539 };
540
541 public static LinkCreationListener2 toLinkCreationListener2(final LinkCreationListener delegate)
542 {
543 return new LinkCreationListener2()
544 {
545
546 public void createdPageRenderLink(Link link, PageRenderRequestParameters parameters)
547 {
548 delegate.createdPageRenderLink(link);
549 }
550
551 public void createdComponentEventLink(Link link, ComponentEventRequestParameters parameters)
552 {
553 delegate.createdComponentEventLink(link);
554 }
555 };
556 }
557
558 /**
559 * @since 5.3
560 */
561 public static String toFileSuffix(String fileName)
562 {
563 int dotx = fileName.lastIndexOf('.');
564
565 return dotx < 0 ? "" : fileName.substring(dotx + 1);
566 }
567
568 /**
569 * Performs an operation and re-throws the IOException that may occur.
570 */
571 public static void performIO(OperationTracker tracker, String description, final IOOperation operation)
572 throws IOException
573 {
574 final Holder<IOException> exceptionHolder = Holder.create();
575
576 tracker.run(description, new Runnable()
577 {
578 public void run()
579 {
580 try
581 {
582 operation.perform();
583 } catch (IOException ex)
584 {
585 exceptionHolder.put(ex);
586 }
587 }
588 });
589
590 if (exceptionHolder.hasValue())
591 throw exceptionHolder.get();
592 }
593
594 /**
595 * Extracts a value from a map of references. Handles the case where the reference does not exist,
596 * and the case where the reference itself now contains null.
597 *
598 * @since 5.3
599 */
600 public static <K, V> V getAndDeref(Map<K, ? extends Reference<V>> map, K key)
601 {
602 Reference<V> ref = map.get(key);
603
604 return ref == null ? null : ref.get();
605 }
606 }
607