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.util.HashMap;
020import java.util.List;
021import java.util.Map;
022
023import javax.script.SimpleBindings;
024
025import org.apache.logging.log4j.Logger;
026import org.apache.logging.log4j.core.LogEvent;
027import org.apache.logging.log4j.core.config.Configuration;
028import org.apache.logging.log4j.core.config.Node;
029import org.apache.logging.log4j.core.config.plugins.Plugin;
030import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
031import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
032import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
033import org.apache.logging.log4j.core.config.plugins.PluginElement;
034import org.apache.logging.log4j.core.impl.LocationAware;
035import org.apache.logging.log4j.core.pattern.PatternFormatter;
036import org.apache.logging.log4j.core.pattern.PatternParser;
037import org.apache.logging.log4j.core.script.AbstractScript;
038import org.apache.logging.log4j.core.script.ScriptRef;
039import org.apache.logging.log4j.status.StatusLogger;
040
041/**
042 * Selects the pattern to use based on the result of executing a Script. The returned value will be used as the "key"
043 * to choose between one of the configured patterns. If no key is returned or there is no match the default
044 * pattern will be used.
045 */
046@Plugin(name = "ScriptPatternSelector", category = Node.CATEGORY, elementType = PatternSelector.ELEMENT_TYPE, printObject = true)
047public class ScriptPatternSelector implements PatternSelector, LocationAware {
048
049    /**
050     * Custom ScriptPatternSelector builder. Use the {@link #newBuilder() builder factory method} to create this.
051     */
052    public static class Builder implements org.apache.logging.log4j.core.util.Builder<ScriptPatternSelector> {
053
054        @PluginElement("Script")
055        private AbstractScript script;
056
057        @PluginElement("PatternMatch")
058        private PatternMatch[] properties;
059
060        @PluginBuilderAttribute("defaultPattern")
061        private String defaultPattern;
062
063        @PluginBuilderAttribute("alwaysWriteExceptions")
064        private boolean alwaysWriteExceptions = true;
065
066        @PluginBuilderAttribute("disableAnsi")
067        private boolean disableAnsi;
068
069        @PluginBuilderAttribute("noConsoleNoAnsi")
070        private boolean noConsoleNoAnsi;
071
072        @PluginConfiguration
073        private Configuration configuration;
074
075        private Builder() {
076            // nothing
077        }
078
079        @Override
080        public ScriptPatternSelector build() {
081            if (script == null) {
082                LOGGER.error("A Script, ScriptFile or ScriptRef element must be provided for this ScriptFilter");
083                return null;
084            }
085            if (script instanceof ScriptRef) {
086                if (configuration.getScriptManager().getScript(script.getName()) == null) {
087                    LOGGER.error("No script with name {} has been declared.", script.getName());
088                    return null;
089                }
090            }
091            if (defaultPattern == null) {
092                defaultPattern = PatternLayout.DEFAULT_CONVERSION_PATTERN;
093            }
094            if (properties == null || properties.length == 0) {
095                LOGGER.warn("No marker patterns were provided");
096                return null;
097            }
098            return new ScriptPatternSelector(script, properties, defaultPattern, alwaysWriteExceptions, disableAnsi,
099                    noConsoleNoAnsi, configuration);
100        }
101
102        public Builder setScript(final AbstractScript script) {
103            this.script = script;
104            return this;
105        }
106
107        public Builder setProperties(final PatternMatch[] properties) {
108            this.properties = properties;
109            return this;
110        }
111
112        public Builder setDefaultPattern(final String defaultPattern) {
113            this.defaultPattern = defaultPattern;
114            return this;
115        }
116
117        public Builder setAlwaysWriteExceptions(final boolean alwaysWriteExceptions) {
118            this.alwaysWriteExceptions = alwaysWriteExceptions;
119            return this;
120        }
121
122        public Builder setDisableAnsi(final boolean disableAnsi) {
123            this.disableAnsi = disableAnsi;
124            return this;
125        }
126
127        public Builder setNoConsoleNoAnsi(final boolean noConsoleNoAnsi) {
128            this.noConsoleNoAnsi = noConsoleNoAnsi;
129            return this;
130        }
131
132        public Builder setConfiguration(final Configuration config) {
133            this.configuration = config;
134            return this;
135        }
136    }
137
138    private final Map<String, PatternFormatter[]> formatterMap = new HashMap<>();
139
140    private final Map<String, String> patternMap = new HashMap<>();
141
142    private final PatternFormatter[] defaultFormatters;
143
144    private final String defaultPattern;
145
146    private static Logger LOGGER = StatusLogger.getLogger();
147    private final AbstractScript script;
148    private final Configuration configuration;
149    private final boolean requiresLocation;
150
151    /**
152     * @deprecated Use {@link #newBuilder()} instead. This will be private in a future version.
153     */
154    @Deprecated
155    public ScriptPatternSelector(final AbstractScript script, final PatternMatch[] properties, final String defaultPattern,
156                                 final boolean alwaysWriteExceptions, final boolean disableAnsi,
157                                 final boolean noConsoleNoAnsi, final Configuration config) {
158        this.script = script;
159        this.configuration = config;
160        if (!(script instanceof ScriptRef)) {
161            config.getScriptManager().addScript(script);
162        }
163        final PatternParser parser = PatternLayout.createPatternParser(config);
164        boolean needsLocation = false;
165        for (final PatternMatch property : properties) {
166            try {
167                final List<PatternFormatter> list = parser.parse(property.getPattern(), alwaysWriteExceptions, disableAnsi, noConsoleNoAnsi);
168                PatternFormatter[] formatters = list.toArray(PatternFormatter.EMPTY_ARRAY);
169                formatterMap.put(property.getKey(), formatters);
170                patternMap.put(property.getKey(), property.getPattern());
171                for (int i = 0; !needsLocation && i < formatters.length; ++i) {
172                    needsLocation = formatters[i].requiresLocation();
173                }
174            } catch (final RuntimeException ex) {
175                throw new IllegalArgumentException("Cannot parse pattern '" + property.getPattern() + "'", ex);
176            }
177        }
178        try {
179            final List<PatternFormatter> list = parser.parse(defaultPattern, alwaysWriteExceptions, disableAnsi, noConsoleNoAnsi);
180            defaultFormatters = list.toArray(PatternFormatter.EMPTY_ARRAY);
181            this.defaultPattern = defaultPattern;
182            for (int i = 0; !needsLocation && i < defaultFormatters.length; ++i) {
183                needsLocation = defaultFormatters[i].requiresLocation();
184            }
185        } catch (final RuntimeException ex) {
186            throw new IllegalArgumentException("Cannot parse pattern '" + defaultPattern + "'", ex);
187        }
188        this.requiresLocation = needsLocation;
189    }
190
191    @Override
192    public boolean requiresLocation() {
193        return requiresLocation;
194    }
195
196    @Override
197    public PatternFormatter[] getFormatters(final LogEvent event) {
198        final SimpleBindings bindings = new SimpleBindings();
199        bindings.putAll(configuration.getProperties());
200        bindings.put("substitutor", configuration.getStrSubstitutor());
201        bindings.put("logEvent", event);
202        final Object object = configuration.getScriptManager().execute(script.getName(), bindings);
203        if (object == null) {
204            return defaultFormatters;
205        }
206        final PatternFormatter[] patternFormatter = formatterMap.get(object.toString());
207
208        return patternFormatter == null ? defaultFormatters : patternFormatter;
209    }
210
211
212    /**
213     * Creates a builder for a custom ScriptPatternSelector.
214     *
215     * @return a ScriptPatternSelector builder.
216     */
217    @PluginBuilderFactory
218    public static Builder newBuilder() {
219        return new Builder();
220    }
221
222    /**
223     * Deprecated, use {@link #newBuilder()} instead.
224     *
225     * @param script
226     * @param properties
227     * @param defaultPattern
228     * @param alwaysWriteExceptions
229     * @param noConsoleNoAnsi
230     * @param configuration
231     * @return a new ScriptPatternSelector
232     * @deprecated Use {@link #newBuilder()} instead.
233     */
234    @Deprecated
235    public static ScriptPatternSelector createSelector(
236            final AbstractScript script,
237            final PatternMatch[] properties,
238            final String defaultPattern,
239            final boolean alwaysWriteExceptions,
240            final boolean noConsoleNoAnsi,
241            final Configuration configuration) {
242        final Builder builder = newBuilder();
243        builder.setScript(script);
244        builder.setProperties(properties);
245        builder.setDefaultPattern(defaultPattern);
246        builder.setAlwaysWriteExceptions(alwaysWriteExceptions);
247        builder.setNoConsoleNoAnsi(noConsoleNoAnsi);
248        builder.setConfiguration(configuration);
249        return builder.build();
250    }
251
252    @Override
253    public String toString() {
254        final StringBuilder sb = new StringBuilder();
255        boolean first = true;
256        for (final Map.Entry<String, String> entry : patternMap.entrySet()) {
257            if (!first) {
258                sb.append(", ");
259            }
260            sb.append("key=\"").append(entry.getKey()).append("\", pattern=\"").append(entry.getValue()).append("\"");
261            first = false;
262        }
263        if (!first) {
264            sb.append(", ");
265        }
266        sb.append("default=\"").append(defaultPattern).append("\"");
267        return sb.toString();
268    }
269}