1   /* 
2    * Copyright (c) 2004-2007 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  /**
28   * Formats messages according to very simple substitution rules. Substitutions
29   * can be made 1, 2 or more arguments.
30   * <p>
31   * For example,
32   * <pre>MessageFormatter.format(&quot;Hi {}.&quot;, &quot;there&quot;);</pre>
33   * will return the string "Hi there.".
34   * <p>
35   * The {} pair is called the <em>formatting anchor</em>. It serves to
36   * designate the location where arguments need to be substituted within the
37   * message pattern.
38   * <p>
39   * In the rare case where you need to place the '{' or '}' in the message
40   * pattern itself but do not want them to be interpreted as a formatting
41   * anchors, you can espace the '{' character with '\', that is the backslash
42   * character. Only the '{' character should be escaped. There is no need to
43   * escape the '}' character. For example, 
44   * <pre>MessageFormatter.format(&quot;Set \\{1,2,3} is not equal to {}.&quot;, &quot;1,2&quot;);</pre>
45   * will return the string "Set {1,2,3} is not equal to 1,2.". 
46   * 
47   * <p>
48   * The escaping behaviour just described can be overridden by 
49   * escaping the escape character '\'. Calling
50   * <pre>MessageFormatter.format(&quot;File name is C:\\\\{}.&quot;, &quot;file.zip&quot;);</pre>
51   * will return the string "File name is C:\file.zip".
52   * 
53   * <p>
54   * See {@link #format(String, Object)}, {@link #format(String, Object, Object)}
55   * and {@link #arrayFormat(String, Object[])} methods for more details.
56   * 
57   * @author Ceki G&uuml;lc&uuml;
58   */
59  public class MessageFormatter {
60    static final char DELIM_START = '{';
61    static final char DELIM_STOP = '}';
62    private static final char ESCAPE_CHAR = '\\';
63    
64    /**
65     * Performs single argument substitution for the 'messagePattern' passed as
66     * parameter.
67     * <p>
68     * For example,
69     * 
70     * <pre>
71     * MessageFormatter.format(&quot;Hi {}.&quot;, &quot;there&quot;);
72     * </pre>
73     * 
74     * will return the string "Hi there.".
75     * <p>
76     * 
77     * @param messagePattern
78     *          The message pattern which will be parsed and formatted
79     * @param argument
80     *          The argument to be substituted in place of the formatting anchor
81     * @return The formatted message
82     */
83    public static String format(String messagePattern, Object arg) {
84      return arrayFormat(messagePattern, new Object[] { arg });
85    }
86  
87    /**
88     * 
89     * Performs a two argument substitution for the 'messagePattern' passed as
90     * parameter.
91     * <p>
92     * For example,
93     * 
94     * <pre>
95     * MessageFormatter.format(&quot;Hi {}. My name is {}.&quot;, &quot;Alice&quot;, &quot;Bob&quot;);
96     * </pre>
97     * 
98     * will return the string "Hi Alice. My name is Bob.".
99     * 
100    * @param messagePattern
101    *          The message pattern which will be parsed and formatted
102    * @param arg1
103    *          The argument to be substituted in place of the first formatting
104    *          anchor
105    * @param arg2
106    *          The argument to be substituted in place of the second formatting
107    *          anchor
108    * @return The formatted message
109    */
110   public static String format(String messagePattern, Object arg1, Object arg2) {
111     return arrayFormat(messagePattern, new Object[] { arg1, arg2 });
112   }
113 
114   /**
115    * Same principle as the {@link #format(String, Object)} and
116    * {@link #format(String, Object, Object)} methods except that any number of
117    * arguments can be passed in an array.
118    * 
119    * @param messagePattern
120    *          The message pattern which will be parsed and formatted
121    * @param argArray
122    *          An array of arguments to be substituted in place of formatting
123    *          anchors
124    * @return The formatted message
125    */
126   public static String arrayFormat(String messagePattern, Object[] argArray) {
127     if (messagePattern == null) {
128       return null;
129     }
130     int i = 0;
131     int len = messagePattern.length();
132     int j = messagePattern.indexOf(DELIM_START);
133 
134     if(argArray == null) {
135       return messagePattern;
136     }
137     
138     StringBuffer sbuf = new StringBuffer(messagePattern.length() + 50);
139 
140     for (int L = 0; L < argArray.length; L++) {
141 
142       j = messagePattern.indexOf(DELIM_START, i);
143 
144       if (j == -1 || (j + 1 == len)) {
145         // no more variables
146         if (i == 0) { // this is a simple string
147           return messagePattern;
148         } else { // add the tail string which contains no variables and return
149           // the result.
150           sbuf.append(messagePattern.substring(i, messagePattern.length()));
151           return sbuf.toString();
152         }
153       } else {
154         char delimStop = messagePattern.charAt(j + 1);
155 
156         if (isEscapedDelimeter(messagePattern, j)) {
157           if(!isDoubleEscaped(messagePattern, j)) {
158             L--; // DELIM_START was escaped, thus should not be incremented
159             sbuf.append(messagePattern.substring(i, j - 1));
160             sbuf.append(DELIM_START);
161             i = j + 1;
162           } else {
163             // The escape character preceding the delemiter start is
164             // itself escaped: "abc x:\\{}"
165             // we have to consume one backward slash
166             sbuf.append(messagePattern.substring(i, j-1));
167             sbuf.append(argArray[L]);
168             i = j + 2;
169           }
170         } else if ((delimStop != DELIM_STOP)) {
171           // invalid DELIM_START/DELIM_STOP pair
172           sbuf.append(messagePattern.substring(i, messagePattern.length()));
173           return sbuf.toString();
174         } else {
175           // normal case
176           sbuf.append(messagePattern.substring(i, j));
177           sbuf.append(argArray[L]);
178           i = j + 2;
179         }
180       }
181     }
182     // append the characters following the last {} pair.
183     sbuf.append(messagePattern.substring(i, messagePattern.length()));
184     return sbuf.toString();
185   }
186 
187   static boolean isEscapedDelimeter(String messagePattern,
188       int delimeterStartIndex) {
189 
190     if (delimeterStartIndex == 0) {
191       return false;
192     }
193     char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1);
194     if (potentialEscape == ESCAPE_CHAR) {
195       return true;
196     } else {
197       return false;
198     }
199   }
200 
201   static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) {
202     if (delimeterStartIndex >= 2
203         && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) {
204       return true;
205     } else {
206       return false;
207     }
208   }
209 }