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    }