001/*
002 *  Copyright 2012 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.util;
017
018import java.io.UnsupportedEncodingException;
019import java.security.MessageDigest;
020import java.security.NoSuchAlgorithmException;
021import java.text.Normalizer;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Iterator;
025import java.util.List;
026import java.util.StringTokenizer;
027
028import org.apache.commons.codec.binary.Base64;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import org.ametys.runtime.i18n.I18nizableText;
033
034/**
035 * A collection of String management utility methods.
036 */
037public final class StringUtils
038{
039    private static final Logger __LOGGER = LoggerFactory.getLogger(StringUtils.class);
040
041    private static final long __DATA_SIZE_NEXT_LIMIT = 1024;
042    private static final List<String> __DATA_SIZE_KEYS = List.of(
043        "PLUGINS_CORE_UI_FORMAT_FILE_SIZE_NOT_ESCAPED_BYTES",
044        "PLUGINS_CORE_UI_FORMAT_FILE_SIZE_NOT_ESCAPED_KB",
045        "PLUGINS_CORE_UI_FORMAT_FILE_SIZE_NOT_ESCAPED_MB",
046        "PLUGINS_CORE_UI_FORMAT_FILE_SIZE_NOT_ESCAPED_GB",
047        "PLUGINS_CORE_UI_FORMAT_FILE_SIZE_NOT_ESCAPED_TB"
048    );
049    
050    private static final String[] __CSV_BEGIN_CHARS = {"=", "@", "+", "-", "\r", "\t"};
051    private static final char __CSV_QUOTE = '"';
052    private static final String __CSV_QUOTE_STR = String.valueOf(__CSV_QUOTE);
053    
054    private StringUtils()
055    {
056        // empty private constructor
057    }
058    
059    /**
060     * Extract String values from a comma seprated list.
061     * @param values the comma separated list
062     * @return a collection of String or an empty collection if string is null or empty.
063     */
064    public static Collection<String> stringToCollection(String values)
065    {
066        Collection<String> result = new ArrayList<>();
067        if (values != null && values.length() > 0)
068        {
069            // Explore the string list with a stringtokenizer with ','.
070            StringTokenizer stk = new StringTokenizer(values, ",");
071
072            while (stk.hasMoreTokens())
073            {
074                // Don't forget to trim
075                result.add(stk.nextToken().trim());
076            }
077        }
078
079        return result;
080    }
081
082    /**
083     * Extract String values from a comma seprated list.
084     * @param values the comma separated list
085     * @return an array of String
086     */
087    public static String[] stringToStringArray(String values)
088    {
089        Collection<String> coll = stringToCollection(values);
090        return coll.toArray(new String[coll.size()]);
091    }
092    
093    /**
094     * Generates a unique String key, based on System.currentTimeMillis()
095     * @return a unique String value
096     */
097    public static String generateKey()
098    {
099        long value;
100        
101        // Find a new value
102        synchronized (StringUtils.class)
103        {
104            value = System.currentTimeMillis();
105
106            try
107            {
108                Thread.sleep(15);
109            }
110            catch (InterruptedException e)
111            {
112                // does nothing, continue
113            }
114        }
115
116        // Convert it to a string using radix 36 (more compact)
117        String longString = Long.toString(value, Character.MAX_RADIX);
118    
119        return longString;
120    }
121    
122    /**
123     * Encrypt a password by using first MD5 Hash and base64 encoding.
124     * @param password The password to be encrypted.
125     * @return The password encrypted or null if the MD5 is not supported
126     */
127    public static String md5Base64(String password)
128    {
129        if (password == null)
130        {
131            return null;
132        }
133        
134        MessageDigest md5;
135        try
136        {
137            md5 = MessageDigest.getInstance("MD5");
138        }
139        catch (NoSuchAlgorithmException e)
140        {
141            // This error exception not be raised since MD5 is embedded in the JDK
142            __LOGGER.error("Cannot encode the password to md5Base64", e);
143            return null;
144        }
145        
146        // MD5-hash the password.
147        md5.reset();
148        try
149        {
150            md5.update(password.getBytes("UTF-8"));
151        }
152        catch (UnsupportedEncodingException e)
153        {
154            throw new IllegalStateException(e);
155        }
156        byte [] hash = md5.digest();
157        
158        // Base64-encode the result.
159        try
160        {
161            return new String(Base64.encodeBase64(hash), "UTF-8");
162        }
163        catch (UnsupportedEncodingException e)
164        {
165            throw new IllegalStateException(e);
166        }
167    }
168    
169    /**
170     * Normalize string. Pass to lower case and remove Unicode accents and diacritics
171     * @param value the value to normalize
172     * @return the normalized value
173     */
174    public static String normalizeStringValue(String value)
175    {
176        return Normalizer.normalize(value.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");
177    }
178    
179    /**
180     * Transform a size to a readable size for data (bytes, KB, MB, etc.).
181     * @param size The size to transform
182     * @return An internationalized text with the size and the unit.
183     */
184    public static I18nizableText toReadableDataSize(Long size)
185    {
186        if (size == 1L)
187        {
188            return _createReadatableDataSize(size, "PLUGINS_CORE_UI_FORMAT_FILE_SIZE_NOT_ESCAPED_BYTE");
189        }
190        return _toReadableDataSize(size, __DATA_SIZE_KEYS.iterator());
191    }
192    
193    private static I18nizableText _toReadableDataSize(Long size, Iterator<String> keys)
194    {
195        String key = keys.next();
196        if (!keys.hasNext() || size < __DATA_SIZE_NEXT_LIMIT)
197        {
198            return _createReadatableDataSize(size, key);
199        }
200        return _toReadableDataSize(size / __DATA_SIZE_NEXT_LIMIT, keys);
201    }
202    
203    private static I18nizableText _createReadatableDataSize(Long size, String key)
204    {
205        return new I18nizableText("plugin.core-ui", key, List.of(size.toString()));
206    }
207    
208    /**
209     * Returns a escaped {@code String} value for a CSV cell enclosed in double quotes.
210     *
211     * <p>Any double quote characters in the value are escaped with another double quote.</p>
212     * <p>If cell value contains a formula (ex: =SOMME(A0:A10)) it could be evaluated by CVS editor.<br>
213     * Use {@link #sanitizeCsv(String)} to avoid formula evaluation</p>
214     *    
215     * <pre>
216     * null                          => ""
217     * =1+2                          => "=1+2"
218     * =1+2'" ;,=1+2                 => "=1+2'"" ;,=1+2"
219     * L'orem ipsut; sit amet, dolor => "L'orem ipsut; sit amet, dolor"
220     * =cmd|' /c Calc.exe'!'A1'      => "=cmd|' /c Calc.exe'!'A1'"
221     * </pre>
222     *
223     * @param value the String value for CSV column. Can be null.
224     * @return the escaped value 
225     */
226    public static String escapeCsv(String value)
227    {
228        StringBuilder sb = new StringBuilder();
229        
230        sb.append(__CSV_QUOTE);
231        
232        if (org.apache.commons.lang3.StringUtils.isNotEmpty(value))
233        {
234            sb.append(org.apache.commons.lang3.StringUtils.replace(value, __CSV_QUOTE_STR, __CSV_QUOTE_STR + __CSV_QUOTE_STR));
235        }
236        
237        sb.append(__CSV_QUOTE);
238        
239        return sb.toString();
240    }
241    
242    /**
243     * Returns a sanitized {@code String} value for a CSV column enclosed in double quotes.
244     *
245     * <p>Any double quote characters in the value are escaped with another double quote.</p>
246     *    
247     * <p>If the value starts with '=', '+', '-', '@', newline or TAB, is prepend with a single quote.</p>
248     * 
249     * <pre>
250     * null                          => ""
251     * =1+2";=1+2                    => "'=1+2"";=1+2"
252     * =1+2'" ;,=1+2                 => "'=1+2'"" ;,=1+2"
253     * L'orem ipsut; sit amet, dolor => "L'orem ipsut; sit amet, dolor"
254     * =cmd|' /c Calc.exe'!'A1'      => "'=cmd|' /c Calc.exe'!'A1'"
255     * </pre>
256     *
257     * @param value the untrusted String value for CSV column. Can be null.
258     * @return the escaped and trusted value 
259     */
260    public static String sanitizeCsv(String value)
261    {
262        StringBuilder sb = new StringBuilder();
263        
264        sb.append(__CSV_QUOTE);
265        
266        if (org.apache.commons.lang3.StringUtils.isNotEmpty(value))
267        {
268            if (org.apache.commons.lang3.StringUtils.startsWithAny(value, __CSV_BEGIN_CHARS))
269            {
270                sb.append("'");
271            }
272            sb.append(org.apache.commons.lang3.StringUtils.replace(value, __CSV_QUOTE_STR, __CSV_QUOTE_STR + __CSV_QUOTE_STR));
273        }
274        
275        sb.append(__CSV_QUOTE);
276        
277        return sb.toString();
278    }
279    
280    /**
281     * Returns a sanitized {@code String} value for a XLS-HTML column (no double quotes enclosing).
282     *
283     * <p>If the value starts with '=', '+', '-', '@', newline or TAB, is prepend with a single quote.</p>
284     * 
285     * <pre>
286     * null                          => StringUtils.EMPTY
287     * =1+2";=1+2                    => '=1+2";=1+2
288     * =1+2'" ;,=1+2                 => '=1+2'" ;,=1+2  
289     * L'orem ipsut; sit amet, dolor => L'orem ipsut; sit amet, dolor
290     * =cmd|' /c Calc.exe'!'A1'      => '=cmd|' /c Calc.exe'!'A1
291     * </pre>
292     *
293     * @param value the untrusted String value for HTML-XLS column. Can be null.
294     * @return the trusted value 
295     */
296    public static String sanitizeXlsHtml(String value)
297    {
298        if (org.apache.commons.lang3.StringUtils.isNotEmpty(value) && org.apache.commons.lang3.StringUtils.startsWithAny(value, __CSV_BEGIN_CHARS))
299        {
300            return "'" + value;
301        }
302        
303        return org.apache.commons.lang3.StringUtils.defaultString(value);
304    }
305}