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);
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);
411 sb.append(Chars.DQUOTE);
412 }
413 }
414
415 protected void asJson(final StringBuilder sb) {
416 MapMessageJsonFormatter.format(sb, data);
417 }
418
419 protected void asJavaUnquoted(final StringBuilder sb) {
420 asJava(sb, false);
421 }
422
423 protected void asJava(final StringBuilder sb) {
424 asJava(sb, true);
425 }
426
427 private void asJava(final StringBuilder sb, boolean quoted) {
428 sb.append('{');
429 for (int i = 0; i < data.size(); i++) {
430 if (i > 0) {
431 sb.append(", ");
432 }
433 sb.append(data.getKeyAt(i)).append(Chars.EQ);
434 if (quoted) {
435 sb.append(Chars.DQUOTE);
436 }
437 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb);
438 if (quoted) {
439 sb.append(Chars.DQUOTE);
440 }
441 }
442 sb.append('}');
443 }
444
445 /**
446 * Constructs a new instance based on an existing Map.
447 * @param map The Map.
448 * @return A new MapMessage
449 */
450 @SuppressWarnings("unchecked")
451 public M newInstance(final Map<String, V> map) {
452 return (M) new MapMessage<>(map);
453 }
454
455 @Override
456 public String toString() {
457 return asString();
458 }
459
460 @Override
461 public void formatTo(final StringBuilder buffer) {
462 format((MapFormat) null, buffer);
463 }
464
465 @Override
466 public void formatTo(final String[] formats, final StringBuilder buffer) {
467 format(getFormat(formats), buffer);
468 }
469
470 @Override
471 public boolean equals(final Object o) {
472 if (this == o) {
473 return true;
474 }
475 if (o == null || this.getClass() != o.getClass()) {
476 return false;
477 }
478
479 final MapMessage<?, ?> that = (MapMessage<?, ?>) o;
480
481 return this.data.equals(that.data);
482 }
483
484 @Override
485 public int hashCode() {
486 return data.hashCode();
487 }
488
489 /**
490 * Always returns null.
491 *
492 * @return null
493 */
494 @Override
495 public Throwable getThrowable() {
496 return null;
497 }
498
499 /**
500 * Default implementation does nothing.
501 *
502 * @since 2.9
503 */
504 protected void validate(final String key, final boolean value) {
505 // do nothing
506 }
507
508 /**
509 * Default implementation does nothing.
510 *
511 * @since 2.9
512 */
513 protected void validate(final String key, final byte value) {
514 // do nothing
515 }
516
517 /**
518 * Default implementation does nothing.
519 *
520 * @since 2.9
521 */
522 protected void validate(final String key, final char value) {
523 // do nothing
524 }
525
526 /**
527 * Default implementation does nothing.
528 *
529 * @since 2.9
530 */
531 protected void validate(final String key, final double value) {
532 // do nothing
533 }
534
535 /**
536 * Default implementation does nothing.
537 *
538 * @since 2.9
539 */
540 protected void validate(final String key, final float value) {
541 // do nothing
542 }
543
544 /**
545 * Default implementation does nothing.
546 *
547 * @since 2.9
548 */
549 protected void validate(final String key, final int value) {
550 // do nothing
551 }
552
553 /**
554 * Default implementation does nothing.
555 *
556 * @since 2.9
557 */
558 protected void validate(final String key, final long value) {
559 // do nothing
560 }
561
562 /**
563 * Default implementation does nothing.
564 *
565 * @since 2.9
566 */
567 protected void validate(final String key, final Object value) {
568 // do nothing
569 }
570
571 /**
572 * Default implementation does nothing.
573 *
574 * @since 2.9
575 */
576 protected void validate(final String key, final short value) {
577 // do nothing
578 }
579
580 /**
581 * Default implementation does nothing.
582 *
583 * @since 2.9
584 */
585 protected void validate(final String key, final String value) {
586 // do nothing
587 }
588
589 /**
590 * Allows subclasses to change a candidate key to an actual key.
591 *
592 * @param candidateKey The candidate key.
593 * @return The candidate key.
594 * @since 2.12
595 */
596 protected String toKey(final String candidateKey) {
597 return candidateKey;
598 }
599
600 /**
601 * Adds an item to the data Map.
602 * @param candidateKey The name of the data item.
603 * @param value The value of the data item.
604 * @return this object
605 * @since 2.9
606 */
607 @SuppressWarnings("unchecked")
608 public M with(final String candidateKey, final boolean value) {
609 final String key = toKey(candidateKey);
610 validate(key, value);
611 data.putValue(key, value);
612 return (M) this;
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 byte 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 char value) {
639 final String key = toKey(candidateKey);
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 candidateKey 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 candidateKey, final double value) {
655 final String key = toKey(candidateKey);
656 validate(key, value);
657 data.putValue(key, value);
658 return (M) this;
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 float 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 int 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 long 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 Object 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 short 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 in fluent style.
738 * @param candidateKey The name of the data item.
739 * @param value The value of the data item.
740 * @return {@code this}
741 */
742 @SuppressWarnings("unchecked")
743 public M with(final String candidateKey, final String value) {
744 final String key = toKey(candidateKey);
745 put(key, value);
746 return (M) this;
747 }
748
749 }