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