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.util.Builder;
044import org.apache.logging.log4j.core.util.ReflectionUtil;
045import org.apache.logging.log4j.core.util.TypeUtil;
046import org.apache.logging.log4j.status.StatusLogger;
047import org.apache.logging.log4j.util.StringBuilders;
048
049/**
050 * Builder class to instantiate and configure a Plugin object using a PluginFactory method or PluginBuilderFactory
051 * builder class.
052 */
053public class PluginBuilder implements Builder<Object> {
054
055    private static final Logger LOGGER = StatusLogger.getLogger();
056
057    private final PluginType<?> pluginType;
058    private final Class<?> clazz;
059
060    private Configuration configuration;
061    private Node node;
062    private LogEvent event;
063
064    /**
065     * Constructs a PluginBuilder for a given PluginType.
066     *
067     * @param pluginType type of plugin to configure
068     */
069    public PluginBuilder(final PluginType<?> pluginType) {
070        this.pluginType = pluginType;
071        this.clazz = pluginType.getPluginClass();
072    }
073
074    /**
075     * Specifies the Configuration to use for constructing the plugin instance.
076     *
077     * @param configuration the configuration to use.
078     * @return {@code this}
079     */
080    public PluginBuilder withConfiguration(final Configuration configuration) {
081        this.configuration = configuration;
082        return this;
083    }
084
085    /**
086     * Specifies the Node corresponding to the plugin object that will be created.
087     *
088     * @param node the plugin configuration node to use.
089     * @return {@code this}
090     */
091    public PluginBuilder withConfigurationNode(final Node node) {
092        this.node = node;
093        return this;
094    }
095
096    /**
097     * Specifies the LogEvent that may be used to provide extra context for string substitutions.
098     *
099     * @param event the event to use for extra information.
100     * @return {@code this}
101     */
102    public PluginBuilder forLogEvent(final LogEvent event) {
103        this.event = event;
104        return this;
105    }
106
107    /**
108     * Builds the plugin object.
109     *
110     * @return the plugin object or {@code null} if there was a problem creating it.
111     */
112    @Override
113    public Object build() {
114        verify();
115        // first try to use a builder class if one is available
116        try {
117            LOGGER.debug("Building Plugin[name={}, class={}].", pluginType.getElementName(),
118                    pluginType.getPluginClass().getName());
119            final Builder<?> builder = createBuilder(this.clazz);
120            if (builder != null) {
121                injectFields(builder);
122                return builder.build();
123            }
124        } catch (final ConfigurationException e) { // LOG4J2-1908
125            LOGGER.error("Could not create plugin of type {} for element {}", this.clazz, node.getName(), e);
126            return null; // no point in trying the factory method
127        } catch (final Exception e) {
128            LOGGER.error("Could not create plugin of type {} for element {}: {}",
129                    this.clazz, node.getName(),
130                    (e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e).toString(), e);
131        }
132        // or fall back to factory method if no builder class is available
133        try {
134            final Method factory = findFactoryMethod(this.clazz);
135            final Object[] params = generateParameters(factory);
136            return factory.invoke(null, params);
137        } catch (final Exception e) {
138            LOGGER.error("Unable to invoke factory method in {} for element {}: {}",
139                    this.clazz, this.node.getName(),
140                    (e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e).toString(), e);
141            return null;
142        }
143    }
144
145    private void verify() {
146        Objects.requireNonNull(this.configuration, "No Configuration object was set.");
147        Objects.requireNonNull(this.node, "No Node object was set.");
148    }
149
150    private static Builder<?> createBuilder(final Class<?> clazz)
151        throws InvocationTargetException, IllegalAccessException {
152        for (final Method method : clazz.getDeclaredMethods()) {
153            if (method.isAnnotationPresent(PluginBuilderFactory.class) &&
154                Modifier.isStatic(method.getModifiers()) &&
155                TypeUtil.isAssignable(Builder.class, method.getReturnType())) {
156                ReflectionUtil.makeAccessible(method);
157                return (Builder<?>) method.invoke(null);
158            }
159        }
160        return null;
161    }
162
163    private void injectFields(final Builder<?> builder) throws IllegalAccessException {
164        final List<Field> fields = TypeUtil.getAllDeclaredFields(builder.getClass());
165        AccessibleObject.setAccessible(fields.toArray(new Field[] {}), true);
166        final StringBuilder log = new StringBuilder();
167        boolean invalid = false;
168        String reason = "";
169        for (final Field field : fields) {
170            log.append(log.length() == 0 ? simpleName(builder) + "(" : ", ");
171            final Annotation[] annotations = field.getDeclaredAnnotations();
172            final String[] aliases = extractPluginAliases(annotations);
173            for (final Annotation a : annotations) {
174                if (a instanceof PluginAliases) {
175                    continue; // already processed
176                }
177                final PluginVisitor<? extends Annotation> visitor =
178                    PluginVisitors.findVisitor(a.annotationType());
179                if (visitor != null) {
180                    final Object value = visitor.setAliases(aliases)
181                        .setAnnotation(a)
182                        .setConversionType(field.getType())
183                        .setStrSubstitutor(configuration.getStrSubstitutor())
184                        .setMember(field)
185                        .visit(configuration, node, event, log);
186                    // don't overwrite default values if the visitor gives us no value to inject
187                    if (value != null) {
188                        field.set(builder, value);
189                    }
190                }
191            }
192            final Collection<ConstraintValidator<?>> validators =
193                ConstraintValidators.findValidators(annotations);
194            final Object value = field.get(builder);
195            for (final ConstraintValidator<?> validator : validators) {
196                if (!validator.isValid(field.getName(), value)) {
197                    invalid = true;
198                    if (!reason.isEmpty()) {
199                        reason += ", ";
200                    }
201                    reason += "field '" + field.getName() + "' has invalid value '" + value + "'";
202                }
203            }
204        }
205        log.append(log.length() == 0 ? builder.getClass().getSimpleName() + "()" : ")");
206        LOGGER.debug(log.toString());
207        if (invalid) {
208            throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid: " + reason);
209        }
210        checkForRemainingAttributes();
211        verifyNodeChildrenUsed();
212    }
213
214    /**
215     * {@code object.getClass().getSimpleName()} returns {@code Builder}, when we want {@code PatternLayout$Builder}.
216     */
217    private static String simpleName(final Object object) {
218        if (object == null) {
219            return "null";
220        }
221        final String cls = object.getClass().getName();
222        final int index = cls.lastIndexOf('.');
223        return index < 0 ? cls : cls.substring(index + 1);
224    }
225
226    private static Method findFactoryMethod(final Class<?> clazz) {
227        for (final Method method : clazz.getDeclaredMethods()) {
228            if (method.isAnnotationPresent(PluginFactory.class) &&
229                Modifier.isStatic(method.getModifiers())) {
230                ReflectionUtil.makeAccessible(method);
231                return method;
232            }
233        }
234        throw new IllegalStateException("No factory method found for class " + clazz.getName());
235    }
236
237    private Object[] generateParameters(final Method factory) {
238        final StringBuilder log = new StringBuilder();
239        final Class<?>[] types = factory.getParameterTypes();
240        final Annotation[][] annotations = factory.getParameterAnnotations();
241        final Object[] args = new Object[annotations.length];
242        boolean invalid = false;
243        for (int i = 0; i < annotations.length; i++) {
244            log.append(log.length() == 0 ? factory.getName() + "(" : ", ");
245            final String[] aliases = extractPluginAliases(annotations[i]);
246            for (final Annotation a : annotations[i]) {
247                if (a instanceof PluginAliases) {
248                    continue; // already processed
249                }
250                final PluginVisitor<? extends Annotation> visitor = PluginVisitors.findVisitor(
251                    a.annotationType());
252                if (visitor != null) {
253                    final Object value = visitor.setAliases(aliases)
254                        .setAnnotation(a)
255                        .setConversionType(types[i])
256                        .setStrSubstitutor(configuration.getStrSubstitutor())
257                        .setMember(factory)
258                        .visit(configuration, node, event, log);
259                    // don't overwrite existing values if the visitor gives us no value to inject
260                    if (value != null) {
261                        args[i] = value;
262                    }
263                }
264            }
265            final Collection<ConstraintValidator<?>> validators =
266                ConstraintValidators.findValidators(annotations[i]);
267            final Object value = args[i];
268            final String argName = "arg[" + i + "](" + simpleName(value) + ")";
269            for (final ConstraintValidator<?> validator : validators) {
270                if (!validator.isValid(argName, value)) {
271                    invalid = true;
272                }
273            }
274        }
275        log.append(log.length() == 0 ? factory.getName() + "()" : ")");
276        checkForRemainingAttributes();
277        verifyNodeChildrenUsed();
278        LOGGER.debug(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            LOGGER.error(sb.toString());
314        }
315    }
316
317    private void verifyNodeChildrenUsed() {
318        final List<Node> children = node.getChildren();
319        if (!(pluginType.isDeferChildren() || children.isEmpty())) {
320            for (final Node child : children) {
321                final String nodeType = node.getType().getElementName();
322                final String start = nodeType.equals(node.getName()) ? node.getName() : nodeType + ' ' + node.getName();
323                LOGGER.error("{} has no parameter that matches element {}", start, child.getName());
324            }
325        }
326    }
327}