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