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 */
017package org.apache.logging.log4j.message;
018
019import java.util.Map;
020
021import org.apache.logging.log4j.util.EnglishEnums;
022
023/**
024 * Represents a Message that conforms to an RFC 5424 StructuredData element along with the syslog message.
025 * <p>
026 * Thread-safety note: the contents of this message can be modified after construction.
027 * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is
028 * logged, because it is undefined whether the logged message string will contain the old values or the modified
029 * values.
030 *
031 * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>
032 */
033public class StructuredDataMessage extends MapMessage {
034
035    private static final long serialVersionUID = 1703221292892071920L;
036    private static final int MAX_LENGTH = 32;
037    private static final int HASHVAL = 31;
038
039    private StructuredDataId id;
040
041    private String message;
042
043    private String type;
044
045    /**
046     * Supported formats.
047     */
048    public enum Format {
049        /** The map should be formatted as XML. */
050        XML,
051        /** Full message format includes the type and message. */
052        FULL
053    }
054
055    /**
056     * Constructor based on a String id.
057     * @param id The String id.
058     * @param msg The message.
059     * @param type The message type.
060     */
061    public StructuredDataMessage(final String id, final String msg, final String type) {
062        this.id = new StructuredDataId(id, null, null);
063        this.message = msg;
064        this.type = type;
065    }
066    /**
067     * Constructor based on a String id.
068     * @param id The String id.
069     * @param msg The message.
070     * @param type The message type.
071     * @param data The StructuredData map.
072     */
073    public StructuredDataMessage(final String id, final String msg, final String type,
074                                 final Map<String, String> data) {
075        super(data);
076        this.id = new StructuredDataId(id, null, null);
077        this.message = msg;
078        this.type = type;
079    }
080
081    /**
082     * Constructor based on a StructuredDataId.
083     * @param id The StructuredDataId.
084     * @param msg The message.
085     * @param type The message type.
086     */
087    public StructuredDataMessage(final StructuredDataId id, final String msg, final String type) {
088        this.id = id;
089        this.message = msg;
090        this.type = type;
091    }
092
093    /**
094     * Constructor based on a StructuredDataId.
095     * @param id The StructuredDataId.
096     * @param msg The message.
097     * @param type The message type.
098     * @param data The StructuredData map.
099     */
100    public StructuredDataMessage(final StructuredDataId id, final String msg, final String type,
101                                 final Map<String, String> data) {
102        super(data);
103        this.id = id;
104        this.message = msg;
105        this.type = type;
106    }
107
108
109    /**
110     * Constructor based on a StructuredDataMessage.
111     * @param msg The StructuredDataMessage.
112     * @param map The StructuredData map.
113     */
114    private StructuredDataMessage(final StructuredDataMessage msg, final Map<String, String> map) {
115        super(map);
116        this.id = msg.id;
117        this.message = msg.message;
118        this.type = msg.type;
119    }
120
121
122    /**
123     * Basic constructor.
124     */
125    protected StructuredDataMessage() {
126
127    }
128
129    /**
130     * Returns the supported formats.
131     * @return An array of the supported format names.
132     */
133    @Override
134    public String[] getFormats() {
135        final String[] formats = new String[Format.values().length];
136        int i = 0;
137        for (final Format format : Format.values()) {
138            formats[i++] = format.name();
139        }
140        return formats;
141    }
142
143    /**
144     * Returns the id.
145     * @return the StructuredDataId.
146     */
147    public StructuredDataId getId() {
148        return id;
149    }
150
151    /**
152     * Sets the id from a String.
153     * @param id The String id.
154     */
155    protected void setId(final String id) {
156        this.id = new StructuredDataId(id, null, null);
157    }
158
159    /**
160     * Sets the id.
161     * @param id The StructuredDataId.
162     */
163    protected void setId(final StructuredDataId id) {
164        this.id = id;
165    }
166
167    /**
168     * Sets the type.
169     * @return the type.
170     */
171    public String getType() {
172        return type;
173    }
174
175    protected void setType(final String type) {
176        if (type.length() > MAX_LENGTH) {
177            throw new IllegalArgumentException("structured data type exceeds maximum length of 32 characters: " + type);
178        }
179        this.type = type;
180    }
181
182    /**
183     * Returns the message.
184     * @return the message.
185     */
186    @Override
187    public String getFormat() {
188        return message;
189    }
190
191    protected void setMessageFormat(final String msg) {
192        this.message = msg;
193    }
194
195
196    @Override
197    protected void validate(final String key, final String value) {
198        validateKey(key);
199    }
200
201    private void validateKey(final String key) {
202        if (key.length() > MAX_LENGTH) {
203            throw new IllegalArgumentException("Structured data keys are limited to 32 characters. key: " + key);
204        }
205        final char[] chars = key.toCharArray();
206        for (final char c : chars) {
207            if (c < '!' || c > '~' || c == '=' || c == ']' || c == '"') {
208                throw new IllegalArgumentException("Structured data keys must contain printable US ASCII characters" +
209                        "and may not contain a space, =, ], or \"");
210            }
211        }
212    }
213
214    /**
215     * Formats the structured data as described in RFC 5424.
216     *
217     * @return The formatted String.
218     */
219    @Override
220    public String asString() {
221        return asString(Format.FULL, null);
222    }
223
224    /**
225     * Formats the structured data as described in RFC 5424.
226     *
227     * @param format The format identifier. Ignored in this implementation.
228     * @return The formatted String.
229     */
230
231    @Override
232    public String asString(final String format) {
233        try {
234            return asString(EnglishEnums.valueOf(Format.class, format), null);
235        } catch (final IllegalArgumentException ex) {
236            return asString();
237        }
238    }
239
240    /**
241     * Formats the structured data as described in RFC 5424.
242     *
243     * @param format           "full" will include the type and message. null will return only the STRUCTURED-DATA as
244     *                         described in RFC 5424
245     * @param structuredDataId The SD-ID as described in RFC 5424. If null the value in the StructuredData
246     *                         will be used.
247     * @return The formatted String.
248     */
249    public final String asString(final Format format, final StructuredDataId structuredDataId) {
250        final StringBuilder sb = new StringBuilder();
251        final boolean full = Format.FULL.equals(format);
252        if (full) {
253            final String type = getType();
254            if (type == null) {
255                return sb.toString();
256            }
257            sb.append(getType()).append(' ');
258        }
259        StructuredDataId id = getId();
260        if (id != null) {
261            id = id.makeId(structuredDataId);
262        } else {
263            id = structuredDataId;
264        }
265        if (id == null || id.getName() == null) {
266            return sb.toString();
267        }
268        sb.append('[');
269        sb.append(id);
270        sb.append(' ');
271        appendMap(sb);
272        sb.append(']');
273        if (full) {
274            final String msg = getFormat();
275            if (msg != null) {
276                sb.append(' ').append(msg);
277            }
278        }
279        return sb.toString();
280    }
281
282    /**
283     * Formats the message and return it.
284     * @return the formatted message.
285     */
286    @Override
287    public String getFormattedMessage() {
288        return asString(Format.FULL, null);
289    }
290
291    /**
292     * Formats the message according the the specified format.
293     * @param formats An array of Strings that provide extra information about how to format the message.
294     * StructuredDataMessage accepts only a format of "FULL" which will cause the event type to be
295     * prepended and the event message to be appended. Specifying any other value will cause only the
296     * StructuredData to be included. The default is "FULL".
297     *
298     * @return the formatted message.
299     */
300    @Override
301    public String getFormattedMessage(final String[] formats) {
302        if (formats != null && formats.length > 0) {
303            for (final String format : formats) {
304                if (Format.XML.name().equalsIgnoreCase(format)) {
305                    return asXML();
306                } else if (Format.FULL.name().equalsIgnoreCase(format)) {
307                    return asString(Format.FULL, null);
308                }
309            }
310            return asString(null, null);
311        }
312        return asString(Format.FULL, null);
313    }
314
315    private String asXML() {
316        final StringBuilder sb = new StringBuilder();
317        final StructuredDataId id = getId();
318        if (id == null || id.getName() == null || type == null) {
319            return sb.toString();
320        }
321        sb.append("<StructuredData>\n");
322        sb.append("<type>").append(type).append("</type>\n");
323        sb.append("<id>").append(id).append("</id>\n");
324        super.asXml(sb);
325        sb.append("</StructuredData>\n");
326        return sb.toString();
327    }
328
329    @Override
330    public String toString() {
331        return asString(null, null);
332    }
333
334
335    @Override
336    public MapMessage newInstance(final Map<String, String> map) {
337        return new StructuredDataMessage(this, map);
338    }
339
340    @Override
341    public boolean equals(final Object o) {
342        if (this == o) {
343            return true;
344        }
345        if (o == null || getClass() != o.getClass()) {
346            return false;
347        }
348
349        final StructuredDataMessage that = (StructuredDataMessage) o;
350
351        if (!super.equals(o)) {
352            return false;
353        }
354        if (type != null ? !type.equals(that.type) : that.type != null) {
355            return false;
356        }
357        if (id != null ? !id.equals(that.id) : that.id != null) {
358            return false;
359        }
360        if (message != null ? !message.equals(that.message) : that.message != null) {
361            return false;
362        }
363
364        return true;
365    }
366
367    @Override
368    public int hashCode() {
369        int result = super.hashCode();
370        result = HASHVAL * result + (type != null ? type.hashCode() : 0);
371        result = HASHVAL * result + (id != null ? id.hashCode() : 0);
372        result = HASHVAL * result + (message != null ? message.hashCode() : 0);
373        return result;
374    }
375}