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     */
017    package org.apache.logging.log4j.message;
018    
019    import java.util.Collections;
020    import java.util.Map;
021    import java.util.SortedMap;
022    import java.util.TreeMap;
023    
024    import org.apache.logging.log4j.util.EnglishEnums;
025    import org.apache.logging.log4j.util.StringBuilders;
026    import 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     */
036    public 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    }