001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.internal;
014
015import org.apache.tapestry5.*;
016import org.apache.tapestry5.func.Mapper;
017import org.apache.tapestry5.ioc.Messages;
018import org.apache.tapestry5.ioc.Orderable;
019import org.apache.tapestry5.ioc.Resource;
020import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
021import org.apache.tapestry5.ioc.internal.util.InternalUtils;
022import org.apache.tapestry5.services.ComponentEventRequestParameters;
023import org.apache.tapestry5.services.LinkCreationListener;
024import org.apache.tapestry5.services.LinkCreationListener2;
025import org.apache.tapestry5.services.PageRenderRequestParameters;
026import org.apache.tapestry5.services.javascript.StylesheetLink;
027
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.OutputStream;
031import java.lang.annotation.Annotation;
032import java.lang.ref.Reference;
033import java.lang.reflect.Type;
034import java.util.Arrays;
035import java.util.List;
036import java.util.Map;
037import java.util.regex.Pattern;
038
039/**
040 * Shared utility methods used by various implementation classes.
041 */
042@SuppressWarnings("all")
043public class TapestryInternalUtils
044{
045    private static final String SLASH = "/";
046
047    private static final Pattern SLASH_PATTERN = Pattern.compile(SLASH);
048
049    private static final Pattern NON_WORD_PATTERN = Pattern.compile("[^\\w]");
050
051    private static final Pattern COMMA_PATTERN = Pattern.compile("\\s*,\\s*");
052
053    private static final int BUFFER_SIZE = 5000;
054
055    /**
056     * Capitalizes the string, and inserts a space before each upper case character (or sequence of upper case
057     * characters). Thus "userId" becomes "User Id", etc. Also, converts underscore into space (and capitalizes the
058     * following word), thus "user_id" also becomes "User Id".
059     */
060    public static String toUserPresentable(String id)
061    {
062        StringBuilder builder = new StringBuilder(id.length() * 2);
063
064        char[] chars = id.toCharArray();
065        boolean postSpace = true;
066        boolean upcaseNext = true;
067
068        for (char ch : chars)
069        {
070            if (upcaseNext)
071            {
072                builder.append(Character.toUpperCase(ch));
073                upcaseNext = false;
074
075                continue;
076            }
077
078            if (ch == '_')
079            {
080                builder.append(' ');
081                upcaseNext = true;
082                continue;
083            }
084
085            boolean upperCase = Character.isUpperCase(ch);
086
087            if (upperCase && !postSpace)
088                builder.append(' ');
089
090            builder.append(ch);
091
092            postSpace = upperCase;
093        }
094
095        return builder.toString();
096    }
097
098    public static Map<String, String> mapFromKeysAndValues(String... keysAndValues)
099    {
100        Map<String, String> result = CollectionFactory.newMap();
101
102        int i = 0;
103        while (i < keysAndValues.length)
104        {
105            String key = keysAndValues[i++];
106            String value = keysAndValues[i++];
107
108            result.put(key, value);
109        }
110
111        return result;
112    }
113
114    /**
115     * Converts a string to an {@link OptionModel}. The string is of the form "value=label". If the equals sign is
116     * omitted, then the same value is used for both value and label.
117     */
118    public static OptionModel toOptionModel(String input)
119    {
120        assert input != null;
121        int equalsx = input.indexOf('=');
122
123        if (equalsx < 0)
124            return new OptionModelImpl(input);
125
126        String value = input.substring(0, equalsx);
127        String label = input.substring(equalsx + 1);
128
129        return new OptionModelImpl(label, value);
130    }
131
132    /**
133     * Parses a string input into a series of value=label pairs compatible with {@link #toOptionModel(String)}. Splits
134     * on commas. Ignores whitespace around commas.
135     *
136     * @param input comma seperated list of terms
137     * @return list of option models
138     */
139    public static List<OptionModel> toOptionModels(String input)
140    {
141        assert input != null;
142        List<OptionModel> result = CollectionFactory.newList();
143
144        for (String term : input.split(","))
145            result.add(toOptionModel(term.trim()));
146
147        return result;
148    }
149
150    /**
151     * Wraps the result of {@link #toOptionModels(String)} as a {@link SelectModel} (with no option groups).
152     *
153     * See TAP5-2184 for why this ends up causing some trouble!
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(String.format("Key/value pair '%s' is not properly formatted (it does not contain an equals sign).", 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 Type getPropertyGenericType()
495            {
496                if (conduit instanceof PropertyConduit2)
497                {
498                        return ((PropertyConduit2) conduit).getPropertyGenericType();
499                }
500                return conduit.getPropertyType();
501            }
502            
503            public Object get(Object instance)
504            {
505                return conduit.get(instance);
506            }
507
508            public String getPropertyName()
509            {
510                return null;
511            }
512        };
513    }
514
515    /**
516     * @param mixinDef the original mixin definition.
517     * @return an Orderable whose id is the mixin name.
518     */
519    public static Orderable<String> mixinTypeAndOrder(String mixinDef)
520    {
521        int idx = mixinDef.indexOf("::");
522        if (idx == -1)
523        {
524            return new Orderable<String>(mixinDef, mixinDef);
525        }
526        String type = mixinDef.substring(0, idx);
527        String[] constraints = splitMixinConstraints(mixinDef.substring(idx + 2));
528
529        return new Orderable<String>(type, type, constraints);
530    }
531
532    public static String[] splitMixinConstraints(String s)
533    {
534        return InternalUtils.isBlank(s) ? null : s.split(";");
535    }
536
537    /**
538     * Common mapper, used primarily with {@link org.apache.tapestry5.func.Flow#map(org.apache.tapestry5.func.Mapper)}
539     *
540     * @since 5.2.0
541     */
542    public static Mapper<Asset, StylesheetLink> assetToStylesheetLink = new Mapper<Asset, StylesheetLink>()
543    {
544        public StylesheetLink map(Asset input)
545        {
546            return new StylesheetLink(input);
547        }
548    };
549
550    public static LinkCreationListener2 toLinkCreationListener2(final LinkCreationListener delegate)
551    {
552        return new LinkCreationListener2()
553        {
554
555            public void createdPageRenderLink(Link link, PageRenderRequestParameters parameters)
556            {
557                delegate.createdPageRenderLink(link);
558            }
559
560            public void createdComponentEventLink(Link link, ComponentEventRequestParameters parameters)
561            {
562                delegate.createdComponentEventLink(link);
563            }
564        };
565    }
566
567    /**
568     * @since 5.3
569     */
570    public static String toFileSuffix(String fileName)
571    {
572        int dotx = fileName.lastIndexOf('.');
573
574        return dotx < 0 ? "" : fileName.substring(dotx + 1);
575    }
576
577    /**
578     * Extracts a value from a  map of references. Handles the case where the reference does not exist,
579     * and the case where the reference itself now contains null.
580     *
581     * @since 5.3
582     */
583    public static <K, V> V getAndDeref(Map<K, ? extends Reference<V>> map, K key)
584    {
585        Reference<V> ref = map.get(key);
586
587        return ref == null ? null : ref.get();
588    }
589
590    /**
591     * Gathers together an array containing all the threads.
592     * @since 5.4 */
593    public static Thread[] getAllThreads() {
594        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
595
596        while (true) {
597            ThreadGroup parentGroup = rootGroup.getParent();
598            if (parentGroup == null) {
599                break;
600            }
601            rootGroup = parentGroup;
602        }
603
604        Thread[] threads = new Thread[rootGroup.activeCount()];
605
606        while (true) {
607            // A really ugly API. threads.length must be larger than
608            // the actual number of threads, just so we can determine
609            // if we're done.
610            int count = rootGroup.enumerate(threads, true);
611            if (count < threads.length) {
612                return Arrays.copyOf(threads, count);
613            }
614            threads = new Thread[threads.length * 2];
615        }
616    }
617}
618