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        return InternalUtils.toUserPresentable(id);
063    }
064
065    public static Map<String, String> mapFromKeysAndValues(String... keysAndValues)
066    {
067        Map<String, String> result = CollectionFactory.newMap();
068
069        int i = 0;
070        while (i < keysAndValues.length)
071        {
072            String key = keysAndValues[i++];
073            String value = keysAndValues[i++];
074
075            result.put(key, value);
076        }
077
078        return result;
079    }
080
081    /**
082     * Converts a string to an {@link OptionModel}. The string is of the form "value=label". If the equals sign is
083     * omitted, then the same value is used for both value and label.
084     */
085    public static OptionModel toOptionModel(String input)
086    {
087        assert input != null;
088        int equalsx = input.indexOf('=');
089
090        if (equalsx < 0)
091            return new OptionModelImpl(input);
092
093        String value = input.substring(0, equalsx);
094        String label = input.substring(equalsx + 1);
095
096        return new OptionModelImpl(label, value);
097    }
098
099    /**
100     * Parses a string input into a series of value=label pairs compatible with {@link #toOptionModel(String)}. Splits
101     * on commas. Ignores whitespace around commas.
102     *
103     * @param input comma seperated list of terms
104     * @return list of option models
105     */
106    public static List<OptionModel> toOptionModels(String input)
107    {
108        assert input != null;
109        List<OptionModel> result = CollectionFactory.newList();
110
111        for (String term : input.split(","))
112            result.add(toOptionModel(term.trim()));
113
114        return result;
115    }
116
117    /**
118     * Wraps the result of {@link #toOptionModels(String)} as a {@link SelectModel} (with no option groups).
119     *
120     * See TAP5-2184 for why this ends up causing some trouble!
121     */
122    public static SelectModel toSelectModel(String input)
123    {
124        List<OptionModel> options = toOptionModels(input);
125
126        return new SelectModelImpl(null, options);
127    }
128
129    /**
130     * Converts a map entry to an {@link OptionModel}.
131     */
132    public static OptionModel toOptionModel(Map.Entry input)
133    {
134        assert input != null;
135        String label = input.getValue() != null ? String.valueOf(input.getValue()) : "";
136
137        return new OptionModelImpl(label, input.getKey());
138    }
139
140    /**
141     * Processes a map input into a series of map entries compatible with {@link #toOptionModel(Map.Entry)}.
142     *
143     * @param input map of elements
144     * @return list of option models
145     */
146    public static <K, V> List<OptionModel> toOptionModels(Map<K, V> input)
147    {
148        assert input != null;
149        List<OptionModel> result = CollectionFactory.newList();
150
151        for (Map.Entry entry : input.entrySet())
152            result.add(toOptionModel(entry));
153
154        return result;
155    }
156
157    /**
158     * Wraps the result of {@link #toOptionModels(Map)} as a {@link SelectModel} (with no option groups).
159     */
160    public static <K, V> SelectModel toSelectModel(Map<K, V> input)
161    {
162        List<OptionModel> options = toOptionModels(input);
163
164        return new SelectModelImpl(null, options);
165    }
166
167    /**
168     * Converts an object to an {@link OptionModel}.
169     */
170    public static OptionModel toOptionModel(Object input)
171    {
172        String label = (input != null ? String.valueOf(input) : "");
173
174        return new OptionModelImpl(label, input);
175    }
176
177    /**
178     * Processes a list input into a series of objects compatible with {@link #toOptionModel(Object)}.
179     *
180     * @param input list of elements
181     * @return list of option models
182     */
183    public static <E> List<OptionModel> toOptionModels(List<E> input)
184    {
185        assert input != null;
186        List<OptionModel> result = CollectionFactory.newList();
187
188        for (E element : input)
189            result.add(toOptionModel(element));
190
191        return result;
192    }
193
194    /**
195     * Wraps the result of {@link #toOptionModels(List)} as a {@link SelectModel} (with no option groups).
196     */
197    public static <E> SelectModel toSelectModel(List<E> input)
198    {
199        List<OptionModel> options = toOptionModels(input);
200
201        return new SelectModelImpl(null, options);
202    }
203
204    /**
205     * Parses a key/value pair where the key and the value are seperated by an equals sign. The key and value are
206     * trimmed of leading and trailing whitespace, and returned as a {@link KeyValue}.
207     */
208    public static KeyValue parseKeyValue(String input)
209    {
210        int pos = input.indexOf('=');
211
212        if (pos < 1)
213            throw new IllegalArgumentException(String.format("Key/value pair '%s' is not properly formatted (it does not contain an equals sign).", input));
214
215        String key = input.substring(0, pos);
216        String value = input.substring(pos + 1);
217
218        return new KeyValue(key.trim(), value.trim());
219    }
220
221    /**
222     * Used to convert a property expression into a key that can be used to locate various resources (Blocks, messages,
223     * etc.). Strips out any punctuation characters, leaving just words characters (letters, number and the
224     * underscore).
225     *
226     * @param expression a property expression
227     * @return the expression with punctuation removed
228     */
229    public static String extractIdFromPropertyExpression(String expression)
230    {
231        return InternalUtils.extractIdFromPropertyExpression(expression);
232    }
233
234    /**
235     * Looks for a label within the messages based on the id. If found, it is used, otherwise the name is converted to a
236     * user presentable form.
237     */
238    public static String defaultLabel(String id, Messages messages, String propertyExpression)
239    {
240        return InternalUtils.defaultLabel(id, messages, propertyExpression);
241    }
242
243    /**
244     * Strips a dotted sequence (such as a property expression, or a qualified class name) down to the last term of that
245     * expression, by locating the last period ('.') in the string.
246     */
247    public static String lastTerm(String input)
248    {
249        int dotx = input.lastIndexOf('.');
250
251        return input.substring(dotx + 1);
252    }
253
254    /**
255     * Converts an list of strings into a space-separated string combining them all, suitable for use as an HTML class
256     * attribute value.
257     *
258     * @param classes classes to combine
259     * @return the joined classes, or null if classes is empty
260     */
261    public static String toClassAttributeValue(List<String> classes)
262    {
263        if (classes.isEmpty())
264            return null;
265
266        return InternalUtils.join(classes, " ");
267    }
268
269    /**
270     * Converts an enum to a label string, allowing for overrides from a message catalog.
271     *
272     * <ul>
273     * <li>As key <em>prefix</em>.<em>name</em> if present. Ex: "ElementType.LOCAL_VARIABLE"
274     * <li>As key <em>name</em> if present, i.e., "LOCAL_VARIABLE".
275     * <li>As a user-presentable version of the name, i.e., "Local Variable".
276     * </ul>
277     *
278     * @param messages the messages to search for the label
279     * @param prefix   prepended to key
280     * @param value    to get a label for
281     * @return the label
282     */
283    public static String getLabelForEnum(Messages messages, String prefix, Enum value)
284    {
285        String name = value.name();
286
287        String key = prefix + "." + name;
288
289        if (messages.contains(key))
290            return messages.get(key);
291
292        if (messages.contains(name))
293            return messages.get(name);
294
295        return toUserPresentable(name.toLowerCase());
296    }
297
298    public static String getLabelForEnum(Messages messages, Enum value)
299    {
300        String prefix = lastTerm(value.getClass().getName());
301
302        return getLabelForEnum(messages, prefix, value);
303    }
304
305    private static String replace(String input, Pattern pattern, String replacement)
306    {
307        return InternalUtils.replace(input, pattern, replacement);
308    }
309
310    /**
311     * Determines if the two values are equal. They are equal if they are the exact same value (including if they are
312     * both null). Otherwise standard equals() comparison is used.
313     *
314     * @param left  value to compare, possibly null
315     * @param right value to compare, possibly null
316     * @return true if same value, both null, or equal
317     */
318    public static <T> boolean isEqual(T left, T right)
319    {
320        if (left == right)
321            return true;
322
323        if (left == null)
324            return false;
325
326        return left.equals(right);
327    }
328
329    /**
330     * Splits a path at each slash.
331     */
332    public static String[] splitPath(String path)
333    {
334        return SLASH_PATTERN.split(path);
335    }
336
337    /**
338     * Splits a value around commas. Whitespace around the commas is removed, as is leading and trailing whitespace.
339     *
340     * @since 5.1.0.0
341     */
342    public static String[] splitAtCommas(String value)
343    {
344        if (InternalUtils.isBlank(value))
345            return InternalConstants.EMPTY_STRING_ARRAY;
346
347        return COMMA_PATTERN.split(value.trim());
348    }
349
350    /**
351     * Copies some content from an input stream to an output stream. It is the caller's responsibility to close the
352     * streams.
353     *
354     * @param in  source of data
355     * @param out sink of data
356     * @throws IOException
357     * @since 5.1.0.0
358     */
359    public static void copy(InputStream in, OutputStream out) throws IOException
360    {
361        byte[] buffer = new byte[BUFFER_SIZE];
362
363        while (true)
364        {
365            int length = in.read(buffer);
366
367            if (length < 0)
368                break;
369
370            out.write(buffer, 0, length);
371        }
372
373        // TAPESTRY-2415: WebLogic needs this flush() call.
374        out.flush();
375    }
376
377    public static boolean isEqual(EventContext left, EventContext right)
378    {
379        if (left == right)
380            return true;
381
382        int count = left.getCount();
383
384        if (count != right.getCount())
385            return false;
386
387        for (int i = 0; i < count; i++)
388        {
389            if (!left.get(Object.class, i).equals(right.get(Object.class, i)))
390                return false;
391        }
392
393        return true;
394    }
395
396    /**
397     * Converts an Asset to an Asset2 if necessary. When actually wrapping an Asset as an Asset2, the asset is assumed
398     * to be variant (i.e., not cacheable).
399     *
400     * @since 5.1.0.0
401     */
402    public static Asset2 toAsset2(final Asset asset)
403    {
404        if (asset instanceof Asset2)
405            return (Asset2) asset;
406
407        return new Asset2()
408        {
409            /** Returns false. */
410            public boolean isInvariant()
411            {
412                return false;
413            }
414
415            public Resource getResource()
416            {
417                return asset.getResource();
418            }
419
420            public String toClientURL()
421            {
422                return asset.toClientURL();
423            }
424
425            @Override
426            public String toString()
427            {
428                return asset.toString();
429            }
430        };
431    }
432
433    public static InternalPropertyConduit toInternalPropertyConduit(final PropertyConduit conduit)
434    {
435        if (conduit instanceof InternalPropertyConduit)
436            return (InternalPropertyConduit) conduit;
437
438        return new InternalPropertyConduit()
439        {
440
441            public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
442            {
443                return conduit.getAnnotation(annotationClass);
444            }
445
446            public void set(Object instance, Object value)
447            {
448                conduit.set(instance, value);
449            }
450
451            public Class getPropertyType()
452            {
453                return conduit.getPropertyType();
454            }
455
456            public Type getPropertyGenericType()
457            {
458                if (conduit instanceof PropertyConduit2)
459                {
460                    return ((PropertyConduit2) conduit).getPropertyGenericType();
461                }
462                return conduit.getPropertyType();
463            }
464            
465            public Object get(Object instance)
466            {
467                return conduit.get(instance);
468            }
469
470            public String getPropertyName()
471            {
472                return null;
473            }
474        };
475    }
476
477    /**
478     * @param mixinDef the original mixin definition.
479     * @return an Orderable whose id is the mixin name.
480     */
481    public static Orderable<String> mixinTypeAndOrder(String mixinDef)
482    {
483        int idx = mixinDef.indexOf("::");
484        if (idx == -1)
485        {
486            return new Orderable<String>(mixinDef, mixinDef);
487        }
488        String type = mixinDef.substring(0, idx);
489        String[] constraints = splitMixinConstraints(mixinDef.substring(idx + 2));
490
491        return new Orderable<String>(type, type, constraints);
492    }
493
494    public static String[] splitMixinConstraints(String s)
495    {
496        return InternalUtils.isBlank(s) ? null : s.split(";");
497    }
498
499    /**
500     * Common mapper, used primarily with {@link org.apache.tapestry5.func.Flow#map(org.apache.tapestry5.func.Mapper)}
501     *
502     * @since 5.2.0
503     */
504    public static Mapper<Asset, StylesheetLink> assetToStylesheetLink = new Mapper<Asset, StylesheetLink>()
505    {
506        public StylesheetLink map(Asset input)
507        {
508            return new StylesheetLink(input);
509        }
510    };
511
512    public static LinkCreationListener2 toLinkCreationListener2(final LinkCreationListener delegate)
513    {
514        return new LinkCreationListener2()
515        {
516
517            public void createdPageRenderLink(Link link, PageRenderRequestParameters parameters)
518            {
519                delegate.createdPageRenderLink(link);
520            }
521
522            public void createdComponentEventLink(Link link, ComponentEventRequestParameters parameters)
523            {
524                delegate.createdComponentEventLink(link);
525            }
526        };
527    }
528
529    /**
530     * @since 5.3
531     */
532    public static String toFileSuffix(String fileName)
533    {
534        int dotx = fileName.lastIndexOf('.');
535
536        return dotx < 0 ? "" : fileName.substring(dotx + 1);
537    }
538
539    /**
540     * Extracts a value from a  map of references. Handles the case where the reference does not exist,
541     * and the case where the reference itself now contains null.
542     *
543     * @since 5.3
544     */
545    public static <K, V> V getAndDeref(Map<K, ? extends Reference<V>> map, K key)
546    {
547        Reference<V> ref = map.get(key);
548
549        return ref == null ? null : ref.get();
550    }
551
552    /**
553     * Gathers together an array containing all the threads.
554     * @since 5.4 */
555    public static Thread[] getAllThreads() {
556        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
557
558        while (true) {
559            ThreadGroup parentGroup = rootGroup.getParent();
560            if (parentGroup == null) {
561                break;
562            }
563            rootGroup = parentGroup;
564        }
565
566        Thread[] threads = new Thread[rootGroup.activeCount()];
567
568        while (true) {
569            // A really ugly API. threads.length must be larger than
570            // the actual number of threads, just so we can determine
571            // if we're done.
572            int count = rootGroup.enumerate(threads, true);
573            if (count < threads.length) {
574                return Arrays.copyOf(threads, count);
575            }
576            threads = new Thread[threads.length * 2];
577        }
578    }
579}
580