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.TreeMap;
022
023import org.apache.logging.log4j.util.EnglishEnums;
024import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
025import org.apache.logging.log4j.util.IndexedStringMap;
026import org.apache.logging.log4j.util.PerformanceSensitive;
027import org.apache.logging.log4j.util.SortedArrayStringMap;
028import org.apache.logging.log4j.util.StringBuilderFormattable;
029import org.apache.logging.log4j.util.StringBuilders;
030import org.apache.logging.log4j.util.Strings;
031
032/**
033 * Represents a Message that consists of a Map.
034 * <p>
035 * Thread-safety note: the contents of this message can be modified after construction.
036 * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is
037 * logged, because it is undefined whether the logged message string will contain the old values or the modified
038 * values.
039 */
040@PerformanceSensitive("allocation")
041@AsynchronouslyFormattable
042public class MapMessage implements MultiformatMessage, StringBuilderFormattable {
043
044    /**
045     * When set as the format specifier causes the Map to be formatted as XML.
046     */
047
048    public enum MapFormat {
049        /** The map should be formatted as XML. */
050        XML,
051        /** The map should be formatted as JSON. */
052        JSON,
053        /** The map should be formatted the same as documented by java.util.AbstractMap.toString(). */
054        JAVA;
055
056        public static MapFormat lookupIgnoreCase(final String format) {
057            return XML.name().equalsIgnoreCase(format) ? XML //
058                    : JSON.name().equalsIgnoreCase(format) ? JSON //
059                    : JAVA.name().equalsIgnoreCase(format) ? JAVA //
060                    : null;
061        }
062
063        public static String[] names() {
064            return new String[] {XML.name(), JSON.name(), JAVA.name()};
065        }
066    }
067
068    private static final long serialVersionUID = -5031471831131487120L;
069
070    private final IndexedStringMap data;
071
072    /**
073     * Constructor.
074     */
075    public MapMessage() {
076        data = new SortedArrayStringMap();
077    }
078
079    /**
080     * Constructor based on an existing Map.
081     * @param map The Map.
082     */
083    public MapMessage(final Map<String, String> map) {
084        this.data = new SortedArrayStringMap(map);
085    }
086
087    @Override
088    public String[] getFormats() {
089        return MapFormat.names();
090    }
091
092    /**
093     * Returns the data elements as if they were parameters on the logging event.
094     * @return the data elements.
095     */
096    @Override
097    public Object[] getParameters() {
098        final Object[] result = new Object[data.size()];
099        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}