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