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.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     */
035    public 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    }