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