001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010–2011 ApexIdentity Inc.
015 * Portions Copyright 2011-2014 ForgeRock AS.
016 */
017
018package org.forgerock.openig.el;
019
020import static org.forgerock.openig.util.StringUtil.asString;
021import static org.forgerock.util.Utils.closeSilently;
022
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.UnsupportedEncodingException;
027import java.lang.reflect.Array;
028import java.lang.reflect.Method;
029import java.lang.reflect.Modifier;
030import java.net.URLDecoder;
031import java.net.URLEncoder;
032import java.nio.charset.Charset;
033import java.util.Collection;
034import java.util.HashMap;
035import java.util.Map;
036import java.util.Properties;
037import java.util.regex.Matcher;
038import java.util.regex.Pattern;
039import java.util.regex.PatternSyntaxException;
040
041import javax.el.FunctionMapper;
042
043import org.forgerock.openig.util.StringUtil;
044import org.forgerock.util.encode.Base64;
045
046/**
047 * Maps between EL function names and methods. In this implementation all public
048 * static methods are automatically exposed as functions.
049 */
050public class Functions extends FunctionMapper {
051
052    /** A mapping of function names with methods to return. */
053    private static final Map<String, Method> METHODS;
054    static {
055        METHODS = new HashMap<String, Method>();
056        for (Method method : Functions.class.getMethods()) {
057            if (Modifier.isStatic(method.getModifiers())) {
058                METHODS.put(method.getName(), method);
059            }
060        }
061    }
062
063    /**
064     * Resolves the specified prefix and local name into a method. In this
065     * implementation, the only supported supported prefix is none ({@code ""}).
066     *
067     * @param prefix the prefix of the function, or {@code ""} if no prefix.
068     * @param localName the short name of the function.
069     * @return the static method to invoke, or {@code null} if no match was
070     * found.
071     */
072    @Override
073    public Method resolveFunction(String prefix, String localName) {
074        if (prefix != null && localName != null && prefix.length() == 0) {
075            return METHODS.get(localName);
076        }
077        // no match was found
078        return null;
079    }
080
081    /**
082     * Returns {@code true} if the object contains the value.
083     *
084     * @param object the object to be searched.
085     * @param value the value to find.
086     * @return the length of the object, or {@code 0} if length could not be
087     * determined.
088     */
089    public static boolean contains(Object object, Object value) {
090        if (object == null || value == null) {
091            return false;
092        } else if (object instanceof CharSequence && value instanceof CharSequence) {
093            return (object.toString().contains(value.toString()));
094        } else if (object instanceof Collection) {
095            return ((Collection<?>) object).contains(value);
096        } else if (object instanceof Object[]) {
097            // doesn't handle primitives (but is cheap)
098            for (Object o : (Object[]) object) {
099                if (o.equals(value)) {
100                    return true;
101                }
102            }
103        } else if (object.getClass().isArray()) {
104            // handles primitives (slightly more expensive)
105            int length = Array.getLength(object);
106            for (int n = 0; n < length; n++) {
107                if (Array.get(object, n).equals(value)) {
108                    return true;
109                }
110            }
111        }
112        // value not contained in object
113        return false;
114    }
115
116    /**
117     * Returns the index within a string of the first occurrence of a specified
118     * substring.
119     *
120     * @param value the string to be searched.
121     * @param substring the value to search for within the string
122     * @return the index of the first instance of substring, or {@code -1} if
123     * not found.
124     */
125    public static int indexOf(String value, String substring) {
126        return (value != null && substring != null ? value.indexOf(substring) : null);
127    }
128
129    /**
130     * Joins an array of strings into a single string value, with a specified
131     * separator.
132     *
133     * @param separator the separator to place between joined elements.
134     * @param values the array of strings to be joined.
135     * @return the string containing the joined strings.
136     */
137    public static String join(String[] values, String separator) {
138        return (values != null ? StringUtil.join(separator, (Object[]) values) : null);
139    }
140
141    /**
142     * Returns the first key found in a map that matches the specified regular
143     * expression pattern, or {@code null} if no such match is found.
144     *
145     * @param map the map whose keys are to be searched.
146     * @param pattern a string containing the regular expression pattern to match.
147     * @return the first matching key, or {@code null} if no match found.
148     */
149    public static String keyMatch(Object map, String pattern) {
150        if (map instanceof Map) {
151            // avoid unnecessary proxying via duck typing
152            Pattern p = null;
153            try {
154                // TODO: cache oft-used patterns?
155                p = Pattern.compile(pattern);
156            } catch (PatternSyntaxException pse) {
157                // invalid pattern results in no match
158                return null;
159            }
160            for (Object key : ((Map<?, ?>) map).keySet()) {
161                if (key instanceof String) {
162                    if (p.matcher((String) key).matches()) {
163                        return (String) key;
164                    }
165                }
166            }
167        }
168        // no match
169        return null;
170    }
171
172    /**
173     * Returns the number of items in a collection, or the number of characters
174     * in a string.
175     *
176     * @param value the object whose length is to be determined.
177     * @return the length of the object, or {@code 0} if length could not be
178     * determined.
179     */
180    public static int length(Object value) {
181        if (value == null) {
182            return 0;
183        } else if (value instanceof CharSequence) {
184            return ((CharSequence) value).length();
185        } else if (value instanceof Collection) {
186            return ((Collection<?>) value).size();
187        } else if (value instanceof Map) {
188            return ((Map<?, ?>) value).size();
189        } else if (value instanceof Object[]) {
190            // doesn't handle primitives (but is cheap)
191            return ((Object[]) value).length;
192        } else if (value.getClass().isArray()) {
193            // handles primitives (slightly more expensive)
194            return Array.getLength(value);
195        }
196        // no items
197        return 0;
198    }
199
200    /**
201     * Returns {@code true} if the string contains the specified regular
202     * expression pattern.
203     *
204     * @param value
205     *            the string to be searched.
206     * @param pattern
207     *            a string containing the regular expression pattern to find.
208     * @return {@code true} if the string contains the specified regular
209     *         expression pattern.
210     */
211    public static boolean matches(String value, String pattern) {
212        try {
213            return Pattern.compile(pattern).matcher(value).find();
214        } catch (PatternSyntaxException pse) {
215            // ignore invalid pattern
216        }
217        return false;
218    }
219
220    /**
221     * Returns an array containing the matches of a regular expression pattern
222     * against a string, or {@code null} if no match is found. The first element
223     * of the array is the entire match, and each subsequent element correlates
224     * to any capture group specified within the regular expression.
225     *
226     * @param value
227     *            the string to be searched.
228     * @param pattern
229     *            a string containing the regular expression pattern to match.
230     * @return an array of matches, or {@code null} if no match found.
231     */
232    public static String[] matchingGroups(String value, String pattern) {
233        try {
234            Pattern p = Pattern.compile(pattern);
235            Matcher m = p.matcher(value);
236            if (m.find()) {
237                int count = m.groupCount();
238                String[] matches = new String[count + 1];
239                matches[0] = m.group(0);
240                for (int n = 1; n <= count; n++) {
241                    matches[n] = m.group(n);
242                }
243                return matches;
244            }
245        } catch (PatternSyntaxException pse) {
246            // ignore invalid pattern
247        }
248        return null;
249    }
250
251    /**
252     * Splits a string into an array of substrings around matches of the given
253     * regular expression.
254     *
255     * @param value
256     *            the string to be split.
257     * @param regex
258     *            the regular expression to split substrings around.
259     * @return the resulting array of split substrings.
260     */
261    public static String[] split(String value, String regex) {
262        return (value != null ? value.split(regex) : null);
263    }
264
265    /**
266     * Converts all of the characters in a string to lower case.
267     *
268     * @param value the string whose characters are to be converted.
269     * @return the string with characters converted to lower case.
270     */
271    public static String toLowerCase(String value) {
272        return (value != null ? value.toLowerCase() : null);
273    }
274
275    /**
276     * Returns the string value of an arbitrary object.
277     *
278     * @param value
279     *            the object whose string value is to be returned.
280     * @return the string value of the object.
281     */
282    public static String toString(Object value) {
283        return (value != null ? value.toString() : null);
284    }
285
286    /**
287     * Converts all of the characters in a string to upper case.
288     *
289     * @param value the string whose characters are to be converted.
290     * @return the string with characters converted to upper case.
291     */
292    public static String toUpperCase(String value) {
293        return (value != null ? value.toUpperCase() : null);
294    }
295
296    /**
297     * Returns a copy of a string with leading and trailing whitespace omitted.
298     *
299     * @param value the string whose white space is to be omitted.
300     * @return the string with leading and trailing white space omitted.
301     */
302    public static String trim(String value) {
303        return (value != null ? value.trim() : null);
304    }
305
306    /**
307     * Returns the URL encoding of the provided string.
308     *
309     * @param value
310     *            the string to be URL encoded, which may be {@code null}.
311     * @return the URL encoding of the provided string, or {@code null} if
312     *         {@code string} was {@code null}.
313     */
314    public static String urlEncode(String value) {
315        try {
316            return value != null ? URLEncoder.encode(value, "UTF-8") : null;
317        } catch (UnsupportedEncodingException e) {
318            return value;
319        }
320    }
321
322    /**
323     * Returns the URL decoding of the provided string.
324     *
325     * @param value
326     *            the string to be URL decoded, which may be {@code null}.
327     * @return the URL decoding of the provided string, or {@code null} if
328     *         {@code string} was {@code null}.
329     */
330    public static String urlDecode(String value) {
331        try {
332            return value != null ? URLDecoder.decode(value, "UTF-8") : null;
333        } catch (UnsupportedEncodingException e) {
334            return value;
335        }
336    }
337
338    /**
339     * Encode the given String input into Base 64.
340     *
341     * @param value
342     *            the string to be Base64 encoded, which may be {@code null}.
343     * @return the Base64 encoding of the provided string, or {@code null} if
344     *         {@code string} was {@code null}.
345     */
346    public static String encodeBase64(final String value) {
347        if (value == null) {
348            return null;
349        }
350        return Base64.encode(value.getBytes());
351    }
352
353    /**
354     * Decode the given Base64 String input.
355     *
356     * @param value
357     *            the string to be Base64 decoded, which may be {@code null}.
358     * @return the decoding of the provided string, or {@code null} if
359     *         {@code string} was {@code null} or if the input was not a Base64 valid input.
360     */
361    public static String decodeBase64(final String value) {
362        if (value == null) {
363            return null;
364        }
365        return new String(Base64.decode(value));
366    }
367
368    /**
369     * Returns the content of the given file as a plain String.
370     *
371     * @param filename
372     *         file to be read
373     * @return the file content as a String or {@literal null} if here was an error (missing file, ...)
374     */
375    public static String read(final String filename) {
376        try {
377            return asString(new FileInputStream(new File(filename)), Charset.defaultCharset());
378        } catch (IOException e) {
379            return null;
380        }
381    }
382
383    /**
384     * Returns the content of the given file as a {@link Properties}.
385     *
386     * @param filename
387     *         file to be read
388     * @return the file content as {@link Properties} or {@literal null} if here was an error (missing file, ...)
389     */
390    public static Properties readProperties(final String filename) {
391        FileInputStream fis = null;
392        try {
393            fis = new FileInputStream(new File(filename));
394            Properties properties = new Properties();
395            properties.load(fis);
396            return properties;
397        } catch (IOException e) {
398            return null;
399        } finally {
400            closeSilently(fis);
401        }
402    }
403}