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.nio.charset.StandardCharsets;
021import java.util.HashMap;
022import java.util.Map;
023
024import org.apache.logging.log4j.core.Layout;
025import org.apache.logging.log4j.core.config.Configuration;
026import org.apache.logging.log4j.core.config.Node;
027import org.apache.logging.log4j.core.config.plugins.Plugin;
028import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
029import org.apache.logging.log4j.core.jackson.XmlConstants;
030import org.apache.logging.log4j.core.util.KeyValuePair;
031
032/**
033 * Appends a series of {@code event} elements as defined in the <a href="log4j.dtd">log4j.dtd</a>.
034 *
035 * <h3>Complete well-formed XML vs. fragment XML</h3>
036 * <p>
037 * If you configure {@code complete="true"}, the appender outputs a well-formed XML document where the default namespace
038 * is the log4j namespace {@value XmlConstants#XML_NAMESPACE}. By default, with {@code complete="false"}, you should
039 * include the output as an <em>external entity</em> in a separate file to form a well-formed XML document.
040 * </p>
041 * <p>
042 * If {@code complete="false"}, the appender does not write the XML processing instruction and the root element.
043 * </p>
044 * <h3>Encoding</h3>
045 * <p>
046 * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise
047 * events containing non-ASCII characters could result in corrupted log files.
048 * </p>
049 * <h3>Pretty vs. compact XML</h3>
050 * <p>
051 * By default, the XML layout is not compact (compact = not "pretty") with {@code compact="false"}, which means the
052 * appender uses end-of-line characters and indents lines to format the XML. If {@code compact="true"}, then no
053 * end-of-line or indentation is used. Message content may contain, of course, end-of-lines.
054 * </p>
055 * <h3>Additional Fields</h3>
056 * <p>
057 * This property allows addition of custom fields into generated JSON.
058 * {@code <XmlLayout><KeyValuePair key="foo" value="bar"/></XmlLayout>} inserts {@code <foo>bar</foo>} directly
059 * into XML output. Supports Lookup expressions.
060 * </p>
061 */
062@Plugin(name = "XmlLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
063public final class XmlLayout extends AbstractJacksonLayout {
064
065    private static final String ROOT_TAG = "Events";
066
067    public static class Builder<B extends Builder<B>> extends AbstractJacksonLayout.Builder<B>
068        implements org.apache.logging.log4j.core.util.Builder<XmlLayout> {
069
070        public Builder() {
071            super();
072            setCharset(StandardCharsets.UTF_8);
073        }
074
075        @Override
076        public XmlLayout build() {
077            return new XmlLayout(getConfiguration(), isLocationInfo(), isProperties(), isComplete(),
078                    isCompact(), getEndOfLine(), getCharset(), isIncludeStacktrace(), isStacktraceAsString(),
079                    isIncludeNullDelimiter(), getAdditionalFields());
080        }
081    }
082
083    /**
084     * @deprecated Use {@link #newBuilder()} instead
085     */
086    @Deprecated
087    protected XmlLayout(final boolean locationInfo, final boolean properties, final boolean complete,
088                        final boolean compact, final Charset charset, final boolean includeStacktrace) {
089        this(null, locationInfo, properties, complete, compact, null, charset, includeStacktrace, false, false, null);
090    }
091
092    private XmlLayout(final Configuration config, final boolean locationInfo, final boolean properties,
093                      final boolean complete, final boolean compact, final String endOfLine, final Charset charset,
094                      final boolean includeStacktrace, final boolean stacktraceAsString,
095                      final boolean includeNullDelimiter,
096                      final KeyValuePair[] additionalFields) {
097        super(config, new JacksonFactory.XML(includeStacktrace, stacktraceAsString).newWriter(
098            locationInfo, properties, compact),
099            charset, compact, complete, false, endOfLine, null, null, includeNullDelimiter,
100            additionalFields);
101    }
102
103    /**
104     * Returns appropriate XML headers.
105     * <ol>
106     * <li>XML processing instruction</li>
107     * <li>XML root element</li>
108     * </ol>
109     *
110     * @return a byte array containing the header.
111     */
112    @Override
113    public byte[] getHeader() {
114        if (!complete) {
115            return null;
116        }
117        final StringBuilder buf = new StringBuilder();
118        buf.append("<?xml version=\"1.0\" encoding=\"");
119        buf.append(this.getCharset().name());
120        buf.append("\"?>");
121        buf.append(this.eol);
122        // Make the log4j namespace the default namespace, no need to use more space with a namespace prefix.
123        buf.append('<');
124        buf.append(ROOT_TAG);
125        buf.append(" xmlns=\"" + XmlConstants.XML_NAMESPACE + "\">");
126        buf.append(this.eol);
127        return buf.toString().getBytes(this.getCharset());
128    }
129
130    /**
131     * Returns appropriate XML footer.
132     *
133     * @return a byte array containing the footer, closing the XML root element.
134     */
135    @Override
136    public byte[] getFooter() {
137        if (!complete) {
138            return null;
139        }
140        return getBytes("</" + ROOT_TAG + '>' + this.eol);
141    }
142
143    /**
144     * Gets this XmlLayout's content format. Specified by:
145     * <ul>
146     * <li>Key: "dtd" Value: "log4j-events.dtd"</li>
147     * <li>Key: "version" Value: "2.0"</li>
148     * </ul>
149     *
150     * @return Map of content format keys supporting XmlLayout
151     */
152    @Override
153    public Map<String, String> getContentFormat() {
154        final Map<String, String> result = new HashMap<>();
155        // result.put("dtd", "log4j-events.dtd");
156        result.put("xsd", "log4j-events.xsd");
157        result.put("version", "2.0");
158        return result;
159    }
160
161    /**
162     * @return The content type.
163     */
164    @Override
165    public String getContentType() {
166        return "text/xml; charset=" + this.getCharset();
167    }
168
169    /**
170     * Creates an XML Layout.
171     *
172     * @param locationInfo If "true", includes the location information in the generated XML.
173     * @param properties If "true", includes the thread context map in the generated XML.
174     * @param complete If "true", includes the XML header and footer, defaults to "false".
175     * @param compact If "true", does not use end-of-lines and indentation, defaults to "false".
176     * @param charset The character set to use, if {@code null}, uses "UTF-8".
177     * @param includeStacktrace
178     *            If "true", includes the stacktrace of any Throwable in the generated XML, defaults to "true".
179     * @return An XML Layout.
180     *
181     * @deprecated Use {@link #newBuilder()} instead
182     */
183    @Deprecated
184    public static XmlLayout createLayout(
185            final boolean locationInfo,
186            final boolean properties,
187            final boolean complete,
188            final boolean compact,
189            final Charset charset,
190            final boolean includeStacktrace) {
191        return new XmlLayout(null, locationInfo, properties, complete, compact, null, charset, includeStacktrace, false,
192                false, null);
193    }
194
195    @PluginBuilderFactory
196    public static <B extends Builder<B>> B newBuilder() {
197        return new Builder<B>().asBuilder();
198    }
199
200    /**
201     * Creates an XML Layout using the default settings.
202     *
203     * @return an XML Layout.
204     */
205    public static XmlLayout createDefaultLayout() {
206        return new XmlLayout(null, false, false, false, false, null, StandardCharsets.UTF_8, true, false, false, null);
207    }
208}