1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
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.StringUtils;
30 import org.apache.commons.lang3.Validate;
31
32 /**
33 * Extends {@link java.text.MessageFormat} to allow pluggable/additional formatting
34 * options for embedded format elements.
35 * <p>
36 * Client code should specify a registry
37 * of {@link FormatFactory} instances associated with {@link String}
38 * format names. This registry will be consulted when the format elements are
39 * parsed from the message pattern. In this way custom patterns can be specified,
40 * and the formats supported by {@link java.text.MessageFormat} can be overridden
41 * at the format and/or format style level (see MessageFormat). A "format element"
42 * embedded in the message pattern is specified (<strong>()?</strong> signifies optionality):
43 * </p>
44 * <p>
45 * <code>{</code><em>argument-number</em><strong>(</strong>{@code ,}<em>format-name</em><b>
46 * (</b>{@code ,}<em>format-style</em><strong>)?)?</strong><code>}</code>
47 * </p>
48 *
49 * <p>
50 * <em>format-name</em> and <em>format-style</em> values are trimmed of surrounding whitespace
51 * in the manner of {@link java.text.MessageFormat}. If <em>format-name</em> denotes
52 * {@code FormatFactory formatFactoryInstance} in {@code registry}, a {@link Format}
53 * matching <em>format-name</em> and <em>format-style</em> is requested from
54 * {@code formatFactoryInstance}. If this is successful, the {@link Format}
55 * found is used for this format element.
56 * </p>
57 *
58 * <p><strong>NOTICE:</strong> The various subformat mutator methods are considered unnecessary; they exist on the parent
59 * class to allow the type of customization which it is the job of this class to provide in
60 * a configurable fashion. These methods have thus been disabled and will throw
61 * {@link UnsupportedOperationException} if called.
62 * </p>
63 *
64 * <p>Limitations inherited from {@link java.text.MessageFormat}:</p>
65 * <ul>
66 * <li>When using "choice" subformats, support for nested formatting instructions is limited
67 * to that provided by the base class.</li>
68 * <li>Thread-safety of {@link Format}s, including {@link MessageFormat} and thus
69 * {@link ExtendedMessageFormat}, is not guaranteed.</li>
70 * </ul>
71 *
72 * @since 2.4
73 * @deprecated As of <a href="https://commons.apache.org/proper/commons-lang/changes-report.html#a3.6">3.6</a>, use Apache Commons Text
74 * <a href="https://commons.apache.org/proper/commons-text/javadocs/api-release/org/apache/commons/text/ExtendedMessageFormat.html">
75 * ExtendedMessageFormat</a>.
76 */
77 @Deprecated
78 public class ExtendedMessageFormat extends MessageFormat {
79
80 private static final long serialVersionUID = -2362048321261811743L;
81 private static final String EMPTY_PATTERN = StringUtils.EMPTY;
82 private static final char START_FMT = ',';
83 private static final char END_FE = '}';
84 private static final char START_FE = '{';
85 private static final char QUOTE = '\'';
86
87 /**
88 * To pattern string.
89 */
90 private String toPattern;
91
92 /**
93 * Our registry of FormatFactory.
94 */
95 private final Map<String, ? extends FormatFactory> registry;
96
97 /**
98 * Create a new ExtendedMessageFormat for the default locale.
99 *
100 * @param pattern the pattern to use, not null
101 * @throws IllegalArgumentException in case of a bad pattern.
102 */
103 public ExtendedMessageFormat(final String pattern) {
104 this(pattern, Locale.getDefault());
105 }
106
107 /**
108 * Create a new ExtendedMessageFormat.
109 *
110 * @param pattern the pattern to use, not null
111 * @param locale the locale to use, not null
112 * @throws IllegalArgumentException in case of a bad pattern.
113 */
114 public ExtendedMessageFormat(final String pattern, final Locale locale) {
115 this(pattern, locale, null);
116 }
117
118 /**
119 * Create a new ExtendedMessageFormat.
120 *
121 * @param pattern the pattern to use, not null.
122 * @param locale the locale to use.
123 * @param registry the registry of format factories, may be null.
124 * @throws IllegalArgumentException in case of a bad pattern.
125 */
126 public ExtendedMessageFormat(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) {
127 super(EMPTY_PATTERN);
128 setLocale(LocaleUtils.toLocale(locale));
129 this.registry = registry;
130 applyPattern(pattern);
131 }
132
133 /**
134 * Create a new ExtendedMessageFormat for the default locale.
135 *
136 * @param pattern the pattern to use, not null
137 * @param registry the registry of format factories, may be null
138 * @throws IllegalArgumentException in case of a bad pattern.
139 */
140 public ExtendedMessageFormat(final String pattern, final Map<String, ? extends FormatFactory> registry) {
141 this(pattern, Locale.getDefault(), registry);
142 }
143
144 /**
145 * Consume a quoted string, adding it to {@code appendTo} if
146 * specified.
147 *
148 * @param pattern pattern to parse
149 * @param pos current parse position
150 * @param appendTo optional StringBuilder to append
151 * @return {@code appendTo}
152 */
153 private StringBuilder appendQuotedString(final String pattern, final ParsePosition pos,
154 final StringBuilder appendTo) {
155 assert pattern.toCharArray()[pos.getIndex()] == QUOTE :
156 "Quoted string must start with quote character";
157
158 // handle quote character at the beginning of the string
159 if (appendTo != null) {
160 appendTo.append(QUOTE);
161 }
162 next(pos);
163
164 final int start = pos.getIndex();
165 final char[] c = pattern.toCharArray();
166 for (int i = pos.getIndex(); i < pattern.length(); i++) {
167 if (c[pos.getIndex()] == QUOTE) {
168 next(pos);
169 return appendTo == null ? null : appendTo.append(c, start,
170 pos.getIndex() - start);
171 }
172 next(pos);
173 }
174 throw new IllegalArgumentException(
175 "Unterminated quoted string at position " + start);
176 }
177
178 /**
179 * Apply the specified pattern.
180 *
181 * @param pattern String
182 */
183 @Override
184 public final void applyPattern(final String pattern) {
185 if (registry == null) {
186 super.applyPattern(pattern);
187 toPattern = super.toPattern();
188 return;
189 }
190 final ArrayList<Format> foundFormats = new ArrayList<>();
191 final ArrayList<String> foundDescriptions = new ArrayList<>();
192 final StringBuilder stripCustom = new StringBuilder(pattern.length());
193
194 final ParsePosition pos = new ParsePosition(0);
195 final char[] c = pattern.toCharArray();
196 int fmtCount = 0;
197 while (pos.getIndex() < pattern.length()) {
198 switch (c[pos.getIndex()]) {
199 case QUOTE:
200 appendQuotedString(pattern, pos, stripCustom);
201 break;
202 case START_FE:
203 fmtCount++;
204 seekNonWs(pattern, pos);
205 final int start = pos.getIndex();
206 final int index = readArgumentIndex(pattern, next(pos));
207 stripCustom.append(START_FE).append(index);
208 seekNonWs(pattern, pos);
209 Format format = null;
210 String formatDescription = null;
211 if (c[pos.getIndex()] == START_FMT) {
212 formatDescription = parseFormatDescription(pattern,
213 next(pos));
214 format = getFormat(formatDescription);
215 if (format == null) {
216 stripCustom.append(START_FMT).append(formatDescription);
217 }
218 }
219 foundFormats.add(format);
220 foundDescriptions.add(format == null ? null : formatDescription);
221 Validate.isTrue(foundFormats.size() == fmtCount);
222 Validate.isTrue(foundDescriptions.size() == fmtCount);
223 if (c[pos.getIndex()] != END_FE) {
224 throw new IllegalArgumentException(
225 "Unreadable format element at position " + start);
226 }
227 // falls-through
228 default:
229 stripCustom.append(c[pos.getIndex()]);
230 next(pos);
231 }
232 }
233 super.applyPattern(stripCustom.toString());
234 toPattern = insertFormats(super.toPattern(), foundDescriptions);
235 if (containsElements(foundFormats)) {
236 final Format[] origFormats = getFormats();
237 // only loop over what we know we have, as MessageFormat on Java 1.3
238 // seems to provide an extra format element:
239 int i = 0;
240 for (final Format f : foundFormats) {
241 if (f != null) {
242 origFormats[i] = f;
243 }
244 i++;
245 }
246 super.setFormats(origFormats);
247 }
248 }
249
250 /**
251 * Learn whether the specified Collection contains non-null elements.
252 *
253 * @param coll to check
254 * @return {@code true} if some Object was found, {@code false} otherwise.
255 */
256 private boolean containsElements(final Collection<?> coll) {
257 if (coll == null || coll.isEmpty()) {
258 return false;
259 }
260 return coll.stream().anyMatch(Objects::nonNull);
261 }
262
263 @Override
264 public boolean equals(final Object obj) {
265 if (this == obj) {
266 return true;
267 }
268 if (!super.equals(obj)) {
269 return false;
270 }
271 if (!(obj instanceof ExtendedMessageFormat)) {
272 return false;
273 }
274 final ExtendedMessageFormat other = (ExtendedMessageFormat) obj;
275 return Objects.equals(registry, other.registry) && Objects.equals(toPattern, other.toPattern);
276 }
277
278 /**
279 * Gets a custom format from a format description.
280 *
281 * @param desc String
282 * @return Format
283 */
284 private Format getFormat(final String desc) {
285 if (registry != null) {
286 String name = desc;
287 String args = null;
288 final int i = desc.indexOf(START_FMT);
289 if (i > 0) {
290 name = desc.substring(0, i).trim();
291 args = desc.substring(i + 1).trim();
292 }
293 final FormatFactory factory = registry.get(name);
294 if (factory != null) {
295 return factory.getFormat(name, args, getLocale());
296 }
297 }
298 return null;
299 }
300
301 /**
302 * Consume quoted string only
303 *
304 * @param pattern pattern to parse
305 * @param pos current parse position
306 */
307 private void getQuotedString(final String pattern, final ParsePosition pos) {
308 appendQuotedString(pattern, pos, null);
309 }
310
311 @Override
312 public int hashCode() {
313 final int prime = 31;
314 final int result = super.hashCode();
315 return prime * result + Objects.hash(registry, toPattern);
316 }
317
318 /**
319 * Insert formats back into the pattern for toPattern() support.
320 *
321 * @param pattern source
322 * @param customPatterns The custom patterns to re-insert, if any
323 * @return full pattern
324 */
325 private String insertFormats(final String pattern, final ArrayList<String> customPatterns) {
326 if (!containsElements(customPatterns)) {
327 return pattern;
328 }
329 final StringBuilder sb = new StringBuilder(pattern.length() * 2);
330 final ParsePosition pos = new ParsePosition(0);
331 int fe = -1;
332 int depth = 0;
333 while (pos.getIndex() < pattern.length()) {
334 final char c = pattern.charAt(pos.getIndex());
335 switch (c) {
336 case QUOTE:
337 appendQuotedString(pattern, pos, sb);
338 break;
339 case START_FE:
340 depth++;
341 sb.append(START_FE).append(readArgumentIndex(pattern, next(pos)));
342 // do not look for custom patterns when they are embedded, e.g. in a choice
343 if (depth == 1) {
344 fe++;
345 final String customPattern = customPatterns.get(fe);
346 if (customPattern != null) {
347 sb.append(START_FMT).append(customPattern);
348 }
349 }
350 break;
351 case END_FE:
352 depth--;
353 // falls-through
354 default:
355 sb.append(c);
356 next(pos);
357 }
358 }
359 return sb.toString();
360 }
361
362 /**
363 * Convenience method to advance parse position by 1
364 *
365 * @param pos ParsePosition
366 * @return {@code pos}
367 */
368 private ParsePosition next(final ParsePosition pos) {
369 pos.setIndex(pos.getIndex() + 1);
370 return pos;
371 }
372
373 /**
374 * Parse the format component of a format element.
375 *
376 * @param pattern string to parse
377 * @param pos current parse position
378 * @return Format description String
379 */
380 private String parseFormatDescription(final String pattern, final ParsePosition pos) {
381 final int start = pos.getIndex();
382 seekNonWs(pattern, pos);
383 final int text = pos.getIndex();
384 int depth = 1;
385 for (; pos.getIndex() < pattern.length(); next(pos)) {
386 switch (pattern.charAt(pos.getIndex())) {
387 case START_FE:
388 depth++;
389 break;
390 case END_FE:
391 depth--;
392 if (depth == 0) {
393 return pattern.substring(text, pos.getIndex());
394 }
395 break;
396 case QUOTE:
397 getQuotedString(pattern, pos);
398 break;
399 default:
400 break;
401 }
402 }
403 throw new IllegalArgumentException(
404 "Unterminated format element at position " + start);
405 }
406
407 /**
408 * Reads the argument index from the current format element
409 *
410 * @param pattern pattern to parse
411 * @param pos current parse position
412 * @return argument index
413 */
414 private int readArgumentIndex(final String pattern, final ParsePosition pos) {
415 final int start = pos.getIndex();
416 seekNonWs(pattern, pos);
417 final StringBuilder result = new StringBuilder();
418 boolean error = false;
419 for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
420 char c = pattern.charAt(pos.getIndex());
421 if (Character.isWhitespace(c)) {
422 seekNonWs(pattern, pos);
423 c = pattern.charAt(pos.getIndex());
424 if (c != START_FMT && c != END_FE) {
425 error = true;
426 continue;
427 }
428 }
429 if ((c == START_FMT || c == END_FE) && result.length() > 0) {
430 try {
431 return Integer.parseInt(result.toString());
432 } catch (final NumberFormatException ignored) {
433 // we've already ensured only digits, so unless something
434 // outlandishly large was specified we should be okay.
435 }
436 }
437 error = !Character.isDigit(c);
438 result.append(c);
439 }
440 if (error) {
441 throw new IllegalArgumentException(
442 "Invalid format argument index at position " + start + ": "
443 + pattern.substring(start, pos.getIndex()));
444 }
445 throw new IllegalArgumentException(
446 "Unterminated format element at position " + start);
447 }
448
449 /**
450 * Consume whitespace from the current parse position.
451 *
452 * @param pattern String to read
453 * @param pos current position
454 */
455 private void seekNonWs(final String pattern, final ParsePosition pos) {
456 int len;
457 final char[] buffer = pattern.toCharArray();
458 do {
459 len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
460 pos.setIndex(pos.getIndex() + len);
461 } while (len > 0 && pos.getIndex() < pattern.length());
462 }
463
464 /**
465 * Throws UnsupportedOperationException - see class Javadoc for details.
466 *
467 * @param formatElementIndex format element index
468 * @param newFormat the new format
469 * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
470 */
471 @Override
472 public void setFormat(final int formatElementIndex, final Format newFormat) {
473 throw new UnsupportedOperationException();
474 }
475
476 /**
477 * Throws UnsupportedOperationException - see class Javadoc for details.
478 *
479 * @param argumentIndex argument index
480 * @param newFormat the new format
481 * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
482 */
483 @Override
484 public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) {
485 throw new UnsupportedOperationException();
486 }
487
488 /**
489 * Throws UnsupportedOperationException - see class Javadoc for details.
490 *
491 * @param newFormats new formats
492 * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
493 */
494 @Override
495 public void setFormats(final Format[] newFormats) {
496 throw new UnsupportedOperationException();
497 }
498
499 /**
500 * Throws UnsupportedOperationException - see class Javadoc for details.
501 *
502 * @param newFormats new formats
503 * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
504 */
505 @Override
506 public void setFormatsByArgumentIndex(final Format[] newFormats) {
507 throw new UnsupportedOperationException();
508 }
509
510 /**
511 * {@inheritDoc}
512 */
513 @Override
514 public String toPattern() {
515 return toPattern;
516 }
517 }