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 */
017
018package org.apache.logging.log4j.core.config.properties;
019
020import java.util.Map;
021import java.util.Properties;
022import java.util.concurrent.TimeUnit;
023
024import org.apache.logging.log4j.Level;
025import org.apache.logging.log4j.core.Appender;
026import org.apache.logging.log4j.core.LoggerContext;
027import org.apache.logging.log4j.core.config.ConfigurationException;
028import org.apache.logging.log4j.core.config.ConfigurationSource;
029import org.apache.logging.log4j.core.config.LoggerConfig;
030import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
031import org.apache.logging.log4j.core.config.builder.api.AppenderRefComponentBuilder;
032import org.apache.logging.log4j.core.config.builder.api.ComponentBuilder;
033import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
034import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
035import org.apache.logging.log4j.core.config.builder.api.FilterComponentBuilder;
036import org.apache.logging.log4j.core.config.builder.api.FilterableComponentBuilder;
037import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
038import org.apache.logging.log4j.core.config.builder.api.LoggableComponentBuilder;
039import org.apache.logging.log4j.core.config.builder.api.LoggerComponentBuilder;
040import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
041import org.apache.logging.log4j.core.config.builder.api.ScriptComponentBuilder;
042import org.apache.logging.log4j.core.config.builder.api.ScriptFileComponentBuilder;
043import org.apache.logging.log4j.core.filter.AbstractFilter.AbstractFilterBuilder;
044import org.apache.logging.log4j.core.util.Builder;
045import org.apache.logging.log4j.util.PropertiesUtil;
046import org.apache.logging.log4j.util.Strings;
047
048/**
049 * Helper builder for parsing properties files into a PropertiesConfiguration.
050 *
051 * @since 2.6
052 */
053public class PropertiesConfigurationBuilder extends ConfigurationBuilderFactory
054    implements Builder<PropertiesConfiguration> {
055
056    private static final String ADVERTISER_KEY = "advertiser";
057    private static final String STATUS_KEY = "status";
058    private static final String SHUTDOWN_HOOK = "shutdownHook";
059    private static final String SHUTDOWN_TIMEOUT = "shutdownTimeout";
060    private static final String VERBOSE = "verbose";
061    private static final String DEST = "dest";
062    private static final String PACKAGES = "packages";
063    private static final String CONFIG_NAME = "name";
064    private static final String MONITOR_INTERVAL = "monitorInterval";
065    private static final String CONFIG_TYPE = "type";
066
067    private final ConfigurationBuilder<PropertiesConfiguration> builder;
068    private LoggerContext loggerContext;
069    private Properties rootProperties;
070
071    public PropertiesConfigurationBuilder() {
072        this.builder = newConfigurationBuilder(PropertiesConfiguration.class);
073    }
074
075    public PropertiesConfigurationBuilder setRootProperties(final Properties rootProperties) {
076        this.rootProperties = rootProperties;
077        return this;
078    }
079
080    public PropertiesConfigurationBuilder setConfigurationSource(final ConfigurationSource source) {
081        builder.setConfigurationSource(source);
082        return this;
083    }
084
085    @Override
086    public PropertiesConfiguration build() {
087        for (final String key : rootProperties.stringPropertyNames()) {
088            if (!key.contains(".")) {
089                builder.addRootProperty(key, rootProperties.getProperty(key));
090            }
091        }
092        builder
093            .setStatusLevel(Level.toLevel(rootProperties.getProperty(STATUS_KEY), Level.ERROR))
094            .setShutdownHook(rootProperties.getProperty(SHUTDOWN_HOOK))
095            .setShutdownTimeout(Long.parseLong(rootProperties.getProperty(SHUTDOWN_TIMEOUT, "0")), TimeUnit.MILLISECONDS)
096            .setVerbosity(rootProperties.getProperty(VERBOSE))
097            .setDestination(rootProperties.getProperty(DEST))
098            .setPackages(rootProperties.getProperty(PACKAGES))
099            .setConfigurationName(rootProperties.getProperty(CONFIG_NAME))
100            .setMonitorInterval(rootProperties.getProperty(MONITOR_INTERVAL, "0"))
101            .setAdvertiser(rootProperties.getProperty(ADVERTISER_KEY));
102
103        final Properties propertyPlaceholders = PropertiesUtil.extractSubset(rootProperties, "property");
104        for (final String key : propertyPlaceholders.stringPropertyNames()) {
105            builder.addProperty(key, propertyPlaceholders.getProperty(key));
106        }
107
108        final Map<String, Properties> scripts = PropertiesUtil.partitionOnCommonPrefixes(
109            PropertiesUtil.extractSubset(rootProperties, "script"));
110        for (final Map.Entry<String, Properties> entry : scripts.entrySet()) {
111            final Properties scriptProps = entry.getValue();
112            final String type = (String) scriptProps.remove("type");
113            if (type == null) {
114                throw new ConfigurationException("No type provided for script - must be Script or ScriptFile");
115            }
116            if (type.equalsIgnoreCase("script")) {
117                builder.add(createScript(scriptProps));
118            } else {
119                builder.add(createScriptFile(scriptProps));
120            }
121        }
122
123        final Properties levelProps = PropertiesUtil.extractSubset(rootProperties, "customLevel");
124        if (levelProps.size() > 0) {
125            for (final String key : levelProps.stringPropertyNames()) {
126                builder.add(builder.newCustomLevel(key, Integer.parseInt(levelProps.getProperty(key))));
127            }
128        }
129
130        final String filterProp = rootProperties.getProperty("filters");
131        if (filterProp != null) {
132            final String[] filterNames = filterProp.split(",");
133            for (final String filterName : filterNames) {
134                final String name = filterName.trim();
135                builder.add(createFilter(name, PropertiesUtil.extractSubset(rootProperties, "filter." + name)));
136            }
137        } else {
138
139            final Map<String, Properties> filters = PropertiesUtil
140                    .partitionOnCommonPrefixes(PropertiesUtil.extractSubset(rootProperties, "filter"));
141            for (final Map.Entry<String, Properties> entry : filters.entrySet()) {
142                builder.add(createFilter(entry.getKey().trim(), entry.getValue()));
143            }
144        }
145
146        final String appenderProp = rootProperties.getProperty("appenders");
147        if (appenderProp != null) {
148            final String[] appenderNames = appenderProp.split(",");
149            for (final String appenderName : appenderNames) {
150                final String name = appenderName.trim();
151                builder.add(createAppender(appenderName.trim(),
152                        PropertiesUtil.extractSubset(rootProperties, "appender." + name)));
153            }
154        } else {
155            final Map<String, Properties> appenders = PropertiesUtil
156                    .partitionOnCommonPrefixes(PropertiesUtil.extractSubset(rootProperties, Appender.ELEMENT_TYPE));
157            for (final Map.Entry<String, Properties> entry : appenders.entrySet()) {
158                builder.add(createAppender(entry.getKey().trim(), entry.getValue()));
159            }
160        }
161
162        final String loggerProp = rootProperties.getProperty("loggers");
163        if (loggerProp != null) {
164            final String[] loggerNames = loggerProp.split(",");
165            for (final String loggerName : loggerNames) {
166                final String name = loggerName.trim();
167                if (!name.equals(LoggerConfig.ROOT)) {
168                    builder.add(createLogger(name, PropertiesUtil.extractSubset(rootProperties, "logger." +
169                            name)));
170                }
171            }
172        } else {
173            final Map<String, Properties> loggers = PropertiesUtil
174                    .partitionOnCommonPrefixes(PropertiesUtil.extractSubset(rootProperties, "logger"));
175            for (final Map.Entry<String, Properties> entry : loggers.entrySet()) {
176                final String name = entry.getKey().trim();
177                if (!name.equals(LoggerConfig.ROOT)) {
178                    builder.add(createLogger(name, entry.getValue()));
179                }
180            }
181        }
182
183        final Properties props = PropertiesUtil.extractSubset(rootProperties, "rootLogger");
184        if (props.size() > 0) {
185            builder.add(createRootLogger(props));
186        }
187
188        builder.setLoggerContext(loggerContext);
189
190        return builder.build(false);
191    }
192
193    private ScriptComponentBuilder createScript(final Properties properties) {
194        final String name = (String) properties.remove("name");
195        final String language = (String) properties.remove("language");
196        final String text = (String) properties.remove("text");
197        final ScriptComponentBuilder scriptBuilder = builder.newScript(name, language, text);
198        return processRemainingProperties(scriptBuilder, properties);
199    }
200
201
202    private ScriptFileComponentBuilder createScriptFile(final Properties properties) {
203        final String name = (String) properties.remove("name");
204        final String path = (String) properties.remove("path");
205        final ScriptFileComponentBuilder scriptFileBuilder = builder.newScriptFile(name, path);
206        return processRemainingProperties(scriptFileBuilder, properties);
207    }
208
209    private AppenderComponentBuilder createAppender(final String key, final Properties properties) {
210        final String name = (String) properties.remove(CONFIG_NAME);
211        if (Strings.isEmpty(name)) {
212            throw new ConfigurationException("No name attribute provided for Appender " + key);
213        }
214        final String type = (String) properties.remove(CONFIG_TYPE);
215        if (Strings.isEmpty(type)) {
216            throw new ConfigurationException("No type attribute provided for Appender " + key);
217        }
218        final AppenderComponentBuilder appenderBuilder = builder.newAppender(name, type);
219        addFiltersToComponent(appenderBuilder, properties);
220        final Properties layoutProps = PropertiesUtil.extractSubset(properties, "layout");
221        if (layoutProps.size() > 0) {
222            appenderBuilder.add(createLayout(name, layoutProps));
223        }
224
225        return processRemainingProperties(appenderBuilder, properties);
226    }
227
228    private FilterComponentBuilder createFilter(final String key, final Properties properties) {
229        final String type = (String) properties.remove(CONFIG_TYPE);
230        if (Strings.isEmpty(type)) {
231            throw new ConfigurationException("No type attribute provided for Appender " + key);
232        }
233        final String onMatch = (String) properties.remove(AbstractFilterBuilder.ATTR_ON_MATCH);
234        final String onMismatch = (String) properties.remove(AbstractFilterBuilder.ATTR_ON_MISMATCH);
235        final FilterComponentBuilder filterBuilder = builder.newFilter(type, onMatch, onMismatch);
236        return processRemainingProperties(filterBuilder, properties);
237    }
238
239    private AppenderRefComponentBuilder createAppenderRef(final String key, final Properties properties) {
240        final String ref = (String) properties.remove("ref");
241        if (Strings.isEmpty(ref)) {
242            throw new ConfigurationException("No ref attribute provided for AppenderRef " + key);
243        }
244        final AppenderRefComponentBuilder appenderRefBuilder = builder.newAppenderRef(ref);
245        final String level = Strings.trimToNull((String) properties.remove("level"));
246        if (!Strings.isEmpty(level)) {
247            appenderRefBuilder.addAttribute("level", level);
248        }
249        return addFiltersToComponent(appenderRefBuilder, properties);
250    }
251
252    private LoggerComponentBuilder createLogger(final String key, final Properties properties) {
253        final String name = (String) properties.remove(CONFIG_NAME);
254        final String location = (String) properties.remove("includeLocation");
255        if (Strings.isEmpty(name)) {
256            throw new ConfigurationException("No name attribute provided for Logger " + key);
257        }
258        final String level = Strings.trimToNull((String) properties.remove("level"));
259        final String type = (String) properties.remove(CONFIG_TYPE);
260        final LoggerComponentBuilder loggerBuilder;
261        boolean includeLocation;
262        if (type != null) {
263            if (type.equalsIgnoreCase("asyncLogger")) {
264                if (location != null) {
265                    includeLocation = Boolean.parseBoolean(location);
266                    loggerBuilder = builder.newAsyncLogger(name, level, includeLocation);
267                } else {
268                    loggerBuilder = builder.newAsyncLogger(name, level);
269                }
270            } else {
271                throw new ConfigurationException("Unknown Logger type " + type + " for Logger " + name);
272            }
273        } else {
274            if (location != null) {
275                includeLocation = Boolean.parseBoolean(location);
276                loggerBuilder = builder.newLogger(name, level, includeLocation);
277            } else {
278                loggerBuilder = builder.newLogger(name, level);
279            }
280        }
281        addLoggersToComponent(loggerBuilder, properties);
282        addFiltersToComponent(loggerBuilder, properties);
283        final String additivity = (String) properties.remove("additivity");
284        if (!Strings.isEmpty(additivity)) {
285            loggerBuilder.addAttribute("additivity", additivity);
286        }
287        return loggerBuilder;
288    }
289
290    private RootLoggerComponentBuilder createRootLogger(final Properties properties) {
291        final String level = Strings.trimToNull((String) properties.remove("level"));
292        final String type = (String) properties.remove(CONFIG_TYPE);
293        final String location = (String) properties.remove("includeLocation");
294        final boolean includeLocation;
295        final RootLoggerComponentBuilder loggerBuilder;
296        if (type != null) {
297            if (type.equalsIgnoreCase("asyncRoot")) {
298                if (location != null) {
299                    includeLocation = Boolean.parseBoolean(location);
300                    loggerBuilder = builder.newAsyncRootLogger(level, includeLocation);
301                } else {
302                    loggerBuilder = builder.newAsyncRootLogger(level);
303                }
304            } else {
305                throw new ConfigurationException("Unknown Logger type for root logger" + type);
306            }
307        } else {
308            if (location != null) {
309                includeLocation = Boolean.parseBoolean(location);
310                loggerBuilder = builder.newRootLogger(level, includeLocation);
311            } else {
312                loggerBuilder = builder.newRootLogger(level);
313            }
314        }
315        addLoggersToComponent(loggerBuilder, properties);
316        return addFiltersToComponent(loggerBuilder, properties);
317    }
318
319    private LayoutComponentBuilder createLayout(final String appenderName, final Properties properties) {
320        final String type = (String) properties.remove(CONFIG_TYPE);
321        if (Strings.isEmpty(type)) {
322            throw new ConfigurationException("No type attribute provided for Layout on Appender " + appenderName);
323        }
324        final LayoutComponentBuilder layoutBuilder = builder.newLayout(type);
325        return processRemainingProperties(layoutBuilder, properties);
326    }
327
328    private static <B extends ComponentBuilder<B>> ComponentBuilder<B> createComponent(final ComponentBuilder<?> parent,
329                                                                                       final String key,
330                                                                                       final Properties properties) {
331        final String name = (String) properties.remove(CONFIG_NAME);
332        final String type = (String) properties.remove(CONFIG_TYPE);
333        if (Strings.isEmpty(type)) {
334            throw new ConfigurationException("No type attribute provided for component " + key);
335        }
336        final ComponentBuilder<B> componentBuilder = parent.getBuilder().newComponent(name, type);
337        return processRemainingProperties(componentBuilder, properties);
338    }
339
340    private static <B extends ComponentBuilder<?>> B processRemainingProperties(final B builder,
341                                                                                final Properties properties) {
342        while (properties.size() > 0) {
343            final String propertyName = properties.stringPropertyNames().iterator().next();
344            final int index = propertyName.indexOf('.');
345            if (index > 0) {
346                final String prefix = propertyName.substring(0, index);
347                final Properties componentProperties = PropertiesUtil.extractSubset(properties, prefix);
348                builder.addComponent(createComponent(builder, prefix, componentProperties));
349            } else {
350                builder.addAttribute(propertyName, properties.getProperty(propertyName));
351                properties.remove(propertyName);
352            }
353        }
354        return builder;
355    }
356
357    private <B extends FilterableComponentBuilder<? extends ComponentBuilder<?>>> B addFiltersToComponent(
358        final B componentBuilder, final Properties properties) {
359        final Map<String, Properties> filters = PropertiesUtil.partitionOnCommonPrefixes(
360            PropertiesUtil.extractSubset(properties, "filter"));
361        for (final Map.Entry<String, Properties> entry : filters.entrySet()) {
362            componentBuilder.add(createFilter(entry.getKey().trim(), entry.getValue()));
363        }
364        return componentBuilder;
365    }
366
367    private <B extends LoggableComponentBuilder<? extends ComponentBuilder<?>>> B addLoggersToComponent(
368        final B loggerBuilder, final Properties properties) {
369        final Map<String, Properties> appenderRefs = PropertiesUtil.partitionOnCommonPrefixes(
370            PropertiesUtil.extractSubset(properties, "appenderRef"));
371        for (final Map.Entry<String, Properties> entry : appenderRefs.entrySet()) {
372            loggerBuilder.add(createAppenderRef(entry.getKey().trim(), entry.getValue()));
373        }
374        return loggerBuilder;
375    }
376
377    public PropertiesConfigurationBuilder setLoggerContext(final LoggerContext loggerContext) {
378        this.loggerContext = loggerContext;
379        return this;
380    }
381
382    public LoggerContext getLoggerContext() {
383        return loggerContext;
384    }
385}