001/** 002 * Copyright (c) 2004-2011 QOS.ch 003 * All rights reserved. 004 * 005 * Permission is hereby granted, free of charge, to any person obtaining 006 * a copy of this software and associated documentation files (the 007 * "Software"), to deal in the Software without restriction, including 008 * without limitation the rights to use, copy, modify, merge, publish, 009 * distribute, sublicense, and/or sell copies of the Software, and to 010 * permit persons to whom the Software is furnished to do so, subject to 011 * the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be 014 * included in all copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 017 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 018 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 019 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 020 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 021 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 022 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 023 * 024 */ 025package org.slf4j.helpers; 026 027import java.text.MessageFormat; 028import java.util.HashMap; 029import java.util.Map; 030 031// contributors: lizongbo: proposed special treatment of array parameter values 032// Joern Huxhorn: pointed out double[] omission, suggested deep array copy 033/** 034 * Formats messages according to very simple substitution rules. Substitutions 035 * can be made 1, 2 or more arguments. 036 * 037 * <p> 038 * For example, 039 * 040 * <pre> 041 * MessageFormatter.format("Hi {}.", "there") 042 * </pre> 043 * 044 * will return the string "Hi there.". 045 * <p> 046 * The {} pair is called the <em>formatting anchor</em>. It serves to designate 047 * the location where arguments need to be substituted within the message 048 * pattern. 049 * <p> 050 * In case your message contains the '{' or the '}' character, you do not have 051 * to do anything special unless the '}' character immediately follows '{'. For 052 * example, 053 * 054 * <pre> 055 * MessageFormatter.format("Set {1,2,3} is not equal to {}.", "1,2"); 056 * </pre> 057 * 058 * will return the string "Set {1,2,3} is not equal to 1,2.". 059 * 060 * <p> 061 * If for whatever reason you need to place the string "{}" in the message 062 * without its <em>formatting anchor</em> meaning, then you need to escape the 063 * '{' character with '\', that is the backslash character. Only the '{' 064 * character should be escaped. There is no need to escape the '}' character. 065 * For example, 066 * 067 * <pre> 068 * MessageFormatter.format("Set \\{} is not equal to {}.", "1,2"); 069 * </pre> 070 * 071 * will return the string "Set {} is not equal to 1,2.". 072 * 073 * <p> 074 * The escaping behavior just described can be overridden by escaping the escape 075 * character '\'. Calling 076 * 077 * <pre> 078 * MessageFormatter.format("File name is C:\\\\{}.", "file.zip"); 079 * </pre> 080 * 081 * will return the string "File name is C:\file.zip". 082 * 083 * <p> 084 * The formatting conventions are different from those of {@link MessageFormat} 085 * which ships with the Java platform. This is justified by the fact that 086 * SLF4J's implementation is 10 times faster than that of {@link MessageFormat}. 087 * This local performance difference is both measurable and significant in the 088 * larger context of the complete logging processing chain. 089 * 090 * <p> 091 * See also {@link #format(String, Object)}, 092 * {@link #format(String, Object, Object)} and 093 * {@link #arrayFormat(String, Object[])} methods for more details. 094 * 095 * @author Ceki Gülcü 096 * @author Joern Huxhorn 097 */ 098final public class MessageFormatter { 099 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 arg 120 * The argument to be substituted in place of the formatting anchor 121 * @return The formatted message 122 */ 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 */ 150 final public static FormattingTuple format(final String messagePattern, Object arg1, Object arg2) { 151 return arrayFormat(messagePattern, new Object[] { arg1, arg2 }); 152 } 153 154 final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) { 155 Throwable throwableCandidate = MessageFormatter.getThrowableCandidate(argArray); 156 Object[] args = argArray; 157 if (throwableCandidate != null) { 158 args = MessageFormatter.trimmedCopy(argArray); 159 } 160 return arrayFormat(messagePattern, args, throwableCandidate); 161 } 162 163 /** 164 * Assumes that argArray only contains arguments with no throwable as last element. 165 * 166 * @param messagePattern 167 * @param argArray 168 */ 169 final public static String basicArrayFormat(final String messagePattern, final Object[] argArray) { 170 FormattingTuple ft = arrayFormat(messagePattern, argArray, null); 171 return ft.getMessage(); 172 } 173 174 public static String basicArrayFormat(NormalizedParameters np) { 175 return basicArrayFormat(np.getMessage(), np.getArguments()); 176 } 177 178 final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray, Throwable throwable) { 179 180 if (messagePattern == null) { 181 return new FormattingTuple(null, argArray, throwable); 182 } 183 184 if (argArray == null) { 185 return new FormattingTuple(messagePattern); 186 } 187 188 int i = 0; 189 int j; 190 // use string builder for better multicore performance 191 StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); 192 193 int L; 194 for (L = 0; L < argArray.length; L++) { 195 196 j = messagePattern.indexOf(DELIM_STR, i); 197 198 if (j == -1) { 199 // no more variables 200 if (i == 0) { // this is a simple string 201 return new FormattingTuple(messagePattern, argArray, throwable); 202 } else { // add the tail string which contains no variables and return 203 // the result. 204 sbuf.append(messagePattern, i, messagePattern.length()); 205 return new FormattingTuple(sbuf.toString(), argArray, throwable); 206 } 207 } else { 208 if (isEscapedDelimeter(messagePattern, j)) { 209 if (!isDoubleEscaped(messagePattern, j)) { 210 L--; // DELIM_START was escaped, thus should not be incremented 211 sbuf.append(messagePattern, i, j - 1); 212 sbuf.append(DELIM_START); 213 i = j + 1; 214 } else { 215 // The escape character preceding the delimiter start is 216 // itself escaped: "abc x:\\{}" 217 // we have to consume one backward slash 218 sbuf.append(messagePattern, i, j - 1); 219 deeplyAppendParameter(sbuf, argArray[L], new HashMap<>()); 220 i = j + 2; 221 } 222 } else { 223 // normal case 224 sbuf.append(messagePattern, i, j); 225 deeplyAppendParameter(sbuf, argArray[L], new HashMap<>()); 226 i = j + 2; 227 } 228 } 229 } 230 // append the characters following the last {} pair. 231 sbuf.append(messagePattern, i, messagePattern.length()); 232 return new FormattingTuple(sbuf.toString(), argArray, throwable); 233 } 234 235 final static boolean isEscapedDelimeter(String messagePattern, int delimeterStartIndex) { 236 237 if (delimeterStartIndex == 0) { 238 return false; 239 } 240 char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1); 241 if (potentialEscape == ESCAPE_CHAR) { 242 return true; 243 } else { 244 return false; 245 } 246 } 247 248 final static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) { 249 if (delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) { 250 return true; 251 } else { 252 return false; 253 } 254 } 255 256 // special treatment of array values was suggested by 'lizongbo' 257 private static void deeplyAppendParameter(StringBuilder sbuf, Object o, Map<Object[], Object> seenMap) { 258 if (o == null) { 259 sbuf.append("null"); 260 return; 261 } 262 if (!o.getClass().isArray()) { 263 safeObjectAppend(sbuf, o); 264 } else { 265 // check for primitive array types because they 266 // unfortunately cannot be cast to Object[] 267 if (o instanceof boolean[]) { 268 booleanArrayAppend(sbuf, (boolean[]) o); 269 } else if (o instanceof byte[]) { 270 byteArrayAppend(sbuf, (byte[]) o); 271 } else if (o instanceof char[]) { 272 charArrayAppend(sbuf, (char[]) o); 273 } else if (o instanceof short[]) { 274 shortArrayAppend(sbuf, (short[]) o); 275 } else if (o instanceof int[]) { 276 intArrayAppend(sbuf, (int[]) o); 277 } else if (o instanceof long[]) { 278 longArrayAppend(sbuf, (long[]) o); 279 } else if (o instanceof float[]) { 280 floatArrayAppend(sbuf, (float[]) o); 281 } else if (o instanceof double[]) { 282 doubleArrayAppend(sbuf, (double[]) o); 283 } else { 284 objectArrayAppend(sbuf, (Object[]) o, seenMap); 285 } 286 } 287 } 288 289 private static void safeObjectAppend(StringBuilder sbuf, Object o) { 290 try { 291 String oAsString = o.toString(); 292 sbuf.append(oAsString); 293 } catch (Throwable t) { 294 Reporter.error("Failed toString() invocation on an object of type [" + o.getClass().getName() + "]", t); 295 sbuf.append("[FAILED toString()]"); 296 } 297 298 } 299 300 private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Map<Object[], Object> seenMap) { 301 sbuf.append('['); 302 if (!seenMap.containsKey(a)) { 303 seenMap.put(a, null); 304 final int len = a.length; 305 for (int i = 0; i < len; i++) { 306 deeplyAppendParameter(sbuf, a[i], seenMap); 307 if (i != len - 1) 308 sbuf.append(", "); 309 } 310 // allow repeats in siblings 311 seenMap.remove(a); 312 } else { 313 sbuf.append("..."); 314 } 315 sbuf.append(']'); 316 } 317 318 private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) { 319 sbuf.append('['); 320 final int len = a.length; 321 for (int i = 0; i < len; i++) { 322 sbuf.append(a[i]); 323 if (i != len - 1) 324 sbuf.append(", "); 325 } 326 sbuf.append(']'); 327 } 328 329 private static void byteArrayAppend(StringBuilder sbuf, byte[] a) { 330 sbuf.append('['); 331 final int len = a.length; 332 for (int i = 0; i < len; i++) { 333 sbuf.append(a[i]); 334 if (i != len - 1) 335 sbuf.append(", "); 336 } 337 sbuf.append(']'); 338 } 339 340 private static void charArrayAppend(StringBuilder sbuf, char[] a) { 341 sbuf.append('['); 342 final int len = a.length; 343 for (int i = 0; i < len; i++) { 344 sbuf.append(a[i]); 345 if (i != len - 1) 346 sbuf.append(", "); 347 } 348 sbuf.append(']'); 349 } 350 351 private static void shortArrayAppend(StringBuilder sbuf, short[] a) { 352 sbuf.append('['); 353 final int len = a.length; 354 for (int i = 0; i < len; i++) { 355 sbuf.append(a[i]); 356 if (i != len - 1) 357 sbuf.append(", "); 358 } 359 sbuf.append(']'); 360 } 361 362 private static void intArrayAppend(StringBuilder sbuf, int[] a) { 363 sbuf.append('['); 364 final int len = a.length; 365 for (int i = 0; i < len; i++) { 366 sbuf.append(a[i]); 367 if (i != len - 1) 368 sbuf.append(", "); 369 } 370 sbuf.append(']'); 371 } 372 373 private static void longArrayAppend(StringBuilder sbuf, long[] a) { 374 sbuf.append('['); 375 final int len = a.length; 376 for (int i = 0; i < len; i++) { 377 sbuf.append(a[i]); 378 if (i != len - 1) 379 sbuf.append(", "); 380 } 381 sbuf.append(']'); 382 } 383 384 private static void floatArrayAppend(StringBuilder sbuf, float[] a) { 385 sbuf.append('['); 386 final int len = a.length; 387 for (int i = 0; i < len; i++) { 388 sbuf.append(a[i]); 389 if (i != len - 1) 390 sbuf.append(", "); 391 } 392 sbuf.append(']'); 393 } 394 395 private static void doubleArrayAppend(StringBuilder sbuf, double[] a) { 396 sbuf.append('['); 397 final int len = a.length; 398 for (int i = 0; i < len; i++) { 399 sbuf.append(a[i]); 400 if (i != len - 1) 401 sbuf.append(", "); 402 } 403 sbuf.append(']'); 404 } 405 406 /** 407 * Helper method to determine if an {@link Object} array contains a {@link Throwable} as last element 408 * 409 * @param argArray 410 * The arguments off which we want to know if it contains a {@link Throwable} as last element 411 * @return if the last {@link Object} in argArray is a {@link Throwable} this method will return it, 412 * otherwise it returns null 413 */ 414 public static Throwable getThrowableCandidate(final Object[] argArray) { 415 return NormalizedParameters.getThrowableCandidate(argArray); 416 } 417 418 /** 419 * Helper method to get all but the last element of an array 420 * 421 * @param argArray 422 * The arguments from which we want to remove the last element 423 * 424 * @return a copy of the array without the last element 425 */ 426 public static Object[] trimmedCopy(final Object[] argArray) { 427 return NormalizedParameters.trimmedCopy(argArray); 428 } 429 430}