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.Collections;
20  import java.util.Map;
21  import java.util.TreeMap;
22  
23  import org.apache.logging.log4j.util.EnglishEnums;
24  import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
25  import org.apache.logging.log4j.util.IndexedStringMap;
26  import org.apache.logging.log4j.util.PerformanceSensitive;
27  import org.apache.logging.log4j.util.SortedArrayStringMap;
28  import org.apache.logging.log4j.util.StringBuilderFormattable;
29  import org.apache.logging.log4j.util.StringBuilders;
30  import org.apache.logging.log4j.util.Strings;
31  
32  /**
33   * Represents a Message that consists of a Map.
34   * <p>
35   * Thread-safety note: the contents of this message can be modified after construction.
36   * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is
37   * logged, because it is undefined whether the logged message string will contain the old values or the modified
38   * values.
39   */
40  @PerformanceSensitive("allocation")
41  @AsynchronouslyFormattable
42  public class MapMessage implements MultiformatMessage, StringBuilderFormattable {
43  
44      /**
45       * When set as the format specifier causes the Map to be formatted as XML.
46       */
47  
48      public enum MapFormat {
49          /** The map should be formatted as XML. */
50          XML,
51          /** The map should be formatted as JSON. */
52          JSON,
53          /** The map should be formatted the same as documented by java.util.AbstractMap.toString(). */
54          JAVA;
55  
56          public static MapFormat lookupIgnoreCase(final String format) {
57              return XML.name().equalsIgnoreCase(format) ? XML //
58                      : JSON.name().equalsIgnoreCase(format) ? JSON //
59                      : JAVA.name().equalsIgnoreCase(format) ? JAVA //
60                      : null;
61          }
62  
63          public static String[] names() {
64              return new String[] {XML.name(), JSON.name(), JAVA.name()};
65          }
66      }
67  
68      private static final long serialVersionUID = -5031471831131487120L;
69  
70      private final IndexedStringMap data;
71  
72      /**
73       * Constructor.
74       */
75      public MapMessage() {
76          data = new SortedArrayStringMap();
77      }
78  
79      /**
80       * Constructor based on an existing Map.
81       * @param map The Map.
82       */
83      public MapMessage(final Map<String, String> map) {
84          this.data = new SortedArrayStringMap(map);
85      }
86  
87      @Override
88      public String[] getFormats() {
89          return MapFormat.names();
90      }
91  
92      /**
93       * Returns the data elements as if they were parameters on the logging event.
94       * @return the data elements.
95       */
96      @Override
97      public Object[] getParameters() {
98          final Object[] result = new Object[data.size()];
99          for (int i = 0; i < data.size(); i++) {
100             result[i] = data.getValueAt(i);
101         }
102         return result;
103     }
104 
105     /**
106      * Returns the message.
107      * @return the message.
108      */
109     @Override
110     public String getFormat() {
111         return Strings.EMPTY;
112     }
113 
114     /**
115      * Returns the message data as an unmodifiable Map.
116      * @return the message data as an unmodifiable map.
117      */
118     public Map<String, String> getData() {
119         final TreeMap<String, String> result = new TreeMap<>(); // returned map must be sorted
120         for (int i = 0; i < data.size(); i++) {
121             result.put(data.getKeyAt(i), (String) data.getValueAt(i));
122         }
123         return Collections.unmodifiableMap(result);
124     }
125 
126     /**
127      * Returns a read-only view of the message data.
128      * @return the read-only message data.
129      */
130     public IndexedReadOnlyStringMap getIndexedReadOnlyStringMap() {
131         return data;
132     }
133 
134     /**
135      * Clear the data.
136      */
137     public void clear() {
138         data.clear();
139     }
140 
141     /**
142      * Add an item to the data Map in fluent style.
143      * @param key The name of the data item.
144      * @param value The value of the data item.
145      * @return {@code this}
146      */
147     public MapMessage with(final String key, final String value) {
148         put(key, value);
149         return this;
150     }
151 
152     /**
153      * Add an item to the data Map.
154      * @param key The name of the data item.
155      * @param value The value of the data item.
156      */
157     public void put(final String key, final String value) {
158         if (value == null) {
159             throw new IllegalArgumentException("No value provided for key " + key);
160         }
161         validate(key, value);
162         data.putValue(key, value);
163     }
164 
165     protected void validate(final String key, final String value) {
166 
167     }
168 
169     /**
170      * Add all the elements from the specified Map.
171      * @param map The Map to add.
172      */
173     public void putAll(final Map<String, String> map) {
174         for (final Map.Entry<String, ?> entry : map.entrySet()) {
175             data.putValue(entry.getKey(), entry.getValue());
176         }
177     }
178 
179     /**
180      * Retrieve the value of the element with the specified key or null if the key is not present.
181      * @param key The name of the element.
182      * @return The value of the element or null if the key is not present.
183      */
184     public String get(final String key) {
185         return data.getValue(key);
186     }
187 
188     /**
189      * Remove the element with the specified name.
190      * @param key The name of the element.
191      * @return The previous value of the element.
192      */
193     public String remove(final String key) {
194         final String result = data.getValue(key);
195         data.remove(key);
196         return result;
197     }
198 
199     /**
200      * Format the Structured data as described in RFC 5424.
201      *
202      * @return The formatted String.
203      */
204     public String asString() {
205         return format((MapFormat) null, new StringBuilder()).toString();
206     }
207 
208     public String asString(final String format) {
209         try {
210             return format(EnglishEnums.valueOf(MapFormat.class, format), new StringBuilder()).toString();
211         } catch (final IllegalArgumentException ex) {
212             return asString();
213         }
214     }
215     /**
216      * Format the Structured data as described in RFC 5424.
217      *
218      * @param format The format identifier. Ignored in this implementation.
219      * @return The formatted String.
220      */
221     private StringBuilder format(final MapFormat format, final StringBuilder sb) {
222         if (format == null) {
223             appendMap(sb);
224         } else {
225             switch (format) {
226                 case XML : {
227                     asXml(sb);
228                     break;
229                 }
230                 case JSON : {
231                     asJson(sb);
232                     break;
233                 }
234                 case JAVA : {
235                     asJava(sb);
236                     break;
237                 }
238                 default : {
239                     appendMap(sb);
240                 }
241             }
242         }
243         return sb;
244     }
245 
246     public void asXml(final StringBuilder sb) {
247         sb.append("<Map>\n");
248         for (int i = 0; i < data.size(); i++) {
249             sb.append("  <Entry key=\"").append(data.getKeyAt(i)).append("\">").append(data.getValueAt(i))
250                     .append("</Entry>\n");
251         }
252         sb.append("</Map>");
253     }
254 
255     /**
256      * Format the message and return it.
257      * @return the formatted message.
258      */
259     @Override
260     public String getFormattedMessage() {
261         return asString();
262     }
263 
264     /**
265      *
266      * @param formats An array of Strings that provide extra information about how to format the message.
267      * MapMessage uses the first format specifier it recognizes. The supported formats are XML, JSON, and
268      * JAVA. The default format is key1="value1" key2="value2" as required by RFC 5424 messages.
269      *
270      * @return The formatted message.
271      */
272     @Override
273     public String getFormattedMessage(final String[] formats) {
274         if (formats == null || formats.length == 0) {
275             return asString();
276         }
277         for (int i = 0; i < formats.length; i++) {
278             final MapFormat mapFormat = MapFormat.lookupIgnoreCase(formats[i]);
279             if (mapFormat != null) {
280                 return format(mapFormat, new StringBuilder()).toString();
281             }
282         }
283         return asString();
284 
285     }
286 
287     protected void appendMap(final StringBuilder sb) {
288         for (int i = 0; i < data.size(); i++) {
289             if (i > 0) {
290                 sb.append(' ');
291             }
292             StringBuilders.appendKeyDqValue(sb, data.getKeyAt(i), data.getValueAt(i));
293         }
294     }
295 
296     protected void asJson(final StringBuilder sb) {
297         sb.append('{');
298         for (int i = 0; i < data.size(); i++) {
299             if (i > 0) {
300                 sb.append(", ");
301             }
302             StringBuilders.appendDqValue(sb, data.getKeyAt(i)).append(':');
303             StringBuilders.appendDqValue(sb, data.getValueAt(i));
304         }
305         sb.append('}');
306     }
307 
308 
309     protected void asJava(final StringBuilder sb) {
310         sb.append('{');
311         for (int i = 0; i < data.size(); i++) {
312             if (i > 0) {
313                 sb.append(", ");
314             }
315             StringBuilders.appendKeyDqValue(sb, data.getKeyAt(i), data.getValueAt(i));
316         }
317         sb.append('}');
318     }
319 
320     public MapMessage newInstance(final Map<String, String> map) {
321         return new MapMessage(map);
322     }
323 
324     @Override
325     public String toString() {
326         return asString();
327     }
328 
329     @Override
330     public void formatTo(final StringBuilder buffer) {
331         format((MapFormat) null, buffer);
332     }
333 
334     @Override
335     public boolean equals(final Object o) {
336         if (this == o) {
337             return true;
338         }
339         if (o == null || this.getClass() != o.getClass()) {
340             return false;
341         }
342 
343         final MapMessage that = (MapMessage) o;
344 
345         return this.data.equals(that.data);
346     }
347 
348     @Override
349     public int hashCode() {
350         return data.hashCode();
351     }
352 
353     /**
354      * Always returns null.
355      *
356      * @return null
357      */
358     @Override
359     public Throwable getThrowable() {
360         return null;
361     }
362 }