/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache license, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the license for the specific language governing permissions and
 * limitations under the license.
 */

package org.apache.logging.log4j.core.config.plugins.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.ConfigurationException;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.PluginAliases;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator;
import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidators;
import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor;
import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitors;
import org.apache.logging.log4j.core.util.Builder;
import org.apache.logging.log4j.core.util.ReflectionUtil;
import org.apache.logging.log4j.core.util.TypeUtil;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.StringBuilders;

Builder class to instantiate and configure a Plugin object using a PluginFactory method or PluginBuilderFactory builder class.
/** * Builder class to instantiate and configure a Plugin object using a PluginFactory method or PluginBuilderFactory * builder class. */
public class PluginBuilder implements Builder<Object> { private static final Logger LOGGER = StatusLogger.getLogger(); private final PluginType<?> pluginType; private final Class<?> clazz; private Configuration configuration; private Node node; private LogEvent event;
Constructs a PluginBuilder for a given PluginType.
Params:
  • pluginType – type of plugin to configure
/** * Constructs a PluginBuilder for a given PluginType. * * @param pluginType type of plugin to configure */
public PluginBuilder(final PluginType<?> pluginType) { this.pluginType = pluginType; this.clazz = pluginType.getPluginClass(); }
Specifies the Configuration to use for constructing the plugin instance.
Params:
  • configuration – the configuration to use.
Returns:this
/** * Specifies the Configuration to use for constructing the plugin instance. * * @param configuration the configuration to use. * @return {@code this} */
public PluginBuilder withConfiguration(final Configuration configuration) { this.configuration = configuration; return this; }
Specifies the Node corresponding to the plugin object that will be created.
Params:
  • node – the plugin configuration node to use.
Returns:this
/** * Specifies the Node corresponding to the plugin object that will be created. * * @param node the plugin configuration node to use. * @return {@code this} */
public PluginBuilder withConfigurationNode(final Node node) { this.node = node; return this; }
Specifies the LogEvent that may be used to provide extra context for string substitutions.
Params:
  • event – the event to use for extra information.
Returns:this
/** * Specifies the LogEvent that may be used to provide extra context for string substitutions. * * @param event the event to use for extra information. * @return {@code this} */
public PluginBuilder forLogEvent(final LogEvent event) { this.event = event; return this; }
Builds the plugin object.
Returns:the plugin object or null if there was a problem creating it.
/** * Builds the plugin object. * * @return the plugin object or {@code null} if there was a problem creating it. */
@Override public Object build() { verify(); // first try to use a builder class if one is available try { LOGGER.debug("Building Plugin[name={}, class={}].", pluginType.getElementName(), pluginType.getPluginClass().getName()); final Builder<?> builder = createBuilder(this.clazz); if (builder != null) { injectFields(builder); return builder.build(); } } catch (final ConfigurationException e) { // LOG4J2-1908 LOGGER.error("Could not create plugin of type {} for element {}", this.clazz, node.getName(), e); return null; // no point in trying the factory method } catch (final Exception e) { LOGGER.error("Could not create plugin of type {} for element {}: {}", this.clazz, node.getName(), (e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e).toString(), e); } // or fall back to factory method if no builder class is available try { final Method factory = findFactoryMethod(this.clazz); final Object[] params = generateParameters(factory); return factory.invoke(null, params); } catch (final Exception e) { LOGGER.error("Unable to invoke factory method in {} for element {}: {}", this.clazz, this.node.getName(), (e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e).toString(), e); return null; } } private void verify() { Objects.requireNonNull(this.configuration, "No Configuration object was set."); Objects.requireNonNull(this.node, "No Node object was set."); } private static Builder<?> createBuilder(final Class<?> clazz) throws InvocationTargetException, IllegalAccessException { for (final Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(PluginBuilderFactory.class) && Modifier.isStatic(method.getModifiers()) && TypeUtil.isAssignable(Builder.class, method.getReturnType())) { ReflectionUtil.makeAccessible(method); return (Builder<?>) method.invoke(null); } } return null; } private void injectFields(final Builder<?> builder) throws IllegalAccessException { final List<Field> fields = TypeUtil.getAllDeclaredFields(builder.getClass()); AccessibleObject.setAccessible(fields.toArray(new Field[] {}), true); final StringBuilder log = new StringBuilder(); boolean invalid = false; String reason = ""; for (final Field field : fields) { log.append(log.length() == 0 ? simpleName(builder) + "(" : ", "); final Annotation[] annotations = field.getDeclaredAnnotations(); final String[] aliases = extractPluginAliases(annotations); for (final Annotation a : annotations) { if (a instanceof PluginAliases) { continue; // already processed } final PluginVisitor<? extends Annotation> visitor = PluginVisitors.findVisitor(a.annotationType()); if (visitor != null) { final Object value = visitor.setAliases(aliases) .setAnnotation(a) .setConversionType(field.getType()) .setStrSubstitutor(configuration.getStrSubstitutor()) .setMember(field) .visit(configuration, node, event, log); // don't overwrite default values if the visitor gives us no value to inject if (value != null) { field.set(builder, value); } } } final Collection<ConstraintValidator<?>> validators = ConstraintValidators.findValidators(annotations); final Object value = field.get(builder); for (final ConstraintValidator<?> validator : validators) { if (!validator.isValid(field.getName(), value)) { invalid = true; if (!reason.isEmpty()) { reason += ", "; } reason += "field '" + field.getName() + "' has invalid value '" + value + "'"; } } } log.append(log.length() == 0 ? builder.getClass().getSimpleName() + "()" : ")"); LOGGER.debug(log.toString()); if (invalid) { throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid: " + reason); } checkForRemainingAttributes(); verifyNodeChildrenUsed(); }
object.getClass().getSimpleName() returns Builder, when we want PatternLayout$Builder.
/** * {@code object.getClass().getSimpleName()} returns {@code Builder}, when we want {@code PatternLayout$Builder}. */
private static String simpleName(final Object object) { if (object == null) { return "null"; } final String cls = object.getClass().getName(); final int index = cls.lastIndexOf('.'); return index < 0 ? cls : cls.substring(index + 1); } private static Method findFactoryMethod(final Class<?> clazz) { for (final Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(PluginFactory.class) && Modifier.isStatic(method.getModifiers())) { ReflectionUtil.makeAccessible(method); return method; } } throw new IllegalStateException("No factory method found for class " + clazz.getName()); } private Object[] generateParameters(final Method factory) { final StringBuilder log = new StringBuilder(); final Class<?>[] types = factory.getParameterTypes(); final Annotation[][] annotations = factory.getParameterAnnotations(); final Object[] args = new Object[annotations.length]; boolean invalid = false; for (int i = 0; i < annotations.length; i++) { log.append(log.length() == 0 ? factory.getName() + "(" : ", "); final String[] aliases = extractPluginAliases(annotations[i]); for (final Annotation a : annotations[i]) { if (a instanceof PluginAliases) { continue; // already processed } final PluginVisitor<? extends Annotation> visitor = PluginVisitors.findVisitor( a.annotationType()); if (visitor != null) { final Object value = visitor.setAliases(aliases) .setAnnotation(a) .setConversionType(types[i]) .setStrSubstitutor(configuration.getStrSubstitutor()) .setMember(factory) .visit(configuration, node, event, log); // don't overwrite existing values if the visitor gives us no value to inject if (value != null) { args[i] = value; } } } final Collection<ConstraintValidator<?>> validators = ConstraintValidators.findValidators(annotations[i]); final Object value = args[i]; final String argName = "arg[" + i + "](" + simpleName(value) + ")"; for (final ConstraintValidator<?> validator : validators) { if (!validator.isValid(argName, value)) { invalid = true; } } } log.append(log.length() == 0 ? factory.getName() + "()" : ")"); checkForRemainingAttributes(); verifyNodeChildrenUsed(); LOGGER.debug(log.toString()); if (invalid) { throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid"); } return args; } private static String[] extractPluginAliases(final Annotation... parmTypes) { String[] aliases = null; for (final Annotation a : parmTypes) { if (a instanceof PluginAliases) { aliases = ((PluginAliases) a).value(); } } return aliases; } private void checkForRemainingAttributes() { final Map<String, String> attrs = node.getAttributes(); if (!attrs.isEmpty()) { final StringBuilder sb = new StringBuilder(); for (final String key : attrs.keySet()) { if (sb.length() == 0) { sb.append(node.getName()); sb.append(" contains "); if (attrs.size() == 1) { sb.append("an invalid element or attribute "); } else { sb.append("invalid attributes "); } } else { sb.append(", "); } StringBuilders.appendDqValue(sb, key); } LOGGER.error(sb.toString()); } } private void verifyNodeChildrenUsed() { final List<Node> children = node.getChildren(); if (!(pluginType.isDeferChildren() || children.isEmpty())) { for (final Node child : children) { final String nodeType = node.getType().getElementName(); final String start = nodeType.equals(node.getName()) ? node.getName() : nodeType + ' ' + node.getName(); LOGGER.error("{} has no parameter that matches element {}", start, child.getName()); } } } }