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