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}