    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
    018    package org.apache.logging.log4j.core.config.plugins.util;
    019
    020    import java.lang.annotation.Annotation;
    021    import java.lang.reflect.AccessibleObject;
    022    import java.lang.reflect.Field;
    023    import java.lang.reflect.InvocationTargetException;
    024    import java.lang.reflect.Method;
    025    import java.lang.reflect.Modifier;
    026    import java.util.Collection;
    027    import java.util.List;
    028    import java.util.Map;
    029
    030    import org.apache.logging.log4j.Logger;
    031    import org.apache.logging.log4j.core.LogEvent;
    032    import org.apache.logging.log4j.core.config.Configuration;
    033    import org.apache.logging.log4j.core.config.ConfigurationException;
    034    import org.apache.logging.log4j.core.config.Node;
    035    import org.apache.logging.log4j.core.config.plugins.PluginAliases;
    036    import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
    037    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    038    import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator;
    039    import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidators;
    040    import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor;
    041    import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitors;
    042    import org.apache.logging.log4j.core.util.Assert;
    043    import org.apache.logging.log4j.core.util.Builder;
    044    import org.apache.logging.log4j.core.util.ReflectionUtil;
    045    import org.apache.logging.log4j.core.util.TypeUtil;
    046    import org.apache.logging.log4j.status.StatusLogger;
    047    import org.apache.logging.log4j.util.Chars;
    048    import 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     */
    054    public 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(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    }