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.core.layout;
018
019import java.nio.charset.Charset;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.logging.log4j.Marker;
025import org.apache.logging.log4j.core.LogEvent;
026import org.apache.logging.log4j.core.config.plugins.Plugin;
027import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
028import org.apache.logging.log4j.core.config.plugins.PluginFactory;
029import org.apache.logging.log4j.core.helpers.Charsets;
030import org.apache.logging.log4j.core.helpers.Strings;
031import org.apache.logging.log4j.core.helpers.Throwables;
032import org.apache.logging.log4j.core.helpers.Transform;
033import org.apache.logging.log4j.message.Message;
034import org.apache.logging.log4j.message.MultiformatMessage;
035
036
037/**
038 * Appends a series of {@code event} elements as defined in the <a href="log4j.dtd">log4j.dtd</a>.
039 *
040 * <h4>Complete well-formed XML vs. fragment XML</h4>
041 * <p>
042 * If you configure {@code complete="true"}, the appender outputs a well-formed XML document where the default namespace
043 * is the log4j namespace {@value #XML_NAMESPACE}. By default, with {@code complete="false"}, you should include the
044 * output as an <em>external entity</em> in a separate file to form a well-formed XML document, in which case the
045 * appender uses {@code namespacePrefix} with a default of {@value #DEFAULT_NS_PREFIX}.
046 * </p>
047 * <p>
048 * A well-formed XML document follows this pattern:
049 * </p>
050 *
051 * <pre>
052 * &lt;?xml version="1.0" encoding=&quotUTF-8&quot?&gt;
053 * &lt;Events xmlns="http://logging.apache.org/log4j/2.0/events"&gt;
054 * &nbsp;&nbsp;&lt;Event logger="com.foo.Bar" timestamp="1373436580419" level="INFO" thread="main"&gt;
055 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;Message>&lt;![CDATA[This is a log message 1]]&gt;&lt;/Message&gt;
056 * &nbsp;&nbsp;&lt;/Event&gt;
057 * &nbsp;&nbsp;&lt;Event logger="com.foo.Baz" timestamp="1373436580420" level="INFO" thread="main"&gt;
058 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;Message>&lt;![CDATA[This is a log message 2]]&gt;&lt;/Message&gt;
059 * &nbsp;&nbsp;&lt;/Event&gt;
060 * &lt;/Events&gt;
061 * </pre>
062 * <p>
063 * If {@code complete="false"}, the appender does not write the XML processing instruction and the root element.
064 * </p>
065 * <p>
066 * This approach enforces the independence of the XMLLayout and the appender where you embed it.
067 * </p>
068 * <h4>Encoding</h4>
069 * <p>
070 * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise
071 * events containing non ASCII characters could result in corrupted log files.
072 * </p>
073 * <h4>Pretty vs. compact XML</h4>
074 * <p>
075 * By default, the XML layout is not compact (a.k.a. not "pretty") with {@code compact="false"}, which means the
076 * appender uses end-of-line characters and indents lines to format the XML. If {@code compact="true"}, then no
077 * end-of-line or indentation is used. Message content may contain, of course, end-of-lines.
078 * </p>
079 */
080@Plugin(name = "XMLLayout", category = "Core", elementType = "layout", printObject = true)
081public class XMLLayout extends AbstractStringLayout {
082
083    private static final String XML_NAMESPACE = "http://logging.apache.org/log4j/2.0/events";
084    private static final String ROOT_TAG = "Events";
085    private static final int DEFAULT_SIZE = 256;
086
087    // We yield to \r\n for the default.
088    private static final String DEFAULT_EOL = "\r\n";
089    private static final String COMPACT_EOL = "";
090    private static final String DEFAULT_INDENT = "  ";
091    private static final String COMPACT_INDENT = "";
092    private static final String DEFAULT_NS_PREFIX = "log4j";
093
094    private static final String[] FORMATS = new String[] {"xml"};
095
096    private final boolean locationInfo;
097    private final boolean properties;
098    private final boolean complete;
099    private final String namespacePrefix;
100    private final String eol;
101    private final String indent1;
102    private final String indent2;
103    private final String indent3;
104
105    protected XMLLayout(final boolean locationInfo, final boolean properties, final boolean complete,
106                        boolean compact, final String nsPrefix, final Charset charset) {
107        super(charset);
108        this.locationInfo = locationInfo;
109        this.properties = properties;
110        this.complete = complete;
111        this.eol = compact ? COMPACT_EOL : DEFAULT_EOL;
112        this.indent1 = compact ? COMPACT_INDENT : DEFAULT_INDENT;
113        this.indent2 = this.indent1 + this.indent1;
114        this.indent3 = this.indent2 + this.indent1;
115        this.namespacePrefix = (Strings.isEmpty(nsPrefix) ? DEFAULT_NS_PREFIX : nsPrefix) + ":";
116    }
117
118    /**
119     * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the log4j.dtd.
120     *
121     * @param event The LogEvent.
122     * @return The XML representation of the LogEvent.
123     */
124    @Override
125    public String toSerializable(final LogEvent event) {
126        final StringBuilder buf = new StringBuilder(DEFAULT_SIZE);
127
128        buf.append(this.indent1);
129        buf.append('<');
130        if (!complete) {
131            buf.append(this.namespacePrefix);
132        }
133        buf.append("Event logger=\"");
134        String name = event.getLoggerName();
135        if (name.isEmpty()) {
136            name = "root";
137        }
138        buf.append(Transform.escapeHtmlTags(name));
139        buf.append("\" timestamp=\"");
140        buf.append(event.getMillis());
141        buf.append("\" level=\"");
142        buf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
143        buf.append("\" thread=\"");
144        buf.append(Transform.escapeHtmlTags(event.getThreadName()));
145        buf.append("\">");
146        buf.append(this.eol);
147
148        final Message msg = event.getMessage();
149        if (msg != null) {
150            boolean xmlSupported = false;
151            if (msg instanceof MultiformatMessage) {
152                final String[] formats = ((MultiformatMessage) msg).getFormats();
153                for (final String format : formats) {
154                    if (format.equalsIgnoreCase("XML")) {
155                        xmlSupported = true;
156                        break;
157                    }
158                }
159            }
160            buf.append(this.indent2);
161            buf.append('<');
162            if (!complete) {
163                buf.append(this.namespacePrefix);
164            }
165            buf.append("Message>");
166            if (xmlSupported) {
167                buf.append(((MultiformatMessage) msg).getFormattedMessage(FORMATS));
168            } else {
169                buf.append("<![CDATA[");
170                // Append the rendered message. Also make sure to escape any
171                // existing CDATA sections.
172                Transform.appendEscapingCDATA(buf, event.getMessage().getFormattedMessage());
173                buf.append("]]>");
174            }
175            buf.append("</");
176            if (!complete) {
177                buf.append(this.namespacePrefix);
178            }
179            buf.append("Message>");
180            buf.append(this.eol);
181        }
182
183        if (event.getContextStack().getDepth() > 0) {
184            buf.append(this.indent2);
185            buf.append('<');
186            if (!complete) {
187                buf.append(this.namespacePrefix);
188            }
189            buf.append("NDC><![CDATA[");
190            Transform.appendEscapingCDATA(buf, event.getContextStack().toString());
191            buf.append("]]></");
192            if (!complete) {
193                buf.append(this.namespacePrefix);
194            }
195            buf.append("NDC>");
196            buf.append(this.eol);
197        }
198
199        if (event.getMarker() != null) {
200            final Marker marker = event.getMarker();
201            buf.append(this.indent2);
202            buf.append('<');
203            if (!complete) {
204                buf.append(this.namespacePrefix);
205            }
206            buf.append("Marker");
207            final Marker parent = marker.getParent();
208            if (parent != null) {
209                buf.append(" parent=\"").append(Transform.escapeHtmlTags(parent.getName())).append("\"");
210            }
211            buf.append('>');
212            buf.append(Transform.escapeHtmlTags(marker.getName()));
213            buf.append("</");
214            if (!complete) {
215                buf.append(this.namespacePrefix);
216            }
217            buf.append("Marker>");
218            buf.append(this.eol);
219        }
220
221        final Throwable throwable = event.getThrown();
222        if (throwable != null) {
223            final List<String> s = Throwables.toStringList(throwable);
224            buf.append(this.indent2);
225            buf.append('<');
226            if (!complete) {
227                buf.append(this.namespacePrefix);
228            }
229            buf.append("Throwable><![CDATA[");
230            for (final String str : s) {
231                Transform.appendEscapingCDATA(buf, str);
232                buf.append(this.eol);
233            }
234            buf.append("]]></");
235            if (!complete) {
236                buf.append(this.namespacePrefix);
237            }
238            buf.append("Throwable>");
239            buf.append(this.eol);
240        }
241
242        if (locationInfo) {
243            final StackTraceElement element = event.getSource();
244            buf.append(this.indent2);
245            buf.append('<');
246            if (!complete) {
247                buf.append(this.namespacePrefix);
248            }
249            buf.append("LocationInfo class=\"");
250            buf.append(Transform.escapeHtmlTags(element.getClassName()));
251            buf.append("\" method=\"");
252            buf.append(Transform.escapeHtmlTags(element.getMethodName()));
253            buf.append("\" file=\"");
254            buf.append(Transform.escapeHtmlTags(element.getFileName()));
255            buf.append("\" line=\"");
256            buf.append(element.getLineNumber());
257            buf.append("\"/>");
258            buf.append(this.eol);
259        }
260
261        if (properties && event.getContextMap().size() > 0) {
262            buf.append(this.indent2);
263            buf.append('<');
264            if (!complete) {
265                buf.append(this.namespacePrefix);
266            }
267            buf.append("Properties>");
268            buf.append(this.eol);
269            for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
270                buf.append(this.indent3);
271                buf.append('<');
272                if (!complete) {
273                    buf.append(this.namespacePrefix);
274                }
275                buf.append("Data name=\"");
276                buf.append(Transform.escapeHtmlTags(entry.getKey()));
277                buf.append("\" value=\"");
278                buf.append(Transform.escapeHtmlTags(String.valueOf(entry.getValue())));
279                buf.append("\"/>");
280                buf.append(this.eol);
281            }
282            buf.append(this.indent2);
283            buf.append("</");
284            if (!complete) {
285                buf.append(this.namespacePrefix);
286            }
287            buf.append("Properties>");
288            buf.append(this.eol);
289        }
290
291        buf.append(this.indent1);
292        buf.append("</");
293        if (!complete) {
294            buf.append(this.namespacePrefix);
295        }
296        buf.append("Event>");
297        buf.append(this.eol);
298
299        return buf.toString();
300    }
301
302    /**
303     * Returns appropriate XML headers.
304     * <ol>
305     * <li>XML processing instruction</li>
306     * <li>XML root element</li>
307     * </ol>
308     *
309     * @return a byte array containing the header.
310     */
311    @Override
312    public byte[] getHeader() {
313        if (!complete) {
314            return null;
315        }
316        final StringBuilder buf = new StringBuilder();
317        buf.append("<?xml version=\"1.0\" encoding=\"");
318        buf.append(this.getCharset().name());
319        buf.append("\"?>");
320        buf.append(this.eol);
321        // Make the log4j namespace the default namespace, no need to use more space with a namespace prefix.
322        buf.append('<');
323        buf.append(ROOT_TAG);
324        buf.append(" xmlns=\"" + XML_NAMESPACE + "\">");
325        buf.append(this.eol);
326        return buf.toString().getBytes(this.getCharset());
327    }
328
329
330    /**
331     * Returns appropriate XML footer.
332     *
333     * @return a byte array containing the footer, closing the XML root element.
334     */
335    @Override
336    public byte[] getFooter() {
337        if (!complete) {
338            return null;
339        }
340        return ("</" + ROOT_TAG + ">" + this.eol).getBytes(getCharset());
341    }
342
343    /**
344     * XMLLayout's content format is specified by:<p/>
345     * Key: "dtd" Value: "log4j-events.dtd"<p/>
346     * Key: "version" Value: "2.0"
347     * @return Map of content format keys supporting XMLLayout
348     */
349    @Override
350    public Map<String, String> getContentFormat() {
351        final Map<String, String> result = new HashMap<String, String>();
352        //result.put("dtd", "log4j-events.dtd");
353        result.put("xsd", "log4j-events.xsd");
354        result.put("version", "2.0");
355        return result;
356    }
357
358    @Override
359    /**
360     * @return The content type.
361     */
362    public String getContentType() {
363        return "text/xml; charset=" + this.getCharset();
364    }
365
366    /**
367     * Creates an XML Layout.
368     *
369     * @param locationInfo If "true", includes the location information in the generated XML.
370     * @param properties If "true", includes the thread context in the generated XML.
371     * @param completeStr If "true", includes the XML header and footer, defaults to "false".
372     * @param compactStr If "true", does not use end-of-lines and indentation, defaults to "false".
373     * @param namespacePrefix The namespace prefix, defaults to {@value #DEFAULT_NS_PREFIX}
374     * @param charsetName The character set to use, if {@code null}, uses "UTF-8".
375     * @return An XML Layout.
376     */
377    @PluginFactory
378    public static XMLLayout createLayout(
379            @PluginAttribute("locationInfo") final String locationInfo,
380            @PluginAttribute("properties") final String properties,
381            @PluginAttribute("complete") final String completeStr,
382            @PluginAttribute("compact") final String compactStr,
383            @PluginAttribute("namespacePrefix") final String namespacePrefix,
384            @PluginAttribute("charset") final String charsetName) {
385        final Charset charset = Charsets.getSupportedCharset(charsetName, Charsets.UTF_8);
386        final boolean info = Boolean.parseBoolean(locationInfo);
387        final boolean props = Boolean.parseBoolean(properties);
388        final boolean complete = Boolean.parseBoolean(completeStr);
389        final boolean compact = Boolean.parseBoolean(compactStr);
390        return new XMLLayout(info, props, complete, compact, namespacePrefix, charset);
391    }
392}