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.plugins.util;
019
020import java.lang.annotation.Annotation;
021import java.lang.reflect.AccessibleObject;
022import java.lang.reflect.Field;
023import java.lang.reflect.InvocationTargetException;
024import java.lang.reflect.Method;
025import java.lang.reflect.Modifier;
026import java.util.Collection;
027import java.util.List;
028import java.util.Map;
029
030import org.apache.logging.log4j.Logger;
031import org.apache.logging.log4j.core.LogEvent;
032import org.apache.logging.log4j.core.config.Configuration;
033import org.apache.logging.log4j.core.config.ConfigurationException;
034import org.apache.logging.log4j.core.config.Node;
035import org.apache.logging.log4j.core.config.plugins.PluginAliases;
036import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
037import org.apache.logging.log4j.core.config.plugins.PluginFactory;
038import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator;
039import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidators;
040import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor;
041import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitors;
042import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
043import org.apache.logging.log4j.core.util.Assert;
044import org.apache.logging.log4j.core.util.Builder;
045import org.apache.logging.log4j.core.util.ReflectionUtil;
046import org.apache.logging.log4j.core.util.TypeUtil;
047import org.apache.logging.log4j.status.StatusLogger;
048import org.apache.logging.log4j.util.StringBuilders;
049
050/**
051 * Builder class to instantiate and configure a Plugin object using a PluginFactory method or PluginBuilderFactory
052 * builder class.
053 */
054public class PluginBuilder implements Builder<Object> {
055
056    private static final Logger LOGGER = StatusLogger.getLogger();
057
058    private final PluginType<?> pluginType;
059    private final Class<?> clazz;
060
061    private Configuration configuration;
062    private Node node;
063    private LogEvent event;
064
065    /**
066     * Constructs a PluginBuilder for a given PluginType.
067     *
068     * @param pluginType type of plugin to configure
069     */
070    public PluginBuilder(final PluginType<?> pluginType) {
071        this.pluginType = pluginType;
072        this.clazz = pluginType.getPluginClass();
073    }
074
075    /**
076     * Specifies the Configuration to use for constructing the plugin instance.
077     *
078     * @param configuration the configuration to use.
079     * @return {@code this}
080     */
081    public PluginBuilder withConfiguration(final Configuration configuration) {
082        this.configuration = configuration;
083        return this;
084    }
085
086    /**
087     * Specifies the Node corresponding to the plugin object that will be created.
088     *
089     * @param node the plugin configuration node to use.
090     * @return {@code this}
091     */
092    public PluginBuilder withConfigurationNode(final Node node) {
093        this.node = node;
094        return this;
095    }
096
097    /**
098     * Specifies the LogEvent that may be used to provide extra context for string substitutions.
099     *
100     * @param event the event to use for extra information.
101     * @return {@code this}
102     */
103    public PluginBuilder forLogEvent(final LogEvent event) {
104        this.event = event;
105        return this;
106    }
107
108    /**
109     * Builds the plugin object.
110     *
111     * @return the plugin object or {@code null} if there was a problem creating it.
112     */
113    @Override
114    public Object build() {
115        verify();
116        // first try to use a builder class if one is available
117        try {
118            LOGGER.debug("Building Plugin[name={}, class={}]. Searching for builder factory method...", pluginType.getElementName(),
119                    pluginType.getPluginClass().getName());
120            final Builder<?> builder = createBuilder(this.clazz);
121            if (builder != null) {
122                injectFields(builder);
123                final Object result = builder.build();
124                LOGGER.debug("Built Plugin[name={}] OK from builder factory method.", pluginType.getElementName());
125                return result;
126            }
127        } catch (final Exception e) {
128            LOGGER.error("Unable to inject fields into builder class for plugin type {}, element {}.", this.clazz,
129                node.getName(), e);
130        }
131        // or fall back to factory method if no builder class is available
132        try {
133            LOGGER.debug("Still building Plugin[name={}, class={}]. Searching for factory method...",
134                    pluginType.getElementName(), pluginType.getPluginClass().getName());
135            final Method factory = findFactoryMethod(this.clazz);
136            final Object[] params = generateParameters(factory);
137            final Object plugin = factory.invoke(null, params);
138            LOGGER.debug("Built Plugin[name={}] OK from factory method.", pluginType.getElementName());
139            return plugin;
140        } catch (final Exception e) {
141            LOGGER.error("Unable to invoke factory method in class {} for element {}.", this.clazz, this.node.getName(),
142                e);
143            return null;
144        }
145    }
146
147    private void verify() {
148        Assert.requireNonNull(this.configuration, "No Configuration object was set.");
149        Assert.requireNonNull(this.node, "No Node object was set.");
150    }
151
152    private static Builder<?> createBuilder(final Class<?> clazz)
153        throws InvocationTargetException, IllegalAccessException {
154        for (final Method method : clazz.getDeclaredMethods()) {
155            if (method.isAnnotationPresent(PluginBuilderFactory.class) &&
156                Modifier.isStatic(method.getModifiers()) &&
157                TypeUtil.isAssignable(Builder.class, method.getGenericReturnType())) {
158                ReflectionUtil.makeAccessible(method);
159                @SuppressWarnings("unchecked")
160                final Builder<?> builder = (Builder<?>) method.invoke(null);
161                LOGGER.debug("Found builder factory method [{}]: {}.", method.getName(), method);
162                return builder;
163            }
164        }
165        LOGGER.debug("No builder factory method found in class {}. Going to try finding a factory method instead.",
166            clazz.getName());
167        return null;
168    }
169
170    private void injectFields(final Builder<?> builder) throws IllegalAccessException {
171        final Field[] fields = builder.getClass().getDeclaredFields();
172        AccessibleObject.setAccessible(fields, true);
173        final StringBuilder log = new StringBuilder();
174        boolean invalid = false;
175        for (final Field field : fields) {
176            log.append(log.length() == 0 ? "with params(" : ", ");
177            final Annotation[] annotations = field.getDeclaredAnnotations();
178            final String[] aliases = extractPluginAliases(annotations);
179            for (final Annotation a : annotations) {
180                if (a instanceof PluginAliases) {
181                    continue; // already processed
182                }
183                final PluginVisitor<? extends Annotation> visitor =
184                    PluginVisitors.findVisitor(a.annotationType());
185                if (visitor != null) {
186                    final Object value = visitor.setAliases(aliases)
187                        .setAnnotation(a)
188                        .setConversionType(field.getType())
189                        .setStrSubstitutor(event == null
190                                ? new ConfigurationStrSubstitutor(configuration.getStrSubstitutor())
191                                : configuration.getStrSubstitutor())
192                        .setMember(field)
193                        .visit(configuration, node, event, log);
194                    // don't overwrite default values if the visitor gives us no value to inject
195                    if (value != null) {
196                        field.set(builder, value);
197                    }
198                }
199            }
200            final Collection<ConstraintValidator<?>> validators =
201                ConstraintValidators.findValidators(annotations);
202            final Object value = field.get(builder);
203            for (final ConstraintValidator<?> validator : validators) {
204                if (!validator.isValid(value)) {
205                    invalid = true;
206                }
207            }
208        }
209        if (log.length() > 0) {
210            log.append(')');
211        }
212        LOGGER.debug("Calling build() on class {} for element {} {}", builder.getClass(), node.getName(),
213            log.toString());
214        if (invalid) {
215            throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid");
216        }
217        checkForRemainingAttributes();
218        verifyNodeChildrenUsed();
219    }
220
221    private static Method findFactoryMethod(final Class<?> clazz) {
222        for (final Method method : clazz.getDeclaredMethods()) {
223            if (method.isAnnotationPresent(PluginFactory.class) &&
224                Modifier.isStatic(method.getModifiers())) {
225                LOGGER.debug("Found factory method [{}]: {}.", method.getName(), method);
226                ReflectionUtil.makeAccessible(method);
227                return method;
228            }
229        }
230        throw new IllegalStateException("No factory method found for class " + clazz.getName());
231    }
232
233    private Object[] generateParameters(final Method factory) {
234        final StringBuilder log = new StringBuilder();
235        final Class<?>[] types = factory.getParameterTypes();
236        final Annotation[][] annotations = factory.getParameterAnnotations();
237        final Object[] args = new Object[annotations.length];
238        boolean invalid = false;
239        for (int i = 0; i < annotations.length; i++) {
240            log.append(log.length() == 0 ? "with params(" : ", ");
241            final String[] aliases = extractPluginAliases(annotations[i]);
242            for (final Annotation a : annotations[i]) {
243                if (a instanceof PluginAliases) {
244                    continue; // already processed
245                }
246                final PluginVisitor<? extends Annotation> visitor = PluginVisitors.findVisitor(
247                    a.annotationType());
248                if (visitor != null) {
249                    final Object value = visitor.setAliases(aliases)
250                        .setAnnotation(a)
251                        .setConversionType(types[i])
252                        .setStrSubstitutor(event == null
253                                ? new ConfigurationStrSubstitutor(configuration.getStrSubstitutor())
254                                : configuration.getStrSubstitutor())
255                        .setMember(factory)
256                        .visit(configuration, node, event, log);
257                    // don't overwrite existing values if the visitor gives us no value to inject
258                    if (value != null) {
259                        args[i] = value;
260                    }
261                }
262            }
263            final Collection<ConstraintValidator<?>> validators =
264                ConstraintValidators.findValidators(annotations[i]);
265            final Object value = args[i];
266            for (final ConstraintValidator<?> validator : validators) {
267                if (!validator.isValid(value)) {
268                    invalid = true;
269                }
270            }
271        }
272        if (log.length() > 0) {
273            log.append(')');
274        }
275        checkForRemainingAttributes();
276        verifyNodeChildrenUsed();
277        LOGGER.debug("Calling {} on class {} for element {} {}", factory.getName(), clazz.getName(), node.getName(),
278            log.toString());
279        if (invalid) {
280            throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid");
281        }
282        return args;
283    }
284
285    private static String[] extractPluginAliases(final Annotation... parmTypes) {
286        String[] aliases = null;
287        for (final Annotation a : parmTypes) {
288            if (a instanceof PluginAliases) {
289                aliases = ((PluginAliases) a).value();
290            }
291        }
292        return aliases;
293    }
294
295    private void checkForRemainingAttributes() {
296        final Map<String, String> attrs = node.getAttributes();
297        if (!attrs.isEmpty()) {
298            final StringBuilder sb = new StringBuilder();
299            for (final String key : attrs.keySet()) {
300                if (sb.length() == 0) {
301                    sb.append(node.getName());
302                    sb.append(" contains ");
303                    if (attrs.size() == 1) {
304                        sb.append("an invalid element or attribute ");
305                    } else {
306                        sb.append("invalid attributes ");
307                    }
308                } else {
309                    sb.append(", ");
310                }
311                StringBuilders.appendDqValue(sb, key);
312
313            }
314            LOGGER.error(sb.toString());
315        }
316    }
317
318    private void verifyNodeChildrenUsed() {
319        final List<Node> children = node.getChildren();
320        if (!(pluginType.isDeferChildren() || children.isEmpty())) {
321            for (final Node child : children) {
322                final String nodeType = node.getType().getElementName();
323                final String start = nodeType.equals(node.getName()) ? node.getName() : nodeType + ' ' + node.getName();
324                LOGGER.error("{} has no parameter that matches element {}", start, child.getName());
325            }
326        }
327    }
328}