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 * http://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.logging.log4j.message;
18
19 import java.util.AbstractMap;
20 import java.util.Collections;
21 import java.util.Map;
22 import java.util.TreeMap;
23
24 import org.apache.logging.log4j.util.BiConsumer;
25 import org.apache.logging.log4j.util.Chars;
26 import org.apache.logging.log4j.util.EnglishEnums;
27 import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
28 import org.apache.logging.log4j.util.IndexedStringMap;
29 import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable;
30 import org.apache.logging.log4j.util.PerformanceSensitive;
31 import org.apache.logging.log4j.util.ReadOnlyStringMap;
32 import org.apache.logging.log4j.util.SortedArrayStringMap;
33 import org.apache.logging.log4j.util.StringBuilders;
34 import org.apache.logging.log4j.util.Strings;
35 import org.apache.logging.log4j.util.TriConsumer;
36
37 /**
38 * Represents a Message that consists of a Map.
39 * <p>
40 * Thread-safety note: the contents of this message can be modified after construction.
41 * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is
42 * logged, because it is undefined whether the logged message string will contain the old values or the modified
43 * values.
44 * </p>
45 * <p>
46 * This class was pulled up from {@link StringMapMessage} to allow for Objects as values.
47 * </p>
48 * @param <M> Allow subclasses to use fluent APIs and override methods that return instances of subclasses.
49 * @param <V> The value type
50 */
51 @PerformanceSensitive("allocation")
52 @AsynchronouslyFormattable
53 public class MapMessage<M extends MapMessage<M, V>, V> implements MultiFormatStringBuilderFormattable {
54
55 private static final long serialVersionUID = -5031471831131487120L;
56
57 /**
58 * When set as the format specifier causes the Map to be formatted as XML.
59 */
60 public enum MapFormat {
61
62 /** The map should be formatted as XML. */
63 XML,
64
65 /** The map should be formatted as JSON. */
66 JSON,
67
68 /** The map should be formatted the same as documented by {@link AbstractMap#toString()}. */
69 JAVA,
70
71 /**
72 * The map should be formatted the same as documented by {@link AbstractMap#toString()} but without quotes.
73 *
74 * @since 2.11.2
75 */
76 JAVA_UNQUOTED;
77
78 /**
79 * Maps a format name to an {@link MapFormat} while ignoring case.
80 *
81 * @param format a MapFormat name
82 * @return a MapFormat
83 */
84 public static MapFormat lookupIgnoreCase(final String format) {
85 return XML.name().equalsIgnoreCase(format) ? XML //
86 : JSON.name().equalsIgnoreCase(format) ? JSON //
87 : JAVA.name().equalsIgnoreCase(format) ? JAVA //
88 : JAVA_UNQUOTED.name().equalsIgnoreCase(format) ? JAVA_UNQUOTED //
89 : null;
90 }
91
92 /**
93 * All {@code MapFormat} names.
94 *
95 * @return All {@code MapFormat} names.
96 */
97 public static String[] names() {
98 return new String[] {XML.name(), JSON.name(), JAVA.name(), JAVA_UNQUOTED.name()};
99 }
100 }
101
102 private final IndexedStringMap data;
103
104 /**
105 * Constructs a new instance.
106 */
107 public MapMessage() {
108 this.data = new SortedArrayStringMap();
109 }
110
111 /**
112 * Constructs a new instance.
113 *
114 * @param initialCapacity the initial capacity.
115 */
116 public MapMessage(final int initialCapacity) {
117 this.data = new SortedArrayStringMap(initialCapacity);
118 }
119
120 /**
121 * Constructs a new instance based on an existing {@link Map}.
122 * @param map The Map.
123 */
124 public MapMessage(final Map<String, V> map) {
125 this.data = new SortedArrayStringMap(map);
126 }
127
128 @Override
129 public String[] getFormats() {
130 return MapFormat.names();
131 }
132
133 /**
134 * Returns the data elements as if they were parameters on the logging event.
135 * @return the data elements.
136 */
137 @Override
138 public Object[] getParameters() {
139 final Object[] result = new Object[data.size()];
140 for (int i = 0; i < data.size(); i++) {
141 result[i] = data.getValueAt(i);
142 }
143 return result;
144 }
145
146 /**
147 * Returns the message.
148 * @return the message.
149 */
150 @Override
151 public String getFormat() {
152 return Strings.EMPTY;
153 }
154
155 /**
156 * Returns the message data as an unmodifiable Map.
157 * @return the message data as an unmodifiable map.
158 */
159 @SuppressWarnings("unchecked")
160 public Map<String, V> getData() {
161 final TreeMap<String, V> result = new TreeMap<>(); // returned map must be sorted
162 for (int i = 0; i < data.size(); i++) {
163 // The Eclipse compiler does not need the typecast to V, but the Oracle compiler sure does.
164 result.put(data.getKeyAt(i), (V) data.getValueAt(i));
165 }
166 return Collections.unmodifiableMap(result);
167 }
168
169 /**
170 * Returns a read-only view of the message data.
171 * @return the read-only message data.
172 */
173 public IndexedReadOnlyStringMap getIndexedReadOnlyStringMap() {
174 return data;
175 }
176
177 /**
178 * Clear the data.
179 */
180 public void clear() {
181 data.clear();
182 }
183
184 /**
185 * Returns {@code true} if this data structure contains the specified key, {@code false} otherwise.
186 *
187 * @param key the key whose presence to check. May be {@code null}.
188 * @return {@code true} if this data structure contains the specified key, {@code false} otherwise
189 * @since 2.9
190 */
191 public boolean containsKey(final String key) {
192 return data.containsKey(key);
193 }
194
195 /**
196 * Adds an item to the data Map.
197 * @param key The name of the data item.
198 * @param value The value of the data item.
199 */
200 public void put(final String key, final String value) {
201 if (value == null) {
202 throw new IllegalArgumentException("No value provided for key " + key);
203 }
204 validate(key, value);
205 data.putValue(key, value);
206 }
207
208 /**
209 * Adds all the elements from the specified Map.
210 * @param map The Map to add.
211 */
212 public void putAll(final Map<String, String> map) {
213 for (final Map.Entry<String, String> entry : map.entrySet()) {
214 data.putValue(entry.getKey(), entry.getValue());
215 }
216 }
217
218 /**
219 * Retrieves the value of the element with the specified key or null if the key is not present.
220 * @param key The name of the element.
221 * @return The value of the element or null if the key is not present.
222 */
223 public String get(final String key) {
224 final Object result = data.getValue(key);
225 return ParameterFormatter.deepToString(result);
226 }
227
228 /**
229 * Removes the element with the specified name.
230 * @param key The name of the element.
231 * @return The previous value of the element.
232 */
233 public String remove(final String key) {
234 final String result = get(key);
235 data.remove(key);
236 return result;
237 }
238
239 /**
240 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.
241 *
242 * @return The formatted String.
243 */
244 public String asString() {
245 return format((MapFormat) null, new StringBuilder()).toString();
246 }
247
248 /**
249 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.
250 *
251 * @param format The format identifier.
252 * @return The formatted String.
253 */
254 public String asString(final String format) {
255 try {
256 return format(EnglishEnums.valueOf(MapFormat.class, format), new StringBuilder()).toString();
257 } catch (final IllegalArgumentException ex) {
258 return asString();
259 }
260 }
261
262 /**
263 * Performs the given action for each key-value pair in this data structure
264 * until all entries have been processed or the action throws an exception.
265 * <p>
266 * Some implementations may not support structural modifications (adding new elements or removing elements) while
267 * iterating over the contents. In such implementations, attempts to add or remove elements from the
268 * {@code BiConsumer}'s {@link BiConsumer#accept(Object, Object)} accept} method may cause a
269 * {@code ConcurrentModificationException} to be thrown.
270 * </p>
271 *
272 * @param action The action to be performed for each key-value pair in this collection
273 * @param <CV> type of the consumer value
274 * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications
275 * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or
276 * {@link #forEach(TriConsumer, Object)}.
277 * @see ReadOnlyStringMap#forEach(BiConsumer)
278 * @since 2.9
279 */
280 public <CV> void forEach(final BiConsumer<String, ? super CV> action) {
281 data.forEach(action);
282 }
283
284 /**
285 * Performs the given action for each key-value pair in this data structure
286 * until all entries have been processed or the action throws an exception.
287 * <p>
288 * The third parameter lets callers pass in a stateful object to be modified with the key-value pairs,
289 * so the TriConsumer implementation itself can be stateless and potentially reusable.
290 * </p>
291 * <p>
292 * Some implementations may not support structural modifications (adding new elements or removing elements) while
293 * iterating over the contents. In such implementations, attempts to add or remove elements from the
294 * {@code TriConsumer}'s {@link TriConsumer#accept(Object, Object, Object) accept} method may cause a
295 * {@code ConcurrentModificationException} to be thrown.
296 * </p>
297 *
298 * @param action The action to be performed for each key-value pair in this collection
299 * @param state the object to be passed as the third parameter to each invocation on the specified
300 * triconsumer
301 * @param <CV> type of the consumer value
302 * @param <S> type of the third parameter
303 * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications
304 * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or
305 * {@link #forEach(TriConsumer, Object)}.
306 * @see ReadOnlyStringMap#forEach(TriConsumer, Object)
307 * @since 2.9
308 */
309 public <CV, S> void forEach(final TriConsumer<String, ? super CV, S> action, final S state) {
310 data.forEach(action, state);
311 }
312
313 /**
314 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.
315 *
316 * @param format The format identifier.
317 * @return The formatted String.
318 */
319 private StringBuilder format(final MapFormat format, final StringBuilder sb) {
320 if (format == null) {
321 appendMap(sb);
322 } else {
323 switch (format) {
324 case XML : {
325 asXml(sb);
326 break;
327 }
328 case JSON : {
329 asJson(sb);
330 break;
331 }
332 case JAVA : {
333 asJava(sb);
334 break;
335 }
336 case JAVA_UNQUOTED:
337 asJavaUnquoted(sb);
338 break;
339 default : {
340 appendMap(sb);
341 }
342 }
343 }
344 return sb;
345 }
346
347 /**
348 * Formats this message as an XML fragment String into the given builder.
349 *
350 * @param sb format into this builder.
351 */
352 public void asXml(final StringBuilder sb) {
353 sb.append("<Map>\n");
354 for (int i = 0; i < data.size(); i++) {
355 sb.append(" <Entry key=\"")
356 .append(data.getKeyAt(i))
357 .append("\">");
358 final int size = sb.length();
359 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
360 StringBuilders.escapeXml(sb, size);
361 sb.append("</Entry>\n");
362 }
363 sb.append("</Map>");
364 }
365
366 /**
367 * Formats the message and return it.
368 * @return the formatted message.
369 */
370 @Override
371 public String getFormattedMessage() {
372 return asString();
373 }
374
375 /**
376 *
377 * @param formats
378 * An array of Strings that provide extra information about how to format the message. MapMessage uses
379 * the first format specifier it recognizes. The supported formats are XML, JSON, and JAVA. The default
380 * format is key1="value1" key2="value2" as required by <a href="https://tools.ietf.org/html/rfc5424">RFC
381 * 5424</a> messages.
382 *
383 * @return The formatted message.
384 */
385 @Override
386 public String getFormattedMessage(final String[] formats) {
387 return format(getFormat(formats), new StringBuilder()).toString();
388 }
389
390 private MapFormat getFormat(final String[] formats) {
391 if (formats == null || formats.length == 0) {
392 return null;
393 }
394 for (int i = 0; i < formats.length; i++) {
395 final MapFormat mapFormat = MapFormat.lookupIgnoreCase(formats[i]);
396 if (mapFormat != null) {
397 return mapFormat;
398 }
399 }
400 return null;
401 }
402
403 protected void appendMap(final StringBuilder sb) {
404 for (int i = 0; i < data.size(); i++) {
405 if (i > 0) {
406 sb.append(' ');
407 }
408 sb.append(data.getKeyAt(i)).append(Chars.EQ).append(Chars.DQUOTE);
409 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
410 sb.append(Chars.DQUOTE);
411 }
412 }
413
414 protected void asJson(final StringBuilder sb) {
415 sb.append('{');
416 for (int i = 0; i < data.size(); i++) {
417 if (i > 0) {
418 sb.append(", ");
419 }
420 sb.append(Chars.DQUOTE);
421 int start = sb.length();
422 sb.append(data.getKeyAt(i));
423 StringBuilders.escapeJson(sb, start);
424 sb.append(Chars.DQUOTE).append(':').append(Chars.DQUOTE);
425 start = sb.length();
426 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
427 StringBuilders.escapeJson(sb, start);
428 sb.append(Chars.DQUOTE);
429 }
430 sb.append('}');
431 }
432
433 protected void asJavaUnquoted(final StringBuilder sb) {
434 asJava(sb, false);
435 }
436
437 protected void asJava(final StringBuilder sb) {
438 asJava(sb, true);
439 }
440
441 private void asJava(final StringBuilder sb, boolean quoted) {
442 sb.append('{');
443 for (int i = 0; i < data.size(); i++) {
444 if (i > 0) {
445 sb.append(", ");
446 }
447 sb.append(data.getKeyAt(i)).append(Chars.EQ);
448 if (quoted) {
449 sb.append(Chars.DQUOTE);
450 }
451 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
452 if (quoted) {
453 sb.append(Chars.DQUOTE);
454 }
455 }
456 sb.append('}');
457 }
458
459 /**
460 * Constructs a new instance based on an existing Map.
461 * @param map The Map.
462 * @return A new MapMessage
463 */
464 @SuppressWarnings("unchecked")
465 public M newInstance(final Map<String, V> map) {
466 return (M) new MapMessage<>(map);
467 }
468
469 @Override
470 public String toString() {
471 return asString();
472 }
473
474 @Override
475 public void formatTo(final StringBuilder buffer) {
476 format((MapFormat) null, buffer);
477 }
478
479 @Override
480 public void formatTo(final String[] formats, final StringBuilder buffer) {
481 format(getFormat(formats), buffer);
482 }
483
484 @Override
485 public boolean equals(final Object o) {
486 if (this == o) {
487 return true;
488 }
489 if (o == null || this.getClass() != o.getClass()) {
490 return false;
491 }
492
493 final MapMessage<?, ?> that = (MapMessage<?, ?>) o;
494
495 return this.data.equals(that.data);
496 }
497
498 @Override
499 public int hashCode() {
500 return data.hashCode();
501 }
502
503 /**
504 * Always returns null.
505 *
506 * @return null
507 */
508 @Override
509 public Throwable getThrowable() {
510 return null;
511 }
512
513 /**
514 * Default implementation does nothing.
515 *
516 * @since 2.9
517 */
518 protected void validate(final String key, final boolean value) {
519 // do nothing
520 }
521
522 /**
523 * Default implementation does nothing.
524 *
525 * @since 2.9
526 */
527 protected void validate(final String key, final byte value) {
528 // do nothing
529 }
530
531 /**
532 * Default implementation does nothing.
533 *
534 * @since 2.9
535 */
536 protected void validate(final String key, final char value) {
537 // do nothing
538 }
539
540 /**
541 * Default implementation does nothing.
542 *
543 * @since 2.9
544 */
545 protected void validate(final String key, final double value) {
546 // do nothing
547 }
548
549 /**
550 * Default implementation does nothing.
551 *
552 * @since 2.9
553 */
554 protected void validate(final String key, final float value) {
555 // do nothing
556 }
557
558 /**
559 * Default implementation does nothing.
560 *
561 * @since 2.9
562 */
563 protected void validate(final String key, final int value) {
564 // do nothing
565 }
566
567 /**
568 * Default implementation does nothing.
569 *
570 * @since 2.9
571 */
572 protected void validate(final String key, final long value) {
573 // do nothing
574 }
575
576 /**
577 * Default implementation does nothing.
578 *
579 * @since 2.9
580 */
581 protected void validate(final String key, final Object value) {
582 // do nothing
583 }
584
585 /**
586 * Default implementation does nothing.
587 *
588 * @since 2.9
589 */
590 protected void validate(final String key, final short value) {
591 // do nothing
592 }
593
594 /**
595 * Default implementation does nothing.
596 *
597 * @since 2.9
598 */
599 protected void validate(final String key, final String value) {
600 // do nothing
601 }
602
603 /**
604 * Adds an item to the data Map.
605 * @param key The name of the data item.
606 * @param value The value of the data item.
607 * @return this object
608 * @since 2.9
609 */
610 @SuppressWarnings("unchecked")
611 public M with(final String key, final boolean value) {
612 validate(key, value);
613 data.putValue(key, value);
614 return (M) this;
615 }
616
617 /**
618 * Adds an item to the data Map.
619 * @param key The name of the data item.
620 * @param value The value of the data item.
621 * @return this object
622 * @since 2.9
623 */
624 @SuppressWarnings("unchecked")
625 public M with(final String key, final byte value) {
626 validate(key, value);
627 data.putValue(key, value);
628 return (M) this;
629 }
630
631 /**
632 * Adds an item to the data Map.
633 * @param key The name of the data item.
634 * @param value The value of the data item.
635 * @return this object
636 * @since 2.9
637 */
638 @SuppressWarnings("unchecked")
639 public M with(final String key, final char value) {
640 validate(key, value);
641 data.putValue(key, value);
642 return (M) this;
643 }
644
645
646 /**
647 * Adds an item to the data Map.
648 * @param key The name of the data item.
649 * @param value The value of the data item.
650 * @return this object
651 * @since 2.9
652 */
653 @SuppressWarnings("unchecked")
654 public M with(final String key, final double value) {
655 validate(key, value);
656 data.putValue(key, value);
657 return (M) this;
658 }
659
660 /**
661 * Adds an item to the data Map.
662 * @param key The name of the data item.
663 * @param value The value of the data item.
664 * @return this object
665 * @since 2.9
666 */
667 @SuppressWarnings("unchecked")
668 public M with(final String key, final float value) {
669 validate(key, value);
670 data.putValue(key, value);
671 return (M) this;
672 }
673
674 /**
675 * Adds an item to the data Map.
676 * @param key The name of the data item.
677 * @param value The value of the data item.
678 * @return this object
679 * @since 2.9
680 */
681 @SuppressWarnings("unchecked")
682 public M with(final String key, final int value) {
683 validate(key, value);
684 data.putValue(key, value);
685 return (M) this;
686 }
687
688 /**
689 * Adds an item to the data Map.
690 * @param key The name of the data item.
691 * @param value The value of the data item.
692 * @return this object
693 * @since 2.9
694 */
695 @SuppressWarnings("unchecked")
696 public M with(final String key, final long value) {
697 validate(key, value);
698 data.putValue(key, value);
699 return (M) this;
700 }
701
702 /**
703 * Adds an item to the data Map.
704 * @param key The name of the data item.
705 * @param value The value of the data item.
706 * @return this object
707 * @since 2.9
708 */
709 @SuppressWarnings("unchecked")
710 public M with(final String key, final Object value) {
711 validate(key, value);
712 data.putValue(key, value);
713 return (M) this;
714 }
715
716 /**
717 * Adds an item to the data Map.
718 * @param key The name of the data item.
719 * @param value The value of the data item.
720 * @return this object
721 * @since 2.9
722 */
723 @SuppressWarnings("unchecked")
724 public M with(final String key, final short value) {
725 validate(key, value);
726 data.putValue(key, value);
727 return (M) this;
728 }
729
730 /**
731 * Adds an item to the data Map in fluent style.
732 * @param key The name of the data item.
733 * @param value The value of the data item.
734 * @return {@code this}
735 */
736 @SuppressWarnings("unchecked")
737 public M with(final String key, final String value) {
738 put(key, value);
739 return (M) this;
740 }
741
742 }