1 /** 2 * Copyright (c) 2004-2011 QOS.ch 3 * All rights reserved. 4 * 5 * Permission is hereby granted, free of charge, to any person obtaining 6 * a copy of this software and associated documentation files (the 7 * "Software"), to deal in the Software without restriction, including 8 * without limitation the rights to use, copy, modify, merge, publish, 9 * distribute, sublicense, and/or sell copies of the Software, and to 10 * permit persons to whom the Software is furnished to do so, subject to 11 * the following conditions: 12 * 13 * The above copyright notice and this permission notice shall be 14 * included in all copies or substantial portions of the Software. 15 * 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 * 24 */ 25 package org.slf4j.helpers; 26 27 import java.text.MessageFormat; 28 import java.util.HashMap; 29 import java.util.Map; 30 31 // contributors: lizongbo: proposed special treatment of array parameter values 32 // Joern Huxhorn: pointed out double[] omission, suggested deep array copy 33 /** 34 * Formats messages according to very simple substitution rules. Substitutions 35 * can be made 1, 2 or more arguments. 36 * 37 * <p> 38 * For example, 39 * 40 * <pre> 41 * MessageFormatter.format("Hi {}.", "there") 42 * </pre> 43 * 44 * will return the string "Hi there.". 45 * <p> 46 * The {} pair is called the <em>formatting anchor</em>. It serves to designate 47 * the location where arguments need to be substituted within the message 48 * pattern. 49 * <p> 50 * In case your message contains the '{' or the '}' character, you do not have 51 * to do anything special unless the '}' character immediately follows '{'. For 52 * example, 53 * 54 * <pre> 55 * MessageFormatter.format("Set {1,2,3} is not equal to {}.", "1,2"); 56 * </pre> 57 * 58 * will return the string "Set {1,2,3} is not equal to 1,2.". 59 * 60 * <p> 61 * If for whatever reason you need to place the string "{}" in the message 62 * without its <em>formatting anchor</em> meaning, then you need to escape the 63 * '{' character with '\', that is the backslash character. Only the '{' 64 * character should be escaped. There is no need to escape the '}' character. 65 * For example, 66 * 67 * <pre> 68 * MessageFormatter.format("Set \\{} is not equal to {}.", "1,2"); 69 * </pre> 70 * 71 * will return the string "Set {} is not equal to 1,2.". 72 * 73 * <p> 74 * The escaping behavior just described can be overridden by escaping the escape 75 * character '\'. Calling 76 * 77 * <pre> 78 * MessageFormatter.format("File name is C:\\\\{}.", "file.zip"); 79 * </pre> 80 * 81 * will return the string "File name is C:\file.zip". 82 * 83 * <p> 84 * The formatting conventions are different than those of {@link MessageFormat} 85 * which ships with the Java platform. This is justified by the fact that 86 * SLF4J's implementation is 10 times faster than that of {@link MessageFormat}. 87 * This local performance difference is both measurable and significant in the 88 * larger context of the complete logging processing chain. 89 * 90 * <p> 91 * See also {@link #format(String, Object)}, 92 * {@link #format(String, Object, Object)} and 93 * {@link #arrayFormat(String, Object[])} methods for more details. 94 * 95 * @author Ceki Gülcü 96 * @author Joern Huxhorn 97 */ 98 final public class MessageFormatter { 99 static final char DELIM_START = '{'; 100 static final char DELIM_STOP = '}'; 101 static final String DELIM_STR = "{}"; 102 private static final char ESCAPE_CHAR = '\\'; 103 104 /** 105 * Performs single argument substitution for the 'messagePattern' passed as 106 * parameter. 107 * <p> 108 * For example, 109 * 110 * <pre> 111 * MessageFormatter.format("Hi {}.", "there"); 112 * </pre> 113 * 114 * will return the string "Hi there.". 115 * <p> 116 * 117 * @param messagePattern 118 * The message pattern which will be parsed and formatted 119 * @param argument 120 * The argument to be substituted in place of the formatting anchor 121 * @return The formatted message 122 */ format(String messagePattern, Object arg)123 final public static FormattingTuple format(String messagePattern, Object arg) { 124 return arrayFormat(messagePattern, new Object[] { arg }); 125 } 126 127 /** 128 * 129 * Performs a two argument substitution for the 'messagePattern' passed as 130 * parameter. 131 * <p> 132 * For example, 133 * 134 * <pre> 135 * MessageFormatter.format("Hi {}. My name is {}.", "Alice", "Bob"); 136 * </pre> 137 * 138 * will return the string "Hi Alice. My name is Bob.". 139 * 140 * @param messagePattern 141 * The message pattern which will be parsed and formatted 142 * @param arg1 143 * The argument to be substituted in place of the first formatting 144 * anchor 145 * @param arg2 146 * The argument to be substituted in place of the second formatting 147 * anchor 148 * @return The formatted message 149 */ format(final String messagePattern, Object arg1, Object arg2)150 final public static FormattingTuple format(final String messagePattern, Object arg1, Object arg2) { 151 return arrayFormat(messagePattern, new Object[] { arg1, arg2 }); 152 } 153 getThrowableCandidate(Object[] argArray)154 static final Throwable getThrowableCandidate(Object[] argArray) { 155 if (argArray == null || argArray.length == 0) { 156 return null; 157 } 158 159 final Object lastEntry = argArray[argArray.length - 1]; 160 if (lastEntry instanceof Throwable) { 161 return (Throwable) lastEntry; 162 } 163 return null; 164 } 165 166 /** 167 * Same principle as the {@link #format(String, Object)} and 168 * {@link #format(String, Object, Object)} methods except that any number of 169 * arguments can be passed in an array. 170 * 171 * @param messagePattern 172 * The message pattern which will be parsed and formatted 173 * @param argArray 174 * An array of arguments to be substituted in place of formatting 175 * anchors 176 * @return The formatted message 177 */ arrayFormat(final String messagePattern, final Object[] argArray)178 final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) { 179 180 Throwable throwableCandidate = getThrowableCandidate(argArray); 181 182 if (messagePattern == null) { 183 return new FormattingTuple(null, argArray, throwableCandidate); 184 } 185 186 if (argArray == null) { 187 return new FormattingTuple(messagePattern); 188 } 189 190 int i = 0; 191 int j; 192 // use string builder for better multicore performance 193 StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); 194 195 int L; 196 for (L = 0; L < argArray.length; L++) { 197 198 j = messagePattern.indexOf(DELIM_STR, i); 199 200 if (j == -1) { 201 // no more variables 202 if (i == 0) { // this is a simple string 203 return new FormattingTuple(messagePattern, argArray, throwableCandidate); 204 } else { // add the tail string which contains no variables and return 205 // the result. 206 sbuf.append(messagePattern.substring(i, messagePattern.length())); 207 return new FormattingTuple(sbuf.toString(), argArray, throwableCandidate); 208 } 209 } else { 210 if (isEscapedDelimeter(messagePattern, j)) { 211 if (!isDoubleEscaped(messagePattern, j)) { 212 L--; // DELIM_START was escaped, thus should not be incremented 213 sbuf.append(messagePattern.substring(i, j - 1)); 214 sbuf.append(DELIM_START); 215 i = j + 1; 216 } else { 217 // The escape character preceding the delimiter start is 218 // itself escaped: "abc x:\\{}" 219 // we have to consume one backward slash 220 sbuf.append(messagePattern.substring(i, j - 1)); 221 deeplyAppendParameter(sbuf, argArray[L], new HashMap<Object[], Object>()); 222 i = j + 2; 223 } 224 } else { 225 // normal case 226 sbuf.append(messagePattern.substring(i, j)); 227 deeplyAppendParameter(sbuf, argArray[L], new HashMap<Object[], Object>()); 228 i = j + 2; 229 } 230 } 231 } 232 // append the characters following the last {} pair. 233 sbuf.append(messagePattern.substring(i, messagePattern.length())); 234 if (L < argArray.length - 1) { 235 return new FormattingTuple(sbuf.toString(), argArray, throwableCandidate); 236 } else { 237 return new FormattingTuple(sbuf.toString(), argArray, null); 238 } 239 } 240 isEscapedDelimeter(String messagePattern, int delimeterStartIndex)241 final static boolean isEscapedDelimeter(String messagePattern, int delimeterStartIndex) { 242 243 if (delimeterStartIndex == 0) { 244 return false; 245 } 246 char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1); 247 if (potentialEscape == ESCAPE_CHAR) { 248 return true; 249 } else { 250 return false; 251 } 252 } 253 isDoubleEscaped(String messagePattern, int delimeterStartIndex)254 final static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) { 255 if (delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) { 256 return true; 257 } else { 258 return false; 259 } 260 } 261 262 // special treatment of array values was suggested by 'lizongbo' deeplyAppendParameter(StringBuilder sbuf, Object o, Map<Object[], Object> seenMap)263 private static void deeplyAppendParameter(StringBuilder sbuf, Object o, Map<Object[], Object> seenMap) { 264 if (o == null) { 265 sbuf.append("null"); 266 return; 267 } 268 if (!o.getClass().isArray()) { 269 safeObjectAppend(sbuf, o); 270 } else { 271 // check for primitive array types because they 272 // unfortunately cannot be cast to Object[] 273 if (o instanceof boolean[]) { 274 booleanArrayAppend(sbuf, (boolean[]) o); 275 } else if (o instanceof byte[]) { 276 byteArrayAppend(sbuf, (byte[]) o); 277 } else if (o instanceof char[]) { 278 charArrayAppend(sbuf, (char[]) o); 279 } else if (o instanceof short[]) { 280 shortArrayAppend(sbuf, (short[]) o); 281 } else if (o instanceof int[]) { 282 intArrayAppend(sbuf, (int[]) o); 283 } else if (o instanceof long[]) { 284 longArrayAppend(sbuf, (long[]) o); 285 } else if (o instanceof float[]) { 286 floatArrayAppend(sbuf, (float[]) o); 287 } else if (o instanceof double[]) { 288 doubleArrayAppend(sbuf, (double[]) o); 289 } else { 290 objectArrayAppend(sbuf, (Object[]) o, seenMap); 291 } 292 } 293 } 294 safeObjectAppend(StringBuilder sbuf, Object o)295 private static void safeObjectAppend(StringBuilder sbuf, Object o) { 296 try { 297 String oAsString = o.toString(); 298 sbuf.append(oAsString); 299 } catch (Throwable t) { 300 System.err.println("SLF4J: Failed toString() invocation on an object of type [" + o.getClass().getName() + "]"); 301 t.printStackTrace(); 302 sbuf.append("[FAILED toString()]"); 303 } 304 305 } 306 objectArrayAppend(StringBuilder sbuf, Object[] a, Map<Object[], Object> seenMap)307 private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Map<Object[], Object> seenMap) { 308 sbuf.append('['); 309 if (!seenMap.containsKey(a)) { 310 seenMap.put(a, null); 311 final int len = a.length; 312 for (int i = 0; i < len; i++) { 313 deeplyAppendParameter(sbuf, a[i], seenMap); 314 if (i != len - 1) 315 sbuf.append(", "); 316 } 317 // allow repeats in siblings 318 seenMap.remove(a); 319 } else { 320 sbuf.append("..."); 321 } 322 sbuf.append(']'); 323 } 324 booleanArrayAppend(StringBuilder sbuf, boolean[] a)325 private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) { 326 sbuf.append('['); 327 final int len = a.length; 328 for (int i = 0; i < len; i++) { 329 sbuf.append(a[i]); 330 if (i != len - 1) 331 sbuf.append(", "); 332 } 333 sbuf.append(']'); 334 } 335 byteArrayAppend(StringBuilder sbuf, byte[] a)336 private static void byteArrayAppend(StringBuilder sbuf, byte[] a) { 337 sbuf.append('['); 338 final int len = a.length; 339 for (int i = 0; i < len; i++) { 340 sbuf.append(a[i]); 341 if (i != len - 1) 342 sbuf.append(", "); 343 } 344 sbuf.append(']'); 345 } 346 charArrayAppend(StringBuilder sbuf, char[] a)347 private static void charArrayAppend(StringBuilder sbuf, char[] a) { 348 sbuf.append('['); 349 final int len = a.length; 350 for (int i = 0; i < len; i++) { 351 sbuf.append(a[i]); 352 if (i != len - 1) 353 sbuf.append(", "); 354 } 355 sbuf.append(']'); 356 } 357 shortArrayAppend(StringBuilder sbuf, short[] a)358 private static void shortArrayAppend(StringBuilder sbuf, short[] a) { 359 sbuf.append('['); 360 final int len = a.length; 361 for (int i = 0; i < len; i++) { 362 sbuf.append(a[i]); 363 if (i != len - 1) 364 sbuf.append(", "); 365 } 366 sbuf.append(']'); 367 } 368 intArrayAppend(StringBuilder sbuf, int[] a)369 private static void intArrayAppend(StringBuilder sbuf, int[] a) { 370 sbuf.append('['); 371 final int len = a.length; 372 for (int i = 0; i < len; i++) { 373 sbuf.append(a[i]); 374 if (i != len - 1) 375 sbuf.append(", "); 376 } 377 sbuf.append(']'); 378 } 379 longArrayAppend(StringBuilder sbuf, long[] a)380 private static void longArrayAppend(StringBuilder sbuf, long[] a) { 381 sbuf.append('['); 382 final int len = a.length; 383 for (int i = 0; i < len; i++) { 384 sbuf.append(a[i]); 385 if (i != len - 1) 386 sbuf.append(", "); 387 } 388 sbuf.append(']'); 389 } 390 floatArrayAppend(StringBuilder sbuf, float[] a)391 private static void floatArrayAppend(StringBuilder sbuf, float[] a) { 392 sbuf.append('['); 393 final int len = a.length; 394 for (int i = 0; i < len; i++) { 395 sbuf.append(a[i]); 396 if (i != len - 1) 397 sbuf.append(", "); 398 } 399 sbuf.append(']'); 400 } 401 doubleArrayAppend(StringBuilder sbuf, double[] a)402 private static void doubleArrayAppend(StringBuilder sbuf, double[] a) { 403 sbuf.append('['); 404 final int len = a.length; 405 for (int i = 0; i < len; i++) { 406 sbuf.append(a[i]); 407 if (i != len - 1) 408 sbuf.append(", "); 409 } 410 sbuf.append(']'); 411 } 412 } 413