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