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