View Javadoc
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 }