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(), isIncludeTimeMillis(), 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,
090                false, false, false, null);
091    }
092
093    private XmlLayout(final Configuration config, final boolean locationInfo, final boolean properties,
094                      final boolean complete, final boolean compact, final String endOfLine, final Charset charset,
095                      final boolean includeStacktrace, final boolean stacktraceAsString,
096                      final boolean includeNullDelimiter, final boolean includeTimeMillis,
097                      final KeyValuePair[] additionalFields) {
098        super(config, new JacksonFactory.XML(includeStacktrace, stacktraceAsString).newWriter(
099            locationInfo, properties, compact, includeTimeMillis),
100            charset, compact, complete, false, endOfLine, null, null, includeNullDelimiter,
101            additionalFields);
102    }
103
104    /**
105     * Returns appropriate XML headers.
106     * <ol>
107     * <li>XML processing instruction</li>
108     * <li>XML root element</li>
109     * </ol>
110     *
111     * @return a byte array containing the header.
112     */
113    @Override
114    public byte[] getHeader() {
115        if (!complete) {
116            return null;
117        }
118        final StringBuilder buf = new StringBuilder();
119        buf.append("<?xml version=\"1.0\" encoding=\"");
120        buf.append(this.getCharset().name());
121        buf.append("\"?>");
122        buf.append(this.eol);
123        // Make the log4j namespace the default namespace, no need to use more space with a namespace prefix.
124        buf.append('<');
125        buf.append(ROOT_TAG);
126        buf.append(" xmlns=\"" + XmlConstants.XML_NAMESPACE + "\">");
127        buf.append(this.eol);
128        return buf.toString().getBytes(this.getCharset());
129    }
130
131    /**
132     * Returns appropriate XML footer.
133     *
134     * @return a byte array containing the footer, closing the XML root element.
135     */
136    @Override
137    public byte[] getFooter() {
138        if (!complete) {
139            return null;
140        }
141        return getBytes("</" + ROOT_TAG + '>' + this.eol);
142    }
143
144    /**
145     * Gets this XmlLayout's content format. Specified by:
146     * <ul>
147     * <li>Key: "dtd" Value: "log4j-events.dtd"</li>
148     * <li>Key: "version" Value: "2.0"</li>
149     * </ul>
150     *
151     * @return Map of content format keys supporting XmlLayout
152     */
153    @Override
154    public Map<String, String> getContentFormat() {
155        final Map<String, String> result = new HashMap<>();
156        // result.put("dtd", "log4j-events.dtd");
157        result.put("xsd", "log4j-events.xsd");
158        result.put("version", "2.0");
159        return result;
160    }
161
162    /**
163     * @return The content type.
164     */
165    @Override
166    public String getContentType() {
167        return "text/xml; charset=" + this.getCharset();
168    }
169
170    /**
171     * Creates an XML Layout.
172     *
173     * @param locationInfo If "true", includes the location information in the generated XML.
174     * @param properties If "true", includes the thread context map in the generated XML.
175     * @param complete If "true", includes the XML header and footer, defaults to "false".
176     * @param compact If "true", does not use end-of-lines and indentation, defaults to "false".
177     * @param charset The character set to use, if {@code null}, uses "UTF-8".
178     * @param includeStacktrace
179     *            If "true", includes the stacktrace of any Throwable in the generated XML, defaults to "true".
180     * @return An XML Layout.
181     *
182     * @deprecated Use {@link #newBuilder()} instead
183     */
184    @Deprecated
185    public static XmlLayout createLayout(
186            final boolean locationInfo,
187            final boolean properties,
188            final boolean complete,
189            final boolean compact,
190            final Charset charset,
191            final boolean includeStacktrace) {
192        return new XmlLayout(null, locationInfo, properties, complete, compact, null, charset, includeStacktrace, false,
193                false, false, null);
194    }
195
196    @PluginBuilderFactory
197    public static <B extends Builder<B>> B newBuilder() {
198        return new Builder<B>().asBuilder();
199    }
200
201    /**
202     * Creates an XML Layout using the default settings.
203     *
204     * @return an XML Layout.
205     */
206    public static XmlLayout createDefaultLayout() {
207        return new XmlLayout(null, false, false, false, false, null, StandardCharsets.UTF_8, true, false, false,
208                false, null);
209    }
210}