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 candidateKey The name of the data item.
198 * @param value The value of the data item.
199 */
200 public void put(final String candidateKey, final String value) {
201 if (value == null) {
202 throw new IllegalArgumentException("No value provided for key " + candidateKey);
203 }
204 final String key = toKey(candidateKey);
205 validate(key, value);
206 data.putValue(key, value);
207 }
208
209 /**
210 * Adds all the elements from the specified Map.
211 * @param map The Map to add.
212 */
213 public void putAll(final Map<String, String> map) {
214 for (final Map.Entry<String, String> entry : map.entrySet()) {
215 data.putValue(entry.getKey(), entry.getValue());
216 }
217 }
218
219 /**
220 * Retrieves the value of the element with the specified key or null if the key is not present.
221 * @param key The name of the element.
222 * @return The value of the element or null if the key is not present.
223 */
224 public String get(final String key) {
225 final Object result = data.getValue(key);
226 return ParameterFormatter.deepToString(result);
227 }
228
229 /**
230 * Removes the element with the specified name.
231 * @param key The name of the element.
232 * @return The previous value of the element.
233 */
234 public String remove(final String key) {
235 final String result = get(key);
236 data.remove(key);
237 return result;
238 }
239
240 /**
241 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.
242 *
243 * @return The formatted String.
244 */
245 public String asString() {
246 return format((MapFormat) null, new StringBuilder()).toString();
247 }
248
249 /**
250 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.
251 *
252 * @param format The format identifier.
253 * @return The formatted String.
254 */
255 public String asString(final String format) {
256 try {
257 return format(EnglishEnums.valueOf(MapFormat.class, format), new StringBuilder()).toString();
258 } catch (final IllegalArgumentException ex) {
259 return asString();
260 }
261 }
262
263 /**
264 * Performs the given action for each key-value pair in this data structure
265 * until all entries have been processed or the action throws an exception.
266 * <p>
267 * Some implementations may not support structural modifications (adding new elements or removing elements) while
268 * iterating over the contents. In such implementations, attempts to add or remove elements from the
269 * {@code BiConsumer}'s {@link BiConsumer#accept(Object, Object)} accept} method may cause a
270 * {@code ConcurrentModificationException} to be thrown.
271 * </p>
272 *
273 * @param action The action to be performed for each key-value pair in this collection
274 * @param <CV> type of the consumer value
275 * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications
276 * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or
277 * {@link #forEach(TriConsumer, Object)}.
278 * @see ReadOnlyStringMap#forEach(BiConsumer)
279 * @since 2.9
280 */
281 public <CV> void forEach(final BiConsumer<String, ? super CV> action) {
282 data.forEach(action);
283 }
284
285 /**
286 * Performs the given action for each key-value pair in this data structure
287 * until all entries have been processed or the action throws an exception.
288 * <p>
289 * The third parameter lets callers pass in a stateful object to be modified with the key-value pairs,
290 * so the TriConsumer implementation itself can be stateless and potentially reusable.
291 * </p>
292 * <p>
293 * Some implementations may not support structural modifications (adding new elements or removing elements) while
294 * iterating over the contents. In such implementations, attempts to add or remove elements from the
295 * {@code TriConsumer}'s {@link TriConsumer#accept(Object, Object, Object) accept} method may cause a
296 * {@code ConcurrentModificationException} to be thrown.
297 * </p>
298 *
299 * @param action The action to be performed for each key-value pair in this collection
300 * @param state the object to be passed as the third parameter to each invocation on the specified
301 * triconsumer
302 * @param <CV> type of the consumer value
303 * @param <S> type of the third parameter
304 * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications
305 * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or
306 * {@link #forEach(TriConsumer, Object)}.
307 * @see ReadOnlyStringMap#forEach(TriConsumer, Object)
308 * @since 2.9
309 */
310 public <CV, S> void forEach(final TriConsumer<String, ? super CV, S> action, final S state) {
311 data.forEach(action, state);
312 }
313
314 /**
315 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.
316 *
317 * @param format The format identifier.
318 * @return The formatted String.
319 */
320 private StringBuilder format(final MapFormat format, final StringBuilder sb) {
321 if (format == null) {
322 appendMap(sb);
323 } else {
324 switch (format) {
325 case XML : {
326 asXml(sb);
327 break;
328 }
329 case JSON : {
330 asJson(sb);
331 break;
332 }
333 case JAVA : {
334 asJava(sb);
335 break;
336 }
337 case JAVA_UNQUOTED:
338 asJavaUnquoted(sb);
339 break;
340 default : {
341 appendMap(sb);
342 }
343 }
344 }
345 return sb;
346 }
347
348 /**
349 * Formats this message as an XML fragment String into the given builder.
350 *
351 * @param sb format into this builder.
352 */
353 public void asXml(final StringBuilder sb) {
354 sb.append("<Map>\n");
355 for (int i = 0; i < data.size(); i++) {
356 sb.append(" <Entry key=\"")
357 .append(data.getKeyAt(i))
358 .append("\">");
359 final int size = sb.length();
360 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
361 StringBuilders.escapeXml(sb, size);
362 sb.append("</Entry>\n");
363 }
364 sb.append("</Map>");
365 }
366
367 /**
368 * Formats the message and return it.
369 * @return the formatted message.
370 */
371 @Override
372 public String getFormattedMessage() {
373 return asString();
374 }
375
376 /**
377 *
378 * @param formats
379 * An array of Strings that provide extra information about how to format the message. MapMessage uses
380 * the first format specifier it recognizes. The supported formats are XML, JSON, and JAVA. The default
381 * format is key1="value1" key2="value2" as required by <a href="https://tools.ietf.org/html/rfc5424">RFC
382 * 5424</a> messages.
383 *
384 * @return The formatted message.
385 */
386 @Override
387 public String getFormattedMessage(final String[] formats) {
388 return format(getFormat(formats), new StringBuilder()).toString();
389 }
390
391 private MapFormat getFormat(final String[] formats) {
392 if (formats == null || formats.length == 0) {
393 return null;
394 }
395 for (int i = 0; i < formats.length; i++) {
396 final MapFormat mapFormat = MapFormat.lookupIgnoreCase(formats[i]);
397 if (mapFormat != null) {
398 return mapFormat;
399 }
400 }
401 return null;
402 }
403
404 protected void appendMap(final StringBuilder sb) {
405 for (int i = 0; i < data.size(); i++) {
406 if (i > 0) {
407 sb.append(' ');
408 }
409 sb.append(data.getKeyAt(i)).append(Chars.EQ).append(Chars.DQUOTE);
410 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
411 sb.append(Chars.DQUOTE);
412 }
413 }
414
415 protected void asJson(final StringBuilder sb) {
416 sb.append('{');
417 for (int i = 0; i < data.size(); i++) {
418 if (i > 0) {
419 sb.append(", ");
420 }
421 sb.append(Chars.DQUOTE);
422 int start = sb.length();
423 sb.append(data.getKeyAt(i));
424 StringBuilders.escapeJson(sb, start);
425 sb.append(Chars.DQUOTE).append(':').append(Chars.DQUOTE);
426 start = sb.length();
427 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
428 StringBuilders.escapeJson(sb, start);
429 sb.append(Chars.DQUOTE);
430 }
431 sb.append('}');
432 }
433
434 protected void asJavaUnquoted(final StringBuilder sb) {
435 asJava(sb, false);
436 }
437
438 protected void asJava(final StringBuilder sb) {
439 asJava(sb, true);
440 }
441
442 private void asJava(final StringBuilder sb, boolean quoted) {
443 sb.append('{');
444 for (int i = 0; i < data.size(); i++) {
445 if (i > 0) {
446 sb.append(", ");
447 }
448 sb.append(data.getKeyAt(i)).append(Chars.EQ);
449 if (quoted) {
450 sb.append(Chars.DQUOTE);
451 }
452 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
453 if (quoted) {
454 sb.append(Chars.DQUOTE);
455 }
456 }
457 sb.append('}');
458 }
459
460 /**
461 * Constructs a new instance based on an existing Map.
462 * @param map The Map.
463 * @return A new MapMessage
464 */
465 @SuppressWarnings("unchecked")
466 public M newInstance(final Map<String, V> map) {
467 return (M) new MapMessage<>(map);
468 }
469
470 @Override
471 public String toString() {
472 return asString();
473 }
474
475 @Override
476 public void formatTo(final StringBuilder buffer) {
477 format((MapFormat) null, buffer);
478 }
479
480 @Override
481 public void formatTo(final String[] formats, final StringBuilder buffer) {
482 format(getFormat(formats), buffer);
483 }
484
485 @Override
486 public boolean equals(final Object o) {
487 if (this == o) {
488 return true;
489 }
490 if (o == null || this.getClass() != o.getClass()) {
491 return false;
492 }
493
494 final MapMessage<?, ?> that = (MapMessage<?, ?>) o;
495
496 return this.data.equals(that.data);
497 }
498
499 @Override
500 public int hashCode() {
501 return data.hashCode();
502 }
503
504 /**
505 * Always returns null.
506 *
507 * @return null
508 */
509 @Override
510 public Throwable getThrowable() {
511 return null;
512 }
513
514 /**
515 * Default implementation does nothing.
516 *
517 * @since 2.9
518 */
519 protected void validate(final String key, final boolean value) {
520 // do nothing
521 }
522
523 /**
524 * Default implementation does nothing.
525 *
526 * @since 2.9
527 */
528 protected void validate(final String key, final byte value) {
529 // do nothing
530 }
531
532 /**
533 * Default implementation does nothing.
534 *
535 * @since 2.9
536 */
537 protected void validate(final String key, final char value) {
538 // do nothing
539 }
540
541 /**
542 * Default implementation does nothing.
543 *
544 * @since 2.9
545 */
546 protected void validate(final String key, final double value) {
547 // do nothing
548 }
549
550 /**
551 * Default implementation does nothing.
552 *
553 * @since 2.9
554 */
555 protected void validate(final String key, final float value) {
556 // do nothing
557 }
558
559 /**
560 * Default implementation does nothing.
561 *
562 * @since 2.9
563 */
564 protected void validate(final String key, final int value) {
565 // do nothing
566 }
567
568 /**
569 * Default implementation does nothing.
570 *
571 * @since 2.9
572 */
573 protected void validate(final String key, final long value) {
574 // do nothing
575 }
576
577 /**
578 * Default implementation does nothing.
579 *
580 * @since 2.9
581 */
582 protected void validate(final String key, final Object value) {
583 // do nothing
584 }
585
586 /**
587 * Default implementation does nothing.
588 *
589 * @since 2.9
590 */
591 protected void validate(final String key, final short value) {
592 // do nothing
593 }
594
595 /**
596 * Default implementation does nothing.
597 *
598 * @since 2.9
599 */
600 protected void validate(final String key, final String value) {
601 // do nothing
602 }
603
604 /**
605 * Allows subclasses to change a candidate key to an actual key.
606 *
607 * @param candidateKey The candidate key.
608 * @return The candidate key.
609 * @since 2.12
610 */
611 protected String toKey(final String candidateKey) {
612 return candidateKey;
613 }
614
615 /**
616 * Adds an item to the data Map.
617 * @param candidateKey The name of the data item.
618 * @param value The value of the data item.
619 * @return this object
620 * @since 2.9
621 */
622 @SuppressWarnings("unchecked")
623 public M with(final String candidateKey, final boolean value) {
624 final String key = toKey(candidateKey);
625 validate(key, value);
626 data.putValue(key, value);
627 return (M) this;
628 }
629
630 /**
631 * Adds an item to the data Map.
632 * @param candidateKey The name of the data item.
633 * @param value The value of the data item.
634 * @return this object
635 * @since 2.9
636 */
637 @SuppressWarnings("unchecked")
638 public M with(final String candidateKey, final byte value) {
639 final String key = toKey(candidateKey);
640 validate(key, value);
641 data.putValue(key, value);
642 return (M) this;
643 }
644
645 /**
646 * Adds an item to the data Map.
647 * @param candidateKey The name of the data item.
648 * @param value The value of the data item.
649 * @return this object
650 * @since 2.9
651 */
652 @SuppressWarnings("unchecked")
653 public M with(final String candidateKey, final char value) {
654 final String key = toKey(candidateKey);
655 validate(key, value);
656 data.putValue(key, value);
657 return (M) this;
658 }
659
660
661 /**
662 * Adds an item to the data Map.
663 * @param candidateKey The name of the data item.
664 * @param value The value of the data item.
665 * @return this object
666 * @since 2.9
667 */
668 @SuppressWarnings("unchecked")
669 public M with(final String candidateKey, final double value) {
670 final String key = toKey(candidateKey);
671 validate(key, value);
672 data.putValue(key, value);
673 return (M) this;
674 }
675
676 /**
677 * Adds an item to the data Map.
678 * @param candidateKey The name of the data item.
679 * @param value The value of the data item.
680 * @return this object
681 * @since 2.9
682 */
683 @SuppressWarnings("unchecked")
684 public M with(final String candidateKey, final float value) {
685 final String key = toKey(candidateKey);
686 validate(key, value);
687 data.putValue(key, value);
688 return (M) this;
689 }
690
691 /**
692 * Adds an item to the data Map.
693 * @param candidateKey The name of the data item.
694 * @param value The value of the data item.
695 * @return this object
696 * @since 2.9
697 */
698 @SuppressWarnings("unchecked")
699 public M with(final String candidateKey, final int value) {
700 final String key = toKey(candidateKey);
701 validate(key, value);
702 data.putValue(key, value);
703 return (M) this;
704 }
705
706 /**
707 * Adds an item to the data Map.
708 * @param candidateKey The name of the data item.
709 * @param value The value of the data item.
710 * @return this object
711 * @since 2.9
712 */
713 @SuppressWarnings("unchecked")
714 public M with(final String candidateKey, final long value) {
715 final String key = toKey(candidateKey);
716 validate(key, value);
717 data.putValue(key, value);
718 return (M) this;
719 }
720
721 /**
722 * Adds an item to the data Map.
723 * @param candidateKey The name of the data item.
724 * @param value The value of the data item.
725 * @return this object
726 * @since 2.9
727 */
728 @SuppressWarnings("unchecked")
729 public M with(final String candidateKey, final Object value) {
730 final String key = toKey(candidateKey);
731 validate(key, value);
732 data.putValue(key, value);
733 return (M) this;
734 }
735
736 /**
737 * Adds an item to the data Map.
738 * @param candidateKey The name of the data item.
739 * @param value The value of the data item.
740 * @return this object
741 * @since 2.9
742 */
743 @SuppressWarnings("unchecked")
744 public M with(final String candidateKey, final short value) {
745 final String key = toKey(candidateKey);
746 validate(key, value);
747 data.putValue(key, value);
748 return (M) this;
749 }
750
751 /**
752 * Adds an item to the data Map in fluent style.
753 * @param candidateKey The name of the data item.
754 * @param value The value of the data item.
755 * @return {@code this}
756 */
757 @SuppressWarnings("unchecked")
758 public M with(final String candidateKey, final String value) {
759 final String key = toKey(candidateKey);
760 put(key, value);
761 return (M) this;
762 }
763
764 }