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;
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 */
035public 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}