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