    001// Copyright 2006-2014 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.
    014package org.apache.tapestry5.commons.internal.util;
    015
    016import java.lang.annotation.Annotation;
    017import java.lang.reflect.Method;
    018import java.util.Collection;
    019import java.util.Collections;
    020import java.util.List;
    021import java.util.Map;
    022import java.util.regex.Matcher;
    023import java.util.regex.Pattern;
    024
    025import org.apache.tapestry5.commons.AnnotationProvider;
    026import org.apache.tapestry5.commons.Locatable;
    027import org.apache.tapestry5.commons.Location;
    028import org.apache.tapestry5.commons.Messages;
    029import org.apache.tapestry5.commons.internal.NullAnnotationProvider;
    030import org.apache.tapestry5.commons.util.CollectionFactory;
    031import org.apache.tapestry5.commons.util.CommonsUtils;
    032
    033/**
    034 * Utility methods class for the Commons package.
    035 */
    036public class InternalCommonsUtils {
    037
    038    /**
    039     * @since 5.3
    040     */
    041    public final static AnnotationProvider NULL_ANNOTATION_PROVIDER = new NullAnnotationProvider();
    042    private static final Pattern NON_WORD_PATTERN = Pattern.compile("[^\\w]");
    043
    044    /**
    045     * Adds a value to a specially organized map where the values are lists of objects. This somewhat simulates a map
    046     * that allows multiple values for the same key.
    047     *
    048     * @param map
    049     *         to store value into
    050     * @param key
    051     *         for which a value is added
    052     * @param value
    053     *         to add
    054     * @param <K>
    055     *         the type of key
    056     * @param <V>
    057     *         the type of the list
    058     */
    059    public static <K, V> void addToMapList(Map<K, List<V>> map, K key, V value)
    060    {
    061        List<V> list = map.get(key);
    062
    063        if (list == null)
    064        {
    065            list = CollectionFactory.newList();
    066            map.put(key, list);
    067        }
    068
    069        list.add(value);
    070    }
    071
    072    /**
    073     * Sniffs the object to see if it is a {@link Location} or {@link Locatable}. Returns null if null or not
    074     * convertable to a location.
    075     */
    076
    077    public static Location locationOf(Object location)
    078    {
    079        if (location == null)
    080            return null;
    081
    082        if (location instanceof Location)
    083            return (Location) location;
    084
    085        if (location instanceof Locatable)
    086            return ((Locatable) location).getLocation();
    087
    088        return null;
    089    }
    090
    091    public static AnnotationProvider toAnnotationProvider(final Method element)
    092    {
    093        if (element == null)
    094            return NULL_ANNOTATION_PROVIDER;
    095
    096        return new AnnotationProvider()
    097        {
    098            @Override
    099            public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
    100            {
    101                return element.getAnnotation(annotationClass);
    102            }
    103        };
    104    }
    105
    106    /**
    107     * Used to convert a property expression into a key that can be used to locate various resources (Blocks, messages,
    108     * etc.). Strips out any punctuation characters, leaving just words characters (letters, number and the
    109     * underscore).
    110     *
    111     * @param expression a property expression
    112     * @return the expression with punctuation removed
    113     */
    114    public static String extractIdFromPropertyExpression(String expression)
    115    {
    116        return replace(expression, NON_WORD_PATTERN, "");
    117    }
    118
    119    public static String replace(String input, Pattern pattern, String replacement)
    120    {
    121        return pattern.matcher(input).replaceAll(replacement);
    122    }
    123
    124    /**
    125     * Looks for a label within the messages based on the id. If found, it is used, otherwise the name is converted to a
    126     * user presentable form.
    127     */
    128    public static String defaultLabel(String id, Messages messages, String propertyExpression)
    129    {
    130        String key = id + "-label";
    131
    132        if (messages.contains(key))
    133            return messages.get(key);
    134
    135        return toUserPresentable(extractIdFromPropertyExpression(InternalCommonsUtils.lastTerm(propertyExpression)));
    136    }
    137
    138    /**
    139     * Capitalizes the string, and inserts a space before each upper case character (or sequence of upper case
    140     * characters). Thus "userId" becomes "User Id", etc. Also, converts underscore into space (and capitalizes the
    141     * following word), thus "user_id" also becomes "User Id".
    142     */
    143    public static String toUserPresentable(String id)
    144    {
    145        StringBuilder builder = new StringBuilder(id.length() * 2);
    146
    147        char[] chars = id.toCharArray();
    148        boolean postSpace = true;
    149        boolean upcaseNext = true;
    150
    151        for (char ch : chars)
    152        {
    153            if (upcaseNext)
    154            {
    155                builder.append(Character.toUpperCase(ch));
    156                upcaseNext = false;
    157
    158                continue;
    159            }
    160
    161            if (ch == '_')
    162            {
    163                builder.append(' ');
    164                upcaseNext = true;
    165                continue;
    166            }
    167
    168            boolean upperCase = Character.isUpperCase(ch);
    169
    170            if (upperCase && !postSpace)
    171                builder.append(' ');
    172
    173            builder.append(ch);
    174
    175            postSpace = upperCase;
    176        }
    177
    178        return builder.toString();
    179    }
    180
    181    /**
    182     * @since 5.3
    183     */
    184    public static AnnotationProvider toAnnotationProvider(final Class element)
    185    {
    186        return new AnnotationProvider()
    187        {
    188            @Override
    189            public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
    190            {
    191                return annotationClass.cast(element.getAnnotation(annotationClass));
    192            }
    193        };
    194    }
    195
    196    /**
    197     * Pattern used to eliminate leading and trailing underscores and dollar signs.
    198     */
    199    static final Pattern NAME_PATTERN = Pattern.compile("^[_|$]*([\\p{javaJavaIdentifierPart}]+?)[_|$]*$",
    200            Pattern.CASE_INSENSITIVE);
    201
    202    /**
    203     * Converts a method to a user presentable string consisting of the containing class name, the method name, and the
    204     * short form of the parameter list (the class name of each parameter type, shorn of the package name portion).
    205     *
    206     * @param method
    207     * @return short string representation
    208     */
    209    public static String asString(Method method)
    210    {
    211        StringBuilder buffer = new StringBuilder();
    212
    213        buffer.append(method.getDeclaringClass().getName());
    214        buffer.append('.');
    215        buffer.append(method.getName());
    216        buffer.append('(');
    217
    218        for (int i = 0; i < method.getParameterTypes().length; i++)
    219        {
    220            if (i > 0)
    221                buffer.append(", ");
    222
    223            String name = method.getParameterTypes()[i].getSimpleName();
    224
    225            buffer.append(name);
    226        }
    227
    228        return buffer.append(')').toString();
    229    }
    230
    231    /**
    232     * Strips leading "_" and "$" and trailing "_" from the name.
    233     */
    234    public static String stripMemberName(String memberName)
    235    {
    236        assert InternalCommonsUtils.isNonBlank(memberName);
    237        Matcher matcher = NAME_PATTERN.matcher(memberName);
    238
    239        if (!matcher.matches())
    240            throw new IllegalArgumentException(String.format("Input '%s' is not a valid Java identifier.", memberName));
    241
    242        return matcher.group(1);
    243    }
    244
    245    /**
    246     * Joins together some number of elements to form a comma separated list.
    247     */
    248    public static String join(List elements)
    249    {
    250        return InternalCommonsUtils.join(elements, ", ");
    251    }
    252
    253    /**
    254     * Joins together some number of elements. If a value in the list is the empty string, it is replaced with the
    255     * string "(blank)".
    256     *
    257     * @param elements
    258     *         objects to be joined together
    259     * @param separator
    260     *         used between elements when joining
    261     */
    262    public static String join(List elements, String separator)
    263    {
    264        switch (elements.size())
    265        {
    266            case 0:
    267                return "";
    268
    269            case 1:
    270                return String.valueOf(elements.get(0));
    271
    272            default:
    273
    274                StringBuilder buffer = new StringBuilder();
    275                boolean first = true;
    276
    277                for (Object o : elements)
    278                {
    279                    if (!first)
    280                        buffer.append(separator);
    281
    282                    String string = String.valueOf(o);
    283
    284                    if (string.equals(""))
    285                        string = "(blank)";
    286
    287                    buffer.append(string);
    288
    289                    first = false;
    290                }
    291
    292                return buffer.toString();
    293        }
    294    }
    295
    296    /**
    297     * Creates a sorted copy of the provided elements, then turns that into a comma separated list.
    298     *
    299     * @return the elements converted to strings, sorted, joined with comma ... or "(none)" if the elements are null or
    300     *         empty
    301     */
    302    public static String joinSorted(Collection elements)
    303    {
    304        if (elements == null || elements.isEmpty())
    305            return "(none)";
    306
    307        List<String> list = CollectionFactory.newList();
    308
    309        for (Object o : elements)
    310            list.add(String.valueOf(o));
    311
    312        Collections.sort(list);
    313
    314        return join(list);
    315    }
    316
    317    /**
    318     * Capitalizes a string, converting the first character to uppercase.
    319     */
    320    public static String capitalize(String input)
    321    {
    322        if (input.length() == 0)
    323            return input;
    324
    325        return input.substring(0, 1).toUpperCase() + input.substring(1);
    326    }
    327
    328    public static boolean isNonBlank(String input)
    329    {
    330        return !CommonsUtils.isBlank(input);
    331    }
    332
    333    /**
    334     * Return true if the input string contains the marker for symbols that must be expanded.
    335     */
    336    public static boolean containsSymbols(String input)
    337    {
    338        return input.contains("${");
    339    }
    340
    341    /**
    342     * Searches the string for the final period ('.') character and returns everything after that. The input string is
    343     * generally a fully qualified class name, though tapestry-core also uses this method for the occasional property
    344     * expression (which is also dot separated). Returns the input string unchanged if it does not contain a period
    345     * character.
    346     */
    347    public static String lastTerm(String input)
    348    {
    349        assert isNonBlank(input);
    350        int dotx = input.lastIndexOf('.');
    351
    352        if (dotx < 0)
    353            return input;
    354
    355        return input.substring(dotx + 1);
    356    }
    357
    358    /**
    359     * Extracts the string keys from a map and returns them in sorted order. The keys are converted to strings.
    360     *
    361     * @param map
    362     *         the map to extract keys from (may be null)
    363     * @return the sorted keys, or the empty set if map is null
    364     */
    365
    366    public static List<String> sortedKeys(Map map)
    367    {
    368        if (map == null)
    369            return Collections.emptyList();
    370
    371        List<String> keys = CollectionFactory.newList();
    372
    373        for (Object o : map.keySet())
    374            keys.add(String.valueOf(o));
    375
    376        Collections.sort(keys);
    377
    378        return keys;
    379    }
    380
    381}