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