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;
029import java.util.Objects;
030
031import org.apache.logging.log4j.Logger;
032import org.apache.logging.log4j.core.LogEvent;
033import org.apache.logging.log4j.core.config.Configuration;
034import org.apache.logging.log4j.core.config.ConfigurationException;
035import org.apache.logging.log4j.core.config.Node;
036import org.apache.logging.log4j.core.config.plugins.PluginAliases;
037import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
038import org.apache.logging.log4j.core.config.plugins.PluginFactory;
039import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator;
040import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidators;
041import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor;
042import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitors;
043import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
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={}].", pluginType.getElementName(),
119                    pluginType.getPluginClass().getName());
120            final Builder<?> builder = createBuilder(this.clazz);
121            if (builder != null) {
122                injectFields(builder);
123                return builder.build();
124            }
125        } catch (final ConfigurationException e) { // LOG4J2-1908
126            LOGGER.error("Could not create plugin of type {} for element {}", this.clazz, node.getName(), e);
127            return null; // no point in trying the factory method
128        } catch (final Throwable t) {
129            LOGGER.error("Could not create plugin of type {} for element {}: {}",
130                    this.clazz, node.getName(),
131                    (t instanceof InvocationTargetException ? ((InvocationTargetException) t).getCause() : t).toString(), t);
132        }
133        // or fall back to factory method if no builder class is available
134        try {
135            final Method factory = findFactoryMethod(this.clazz);
136            final Object[] params = generateParameters(factory);
137            return factory.invoke(null, params);
138        } catch (final Throwable t) {
139            LOGGER.error("Unable to invoke factory method in {} for element {}: {}",
140                    this.clazz, this.node.getName(),
141                    (t instanceof InvocationTargetException ? ((InvocationTargetException) t).getCause() : t).toString(), t);
142            return null;
143        }
144    }
145
146    private void verify() {
147        Objects.requireNonNull(this.configuration, "No Configuration object was set.");
148        Objects.requireNonNull(this.node, "No Node object was set.");
149    }
150
151    private static Builder<?> createBuilder(final Class<?> clazz)
152        throws InvocationTargetException, IllegalAccessException {
153        for (final Method method : clazz.getDeclaredMethods()) {
154            if (method.isAnnotationPresent(PluginBuilderFactory.class) &&
155                Modifier.isStatic(method.getModifiers()) &&
156                TypeUtil.isAssignable(Builder.class, method.getReturnType())) {
157                ReflectionUtil.makeAccessible(method);
158                return (Builder<?>) method.invoke(null);
159            }
160        }
161        return null;
162    }
163
164    private void injectFields(final Builder<?> builder) throws IllegalAccessException {
165        final List<Field> fields = TypeUtil.getAllDeclaredFields(builder.getClass());
166        AccessibleObject.setAccessible(fields.toArray(new Field[] {}), true);
167        final StringBuilder log = new StringBuilder();
168        boolean invalid = false;
169        String reason = "";
170        for (final Field field : fields) {
171            log.append(log.length() == 0 ? simpleName(builder) + "(" : ", ");
172            final Annotation[] annotations = field.getDeclaredAnnotations();
173            final String[] aliases = extractPluginAliases(annotations);
174            for (final Annotation a : annotations) {
175                if (a instanceof PluginAliases) {
176                    continue; // already processed
177                }
178                final PluginVisitor<? extends Annotation> visitor =
179                    PluginVisitors.findVisitor(a.annotationType());
180                if (visitor != null) {
181                    final Object value = visitor.setAliases(aliases)
182                        .setAnnotation(a)
183                        .setConversionType(field.getType())
184                        .setStrSubstitutor(event == null
185                                ? new ConfigurationStrSubstitutor(configuration.getStrSubstitutor())
186                                : configuration.getStrSubstitutor())
187                        .setMember(field)
188                        .visit(configuration, node, event, log);
189                    // don't overwrite default values if the visitor gives us no value to inject
190                    if (value != null) {
191                        field.set(builder, value);
192                    }
193                }
194            }
195            final Collection<ConstraintValidator<?>> validators =
196                ConstraintValidators.findValidators(annotations);
197            final Object value = field.get(builder);
198            for (final ConstraintValidator<?> validator : validators) {
199                if (!validator.isValid(field.getName(), value)) {
200                    invalid = true;
201                    if (!reason.isEmpty()) {
202                        reason += ", ";
203                    }
204                    reason += "field '" + field.getName() + "' has invalid value '" + value + "'";
205                }
206            }
207        }
208        log.append(log.length() == 0 ? builder.getClass().getSimpleName() + "()" : ")");
209        LOGGER.debug(log.toString());
210        if (invalid) {
211            throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid: " + reason);
212        }
213        checkForRemainingAttributes();
214        verifyNodeChildrenUsed();
215    }
216
217    /**
218     * {@code object.getClass().getSimpleName()} returns {@code Builder}, when we want {@code PatternLayout$Builder}.
219     */
220    private static String simpleName(final Object object) {
221        if (object == null) {
222            return "null";
223        }
224        final String cls = object.getClass().getName();
225        final int index = cls.lastIndexOf('.');
226        return index < 0 ? cls : cls.substring(index + 1);
227    }
228
229    private static Method findFactoryMethod(final Class<?> clazz) {
230        for (final Method method : clazz.getDeclaredMethods()) {
231            if (method.isAnnotationPresent(PluginFactory.class) &&
232                Modifier.isStatic(method.getModifiers())) {
233                ReflectionUtil.makeAccessible(method);
234                return method;
235            }
236        }
237        throw new IllegalStateException("No factory method found for class " + clazz.getName());
238    }
239
240    private Object[] generateParameters(final Method factory) {
241        final StringBuilder log = new StringBuilder();
242        final Class<?>[] types = factory.getParameterTypes();
243        final Annotation[][] annotations = factory.getParameterAnnotations();
244        final Object[] args = new Object[annotations.length];
245        boolean invalid = false;
246        for (int i = 0; i < annotations.length; i++) {
247            log.append(log.length() == 0 ? factory.getName() + "(" : ", ");
248            final String[] aliases = extractPluginAliases(annotations[i]);
249            for (final Annotation a : annotations[i]) {
250                if (a instanceof PluginAliases) {
251                    continue; // already processed
252                }
253                final PluginVisitor<? extends Annotation> visitor = PluginVisitors.findVisitor(
254                    a.annotationType());
255                if (visitor != null) {
256                    final Object value = visitor.setAliases(aliases)
257                        .setAnnotation(a)
258                        .setConversionType(types[i])
259                        .setStrSubstitutor(event == null
260                                ? new ConfigurationStrSubstitutor(configuration.getStrSubstitutor())
261                                : configuration.getStrSubstitutor())
262                        .setMember(factory)
263                        .visit(configuration, node, event, log);
264                    // don't overwrite existing values if the visitor gives us no value to inject
265                    if (value != null) {
266                        args[i] = value;
267                    }
268                }
269            }
270            final Collection<ConstraintValidator<?>> validators =
271                ConstraintValidators.findValidators(annotations[i]);
272            final Object value = args[i];
273            final String argName = "arg[" + i + "](" + simpleName(value) + ")";
274            for (final ConstraintValidator<?> validator : validators) {
275                if (!validator.isValid(argName, value)) {
276                    invalid = true;
277                }
278            }
279        }
280        log.append(log.length() == 0 ? factory.getName() + "()" : ")");
281        checkForRemainingAttributes();
282        verifyNodeChildrenUsed();
283        LOGGER.debug(log.toString());
284        if (invalid) {
285            throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid");
286        }
287        return args;
288    }
289
290    private static String[] extractPluginAliases(final Annotation... parmTypes) {
291        String[] aliases = null;
292        for (final Annotation a : parmTypes) {
293            if (a instanceof PluginAliases) {
294                aliases = ((PluginAliases) a).value();
295            }
296        }
297        return aliases;
298    }
299
300    private void checkForRemainingAttributes() {
301        final Map<String, String> attrs = node.getAttributes();
302        if (!attrs.isEmpty()) {
303            final StringBuilder sb = new StringBuilder();
304            for (final String key : attrs.keySet()) {
305                if (sb.length() == 0) {
306                    sb.append(node.getName());
307                    sb.append(" contains ");
308                    if (attrs.size() == 1) {
309                        sb.append("an invalid element or attribute ");
310                    } else {
311                        sb.append("invalid attributes ");
312                    }
313                } else {
314                    sb.append(", ");
315                }
316                StringBuilders.appendDqValue(sb, key);
317            }
318            LOGGER.error(sb.toString());
319        }
320    }
321
322    private void verifyNodeChildrenUsed() {
323        final List<Node> children = node.getChildren();
324        if (!(pluginType.isDeferChildren() || children.isEmpty())) {
325            for (final Node child : children) {
326                final String nodeType = node.getType().getElementName();
327                final String start = nodeType.equals(node.getName()) ? node.getName() : nodeType + ' ' + node.getName();
328                LOGGER.error("{} has no parameter that matches element {}", start, child.getName());
329            }
330        }
331    }
332}