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