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    }