View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements. See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License. You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17
    18  package org.apache.logging.log4j.core.config.plugins.util;
    19
    20  import java.lang.annotation.Annotation;
    21  import java.lang.reflect.AccessibleObject;
    22  import java.lang.reflect.Field;
    23  import java.lang.reflect.InvocationTargetException;
    24  import java.lang.reflect.Method;
    25  import java.lang.reflect.Modifier;
    26  import java.util.Collection;
    27  import java.util.List;
    28  import java.util.Map;
    29
    30  import org.apache.logging.log4j.Logger;
    31  import org.apache.logging.log4j.core.LogEvent;
    32  import org.apache.logging.log4j.core.config.Configuration;
    33  import org.apache.logging.log4j.core.config.ConfigurationException;
    34  import org.apache.logging.log4j.core.config.Node;
    35  import org.apache.logging.log4j.core.config.plugins.PluginAliases;
    36  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
    37  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    38  import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator;
    39  import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidators;
    40  import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor;
    41  import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitors;
    42  import org.apache.logging.log4j.core.util.Assert;
    43  import org.apache.logging.log4j.core.util.Builder;
    44  import org.apache.logging.log4j.core.util.ReflectionUtil;
    45  import org.apache.logging.log4j.core.util.TypeUtil;
    46  import org.apache.logging.log4j.status.StatusLogger;
    47  import org.apache.logging.log4j.util.Chars;
    48  import org.apache.logging.log4j.util.StringBuilders;
    49
    50  /**
    51   * Builder class to instantiate and configure a Plugin object using a PluginFactory method or PluginBuilderFactory
    52   * builder class.
    53   */
    54  public class PluginBuilder implements Builder<Object> {
    55
    56      private static final Logger LOGGER = StatusLogger.getLogger();
    57
    58      private final PluginType<?> pluginType;
    59      private final Class<?> clazz;
    60
    61      private Configuration configuration;
    62      private Node node;
    63      private LogEvent event;
    64
    65      /**
    66       * Constructs a PluginBuilder for a given PluginType.
    67       *
    68       * @param pluginType type of plugin to configure
    69       */
    70      public PluginBuilder(final PluginType<?> pluginType) {
    71          this.pluginType = pluginType;
    72          this.clazz = pluginType.getPluginClass();
    73      }
    74
    75      /**
    76       * Specifies the Configuration to use for constructing the plugin instance.
    77       *
    78       * @param configuration the configuration to use.
    79       * @return {@code this}
    80       */
    81      public PluginBuilder withConfiguration(final Configuration configuration) {
    82          this.configuration = configuration;
    83          return this;
    84      }
    85
    86      /**
    87       * Specifies the Node corresponding to the plugin object that will be created.
    88       *
    89       * @param node the plugin configuration node to use.
    90       * @return {@code this}
    91       */
    92      public PluginBuilder withConfigurationNode(final Node node) {
    93          this.node = node;
    94          return this;
    95      }
    96
    97      /**
    98       * Specifies the LogEvent that may be used to provide extra context for string substitutions.
    99       *
    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(configuration.getStrSubstitutor())
    190                         .setMember(field)
    191                         .visit(configuration, node, event, log);
    192                     // don't overwrite default values if the visitor gives us no value to inject
    193                     if (value != null) {
    194                         field.set(builder, value);
    195                     }
    196                 }
    197             }
    198             final Collection<ConstraintValidator<?>> validators =
    199                 ConstraintValidators.findValidators(annotations);
    200             final Object value = field.get(builder);
    201             for (final ConstraintValidator<?> validator : validators) {
    202                 if (!validator.isValid(value)) {
    203                     invalid = true;
    204                 }
    205             }
    206         }
    207         if (log.length() > 0) {
    208             log.append(')');
    209         }
    210         LOGGER.debug("Calling build() on class {} for element {} {}", builder.getClass(), node.getName(),
    211             log.toString());
    212         if (invalid) {
    213             throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid");
    214         }
    215         checkForRemainingAttributes();
    216         verifyNodeChildrenUsed();
    217     }
    218
    219     private static Method findFactoryMethod(final Class<?> clazz) {
    220         for (final Method method : clazz.getDeclaredMethods()) {
    221             if (method.isAnnotationPresent(PluginFactory.class) &&
    222                 Modifier.isStatic(method.getModifiers())) {
    223                 LOGGER.debug("Found factory method [{}]: {}.", method.getName(), method);
    224                 ReflectionUtil.makeAccessible(method);
    225                 return method;
    226             }
    227         }
    228         throw new IllegalStateException("No factory method found for class " + clazz.getName());
    229     }
    230
    231     private Object[] generateParameters(final Method factory) {
    232         final StringBuilder log = new StringBuilder();
    233         final Class<?>[] types = factory.getParameterTypes();
    234         final Annotation[][] annotations = factory.getParameterAnnotations();
    235         final Object[] args = new Object[annotations.length];
    236         boolean invalid = false;
    237         for (int i = 0; i < annotations.length; i++) {
    238             log.append(log.length() == 0 ? "with params(" : ", ");
    239             final String[] aliases = extractPluginAliases(annotations[i]);
    240             for (final Annotation a : annotations[i]) {
    241                 if (a instanceof PluginAliases) {
    242                     continue; // already processed
    243                 }
    244                 final PluginVisitor<? extends Annotation> visitor = PluginVisitors.findVisitor(
    245                     a.annotationType());
    246                 if (visitor != null) {
    247                     final Object value = visitor.setAliases(aliases)
    248                         .setAnnotation(a)
    249                         .setConversionType(types[i])
    250                         .setStrSubstitutor(configuration.getStrSubstitutor())
    251                         .setMember(factory)
    252                         .visit(configuration, node, event, log);
    253                     // don't overwrite existing values if the visitor gives us no value to inject
    254                     if (value != null) {
    255                         args[i] = value;
    256                     }
    257                 }
    258             }
    259             final Collection<ConstraintValidator<?>> validators =
    260                 ConstraintValidators.findValidators(annotations[i]);
    261             final Object value = args[i];
    262             for (final ConstraintValidator<?> validator : validators) {
    263                 if (!validator.isValid(value)) {
    264                     invalid = true;
    265                 }
    266             }
    267         }
    268         if (log.length() > 0) {
    269             log.append(')');
    270         }
    271         checkForRemainingAttributes();
    272         verifyNodeChildrenUsed();
    273         LOGGER.debug("Calling {} on class {} for element {} {}", factory.getName(), clazz.getName(), node.getName(),
    274             log.toString());
    275         if (invalid) {
    276             throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid");
    277         }
    278         return args;
    279     }
    280
    281     private static String[] extractPluginAliases(final Annotation... parmTypes) {
    282         String[] aliases = null;
    283         for (final Annotation a : parmTypes) {
    284             if (a instanceof PluginAliases) {
    285                 aliases = ((PluginAliases) a).value();
    286             }
    287         }
    288         return aliases;
    289     }
    290
    291     private void checkForRemainingAttributes() {
    292         final Map<String, String> attrs = node.getAttributes();
    293         if (!attrs.isEmpty()) {
    294             final StringBuilder sb = new StringBuilder();
    295             for (final String key : attrs.keySet()) {
    296                 if (sb.length() == 0) {
    297                     sb.append(node.getName());
    298                     sb.append(" contains ");
    299                     if (attrs.size() == 1) {
    300                         sb.append("an invalid element or attribute ");
    301                     } else {
    302                         sb.append("invalid attributes ");
    303                     }
    304                 } else {
    305                     sb.append(", ");
    306                 }
    307                 StringBuilders.appendDqValue(sb, key);
    308
    309             }
    310             LOGGER.error(sb.toString());
    311         }
    312     }
    313
    314     private void verifyNodeChildrenUsed() {
    315         final List<Node> children = node.getChildren();
    316         if (!(pluginType.isDeferChildren() || children.isEmpty())) {
    317             for (final Node child : children) {
    318                 final String nodeType = node.getType().getElementName();
    319                 final String start = nodeType.equals(node.getName()) ? node.getName() : nodeType + ' ' + node.getName();
    320                 LOGGER.error("{} has no parameter that matches element {}", start, child.getName());
    321             }
    322         }
    323     }
    324 }