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.StringBuilderFormattable;
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 StringBuilderFormattable {
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    /**
257     * Returns the message.
258     * @return the message.
259     */
260    @Override
261    public String getFormat() {
262        return message;
263    }
264
265    protected void setMessageFormat(final String msg) {
266        this.message = msg;
267    }
268
269    /**
270     * Formats the structured data as described in RFC 5424.
271     *
272     * @return The formatted String.
273     */
274    @Override
275    public String asString() {
276        return asString(Format.FULL, null);
277    }
278
279    /**
280     * Formats the structured data as described in RFC 5424.
281     *
282     * @param format The format identifier. Ignored in this implementation.
283     * @return The formatted String.
284     */
285
286    @Override
287    public String asString(final String format) {
288        try {
289            return asString(EnglishEnums.valueOf(Format.class, format), null);
290        } catch (final IllegalArgumentException ex) {
291            return asString();
292        }
293    }
294
295    /**
296     * Formats the structured data as described in RFC 5424.
297     *
298     * @param format           "full" will include the type and message. null will return only the STRUCTURED-DATA as
299     *                         described in RFC 5424
300     * @param structuredDataId The SD-ID as described in RFC 5424. If null the value in the StructuredData
301     *                         will be used.
302     * @return The formatted String.
303     */
304    public final String asString(final Format format, final StructuredDataId structuredDataId) {
305        final StringBuilder sb = new StringBuilder();
306        asString(format, structuredDataId, sb);
307        return sb.toString();
308    }
309
310    /**
311     * Formats the structured data as described in RFC 5424.
312     *
313     * @param format           "full" will include the type and message. null will return only the STRUCTURED-DATA as
314     *                         described in RFC 5424
315     * @param structuredDataId The SD-ID as described in RFC 5424. If null the value in the StructuredData
316     *                         will be used.
317     * @param sb The StringBuilder to append the formatted message to.
318     */
319    public final void asString(final Format format, final StructuredDataId structuredDataId, final StringBuilder sb) {
320        final boolean full = Format.FULL.equals(format);
321        if (full) {
322            final String myType = getType();
323            if (myType == null) {
324                return;
325            }
326            sb.append(getType()).append(' ');
327        }
328        StructuredDataId sdId = getId();
329        if (sdId != null) {
330            sdId = sdId.makeId(structuredDataId); // returns sdId if structuredDataId is null
331        } else {
332            sdId = structuredDataId;
333        }
334        if (sdId == null || sdId.getName() == null) {
335            return;
336        }
337        sb.append('[');
338        StringBuilders.appendValue(sb, sdId); // avoids toString if implements StringBuilderFormattable
339        sb.append(' ');
340        appendMap(sb);
341        sb.append(']');
342        if (full) {
343            final String msg = getFormat();
344            if (msg != null) {
345                sb.append(' ').append(msg);
346            }
347        }
348    }
349
350    /**
351     * Formats the message and return it.
352     * @return the formatted message.
353     */
354    @Override
355    public String getFormattedMessage() {
356        return asString(Format.FULL, null);
357    }
358
359    /**
360     * Formats the message according the the specified format.
361     * @param formats An array of Strings that provide extra information about how to format the message.
362     * StructuredDataMessage accepts only a format of "FULL" which will cause the event type to be
363     * prepended and the event message to be appended. Specifying any other value will cause only the
364     * StructuredData to be included. The default is "FULL".
365     *
366     * @return the formatted message.
367     */
368    @Override
369    public String getFormattedMessage(final String[] formats) {
370        if (formats != null && formats.length > 0) {
371            for (int i = 0; i < formats.length; i++) {
372                final String format = formats[i];
373                if (Format.XML.name().equalsIgnoreCase(format)) {
374                    return asXml();
375                } else if (Format.FULL.name().equalsIgnoreCase(format)) {
376                    return asString(Format.FULL, null);
377                }
378            }
379            return asString(null, null);
380        }
381        return asString(Format.FULL, null);
382    }
383
384    private String asXml() {
385        final StringBuilder sb = new StringBuilder();
386        final StructuredDataId sdId = getId();
387        if (sdId == null || sdId.getName() == null || type == null) {
388            return sb.toString();
389        }
390        sb.append("<StructuredData>\n");
391        sb.append("<type>").append(type).append("</type>\n");
392        sb.append("<id>").append(sdId).append("</id>\n");
393        super.asXml(sb);
394        sb.append("</StructuredData>\n");
395        return sb.toString();
396    }
397
398    @Override
399    public String toString() {
400        return asString(null, null);
401    }
402
403
404    @Override
405    public StructuredDataMessage newInstance(final Map<String, String> map) {
406        return new StructuredDataMessage(this, map);
407    }
408
409    @Override
410    public boolean equals(final Object o) {
411        if (this == o) {
412            return true;
413        }
414        if (o == null || getClass() != o.getClass()) {
415            return false;
416        }
417
418        final StructuredDataMessage that = (StructuredDataMessage) o;
419
420        if (!super.equals(o)) {
421            return false;
422        }
423        if (type != null ? !type.equals(that.type) : that.type != null) {
424            return false;
425        }
426        if (id != null ? !id.equals(that.id) : that.id != null) {
427            return false;
428        }
429        if (message != null ? !message.equals(that.message) : that.message != null) {
430            return false;
431        }
432
433        return true;
434    }
435
436    @Override
437    public int hashCode() {
438        int result = super.hashCode();
439        result = HASHVAL * result + (type != null ? type.hashCode() : 0);
440        result = HASHVAL * result + (id != null ? id.hashCode() : 0);
441        result = HASHVAL * result + (message != null ? message.hashCode() : 0);
442        return result;
443    }
444
445    @Override
446    protected void validate(final String key, final boolean value) {
447        validateKey(key);
448    }
449
450    /**
451     * @since 2.9
452     */
453    @Override
454    protected void validate(final String key, final byte value) {
455        validateKey(key);
456    }
457
458    /**
459     * @since 2.9
460     */
461    @Override
462    protected void validate(final String key, final char value) {
463        validateKey(key);
464    }
465    
466    /**
467     * @since 2.9
468     */
469    @Override
470    protected void validate(final String key, final double value) {
471        validateKey(key);
472    }
473    
474    /**
475     * @since 2.9
476     */
477    @Override
478    protected void validate(final String key, final float value) {
479        validateKey(key);
480    }
481
482    /**
483     * @since 2.9
484     */
485    @Override
486    protected void validate(final String key, final int value) {
487        validateKey(key);
488    }
489
490    /**
491     * @since 2.9
492     */
493    @Override
494    protected void validate(final String key, final long value) {
495        validateKey(key);
496    }
497
498    /**
499     * @since 2.9
500     */
501    @Override
502    protected void validate(final String key, final Object value) {
503        validateKey(key);
504    }
505
506    /**
507     * @since 2.9
508     */
509    @Override
510    protected void validate(final String key, final short value) {
511        validateKey(key);
512    }
513
514    @Override
515    protected void validate(final String key, final String value) {
516        validateKey(key);
517    }
518
519    protected void validateKey(final String key) {
520        if (maxLength > 0 && key.length() > maxLength) {
521            throw new IllegalArgumentException("Structured data keys are limited to " + maxLength +
522                    " characters. key: " + key);
523        }
524        for (int i = 0; i < key.length(); i++) {
525            final char c = key.charAt(i);
526            if (c < '!' || c > '~' || c == '=' || c == ']' || c == '"') {
527                throw new IllegalArgumentException("Structured data keys must contain printable US ASCII characters" +
528                        "and may not contain a space, =, ], or \"");
529            }
530        }
531    }
532
533}