    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    }