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