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}