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