    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
    018    package org.apache.logging.log4j.message;
    019
    020    import java.util.Map;
    021
    022    import org.apache.logging.log4j.util.EnglishEnums;
    023
    024    /**
    025     * Represents a Message that conforms to an RFC 5424 StructuredData element along with the syslog message.
    026     * <p>
    027     * Thread-safety note: the contents of this message can be modified after construction.
    028     * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is
    029     * logged, because it is undefined whether the logged message string will contain the old values or the modified
    030     * values.
    031     * </p>
    032     *
    033     * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>
    034     */
    035    public class StructuredDataMessage extends MapMessage {
    036
    037        private static final long serialVersionUID = 1703221292892071920L;
    038        private static final int MAX_LENGTH = 32;
    039        private static final int HASHVAL = 31;
    040
    041        private StructuredDataId id;
    042
    043        private String message;
    044
    045        private String type;
    046
    047        /**
    048         * Supported formats.
    049         */
    050        public enum Format {
    051            /** The map should be formatted as XML. */
    052            XML,
    053            /** Full message format includes the type and message. */
    054            FULL
    055        }
    056
    057        /**
    058         * Creates a StructuredDataMessage using an ID (max 32 characters), message, and type (max 32 characters).
    059         * @param id The String id.
    060         * @param msg The message.
    061         * @param type The message type.
    062         */
    063        public StructuredDataMessage(final String id, final String msg, final String type) {
    064            this.id = new StructuredDataId(id, null, null);
    065            this.message = msg;
    066            this.type = type;
    067        }
    068        /**
    069         * Creates a StructuredDataMessage using an ID (max 32 characters), message, type (max 32 characters), and an
    070         * initial map of structured data to include.
    071         * @param id The String id.
    072         * @param msg The message.
    073         * @param type The message type.
    074         * @param data The StructuredData map.
    075         */
    076        public StructuredDataMessage(final String id, final String msg, final String type,
    077                                     final Map<String, String> data) {
    078            super(data);
    079            this.id = new StructuredDataId(id, null, null);
    080            this.message = msg;
    081            this.type = type;
    082        }
    083
    084        /**
    085         * Creates a StructuredDataMessage using a StructuredDataId, message, and type (max 32 characters).
    086         * @param id The StructuredDataId.
    087         * @param msg The message.
    088         * @param type The message type.
    089         */
    090        public StructuredDataMessage(final StructuredDataId id, final String msg, final String type) {
    091            this.id = id;
    092            this.message = msg;
    093            this.type = type;
    094        }
    095
    096        /**
    097         * Creates a StructuredDataMessage using a StructuredDataId, message, type (max 32 characters), and an initial map
    098         * of structured data to include.
    099         * @param id The StructuredDataId.
    100         * @param msg The message.
    101         * @param type The message type.
    102         * @param data The StructuredData map.
    103         */
    104        public StructuredDataMessage(final StructuredDataId id, final String msg, final String type,
    105                                     final Map<String, String> data) {
    106            super(data);
    107            this.id = id;
    108            this.message = msg;
    109            this.type = type;
    110        }
    111
    112
    113        /**
    114         * Constructor based on a StructuredDataMessage.
    115         * @param msg The StructuredDataMessage.
    116         * @param map The StructuredData map.
    117         */
    118        private StructuredDataMessage(final StructuredDataMessage msg, final Map<String, String> map) {
    119            super(map);
    120            this.id = msg.id;
    121            this.message = msg.message;
    122            this.type = msg.type;
    123        }
    124
    125
    126        /**
    127         * Basic constructor.
    128         */
    129        protected StructuredDataMessage() {
    130
    131        }
    132
    133        /**
    134         * Returns the supported formats.
    135         * @return An array of the supported format names.
    136         */
    137        @Override
    138        public String[] getFormats() {
    139            final String[] formats = new String[Format.values().length];
    140            int i = 0;
    141            for (final Format format : Format.values()) {
    142                formats[i++] = format.name();
    143            }
    144            return formats;
    145        }
    146
    147        /**
    148         * Returns this message id.
    149         * @return the StructuredDataId.
    150         */
    151        public StructuredDataId getId() {
    152            return id;
    153        }
    154
    155        /**
    156         * Sets the id from a String. This ID can be at most 32 characters long.
    157         * @param id The String id.
    158         */
    159        protected void setId(final String id) {
    160            this.id = new StructuredDataId(id, null, null);
    161        }
    162
    163        /**
    164         * Sets the id.
    165         * @param id The StructuredDataId.
    166         */
    167        protected void setId(final StructuredDataId id) {
    168            this.id = id;
    169        }
    170
    171        /**
    172         * Returns this message type.
    173         * @return the type.
    174         */
    175        public String getType() {
    176            return type;
    177        }
    178
    179        protected void setType(final String type) {
    180            if (type.length() > MAX_LENGTH) {
    181                throw new IllegalArgumentException("structured data type exceeds maximum length of 32 characters: " + type);
    182            }
    183            this.type = type;
    184        }
    185
    186        /**
    187         * Returns the message.
    188         * @return the message.
    189         */
    190        @Override
    191        public String getFormat() {
    192            return message;
    193        }
    194
    195        protected void setMessageFormat(final String msg) {
    196            this.message = msg;
    197        }
    198
    199
    200        @Override
    201        protected void validate(final String key, final String value) {
    202            validateKey(key);
    203        }
    204
    205        private void validateKey(final String key) {
    206            if (key.length() > MAX_LENGTH) {
    207                throw new IllegalArgumentException("Structured data keys are limited to 32 characters. key: " + key);
    208            }
    209            final char[] chars = key.toCharArray();
    210            for (final char c : chars) {
    211                if (c < '!' || c > '~' || c == '=' || c == ']' || c == '"') {
    212                    throw new IllegalArgumentException("Structured data keys must contain printable US ASCII characters" +
    213                            "and may not contain a space, =, ], or \"");
    214                }
    215            }
    216        }
    217
    218        /**
    219         * Formats the structured data as described in RFC 5424.
    220         *
    221         * @return The formatted String.
    222         */
    223        @Override
    224        public String asString() {
    225            return asString(Format.FULL, null);
    226        }
    227
    228        /**
    229         * Formats the structured data as described in RFC 5424.
    230         *
    231         * @param format The format identifier. Ignored in this implementation.
    232         * @return The formatted String.
    233         */
    234
    235        @Override
    236        public String asString(final String format) {
    237            try {
    238                return asString(EnglishEnums.valueOf(Format.class, format), null);
    239            } catch (final IllegalArgumentException ex) {
    240                return asString();
    241            }
    242        }
    243
    244        /**
    245         * Formats the structured data as described in RFC 5424.
    246         *
    247         * @param format           "full" will include the type and message. null will return only the STRUCTURED-DATA as
    248         *                         described in RFC 5424
    249         * @param structuredDataId The SD-ID as described in RFC 5424. If null the value in the StructuredData
    250         *                         will be used.
    251         * @return The formatted String.
    252         */
    253        public final String asString(final Format format, final StructuredDataId structuredDataId) {
    254            final StringBuilder sb = new StringBuilder();
    255            final boolean full = Format.FULL.equals(format);
    256            if (full) {
    257                final String myType = getType();
    258                if (myType == null) {
    259                    return sb.toString();
    260                }
    261                sb.append(getType()).append(' ');
    262            }
    263            StructuredDataId sdId = getId();
    264            if (sdId != null) {
    265                sdId = sdId.makeId(structuredDataId);
    266            } else {
    267                sdId = structuredDataId;
    268            }
    269            if (sdId == null || sdId.getName() == null) {
    270                return sb.toString();
    271            }
    272            sb.append('[');
    273            sb.append(sdId);
    274            sb.append(' ');
    275            appendMap(sb);
    276            sb.append(']');
    277            if (full) {
    278                final String msg = getFormat();
    279                if (msg != null) {
    280                    sb.append(' ').append(msg);
    281                }
    282            }
    283            return sb.toString();
    284        }
    285
    286        /**
    287         * Formats the message and return it.
    288         * @return the formatted message.
    289         */
    290        @Override
    291        public String getFormattedMessage() {
    292            return asString(Format.FULL, null);
    293        }
    294
    295        /**
    296         * Formats the message according the the specified format.
    297         * @param formats An array of Strings that provide extra information about how to format the message.
    298         * StructuredDataMessage accepts only a format of "FULL" which will cause the event type to be
    299         * prepended and the event message to be appended. Specifying any other value will cause only the
    300         * StructuredData to be included. The default is "FULL".
    301         *
    302         * @return the formatted message.
    303         */
    304        @Override
    305        public String getFormattedMessage(final String[] formats) {
    306            if (formats != null && formats.length > 0) {
    307                for (final String format : formats) {
    308                    if (Format.XML.name().equalsIgnoreCase(format)) {
    309                        return asXml();
    310                    } else if (Format.FULL.name().equalsIgnoreCase(format)) {
    311                        return asString(Format.FULL, null);
    312                    }
    313                }
    314                return asString(null, null);
    315            }
    316            return asString(Format.FULL, null);
    317        }
    318
    319        private String asXml() {
    320            final StringBuilder sb = new StringBuilder();
    321            final StructuredDataId sdId = getId();
    322            if (sdId == null || sdId.getName() == null || type == null) {
    323                return sb.toString();
    324            }
    325            sb.append("<StructuredData>\n");
    326            sb.append("<type>").append(type).append("</type>\n");
    327            sb.append("<id>").append(sdId).append("</id>\n");
    328            super.asXml(sb);
    329            sb.append("</StructuredData>\n");
    330            return sb.toString();
    331        }
    332
    333        @Override
    334        public String toString() {
    335            return asString(null, null);
    336        }
    337
    338
    339        @Override
    340        public MapMessage newInstance(final Map<String, String> map) {
    341            return new StructuredDataMessage(this, map);
    342        }
    343
    344        @Override
    345        public boolean equals(final Object o) {
    346            if (this == o) {
    347                return true;
    348            }
    349            if (o == null || getClass() != o.getClass()) {
    350                return false;
    351            }
    352
    353            final StructuredDataMessage that = (StructuredDataMessage) o;
    354
    355            if (!super.equals(o)) {
    356                return false;
    357            }
    358            if (type != null ? !type.equals(that.type) : that.type != null) {
    359                return false;
    360            }
    361            if (id != null ? !id.equals(that.id) : that.id != null) {
    362                return false;
    363            }
    364            if (message != null ? !message.equals(that.message) : that.message != null) {
    365                return false;
    366            }
    367
    368            return true;
    369        }
    370
    371        @Override
    372        public int hashCode() {
    373            int result = super.hashCode();
    374            result = HASHVAL * result + (type != null ? type.hashCode() : 0);
    375            result = HASHVAL * result + (id != null ? id.hashCode() : 0);
    376            result = HASHVAL * result + (message != null ? message.hashCode() : 0);
    377            return result;
    378        }
    379    }