    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