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.AbstractMap;
020import java.util.Collections;
021import java.util.Map;
022import java.util.TreeMap;
023
024import org.apache.logging.log4j.util.BiConsumer;
025import org.apache.logging.log4j.util.Chars;
026import org.apache.logging.log4j.util.EnglishEnums;
027import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
028import org.apache.logging.log4j.util.IndexedStringMap;
029import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable;
030import org.apache.logging.log4j.util.PerformanceSensitive;
031import org.apache.logging.log4j.util.ReadOnlyStringMap;
032import org.apache.logging.log4j.util.SortedArrayStringMap;
033import org.apache.logging.log4j.util.StringBuilders;
034import org.apache.logging.log4j.util.Strings;
035import org.apache.logging.log4j.util.TriConsumer;
036
037/**
038 * Represents a Message that consists of a Map.
039 * <p>
040 * Thread-safety note: the contents of this message can be modified after construction.
041 * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is
042 * logged, because it is undefined whether the logged message string will contain the old values or the modified
043 * values.
044 * </p>
045 * <p>
046 * This class was pulled up from {@link StringMapMessage} to allow for Objects as values.
047 * </p>
048 * @param <M> Allow subclasses to use fluent APIs and override methods that return instances of subclasses.
049 * @param <V> The value type
050 */
051@PerformanceSensitive("allocation")
052@AsynchronouslyFormattable
053public class MapMessage<M extends MapMessage<M, V>, V> implements MultiFormatStringBuilderFormattable {
054
055    private static final long serialVersionUID = -5031471831131487120L;
056
057    /**
058     * When set as the format specifier causes the Map to be formatted as XML.
059     */
060    public enum MapFormat {
061
062        /** The map should be formatted as XML. */
063        XML,
064
065        /** The map should be formatted as JSON. */
066        JSON,
067
068        /** The map should be formatted the same as documented by {@link AbstractMap#toString()}. */
069        JAVA,
070
071        /**
072         * The map should be formatted the same as documented by {@link AbstractMap#toString()} but without quotes.
073         *
074         * @since 2.11.2
075         */
076        JAVA_UNQUOTED;
077
078        /**
079         * Maps a format name to an {@link MapFormat} while ignoring case.
080         *
081         * @param format a MapFormat name
082         * @return a MapFormat
083         */
084        public static MapFormat lookupIgnoreCase(final String format) {
085            return XML.name().equalsIgnoreCase(format) ? XML //
086                    : JSON.name().equalsIgnoreCase(format) ? JSON //
087                    : JAVA.name().equalsIgnoreCase(format) ? JAVA //
088                    : JAVA_UNQUOTED.name().equalsIgnoreCase(format) ? JAVA_UNQUOTED //
089                    : null;
090        }
091
092        /**
093         * All {@code MapFormat} names.
094         *
095         * @return All {@code MapFormat} names.
096         */
097        public static String[] names() {
098            return new String[] {XML.name(), JSON.name(), JAVA.name(), JAVA_UNQUOTED.name()};
099        }
100    }
101
102    private final IndexedStringMap data;
103
104    /**
105     * Constructs a new instance.
106     */
107    public MapMessage() {
108        this.data = new SortedArrayStringMap();
109    }
110
111    /**
112     * Constructs a new instance.
113     *
114     * @param  initialCapacity the initial capacity.
115     */
116    public MapMessage(final int initialCapacity) {
117        this.data = new SortedArrayStringMap(initialCapacity);
118    }
119
120    /**
121     * Constructs a new instance based on an existing {@link Map}.
122     * @param map The Map.
123     */
124    public MapMessage(final Map<String, V> map) {
125        this.data = new SortedArrayStringMap(map);
126    }
127
128    @Override
129    public String[] getFormats() {
130        return MapFormat.names();
131    }
132
133    /**
134     * Returns the data elements as if they were parameters on the logging event.
135     * @return the data elements.
136     */
137    @Override
138    public Object[] getParameters() {
139        final Object[] result = new Object[data.size()];
140        for (int i = 0; i < data.size(); i++) {
141            result[i] = data.getValueAt(i);
142        }
143        return result;
144    }
145
146    /**
147     * Returns the message.
148     * @return the message.
149     */
150    @Override
151    public String getFormat() {
152        return Strings.EMPTY;
153    }
154
155    /**
156     * Returns the message data as an unmodifiable Map.
157     * @return the message data as an unmodifiable map.
158     */
159    @SuppressWarnings("unchecked")
160    public Map<String, V> getData() {
161        final TreeMap<String, V> result = new TreeMap<>(); // returned map must be sorted
162        for (int i = 0; i < data.size(); i++) {
163            // The Eclipse compiler does not need the typecast to V, but the Oracle compiler sure does.
164            result.put(data.getKeyAt(i), (V) data.getValueAt(i));
165        }
166        return Collections.unmodifiableMap(result);
167    }
168
169    /**
170     * Returns a read-only view of the message data.
171     * @return the read-only message data.
172     */
173    public IndexedReadOnlyStringMap getIndexedReadOnlyStringMap() {
174        return data;
175    }
176
177    /**
178     * Clear the data.
179     */
180    public void clear() {
181        data.clear();
182    }
183
184    /**
185     * Returns {@code true} if this data structure contains the specified key, {@code false} otherwise.
186     *
187     * @param key the key whose presence to check. May be {@code null}.
188     * @return {@code true} if this data structure contains the specified key, {@code false} otherwise
189     * @since 2.9
190     */
191    public boolean containsKey(final String key) {
192        return data.containsKey(key);
193    }
194
195    /**
196     * Adds an item to the data Map.
197     * @param candidateKey The name of the data item.
198     * @param value The value of the data item.
199     */
200    public void put(final String candidateKey, final String value) {
201        if (value == null) {
202            throw new IllegalArgumentException("No value provided for key " + candidateKey);
203        }
204        final String key = toKey(candidateKey);
205        validate(key, value);
206        data.putValue(key, value);
207    }
208
209    /**
210     * Adds all the elements from the specified Map.
211     * @param map The Map to add.
212     */
213    public void putAll(final Map<String, String> map) {
214        for (final Map.Entry<String, String> entry : map.entrySet()) {
215            data.putValue(entry.getKey(), entry.getValue());
216        }
217    }
218
219    /**
220     * Retrieves the value of the element with the specified key or null if the key is not present.
221     * @param key The name of the element.
222     * @return The value of the element or null if the key is not present.
223     */
224    public String get(final String key) {
225        final Object result = data.getValue(key);
226        return ParameterFormatter.deepToString(result);
227    }
228
229    /**
230     * Removes the element with the specified name.
231     * @param key The name of the element.
232     * @return The previous value of the element.
233     */
234    public String remove(final String key) {
235        final String result = get(key);
236        data.remove(key);
237        return result;
238    }
239
240    /**
241     * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.
242     *
243     * @return The formatted String.
244     */
245    public String asString() {
246        return format((MapFormat) null, new StringBuilder()).toString();
247    }
248
249    /**
250     * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.
251     *
252     * @param format The format identifier.
253     * @return The formatted String.
254     */
255    public String asString(final String format) {
256        try {
257            return format(EnglishEnums.valueOf(MapFormat.class, format), new StringBuilder()).toString();
258        } catch (final IllegalArgumentException ex) {
259            return asString();
260        }
261    }
262
263    /**
264     * Performs the given action for each key-value pair in this data structure
265     * until all entries have been processed or the action throws an exception.
266     * <p>
267     * Some implementations may not support structural modifications (adding new elements or removing elements) while
268     * iterating over the contents. In such implementations, attempts to add or remove elements from the
269     * {@code BiConsumer}'s {@link BiConsumer#accept(Object, Object)} accept} method may cause a
270     * {@code ConcurrentModificationException} to be thrown.
271     * </p>
272     *
273     * @param action The action to be performed for each key-value pair in this collection
274     * @param <CV> type of the consumer value
275     * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications
276     *          to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or
277     *          {@link #forEach(TriConsumer, Object)}.
278     * @see ReadOnlyStringMap#forEach(BiConsumer)
279     * @since 2.9
280     */
281    public <CV> void forEach(final BiConsumer<String, ? super CV> action) {
282        data.forEach(action);
283    }
284
285    /**
286     * Performs the given action for each key-value pair in this data structure
287     * until all entries have been processed or the action throws an exception.
288     * <p>
289     * The third parameter lets callers pass in a stateful object to be modified with the key-value pairs,
290     * so the TriConsumer implementation itself can be stateless and potentially reusable.
291     * </p>
292     * <p>
293     * Some implementations may not support structural modifications (adding new elements or removing elements) while
294     * iterating over the contents. In such implementations, attempts to add or remove elements from the
295     * {@code TriConsumer}'s {@link TriConsumer#accept(Object, Object, Object) accept} method may cause a
296     * {@code ConcurrentModificationException} to be thrown.
297     * </p>
298     *
299     * @param action The action to be performed for each key-value pair in this collection
300     * @param state the object to be passed as the third parameter to each invocation on the specified
301     *          triconsumer
302     * @param <CV> type of the consumer value
303     * @param <S> type of the third parameter
304     * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications
305     *          to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or
306     *          {@link #forEach(TriConsumer, Object)}.
307     * @see ReadOnlyStringMap#forEach(TriConsumer, Object)
308     * @since 2.9
309     */
310    public <CV, S> void forEach(final TriConsumer<String, ? super CV, S> action, final S state) {
311        data.forEach(action, state);
312    }
313
314    /**
315     * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.
316     *
317     * @param format The format identifier.
318     * @return The formatted String.
319     */
320    private StringBuilder format(final MapFormat format, final StringBuilder sb) {
321        if (format == null) {
322            appendMap(sb);
323        } else {
324            switch (format) {
325                case XML : {
326                    asXml(sb);
327                    break;
328                }
329                case JSON : {
330                    asJson(sb);
331                    break;
332                }
333                case JAVA : {
334                    asJava(sb);
335                    break;
336                }
337                case JAVA_UNQUOTED:
338                    asJavaUnquoted(sb);
339                    break;
340                default : {
341                    appendMap(sb);
342                }
343            }
344        }
345        return sb;
346    }
347
348    /**
349     * Formats this message as an XML fragment String into the given builder.
350     *
351     * @param sb format into this builder.
352     */
353    public void asXml(final StringBuilder sb) {
354        sb.append("<Map>\n");
355        for (int i = 0; i < data.size(); i++) {
356            sb.append("  <Entry key=\"")
357                    .append(data.getKeyAt(i))
358                    .append("\">");
359            final int size = sb.length();
360            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
361            StringBuilders.escapeXml(sb, size);
362            sb.append("</Entry>\n");
363        }
364        sb.append("</Map>");
365    }
366
367    /**
368     * Formats the message and return it.
369     * @return the formatted message.
370     */
371    @Override
372    public String getFormattedMessage() {
373        return asString();
374    }
375
376    /**
377     *
378     * @param formats
379     *            An array of Strings that provide extra information about how to format the message. MapMessage uses
380     *            the first format specifier it recognizes. The supported formats are XML, JSON, and JAVA. The default
381     *            format is key1="value1" key2="value2" as required by <a href="https://tools.ietf.org/html/rfc5424">RFC
382     *            5424</a> messages.
383     *
384     * @return The formatted message.
385     */
386    @Override
387    public String getFormattedMessage(final String[] formats) {
388        return format(getFormat(formats), new StringBuilder()).toString();
389    }
390
391    private MapFormat getFormat(final String[] formats) {
392        if (formats == null || formats.length == 0) {
393            return null;
394        }
395        for (int i = 0; i < formats.length; i++) {
396            final MapFormat mapFormat = MapFormat.lookupIgnoreCase(formats[i]);
397            if (mapFormat != null) {
398                return mapFormat;
399            }
400        }
401        return null;
402    }
403
404    protected void appendMap(final StringBuilder sb) {
405        for (int i = 0; i < data.size(); i++) {
406            if (i > 0) {
407                sb.append(' ');
408            }
409            sb.append(data.getKeyAt(i)).append(Chars.EQ).append(Chars.DQUOTE);
410            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
411            sb.append(Chars.DQUOTE);
412        }
413    }
414
415    protected void asJson(final StringBuilder sb) {
416        sb.append('{');
417        for (int i = 0; i < data.size(); i++) {
418            if (i > 0) {
419                sb.append(", ");
420            }
421            sb.append(Chars.DQUOTE);
422            int start = sb.length();
423            sb.append(data.getKeyAt(i));
424            StringBuilders.escapeJson(sb, start);
425            sb.append(Chars.DQUOTE).append(':').append(Chars.DQUOTE);
426            start = sb.length();
427            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
428            StringBuilders.escapeJson(sb, start);
429            sb.append(Chars.DQUOTE);
430        }
431        sb.append('}');
432    }
433
434    protected void asJavaUnquoted(final StringBuilder sb) {
435        asJava(sb, false);
436    }
437
438    protected void asJava(final StringBuilder sb) {
439        asJava(sb, true);
440    }
441
442    private void asJava(final StringBuilder sb, boolean quoted) {
443        sb.append('{');
444        for (int i = 0; i < data.size(); i++) {
445            if (i > 0) {
446                sb.append(", ");
447            }
448            sb.append(data.getKeyAt(i)).append(Chars.EQ);
449            if (quoted) {
450                sb.append(Chars.DQUOTE);
451            }
452            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
453            if (quoted) {
454                sb.append(Chars.DQUOTE);
455            }
456        }
457        sb.append('}');
458    }
459
460    /**
461     * Constructs a new instance based on an existing Map.
462     * @param map The Map.
463     * @return A new MapMessage
464     */
465    @SuppressWarnings("unchecked")
466    public M newInstance(final Map<String, V> map) {
467        return (M) new MapMessage<>(map);
468    }
469
470    @Override
471    public String toString() {
472        return asString();
473    }
474
475    @Override
476    public void formatTo(final StringBuilder buffer) {
477        format((MapFormat) null, buffer);
478    }
479
480    @Override
481    public void formatTo(final String[] formats, final StringBuilder buffer) {
482        format(getFormat(formats), buffer);
483    }
484
485    @Override
486    public boolean equals(final Object o) {
487        if (this == o) {
488            return true;
489        }
490        if (o == null || this.getClass() != o.getClass()) {
491            return false;
492        }
493
494        final MapMessage<?, ?> that = (MapMessage<?, ?>) o;
495
496        return this.data.equals(that.data);
497    }
498
499    @Override
500    public int hashCode() {
501        return data.hashCode();
502    }
503
504    /**
505     * Always returns null.
506     *
507     * @return null
508     */
509    @Override
510    public Throwable getThrowable() {
511        return null;
512    }
513
514    /**
515     * Default implementation does nothing.
516     *
517     * @since 2.9
518     */
519    protected void validate(final String key, final boolean value) {
520        // do nothing
521    }
522
523    /**
524     * Default implementation does nothing.
525     *
526     * @since 2.9
527     */
528    protected void validate(final String key, final byte value) {
529        // do nothing
530    }
531
532    /**
533     * Default implementation does nothing.
534     *
535     * @since 2.9
536     */
537    protected void validate(final String key, final char value) {
538        // do nothing
539    }
540
541    /**
542     * Default implementation does nothing.
543     *
544     * @since 2.9
545     */
546    protected void validate(final String key, final double value) {
547        // do nothing
548    }
549
550    /**
551     * Default implementation does nothing.
552     *
553     * @since 2.9
554     */
555    protected void validate(final String key, final float value) {
556        // do nothing
557    }
558
559    /**
560     * Default implementation does nothing.
561     *
562     * @since 2.9
563     */
564    protected void validate(final String key, final int value) {
565        // do nothing
566    }
567
568    /**
569     * Default implementation does nothing.
570     *
571     * @since 2.9
572     */
573    protected void validate(final String key, final long value) {
574        // do nothing
575    }
576
577    /**
578     * Default implementation does nothing.
579     *
580     * @since 2.9
581     */
582    protected void validate(final String key, final Object value) {
583        // do nothing
584    }
585
586    /**
587     * Default implementation does nothing.
588     *
589     * @since 2.9
590     */
591    protected void validate(final String key, final short value) {
592        // do nothing
593    }
594
595    /**
596     * Default implementation does nothing.
597     *
598     * @since 2.9
599     */
600    protected void validate(final String key, final String value) {
601        // do nothing
602    }
603
604    /**
605     * Allows subclasses to change a candidate key to an actual key.
606     *
607     * @param candidateKey The candidate key.
608     * @return The candidate key.
609     * @since 2.12
610     */
611    protected String toKey(final String candidateKey) {
612        return candidateKey;
613    }
614
615    /**
616     * Adds an item to the data Map.
617     * @param candidateKey The name of the data item.
618     * @param value The value of the data item.
619     * @return this object
620     * @since 2.9
621     */
622    @SuppressWarnings("unchecked")
623    public M with(final String candidateKey, final boolean value) {
624        final String key = toKey(candidateKey);
625        validate(key, value);
626        data.putValue(key, value);
627        return (M) this;
628    }
629
630    /**
631     * Adds an item to the data Map.
632     * @param candidateKey The name of the data item.
633     * @param value The value of the data item.
634     * @return this object
635     * @since 2.9
636     */
637    @SuppressWarnings("unchecked")
638    public M with(final String candidateKey, final byte value) {
639        final String key = toKey(candidateKey);
640        validate(key, value);
641        data.putValue(key, value);
642        return (M) this;
643    }
644
645    /**
646     * Adds an item to the data Map.
647     * @param candidateKey The name of the data item.
648     * @param value The value of the data item.
649     * @return this object
650     * @since 2.9
651     */
652    @SuppressWarnings("unchecked")
653    public M with(final String candidateKey, final char value) {
654        final String key = toKey(candidateKey);
655        validate(key, value);
656        data.putValue(key, value);
657        return (M) this;
658    }
659
660
661    /**
662     * Adds an item to the data Map.
663     * @param candidateKey The name of the data item.
664     * @param value The value of the data item.
665     * @return this object
666     * @since 2.9
667     */
668    @SuppressWarnings("unchecked")
669    public M with(final String candidateKey, final double value) {
670        final String key = toKey(candidateKey);
671        validate(key, value);
672        data.putValue(key, value);
673        return (M) this;
674    }
675
676    /**
677     * Adds an item to the data Map.
678     * @param candidateKey The name of the data item.
679     * @param value The value of the data item.
680     * @return this object
681     * @since 2.9
682     */
683    @SuppressWarnings("unchecked")
684    public M with(final String candidateKey, final float value) {
685        final String key = toKey(candidateKey);
686        validate(key, value);
687        data.putValue(key, value);
688        return (M) this;
689    }
690
691    /**
692     * Adds an item to the data Map.
693     * @param candidateKey The name of the data item.
694     * @param value The value of the data item.
695     * @return this object
696     * @since 2.9
697     */
698    @SuppressWarnings("unchecked")
699    public M with(final String candidateKey, final int value) {
700        final String key = toKey(candidateKey);
701        validate(key, value);
702        data.putValue(key, value);
703        return (M) this;
704    }
705
706    /**
707     * Adds an item to the data Map.
708     * @param candidateKey The name of the data item.
709     * @param value The value of the data item.
710     * @return this object
711     * @since 2.9
712     */
713    @SuppressWarnings("unchecked")
714    public M with(final String candidateKey, final long value) {
715        final String key = toKey(candidateKey);
716        validate(key, value);
717        data.putValue(key, value);
718        return (M) this;
719    }
720
721    /**
722     * Adds an item to the data Map.
723     * @param candidateKey The name of the data item.
724     * @param value The value of the data item.
725     * @return this object
726     * @since 2.9
727     */
728    @SuppressWarnings("unchecked")
729    public M with(final String candidateKey, final Object value) {
730        final String key = toKey(candidateKey);
731        validate(key, value);
732        data.putValue(key, value);
733        return (M) this;
734    }
735
736    /**
737     * Adds an item to the data Map.
738     * @param candidateKey The name of the data item.
739     * @param value The value of the data item.
740     * @return this object
741     * @since 2.9
742     */
743    @SuppressWarnings("unchecked")
744    public M with(final String candidateKey, final short value) {
745        final String key = toKey(candidateKey);
746        validate(key, value);
747        data.putValue(key, value);
748        return (M) this;
749    }
750
751    /**
752     * Adds an item to the data Map in fluent style.
753     * @param candidateKey The name of the data item.
754     * @param value The value of the data item.
755     * @return {@code this}
756     */
757    @SuppressWarnings("unchecked")
758    public M with(final String candidateKey, final String value) {
759        final String key = toKey(candidateKey);
760        put(key, value);
761        return (M) this;
762    }
763
764}