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
015package org.apache.tapestry5.internal;
016
017import org.apache.tapestry5.*;
018import org.apache.tapestry5.func.Mapper;
019import org.apache.tapestry5.internal.util.Holder;
020import org.apache.tapestry5.ioc.Messages;
021import org.apache.tapestry5.ioc.OperationTracker;
022import org.apache.tapestry5.ioc.Orderable;
023import org.apache.tapestry5.ioc.Resource;
024import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
025import org.apache.tapestry5.ioc.internal.util.InternalUtils;
026import org.apache.tapestry5.services.ComponentEventRequestParameters;
027import org.apache.tapestry5.services.LinkCreationListener;
028import org.apache.tapestry5.services.LinkCreationListener2;
029import org.apache.tapestry5.services.PageRenderRequestParameters;
030import org.apache.tapestry5.services.javascript.StylesheetLink;
031
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.OutputStream;
035import java.lang.annotation.Annotation;
036import java.lang.ref.Reference;
037import java.util.List;
038import java.util.Map;
039import java.util.regex.Pattern;
040
041/**
042 * Shared utility methods used by various implementation classes.
043 */
044@SuppressWarnings("all")
045public 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