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