1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.lang3.text;
18
19 import java.text.Format;
20 import java.text.MessageFormat;
21 import java.text.ParsePosition;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Iterator;
25 import java.util.Locale;
26 import java.util.Map;
27
28 import org.apache.commons.lang3.ObjectUtils;
29 import org.apache.commons.lang3.Validate;
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 public class ExtendedMessageFormat extends MessageFormat {
71 private static final long serialVersionUID = -2362048321261811743L;
72 private static final int HASH_SEED = 31;
73
74 private static final String DUMMY_PATTERN = "";
75 private static final String ESCAPED_QUOTE = "''";
76 private static final char START_FMT = ',';
77 private static final char END_FE = '}';
78 private static final char START_FE = '{';
79 private static final char QUOTE = '\'';
80
81 private String toPattern;
82 private final Map<String, ? extends FormatFactory> registry;
83
84
85
86
87
88
89
90 public ExtendedMessageFormat(final String pattern) {
91 this(pattern, Locale.getDefault());
92 }
93
94
95
96
97
98
99
100
101 public ExtendedMessageFormat(final String pattern, final Locale locale) {
102 this(pattern, locale, null);
103 }
104
105
106
107
108
109
110
111
112 public ExtendedMessageFormat(final String pattern, final Map<String, ? extends FormatFactory> registry) {
113 this(pattern, Locale.getDefault(), registry);
114 }
115
116
117
118
119
120
121
122
123
124 public ExtendedMessageFormat(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) {
125 super(DUMMY_PATTERN);
126 setLocale(locale);
127 this.registry = registry;
128 applyPattern(pattern);
129 }
130
131
132
133
134 @Override
135 public String toPattern() {
136 return toPattern;
137 }
138
139
140
141
142
143
144 @Override
145 public final void applyPattern(final String pattern) {
146 if (registry == null) {
147 super.applyPattern(pattern);
148 toPattern = super.toPattern();
149 return;
150 }
151 final ArrayList<Format> foundFormats = new ArrayList<Format>();
152 final ArrayList<String> foundDescriptions = new ArrayList<String>();
153 final StringBuilder stripCustom = new StringBuilder(pattern.length());
154
155 final ParsePosition pos = new ParsePosition(0);
156 final char[] c = pattern.toCharArray();
157 int fmtCount = 0;
158 while (pos.getIndex() < pattern.length()) {
159 switch (c[pos.getIndex()]) {
160 case QUOTE:
161 appendQuotedString(pattern, pos, stripCustom, true);
162 break;
163 case START_FE:
164 fmtCount++;
165 seekNonWs(pattern, pos);
166 final int start = pos.getIndex();
167 final int index = readArgumentIndex(pattern, next(pos));
168 stripCustom.append(START_FE).append(index);
169 seekNonWs(pattern, pos);
170 Format format = null;
171 String formatDescription = null;
172 if (c[pos.getIndex()] == START_FMT) {
173 formatDescription = parseFormatDescription(pattern,
174 next(pos));
175 format = getFormat(formatDescription);
176 if (format == null) {
177 stripCustom.append(START_FMT).append(formatDescription);
178 }
179 }
180 foundFormats.add(format);
181 foundDescriptions.add(format == null ? null : formatDescription);
182 Validate.isTrue(foundFormats.size() == fmtCount);
183 Validate.isTrue(foundDescriptions.size() == fmtCount);
184 if (c[pos.getIndex()] != END_FE) {
185 throw new IllegalArgumentException(
186 "Unreadable format element at position " + start);
187 }
188
189 default:
190 stripCustom.append(c[pos.getIndex()]);
191 next(pos);
192 }
193 }
194 super.applyPattern(stripCustom.toString());
195 toPattern = insertFormats(super.toPattern(), foundDescriptions);
196 if (containsElements(foundFormats)) {
197 final Format[] origFormats = getFormats();
198
199
200 int i = 0;
201 for (final Iterator<Format> it = foundFormats.iterator(); it.hasNext(); i++) {
202 final Format f = it.next();
203 if (f != null) {
204 origFormats[i] = f;
205 }
206 }
207 super.setFormats(origFormats);
208 }
209 }
210
211
212
213
214
215
216
217
218 @Override
219 public void setFormat(final int formatElementIndex, final Format newFormat) {
220 throw new UnsupportedOperationException();
221 }
222
223
224
225
226
227
228
229
230 @Override
231 public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) {
232 throw new UnsupportedOperationException();
233 }
234
235
236
237
238
239
240
241 @Override
242 public void setFormats(final Format[] newFormats) {
243 throw new UnsupportedOperationException();
244 }
245
246
247
248
249
250
251
252 @Override
253 public void setFormatsByArgumentIndex(final Format[] newFormats) {
254 throw new UnsupportedOperationException();
255 }
256
257
258
259
260
261
262
263 @Override
264 public boolean equals(final Object obj) {
265 if (obj == this) {
266 return true;
267 }
268 if (obj == null) {
269 return false;
270 }
271 if (!super.equals(obj)) {
272 return false;
273 }
274 if (ObjectUtils.notEqual(getClass(), obj.getClass())) {
275 return false;
276 }
277 final ExtendedMessageFormat rhs = (ExtendedMessageFormat)obj;
278 if (ObjectUtils.notEqual(toPattern, rhs.toPattern)) {
279 return false;
280 }
281 if (ObjectUtils.notEqual(registry, rhs.registry)) {
282 return false;
283 }
284 return true;
285 }
286
287
288
289
290
291
292 @Override
293 public int hashCode() {
294 int result = super.hashCode();
295 result = HASH_SEED * result + ObjectUtils.hashCode(registry);
296 result = HASH_SEED * result + ObjectUtils.hashCode(toPattern);
297 return result;
298 }
299
300
301
302
303
304
305
306 private Format getFormat(final String desc) {
307 if (registry != null) {
308 String name = desc;
309 String args = null;
310 final int i = desc.indexOf(START_FMT);
311 if (i > 0) {
312 name = desc.substring(0, i).trim();
313 args = desc.substring(i + 1).trim();
314 }
315 final FormatFactory factory = registry.get(name);
316 if (factory != null) {
317 return factory.getFormat(name, args, getLocale());
318 }
319 }
320 return null;
321 }
322
323
324
325
326
327
328
329
330 private int readArgumentIndex(final String pattern, final ParsePosition pos) {
331 final int start = pos.getIndex();
332 seekNonWs(pattern, pos);
333 final StringBuilder result = new StringBuilder();
334 boolean error = false;
335 for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
336 char c = pattern.charAt(pos.getIndex());
337 if (Character.isWhitespace(c)) {
338 seekNonWs(pattern, pos);
339 c = pattern.charAt(pos.getIndex());
340 if (c != START_FMT && c != END_FE) {
341 error = true;
342 continue;
343 }
344 }
345 if ((c == START_FMT || c == END_FE) && result.length() > 0) {
346 try {
347 return Integer.parseInt(result.toString());
348 } catch (final NumberFormatException e) {
349
350
351 }
352 }
353 error = !Character.isDigit(c);
354 result.append(c);
355 }
356 if (error) {
357 throw new IllegalArgumentException(
358 "Invalid format argument index at position " + start + ": "
359 + pattern.substring(start, pos.getIndex()));
360 }
361 throw new IllegalArgumentException(
362 "Unterminated format element at position " + start);
363 }
364
365
366
367
368
369
370
371
372 private String parseFormatDescription(final String pattern, final ParsePosition pos) {
373 final int start = pos.getIndex();
374 seekNonWs(pattern, pos);
375 final int text = pos.getIndex();
376 int depth = 1;
377 for (; pos.getIndex() < pattern.length(); next(pos)) {
378 switch (pattern.charAt(pos.getIndex())) {
379 case START_FE:
380 depth++;
381 break;
382 case END_FE:
383 depth--;
384 if (depth == 0) {
385 return pattern.substring(text, pos.getIndex());
386 }
387 break;
388 case QUOTE:
389 getQuotedString(pattern, pos, false);
390 break;
391 }
392 }
393 throw new IllegalArgumentException(
394 "Unterminated format element at position " + start);
395 }
396
397
398
399
400
401
402
403
404 private String insertFormats(final String pattern, final ArrayList<String> customPatterns) {
405 if (!containsElements(customPatterns)) {
406 return pattern;
407 }
408 final StringBuilder sb = new StringBuilder(pattern.length() * 2);
409 final ParsePosition pos = new ParsePosition(0);
410 int fe = -1;
411 int depth = 0;
412 while (pos.getIndex() < pattern.length()) {
413 final char c = pattern.charAt(pos.getIndex());
414 switch (c) {
415 case QUOTE:
416 appendQuotedString(pattern, pos, sb, false);
417 break;
418 case START_FE:
419 depth++;
420 if (depth == 1) {
421 fe++;
422 sb.append(START_FE).append(
423 readArgumentIndex(pattern, next(pos)));
424 final String customPattern = customPatterns.get(fe);
425 if (customPattern != null) {
426 sb.append(START_FMT).append(customPattern);
427 }
428 }
429 break;
430 case END_FE:
431 depth--;
432
433 default:
434 sb.append(c);
435 next(pos);
436 }
437 }
438 return sb.toString();
439 }
440
441
442
443
444
445
446
447 private void seekNonWs(final String pattern, final ParsePosition pos) {
448 int len = 0;
449 final char[] buffer = pattern.toCharArray();
450 do {
451 len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
452 pos.setIndex(pos.getIndex() + len);
453 } while (len > 0 && pos.getIndex() < pattern.length());
454 }
455
456
457
458
459
460
461
462 private ParsePosition next(final ParsePosition pos) {
463 pos.setIndex(pos.getIndex() + 1);
464 return pos;
465 }
466
467
468
469
470
471
472
473
474
475
476
477 private StringBuilder appendQuotedString(final String pattern, final ParsePosition pos,
478 final StringBuilder appendTo, final boolean escapingOn) {
479 final int start = pos.getIndex();
480 final char[] c = pattern.toCharArray();
481 if (escapingOn && c[start] == QUOTE) {
482 next(pos);
483 return appendTo == null ? null : appendTo.append(QUOTE);
484 }
485 int lastHold = start;
486 for (int i = pos.getIndex(); i < pattern.length(); i++) {
487 if (escapingOn && pattern.substring(i).startsWith(ESCAPED_QUOTE)) {
488 appendTo.append(c, lastHold, pos.getIndex() - lastHold).append(
489 QUOTE);
490 pos.setIndex(i + ESCAPED_QUOTE.length());
491 lastHold = pos.getIndex();
492 continue;
493 }
494 switch (c[pos.getIndex()]) {
495 case QUOTE:
496 next(pos);
497 return appendTo == null ? null : appendTo.append(c, lastHold,
498 pos.getIndex() - lastHold);
499 default:
500 next(pos);
501 }
502 }
503 throw new IllegalArgumentException(
504 "Unterminated quoted string at position " + start);
505 }
506
507
508
509
510
511
512
513
514 private void getQuotedString(final String pattern, final ParsePosition pos,
515 final boolean escapingOn) {
516 appendQuotedString(pattern, pos, null, escapingOn);
517 }
518
519
520
521
522
523
524 private boolean containsElements(final Collection<?> coll) {
525 if (coll == null || coll.isEmpty()) {
526 return false;
527 }
528 for (final Object name : coll) {
529 if (name != null) {
530 return true;
531 }
532 }
533 return false;
534 }
535 }