| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache license, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the license for the specific language governing permissions and |
| * limitations under the license. |
| */ |
| package org.apache.logging.log4j.message; |
| |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.TreeMap; |
| |
| import org.apache.logging.log4j.util.BiConsumer; |
| import org.apache.logging.log4j.util.Chars; |
| import org.apache.logging.log4j.util.EnglishEnums; |
| import org.apache.logging.log4j.util.IndexedReadOnlyStringMap; |
| import org.apache.logging.log4j.util.IndexedStringMap; |
| import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable; |
| import org.apache.logging.log4j.util.PerformanceSensitive; |
| import org.apache.logging.log4j.util.ReadOnlyStringMap; |
| import org.apache.logging.log4j.util.SortedArrayStringMap; |
| import org.apache.logging.log4j.util.StringBuilders; |
| import org.apache.logging.log4j.util.Strings; |
| import org.apache.logging.log4j.util.TriConsumer; |
| |
| /** |
| * Represents a Message that consists of a Map. |
| * <p> |
| * Thread-safety note: the contents of this message can be modified after construction. |
| * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is |
| * logged, because it is undefined whether the logged message string will contain the old values or the modified |
| * values. |
| * </p> |
| * <p> |
| * This class was pulled up from {@link StringMapMessage} to allow for Objects as values. |
| * </p> |
| * @param <M> Allow subclasses to use fluent APIs and override methods that return instances of subclasses. |
| * @param <V> The value type |
| */ |
| @PerformanceSensitive("allocation") |
| @AsynchronouslyFormattable |
| public class MapMessage<M extends MapMessage<M, V>, V> implements MultiFormatStringBuilderFormattable { |
| |
| private static final long serialVersionUID = -5031471831131487120L; |
| |
| /** |
| * When set as the format specifier causes the Map to be formatted as XML. |
| */ |
| public enum MapFormat { |
| |
| /** The map should be formatted as XML. */ |
| XML, |
| |
| /** The map should be formatted as JSON. */ |
| JSON, |
| |
| /** The map should be formatted the same as documented by java.util.AbstractMap.toString(). */ |
| JAVA; |
| |
| /** |
| * Maps a format name to an {@link MapFormat} while ignoring case. |
| * |
| * @param format a MapFormat name |
| * @return a MapFormat |
| */ |
| public static MapFormat lookupIgnoreCase(final String format) { |
| return XML.name().equalsIgnoreCase(format) ? XML // |
| : JSON.name().equalsIgnoreCase(format) ? JSON // |
| : JAVA.name().equalsIgnoreCase(format) ? JAVA // |
| : null; |
| } |
| |
| /** |
| * All {@code MapFormat} names. |
| * |
| * @return All {@code MapFormat} names. |
| */ |
| public static String[] names() { |
| return new String[] {XML.name(), JSON.name(), JAVA.name()}; |
| } |
| } |
| |
| private final IndexedStringMap data; |
| |
| /** |
| * Constructs a new instance. |
| */ |
| public MapMessage() { |
| this.data = new SortedArrayStringMap(); |
| } |
| |
| /** |
| * Constructs a new instance. |
| * |
| * @param initialCapacity the initial capacity. |
| */ |
| public MapMessage(final int initialCapacity) { |
| this.data = new SortedArrayStringMap(initialCapacity); |
| } |
| |
| /** |
| * Constructs a new instance based on an existing {@link Map}. |
| * @param map The Map. |
| */ |
| public MapMessage(final Map<String, V> map) { |
| this.data = new SortedArrayStringMap(map); |
| } |
| |
| @Override |
| public String[] getFormats() { |
| return MapFormat.names(); |
| } |
| |
| /** |
| * Returns the data elements as if they were parameters on the logging event. |
| * @return the data elements. |
| */ |
| @Override |
| public Object[] getParameters() { |
| final Object[] result = new Object[data.size()]; |
| for (int i = 0; i < data.size(); i++) { |
| result[i] = data.getValueAt(i); |
| } |
| return result; |
| } |
| |
| /** |
| * Returns the message. |
| * @return the message. |
| */ |
| @Override |
| public String getFormat() { |
| return Strings.EMPTY; |
| } |
| |
| /** |
| * Returns the message data as an unmodifiable Map. |
| * @return the message data as an unmodifiable map. |
| */ |
| @SuppressWarnings("unchecked") |
| public Map<String, V> getData() { |
| final TreeMap<String, V> result = new TreeMap<>(); // returned map must be sorted |
| for (int i = 0; i < data.size(); i++) { |
| // The Eclipse compiler does not need the typecast to V, but the Oracle compiler sure does. |
| result.put(data.getKeyAt(i), (V) data.getValueAt(i)); |
| } |
| return Collections.unmodifiableMap(result); |
| } |
| |
| /** |
| * Returns a read-only view of the message data. |
| * @return the read-only message data. |
| */ |
| public IndexedReadOnlyStringMap getIndexedReadOnlyStringMap() { |
| return data; |
| } |
| |
| /** |
| * Clear the data. |
| */ |
| public void clear() { |
| data.clear(); |
| } |
| |
| /** |
| * Returns {@code true} if this data structure contains the specified key, {@code false} otherwise. |
| * |
| * @param key the key whose presence to check. May be {@code null}. |
| * @return {@code true} if this data structure contains the specified key, {@code false} otherwise |
| * @since 2.9 |
| */ |
| public boolean containsKey(final String key) { |
| return data.containsKey(key); |
| } |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| */ |
| public void put(final String key, final String value) { |
| if (value == null) { |
| throw new IllegalArgumentException("No value provided for key " + key); |
| } |
| validate(key, value); |
| data.putValue(key, value); |
| } |
| |
| /** |
| * Adds all the elements from the specified Map. |
| * @param map The Map to add. |
| */ |
| public void putAll(final Map<String, String> map) { |
| for (final Map.Entry<String, String> entry : map.entrySet()) { |
| data.putValue(entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| /** |
| * Retrieves the value of the element with the specified key or null if the key is not present. |
| * @param key The name of the element. |
| * @return The value of the element or null if the key is not present. |
| */ |
| public String get(final String key) { |
| Object result = data.getValue(key); |
| return ParameterFormatter.deepToString(result); |
| } |
| |
| /** |
| * Removes the element with the specified name. |
| * @param key The name of the element. |
| * @return The previous value of the element. |
| */ |
| public String remove(final String key) { |
| final String result = get(key); |
| data.remove(key); |
| return result; |
| } |
| |
| /** |
| * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>. |
| * |
| * @return The formatted String. |
| */ |
| public String asString() { |
| return format((MapFormat) null, new StringBuilder()).toString(); |
| } |
| |
| /** |
| * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>. |
| * |
| * @param format The format identifier. |
| * @return The formatted String. |
| */ |
| public String asString(final String format) { |
| try { |
| return format(EnglishEnums.valueOf(MapFormat.class, format), new StringBuilder()).toString(); |
| } catch (final IllegalArgumentException ex) { |
| return asString(); |
| } |
| } |
| |
| /** |
| * Performs the given action for each key-value pair in this data structure |
| * until all entries have been processed or the action throws an exception. |
| * <p> |
| * Some implementations may not support structural modifications (adding new elements or removing elements) while |
| * iterating over the contents. In such implementations, attempts to add or remove elements from the |
| * {@code BiConsumer}'s {@link BiConsumer#accept(Object, Object)} accept} method may cause a |
| * {@code ConcurrentModificationException} to be thrown. |
| * </p> |
| * |
| * @param action The action to be performed for each key-value pair in this collection |
| * @param <CV> type of the consumer value |
| * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications |
| * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or |
| * {@link #forEach(TriConsumer, Object)}. |
| * @see ReadOnlyStringMap#forEach(BiConsumer) |
| * @since 2.9 |
| */ |
| public <CV> void forEach(final BiConsumer<String, ? super CV> action) { |
| data.forEach(action); |
| } |
| |
| /** |
| * Performs the given action for each key-value pair in this data structure |
| * until all entries have been processed or the action throws an exception. |
| * <p> |
| * The third parameter lets callers pass in a stateful object to be modified with the key-value pairs, |
| * so the TriConsumer implementation itself can be stateless and potentially reusable. |
| * </p> |
| * <p> |
| * Some implementations may not support structural modifications (adding new elements or removing elements) while |
| * iterating over the contents. In such implementations, attempts to add or remove elements from the |
| * {@code TriConsumer}'s {@link TriConsumer#accept(Object, Object, Object) accept} method may cause a |
| * {@code ConcurrentModificationException} to be thrown. |
| * </p> |
| * |
| * @param action The action to be performed for each key-value pair in this collection |
| * @param state the object to be passed as the third parameter to each invocation on the specified |
| * triconsumer |
| * @param <CV> type of the consumer value |
| * @param <S> type of the third parameter |
| * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications |
| * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or |
| * {@link #forEach(TriConsumer, Object)}. |
| * @see ReadOnlyStringMap#forEach(TriConsumer, Object) |
| * @since 2.9 |
| */ |
| public <CV, S> void forEach(final TriConsumer<String, ? super CV, S> action, final S state) { |
| data.forEach(action, state); |
| } |
| |
| /** |
| * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>. |
| * |
| * @param format The format identifier. |
| * @return The formatted String. |
| */ |
| private StringBuilder format(final MapFormat format, final StringBuilder sb) { |
| if (format == null) { |
| appendMap(sb); |
| } else { |
| switch (format) { |
| case XML : { |
| asXml(sb); |
| break; |
| } |
| case JSON : { |
| asJson(sb); |
| break; |
| } |
| case JAVA : { |
| asJava(sb); |
| break; |
| } |
| default : { |
| appendMap(sb); |
| } |
| } |
| } |
| return sb; |
| } |
| |
| /** |
| * Formats this message as an XML fragment String into the given builder. |
| * |
| * @param sb format into this builder. |
| */ |
| public void asXml(final StringBuilder sb) { |
| sb.append("<Map>\n"); |
| for (int i = 0; i < data.size(); i++) { |
| sb.append(" <Entry key=\"") |
| .append(data.getKeyAt(i)) |
| .append("\">"); |
| int size = sb.length(); |
| ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null); |
| StringBuilders.escapeXml(sb, size); |
| sb.append("</Entry>\n"); |
| } |
| sb.append("</Map>"); |
| } |
| |
| /** |
| * Formats the message and return it. |
| * @return the formatted message. |
| */ |
| @Override |
| public String getFormattedMessage() { |
| return asString(); |
| } |
| |
| /** |
| * |
| * @param formats |
| * An array of Strings that provide extra information about how to format the message. MapMessage uses |
| * the first format specifier it recognizes. The supported formats are XML, JSON, and JAVA. The default |
| * format is key1="value1" key2="value2" as required by <a href="https://tools.ietf.org/html/rfc5424">RFC |
| * 5424</a> messages. |
| * |
| * @return The formatted message. |
| */ |
| @Override |
| public String getFormattedMessage(final String[] formats) { |
| return format(getFormat(formats), new StringBuilder()).toString(); |
| } |
| |
| private MapFormat getFormat(final String[] formats) { |
| if (formats == null || formats.length == 0) { |
| return null; |
| } |
| for (int i = 0; i < formats.length; i++) { |
| final MapFormat mapFormat = MapFormat.lookupIgnoreCase(formats[i]); |
| if (mapFormat != null) { |
| return mapFormat; |
| } |
| } |
| return null; |
| } |
| |
| protected void appendMap(final StringBuilder sb) { |
| for (int i = 0; i < data.size(); i++) { |
| if (i > 0) { |
| sb.append(' '); |
| } |
| sb.append(data.getKeyAt(i)).append(Chars.EQ).append(Chars.DQUOTE); |
| ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null); |
| sb.append(Chars.DQUOTE); |
| } |
| } |
| |
| protected void asJson(final StringBuilder sb) { |
| sb.append('{'); |
| for (int i = 0; i < data.size(); i++) { |
| if (i > 0) { |
| sb.append(", "); |
| } |
| sb.append(Chars.DQUOTE); |
| int start = sb.length(); |
| sb.append(data.getKeyAt(i)); |
| StringBuilders.escapeJson(sb, start); |
| sb.append(Chars.DQUOTE).append(':').append(Chars.DQUOTE); |
| start = sb.length(); |
| ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null); |
| StringBuilders.escapeJson(sb, start); |
| sb.append(Chars.DQUOTE); |
| } |
| sb.append('}'); |
| } |
| |
| |
| protected void asJava(final StringBuilder sb) { |
| sb.append('{'); |
| for (int i = 0; i < data.size(); i++) { |
| if (i > 0) { |
| sb.append(", "); |
| } |
| sb.append(data.getKeyAt(i)).append(Chars.EQ).append(Chars.DQUOTE); |
| ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null); |
| sb.append(Chars.DQUOTE); |
| } |
| sb.append('}'); |
| } |
| |
| /** |
| * Constructs a new instance based on an existing Map. |
| * @param map The Map. |
| * @return A new MapMessage |
| */ |
| @SuppressWarnings("unchecked") |
| public M newInstance(final Map<String, V> map) { |
| return (M) new MapMessage<>(map); |
| } |
| |
| @Override |
| public String toString() { |
| return asString(); |
| } |
| |
| @Override |
| public void formatTo(final StringBuilder buffer) { |
| format((MapFormat) null, buffer); |
| } |
| |
| @Override |
| public void formatTo(String[] formats, StringBuilder buffer) { |
| format(getFormat(formats), buffer); |
| } |
| |
| @Override |
| public boolean equals(final Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || this.getClass() != o.getClass()) { |
| return false; |
| } |
| |
| final MapMessage<?, ?> that = (MapMessage<?, ?>) o; |
| |
| return this.data.equals(that.data); |
| } |
| |
| @Override |
| public int hashCode() { |
| return data.hashCode(); |
| } |
| |
| /** |
| * Always returns null. |
| * |
| * @return null |
| */ |
| @Override |
| public Throwable getThrowable() { |
| return null; |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final boolean value) { |
| // do nothing |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final byte value) { |
| // do nothing |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final char value) { |
| // do nothing |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final double value) { |
| // do nothing |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final float value) { |
| // do nothing |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final int value) { |
| // do nothing |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final long value) { |
| // do nothing |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final Object value) { |
| // do nothing |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final short value) { |
| // do nothing |
| } |
| |
| /** |
| * Default implementation does nothing. |
| * |
| * @since 2.9 |
| */ |
| protected void validate(final String key, final String value) { |
| // do nothing |
| } |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return this object |
| * @since 2.9 |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final boolean value) { |
| validate(key, value); |
| data.putValue(key, value); |
| return (M) this; |
| } |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return this object |
| * @since 2.9 |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final byte value) { |
| validate(key, value); |
| data.putValue(key, value); |
| return (M) this; |
| } |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return this object |
| * @since 2.9 |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final char value) { |
| validate(key, value); |
| data.putValue(key, value); |
| return (M) this; |
| } |
| |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return this object |
| * @since 2.9 |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final double value) { |
| validate(key, value); |
| data.putValue(key, value); |
| return (M) this; |
| } |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return this object |
| * @since 2.9 |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final float value) { |
| validate(key, value); |
| data.putValue(key, value); |
| return (M) this; |
| } |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return this object |
| * @since 2.9 |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final int value) { |
| validate(key, value); |
| data.putValue(key, value); |
| return (M) this; |
| } |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return this object |
| * @since 2.9 |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final long value) { |
| validate(key, value); |
| data.putValue(key, value); |
| return (M) this; |
| } |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return this object |
| * @since 2.9 |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final Object value) { |
| validate(key, value); |
| data.putValue(key, value); |
| return (M) this; |
| } |
| |
| /** |
| * Adds an item to the data Map. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return this object |
| * @since 2.9 |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final short value) { |
| validate(key, value); |
| data.putValue(key, value); |
| return (M) this; |
| } |
| |
| /** |
| * Adds an item to the data Map in fluent style. |
| * @param key The name of the data item. |
| * @param value The value of the data item. |
| * @return {@code this} |
| */ |
| @SuppressWarnings("unchecked") |
| public M with(final String key, final String value) { |
| put(key, value); |
| return (M) this; |
| } |
| |
| } |