/
MessageFormatter.java
344 lines (318 loc) · 9.76 KB
/
MessageFormatter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
/*
* Lilith - a log event viewer.
* Copyright (C) 2007-2017 Joern Huxhorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2007-2017 Joern Huxhorn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.huxhorn.lilith.data.logging;
import de.huxhorn.sulky.formatting.SafeString;
import java.util.Arrays;
/**
* <p>Replacement for org.slf4j.helpers.MessageFormatter.</p>
* <p>
* In contrast to the mentioned class, the formatting of message pattern and arguments into the actual message
* is split into three parts:
* </p>
* <ol>
* <li>Counting of placeholders in the message pattern (cheap)</li>
* <li>Conversion of argument array into an ArgumentResult, containing the arguments converted to String as well as
* an optional Throwable if available (relatively cheap)</li>
* <li>Replacement of placeholders in a message pattern with arguments given as String[]. (most expensive)</li>
* </ol>
* <p>
* That way only the first two steps have to be done during event creation while the most expensive part, i.e. the
* actual construction of the message, is only done on demand.
* </p>
*/
public final class MessageFormatter
{
private static final char DELIMITER_START = '{';
private static final char DELIMITER_STOP = '}';
private static final char ESCAPE_CHAR = '\\';
static
{
new MessageFormatter(); // stfu
}
private MessageFormatter() {}
/**
* Replace placeholders in the given messagePattern with arguments.
*
* @param messagePattern the message pattern containing placeholders.
* @param arguments the arguments to be used to replace placeholders.
* @return the formatted message.
*/
@SuppressWarnings("PMD.AvoidReassigningLoopVariables")
public static String format(String messagePattern, String[] arguments)
{
if(messagePattern == null || arguments == null || arguments.length == 0)
{
return messagePattern;
}
StringBuilder result = new StringBuilder();
int escapeCounter = 0;
int currentArgument = 0;
for(int i = 0; i < messagePattern.length(); i++)
{
char curChar = messagePattern.charAt(i);
if(curChar == ESCAPE_CHAR)
{
escapeCounter++;
}
else
{
if(curChar == DELIMITER_START
&& (i < messagePattern.length() - 1)
&& (messagePattern.charAt(i + 1) == DELIMITER_STOP))
{
// write escaped escape chars
int escapedEscapes = escapeCounter / 2;
for(int j = 0; j < escapedEscapes; j++)
{
result.append(ESCAPE_CHAR);
}
if(escapeCounter % 2 == 1)
{
// i.e. escaped
// write escaped escape chars
result.append(DELIMITER_START);
result.append(DELIMITER_STOP);
}
else
{
// unescaped
if(currentArgument < arguments.length)
{
result.append(arguments[currentArgument]);
}
else
{
result.append(DELIMITER_START).append(DELIMITER_STOP);
}
currentArgument++;
}
// this is an optimization: charAt(i+1) has already been checked.
// @cs-: ModifiedControlVariable
i++;
escapeCounter = 0;
continue;
}
// any other char beside ESCAPE or DELIMITER_START/STOP-combo
// write unescaped escape chars
if(escapeCounter > 0)
{
for(int j = 0; j < escapeCounter; j++)
{
result.append(ESCAPE_CHAR);
}
escapeCounter = 0;
}
result.append(curChar);
}
}
return result.toString();
}
/**
* Counts the number of unescaped placeholders in the given messagePattern.
*
* @param messagePattern the message pattern to be analyzed.
* @return the number of unescaped placeholders.
*/
@SuppressWarnings("PMD.AvoidReassigningLoopVariables")
public static int countArgumentPlaceholders(String messagePattern)
{
if(messagePattern == null)
{
return 0;
}
if(-1 == messagePattern.indexOf(DELIMITER_START))
{
// Special case: no placeholders at all.
// This is an optimization because charAt checks bounds for every
// single call while indexOf(char) isn't.
// Big messages without placeholders will benefit from this shortcut.
// the result of indexOf can't be used as start index in the loop
// below because it could still be escaped.
return 0;
}
int result = 0;
boolean isEscaped = false;
for(int i = 0; i < messagePattern.length(); i++)
{
char curChar = messagePattern.charAt(i);
if(curChar == ESCAPE_CHAR)
{
isEscaped = !isEscaped;
}
else if(curChar == DELIMITER_START)
{
if(!isEscaped
&& (i < messagePattern.length() - 1)
&& (messagePattern.charAt(i + 1) == DELIMITER_STOP))
{
result++;
// this is an optimization: charAt(i+1) has already been checked.
// @cs-: ModifiedControlVariable
i++;
}
isEscaped = false;
}
else
{
isEscaped = false;
}
}
return result;
}
/**
* <p>This method returns a MessageFormatter.ArgumentResult which contains the arguments converted to String
* as well as an optional Throwable.</p>
*
* <p>If the last argument is a Throwable and is NOT used up by a placeholder in the message pattern it is returned
* in MessageFormatter.ArgumentResult.getThrowable() and won't be contained in the created String[].</p>
* <p>If it is used up getThrowable will return null even if the last argument was a Throwable!</p>
*
* @param messagePattern the message pattern that to be checked for placeholders.
* @param arguments the argument array to be converted.
* @return a MessageFormatter.ArgumentResult containing the converted formatted message and optionally a Throwable.
*/
public static ArgumentResult evaluateArguments(String messagePattern, Object[] arguments)
{
if(arguments == null)
{
return null;
}
int argsCount = countArgumentPlaceholders(messagePattern);
int resultArgCount = arguments.length;
Throwable throwable = null;
if(argsCount < arguments.length
&& arguments[arguments.length - 1] instanceof Throwable)
{
throwable = (Throwable) arguments[arguments.length - 1];
resultArgCount--;
}
String[] stringArgs;
if(argsCount == 1 && throwable == null && arguments.length > 1)
{
// special case
stringArgs = new String[1];
stringArgs[0] = SafeString.toString(arguments,
SafeString.StringWrapping.CONTAINED, SafeString.StringStyle.GROOVY, SafeString.MapStyle.GROOVY);
}
else
{
stringArgs = new String[resultArgCount];
for(int i = 0; i < stringArgs.length; i++)
{
stringArgs[i] = SafeString.toString(arguments[i],
SafeString.StringWrapping.CONTAINED, SafeString.StringStyle.GROOVY, SafeString.MapStyle.GROOVY);
}
}
return new ArgumentResult(stringArgs, throwable);
}
/**
* <p>This is just a simple class containing the result of an evaluateArgument call. It's necessary because we need to
* return two results, i.e. the resulting String[] and the optional Throwable.</p>
*
* <p>This class is not Serializable because serializing a Throwable is generally a bad idea if the data is supposed
* to leave the current VM since it may result in ClassNotFoundExceptions if the given Throwable is not
* available/different in the deserializing VM.</p>
*/
@SuppressWarnings({"PMD.MethodReturnsInternalArray", "PMD.ArrayIsStoredDirectly"})
public static class ArgumentResult
{
private final String[] arguments;
private final Throwable throwable;
public ArgumentResult(String[] arguments, Throwable throwable)
{
this.arguments = arguments;
this.throwable = throwable;
}
public String[] getArguments()
{
return arguments;
}
public Throwable getThrowable()
{
return throwable;
}
@Override
public String toString()
{
final StringBuilder result = new StringBuilder(500);
result.append("ArgumentResult{arguments=");
if(arguments == null)
{
result.append("null");
}
else
{
result.append('[');
boolean isFirst = true;
for (String current : arguments)
{
if (!isFirst)
{
result.append(", ");
}
else
{
isFirst = false;
}
if (current != null)
{
result.append('"').append(current).append('"');
}
else
{
result.append("null");
}
}
result.append(']');
}
result.append(", throwable=").append(throwable).append('}');
return result.toString();
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ArgumentResult that = (ArgumentResult) o;
return Arrays.equals(arguments, that.arguments)
&& (throwable != null ? throwable.equals(that.throwable) : that.throwable == null);
}
@Override
public int hashCode()
{
int result = Arrays.hashCode(arguments);
result = 31 * result + (throwable != null ? throwable.hashCode() : 0);
return result;
}
}
}