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<>();
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<>(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 in fluent style.
114     * @param key The name of the data item.
115     * @param value The value of the data item.
116     * @return {@code this}
117     */
118    public MapMessage with(final String key, final String value) {
119        put(key, value);
120        return this;
121    }
122
123    /**
124     * Add an item to the data Map.
125     * @param key The name of the data item.
126     * @param value The value of the data item.
127     */
128    public void put(final String key, final String value) {
129        if (value == null) {
130            throw new IllegalArgumentException("No value provided for key " + key);
131        }
132        validate(key, value);
133        data.put(key, value);
134    }
135
136    protected void validate(final String key, final String value) {
137
138    }
139
140    /**
141     * Add all the elements from the specified Map.
142     * @param map The Map to add.
143     */
144    public void putAll(final Map<String, String> map) {
145        data.putAll(map);
146    }
147
148    /**
149     * Retrieve the value of the element with the specified key or null if the key is not present.
150     * @param key The name of the element.
151     * @return The value of the element or null if the key is not present.
152     */
153    public String get(final String key) {
154        return data.get(key);
155    }
156
157    /**
158     * Remove the element with the specified name.
159     * @param key The name of the element.
160     * @return The previous value of the element.
161     */
162    public String remove(final String key) {
163        return data.remove(key);
164    }
165
166    /**
167     * Format the Structured data as described in RFC 5424.
168     *
169     * @return The formatted String.
170     */
171    public String asString() {
172        return asString((MapFormat) null);
173    }
174
175    public String asString(final String format) {
176        try {
177            return asString(EnglishEnums.valueOf(MapFormat.class, format));
178        } catch (final IllegalArgumentException ex) {
179            return asString();
180        }
181    }
182    /**
183     * Format the Structured data as described in RFC 5424.
184     *
185     * @param format The format identifier. Ignored in this implementation.
186     * @return The formatted String.
187     */
188    private String asString(final MapFormat format) {
189        final StringBuilder sb = new StringBuilder();
190        if (format == null) {
191            appendMap(sb);
192        } else {
193            switch (format) {
194                case XML : {
195                    asXml(sb);
196                    break;
197                }
198                case JSON : {
199                    asJson(sb);
200                    break;
201                }
202                case JAVA : {
203                    asJava(sb);
204                    break;
205                }
206                default : {
207                    appendMap(sb);
208                }
209            }
210        }
211        return sb.toString();
212    }
213
214    public void asXml(final StringBuilder sb) {
215        sb.append("<Map>\n");
216        for (final Map.Entry<String, String> entry : data.entrySet()) {
217            sb.append("  <Entry key=\"").append(entry.getKey()).append("\">").append(entry.getValue())
218              .append("</Entry>\n");
219        }
220        sb.append("</Map>");
221    }
222
223    /**
224     * Format the message and return it.
225     * @return the formatted message.
226     */
227    @Override
228    public String getFormattedMessage() {
229        return asString();
230    }
231
232    /**
233     *
234     * @param formats An array of Strings that provide extra information about how to format the message.
235     * MapMessage uses the first format specifier it recognizes. The supported formats are XML, JSON, and
236     * JAVA. The default format is key1="value1" key2="value2" as required by RFC 5424 messages.
237     *
238     * @return The formatted message.
239     */
240    @Override
241    public String getFormattedMessage(final String[] formats) {
242        if (formats == null || formats.length == 0) {
243            return asString();
244        }
245        for (final String format : formats) {
246            for (final MapFormat mapFormat : MapFormat.values()) {
247                if (mapFormat.name().equalsIgnoreCase(format)) {
248                    return asString(mapFormat);
249                }
250            }
251        }
252        return asString();
253
254    }
255
256    protected void appendMap(final StringBuilder sb) {
257        boolean first = true;
258        for (final Map.Entry<String, String> entry : data.entrySet()) {
259            if (!first) {
260                sb.append(' ');
261            }
262            first = false;
263            StringBuilders.appendKeyDqValue(sb, entry);
264        }
265    }
266
267    protected void asJson(final StringBuilder sb) {
268        boolean first = true;
269        sb.append('{');
270        for (final Map.Entry<String, String> entry : data.entrySet()) {
271            if (!first) {
272                sb.append(", ");
273            }
274            first = false;
275            StringBuilders.appendDqValue(sb, entry.getKey()).append(':');
276            StringBuilders.appendDqValue(sb, entry.getValue());
277        }
278        sb.append('}');
279    }
280
281
282    protected void asJava(final StringBuilder sb) {
283        boolean first = true;
284        sb.append('{');
285        for (final Map.Entry<String, String> entry : data.entrySet()) {
286            if (!first) {
287                sb.append(", ");
288            }
289            first = false;
290            StringBuilders.appendKeyDqValue(sb, entry);
291        }
292        sb.append('}');
293    }
294
295    public MapMessage newInstance(final Map<String, String> map) {
296        return new MapMessage(map);
297    }
298
299    @Override
300    public String toString() {
301        return asString();
302    }
303
304    @Override
305    public boolean equals(final Object o) {
306        if (this == o) {
307            return true;
308        }
309        if (o == null || this.getClass() != o.getClass()) {
310            return false;
311        }
312
313        final MapMessage that = (MapMessage) o;
314
315        return this.data.equals(that.data);
316    }
317
318    @Override
319    public int hashCode() {
320        return data.hashCode();
321    }
322
323    /**
324     * Always returns null.
325     *
326     * @return null
327     */
328    @Override
329    public Throwable getThrowable() {
330        return null;
331    }
332}