001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.message;
018
019import java.util.Collections;
020import java.util.Map;
021import java.util.SortedMap;
022import java.util.TreeMap;
023
024import org.apache.logging.log4j.util.EnglishEnums;
025import org.apache.logging.log4j.util.Strings;
026
027/**
028 * Represents a Message that consists of a Map.
029 * <p>
030 * Thread-safety note: the contents of this message can be modified after construction.
031 * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is
032 * logged, because it is undefined whether the logged message string will contain the old values or the modified
033 * values.
034 */
035public class MapMessage implements MultiformatMessage {
036    /**
037     * When set as the format specifier causes the Map to be formatted as XML.
038     */
039
040    public enum MapFormat {
041        /** The map should be formatted as XML. */
042        XML,
043        /** The map should be formatted as JSON. */
044        JSON,
045        /** The map should be formatted the same as documented by java.util.AbstractMap.toString(). */
046        JAVA
047    }
048
049    private static final long serialVersionUID = -5031471831131487120L;
050
051    private final SortedMap<String, String> data;
052
053    /**
054     * Constructor.
055     */
056    public MapMessage() {
057        data = new TreeMap<String, String>();
058    }
059
060    /**
061     * Constructor based on an existing Map.
062     * @param map The Map.
063     */
064    public MapMessage(final Map<String, String> map) {
065        this.data = map instanceof SortedMap ? (SortedMap<String, String>) map : new TreeMap<String, String>(map);
066    }
067
068    @Override
069    public String[] getFormats() {
070        final String[] formats = new String[MapFormat.values().length];
071        int i = 0;
072        for (final MapFormat format : MapFormat.values()) {
073            formats[i++] = format.name();
074        }
075        return formats;
076    }
077
078    /**
079     * Returns the data elements as if they were parameters on the logging event.
080     * @return the data elements.
081     */
082    @Override
083    public Object[] getParameters() {
084        return data.values().toArray();
085    }
086
087    /**
088     * Returns the message.
089     * @return the message.
090     */
091    @Override
092    public String getFormat() {
093        return Strings.EMPTY;
094    }
095
096    /**
097     * Returns the message data as an unmodifiable Map.
098     * @return the message data as an unmodifiable map.
099     */
100    public Map<String, String> getData() {
101        return Collections.unmodifiableMap(data);
102    }
103
104    /**
105     * Clear the data.
106     */
107    public void clear() {
108        data.clear();
109    }
110
111    /**
112     * Add an item to the data Map.
113     * @param key The name of the data item.
114     * @param value The value of the data item.
115     */
116    public void put(final String key, final String value) {
117        if (value == null) {
118            throw new IllegalArgumentException("No value provided for key " + key);
119        }
120        validate(key, value);
121        data.put(key, value);
122    }
123
124    protected void validate(final String key, final String value) {
125
126    }
127
128    /**
129     * Add all the elements from the specified Map.
130     * @param map The Map to add.
131     */
132    public void putAll(final Map<String, String> map) {
133        data.putAll(map);
134    }
135
136    /**
137     * Retrieve the value of the element with the specified key or null if the key is not present.
138     * @param key The name of the element.
139     * @return The value of the element or null if the key is not present.
140     */
141    public String get(final String key) {
142        return data.get(key);
143    }
144
145    /**
146     * Remove the element with the specified name.
147     * @param key The name of the element.
148     * @return The previous value of the element.
149     */
150    public String remove(final String key) {
151        return data.remove(key);
152    }
153
154    /**
155     * Format the Structured data as described in RFC 5424.
156     *
157     * @return The formatted String.
158     */
159    public String asString() {
160        return asString((MapFormat) null);
161    }
162
163    public String asString(final String format) {
164        try {
165            return asString(EnglishEnums.valueOf(MapFormat.class, format));
166        } catch (final IllegalArgumentException ex) {
167            return asString();
168        }
169    }
170    /**
171     * Format the Structured data as described in RFC 5424.
172     *
173     * @param format The format identifier. Ignored in this implementation.
174     * @return The formatted String.
175     */
176    private String asString(final MapFormat format) {
177        final StringBuilder sb = new StringBuilder();
178        if (format == null) {
179            appendMap(sb);
180        } else {
181            switch (format) {
182                case XML : {
183                    asXml(sb);
184                    break;
185                }
186                case JSON : {
187                    asJson(sb);
188                    break;
189                }
190                case JAVA : {
191                    asJava(sb);
192                    break;
193                }
194                default : {
195                    appendMap(sb);
196                }
197            }
198        }
199        return sb.toString();
200    }
201
202    public void asXml(final StringBuilder sb) {
203        sb.append("<Map>\n");
204        for (final Map.Entry<String, String> entry : data.entrySet()) {
205            sb.append("  <Entry key=\"").append(entry.getKey()).append("\">").append(entry.getValue())
206              .append("</Entry>\n");
207        }
208        sb.append("</Map>");
209    }
210
211    /**
212     * Format the message and return it.
213     * @return the formatted message.
214     */
215    @Override
216    public String getFormattedMessage() {
217        return asString();
218    }
219
220    /**
221     *
222     * @param formats An array of Strings that provide extra information about how to format the message.
223     * MapMessage uses the first format specifier it recognizes. The supported formats are XML, JSON, and
224     * JAVA. The default format is key1="value1" key2="value2" as required by RFC 5424 messages.
225     *
226     * @return The formatted message.
227     */
228    @Override
229    public String getFormattedMessage(final String[] formats) {
230        if (formats == null || formats.length == 0) {
231            return asString();
232        }
233        for (final String format : formats) {
234            for (final MapFormat mapFormat : MapFormat.values()) {
235                if (mapFormat.name().equalsIgnoreCase(format)) {
236                    return asString(mapFormat);
237                }
238            }
239        }
240        return asString();
241
242    }
243
244    protected void appendMap(final StringBuilder sb) {
245        boolean first = true;
246        for (final Map.Entry<String, String> entry : data.entrySet()) {
247            if (!first) {
248                sb.append(' ');
249            }
250            first = false;
251            sb.append(entry.getKey()).append("=\"").append(entry.getValue()).append('"');
252        }
253    }
254
255    protected void asJson(final StringBuilder sb) {
256        boolean first = true;
257        sb.append('{');
258        for (final Map.Entry<String, String> entry : data.entrySet()) {
259            if (!first) {
260                sb.append(", ");
261            }
262            first = false;
263            sb.append('"').append(entry.getKey()).append("\":");
264            sb.append('"').append(entry.getValue()).append('"');
265        }
266        sb.append('}');
267    }
268
269
270    protected void asJava(final StringBuilder sb) {
271        boolean first = true;
272        sb.append('{');
273        for (final Map.Entry<String, String> entry : data.entrySet()) {
274            if (!first) {
275                sb.append(", ");
276            }
277            first = false;
278            sb.append(entry.getKey()).append("=\"").append(entry.getValue()).append('"');
279        }
280        sb.append('}');
281    }
282
283    public MapMessage newInstance(final Map<String, String> map) {
284        return new MapMessage(map);
285    }
286
287    @Override
288    public String toString() {
289        return asString();
290    }
291
292    @Override
293    public boolean equals(final Object o) {
294        if (this == o) {
295            return true;
296        }
297        if (o == null || this.getClass() != o.getClass()) {
298            return false;
299        }
300
301        final MapMessage that = (MapMessage) o;
302
303        return this.data.equals(that.data);
304    }
305
306    @Override
307    public int hashCode() {
308        return data.hashCode();
309    }
310
311    /**
312     * Always returns null.
313     *
314     * @return null
315     */
316    @Override
317    public Throwable getThrowable() {
318        return null;
319    }
320}