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
018package org.apache.logging.log4j.message;
019
020import java.util.Map;
021
022import org.apache.logging.log4j.util.EnglishEnums;
023import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable;
024import org.apache.logging.log4j.util.StringBuilders;
025
026/**
027 * Represents a Message that conforms to an RFC 5424 StructuredData element along with the syslog message.
028 * <p>
029 * Thread-safety note: the contents of this message can be modified after construction.
030 * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is
031 * logged, because it is undefined whether the logged message string will contain the old values or the modified
032 * values.
033 * </p>
034 *
035 * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>
036 */
037@AsynchronouslyFormattable
038public class StructuredDataMessage extends MapMessage<StructuredDataMessage, String> implements MultiFormatStringBuilderFormattable {
039
040    private static final long serialVersionUID = 1703221292892071920L;
041    private static final int MAX_LENGTH = 32;
042    private static final int HASHVAL = 31;
043
044    private StructuredDataId id;
045
046    private String message;
047
048    private String type;
049
050    private final int maxLength;
051
052    /**
053     * Supported formats.
054     */
055    public enum Format {
056        /** The map should be formatted as XML. */
057        XML,
058        /** Full message format includes the type and message. */
059        FULL
060    }
061
062    /**
063     * Creates a StructuredDataMessage using an ID (max 32 characters), message, and type (max 32 characters).
064     * @param id The String id.
065     * @param msg The message.
066     * @param type The message type.
067     */
068    public StructuredDataMessage(final String id, final String msg, final String type) {
069        this(id, msg, type, MAX_LENGTH);
070    }
071
072    /**
073     * Creates a StructuredDataMessage using an ID (user specified max characters), message, and type (user specified
074     * maximum number of characters).
075     * @param id The String id.
076     * @param msg The message.
077     * @param type The message type.
078     * @param maxLength The maximum length of keys;
079     * @since 2.9
080     */
081    public StructuredDataMessage(final String id, final String msg, final String type, final int maxLength) {
082        this.id = new StructuredDataId(id, null, null, maxLength);
083        this.message = msg;
084        this.type = type;
085        this.maxLength = maxLength;
086    }
087    /**
088     * Creates a StructuredDataMessage using an ID (max 32 characters), message, type (max 32 characters), and an
089     * initial map of structured data to include.
090     * @param id The String id.
091     * @param msg The message.
092     * @param type The message type.
093     * @param data The StructuredData map.
094     */
095    public StructuredDataMessage(final String id, final String msg, final String type,
096                                 final Map<String, String> data) {
097        this(id, msg, type, data, MAX_LENGTH);
098    }
099
100    /**
101     * Creates a StructuredDataMessage using an (user specified max characters), message, and type (user specified
102     * maximum number of characters, and an initial map of structured data to include.
103     * @param id The String id.
104     * @param msg The message.
105     * @param type The message type.
106     * @param data The StructuredData map.
107     * @param maxLength The maximum length of keys;
108     * @since 2.9
109     */
110    public StructuredDataMessage(final String id, final String msg, final String type,
111                                 final Map<String, String> data, final int maxLength) {
112        super(data);
113        this.id = new StructuredDataId(id, null, null, maxLength);
114        this.message = msg;
115        this.type = type;
116        this.maxLength = maxLength;
117    }
118
119    /**
120     * Creates a StructuredDataMessage using a StructuredDataId, message, and type (max 32 characters).
121     * @param id The StructuredDataId.
122     * @param msg The message.
123     * @param type The message type.
124     */
125    public StructuredDataMessage(final StructuredDataId id, final String msg, final String type) {
126        this(id, msg, type, MAX_LENGTH);
127    }
128
129    /**
130     * Creates a StructuredDataMessage using a StructuredDataId, message, and type (max 32 characters).
131     * @param id The StructuredDataId.
132     * @param msg The message.
133     * @param type The message type.
134     * @param maxLength The maximum length of keys;
135     * @since 2.9
136     */
137    public StructuredDataMessage(final StructuredDataId id, final String msg, final String type, final int maxLength) {
138        this.id = id;
139        this.message = msg;
140        this.type = type;
141        this.maxLength = maxLength;
142    }
143
144    /**
145     * Creates a StructuredDataMessage using a StructuredDataId, message, type (max 32 characters), and an initial map
146     * of structured data to include.
147     * @param id The StructuredDataId.
148     * @param msg The message.
149     * @param type The message type.
150     * @param data The StructuredData map.
151     */
152    public StructuredDataMessage(final StructuredDataId id, final String msg, final String type,
153                                 final Map<String, String> data) {
154        this(id, msg, type, data, MAX_LENGTH);
155    }
156
157    /**
158     * Creates a StructuredDataMessage using a StructuredDataId, message, type (max 32 characters), and an initial map
159     * of structured data to include.
160     * @param id The StructuredDataId.
161     * @param msg The message.
162     * @param type The message type.
163     * @param data The StructuredData map.
164     * @param maxLength The maximum length of keys;
165     * @since 2.9
166     */
167    public StructuredDataMessage(final StructuredDataId id, final String msg, final String type,
168                                 final Map<String, String> data, final int maxLength) {
169        super(data);
170        this.id = id;
171        this.message = msg;
172        this.type = type;
173        this.maxLength = maxLength;
174    }
175
176
177    /**
178     * Constructor based on a StructuredDataMessage.
179     * @param msg The StructuredDataMessage.
180     * @param map The StructuredData map.
181     */
182    private StructuredDataMessage(final StructuredDataMessage msg, final Map<String, String> map) {
183        super(map);
184        this.id = msg.id;
185        this.message = msg.message;
186        this.type = msg.type;
187        this.maxLength = MAX_LENGTH;
188    }
189
190
191    /**
192     * Basic constructor.
193     */
194    protected StructuredDataMessage() {
195        maxLength = MAX_LENGTH;
196    }
197
198    /**
199     * Returns the supported formats.
200     * @return An array of the supported format names.
201     */
202    @Override
203    public String[] getFormats() {
204        final String[] formats = new String[Format.values().length];
205        int i = 0;
206        for (final Format format : Format.values()) {
207            formats[i++] = format.name();
208        }
209        return formats;
210    }
211
212    /**
213     * Returns this message id.
214     * @return the StructuredDataId.
215     */
216    public StructuredDataId getId() {
217        return id;
218    }
219
220    /**
221     * Sets the id from a String. This ID can be at most 32 characters long.
222     * @param id The String id.
223     */
224    protected void setId(final String id) {
225        this.id = new StructuredDataId(id, null, null);
226    }
227
228    /**
229     * Sets the id.
230     * @param id The StructuredDataId.
231     */
232    protected void setId(final StructuredDataId id) {
233        this.id = id;
234    }
235
236    /**
237     * Returns this message type.
238     * @return the type.
239     */
240    public String getType() {
241        return type;
242    }
243
244    protected void setType(final String type) {
245        if (type.length() > MAX_LENGTH) {
246            throw new IllegalArgumentException("structured data type exceeds maximum length of 32 characters: " + type);
247        }
248        this.type = type;
249    }
250
251    @Override
252    public void formatTo(final StringBuilder buffer) {
253        asString(Format.FULL, null, buffer);
254    }
255
256    @Override
257    public void formatTo(String[] formats, StringBuilder buffer) {
258        asString(getFormat(formats), null, buffer);
259    }
260
261    /**
262     * Returns the message.
263     * @return the message.
264     */
265    @Override
266    public String getFormat() {
267        return message;
268    }
269
270    protected void setMessageFormat(final String msg) {
271        this.message = msg;
272    }
273
274    /**
275     * Formats the structured data as described in RFC 5424.
276     *
277     * @return The formatted String.
278     */
279    @Override
280    public String asString() {
281        return asString(Format.FULL, null);
282    }
283
284    /**
285     * Formats the structured data as described in RFC 5424.
286     *
287     * @param format The format identifier. Ignored in this implementation.
288     * @return The formatted String.
289     */
290
291    @Override
292    public String asString(final String format) {
293        try {
294            return asString(EnglishEnums.valueOf(Format.class, format), null);
295        } catch (final IllegalArgumentException ex) {
296            return asString();
297        }
298    }
299
300    /**
301     * Formats the structured data as described in RFC 5424.
302     *
303     * @param format           "full" will include the type and message. null will return only the STRUCTURED-DATA as
304     *                         described in RFC 5424
305     * @param structuredDataId The SD-ID as described in RFC 5424. If null the value in the StructuredData
306     *                         will be used.
307     * @return The formatted String.
308     */
309    public final String asString(final Format format, final StructuredDataId structuredDataId) {
310        final StringBuilder sb = new StringBuilder();
311        asString(format, structuredDataId, sb);
312        return sb.toString();
313    }
314
315    /**
316     * Formats the structured data as described in RFC 5424.
317     *
318     * @param format           "full" will include the type and message. null will return only the STRUCTURED-DATA as
319     *                         described in RFC 5424
320     * @param structuredDataId The SD-ID as described in RFC 5424. If null the value in the StructuredData
321     *                         will be used.
322     * @param sb The StringBuilder to append the formatted message to.
323     */
324    public final void asString(final Format format, final StructuredDataId structuredDataId, final StringBuilder sb) {
325        final boolean full = Format.FULL.equals(format);
326        if (full) {
327            final String myType = getType();
328            if (myType == null) {
329                return;
330            }
331            sb.append(getType()).append(' ');
332        }
333        StructuredDataId sdId = getId();
334        if (sdId != null) {
335            sdId = sdId.makeId(structuredDataId); // returns sdId if structuredDataId is null
336        } else {
337            sdId = structuredDataId;
338        }
339        if (sdId == null || sdId.getName() == null) {
340            return;
341        }
342        if (Format.XML.equals(format)) {
343            asXml(sdId, sb);
344            return;
345        }
346        sb.append('[');
347        StringBuilders.appendValue(sb, sdId); // avoids toString if implements StringBuilderFormattable
348        sb.append(' ');
349        appendMap(sb);
350        sb.append(']');
351        if (full) {
352            final String msg = getFormat();
353            if (msg != null) {
354                sb.append(' ').append(msg);
355            }
356        }
357    }
358
359    private void asXml(StructuredDataId structuredDataId, StringBuilder sb) {
360        sb.append("<StructuredData>\n");
361        sb.append("<type>").append(type).append("</type>\n");
362        sb.append("<id>").append(structuredDataId).append("</id>\n");
363        super.asXml(sb);
364        sb.append("\n</StructuredData>\n");
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(Format.FULL, null);
374    }
375
376    /**
377     * Formats the message according the the specified format.
378     * @param formats An array of Strings that provide extra information about how to format the message.
379     * StructuredDataMessage accepts only a format of "FULL" which will cause the event type to be
380     * prepended and the event message to be appended. Specifying any other value will cause only the
381     * StructuredData to be included. The default is "FULL".
382     *
383     * @return the formatted message.
384     */
385    @Override
386    public String getFormattedMessage(final String[] formats) {
387        return asString(getFormat(formats), null);
388    }
389
390    private Format getFormat(String[] formats) {
391        if (formats != null && formats.length > 0) {
392            for (int i = 0; i < formats.length; i++) {
393                final String format = formats[i];
394                if (Format.XML.name().equalsIgnoreCase(format)) {
395                    return Format.XML;
396                } else if (Format.FULL.name().equalsIgnoreCase(format)) {
397                    return Format.FULL;
398                }
399            }
400            return null;
401        }
402        return Format.FULL;
403    }
404
405    @Override
406    public String toString() {
407        return asString(null, null);
408    }
409
410
411    @Override
412    public StructuredDataMessage newInstance(final Map<String, String> map) {
413        return new StructuredDataMessage(this, map);
414    }
415
416    @Override
417    public boolean equals(final Object o) {
418        if (this == o) {
419            return true;
420        }
421        if (o == null || getClass() != o.getClass()) {
422            return false;
423        }
424
425        final StructuredDataMessage that = (StructuredDataMessage) o;
426
427        if (!super.equals(o)) {
428            return false;
429        }
430        if (type != null ? !type.equals(that.type) : that.type != null) {
431            return false;
432        }
433        if (id != null ? !id.equals(that.id) : that.id != null) {
434            return false;
435        }
436        if (message != null ? !message.equals(that.message) : that.message != null) {
437            return false;
438        }
439
440        return true;
441    }
442
443    @Override
444    public int hashCode() {
445        int result = super.hashCode();
446        result = HASHVAL * result + (type != null ? type.hashCode() : 0);
447        result = HASHVAL * result + (id != null ? id.hashCode() : 0);
448        result = HASHVAL * result + (message != null ? message.hashCode() : 0);
449        return result;
450    }
451
452    @Override
453    protected void validate(final String key, final boolean value) {
454        validateKey(key);
455    }
456
457    /**
458     * @since 2.9
459     */
460    @Override
461    protected void validate(final String key, final byte value) {
462        validateKey(key);
463    }
464
465    /**
466     * @since 2.9
467     */
468    @Override
469    protected void validate(final String key, final char value) {
470        validateKey(key);
471    }
472    
473    /**
474     * @since 2.9
475     */
476    @Override
477    protected void validate(final String key, final double value) {
478        validateKey(key);
479    }
480    
481    /**
482     * @since 2.9
483     */
484    @Override
485    protected void validate(final String key, final float value) {
486        validateKey(key);
487    }
488
489    /**
490     * @since 2.9
491     */
492    @Override
493    protected void validate(final String key, final int value) {
494        validateKey(key);
495    }
496
497    /**
498     * @since 2.9
499     */
500    @Override
501    protected void validate(final String key, final long value) {
502        validateKey(key);
503    }
504
505    /**
506     * @since 2.9
507     */
508    @Override
509    protected void validate(final String key, final Object value) {
510        validateKey(key);
511    }
512
513    /**
514     * @since 2.9
515     */
516    @Override
517    protected void validate(final String key, final short value) {
518        validateKey(key);
519    }
520
521    @Override
522    protected void validate(final String key, final String value) {
523        validateKey(key);
524    }
525
526    protected void validateKey(final String key) {
527        if (maxLength > 0 && key.length() > maxLength) {
528            throw new IllegalArgumentException("Structured data keys are limited to " + maxLength +
529                    " characters. key: " + key);
530        }
531        for (int i = 0; i < key.length(); i++) {
532            final char c = key.charAt(i);
533            if (c < '!' || c > '~' || c == '=' || c == ']' || c == '"') {
534                throw new IllegalArgumentException("Structured data keys must contain printable US ASCII characters" +
535                        "and may not contain a space, =, ], or \"");
536            }
537        }
538    }
539
540}