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    
048    /**
049     * Builder class to instantiate and configure a Plugin object using a PluginFactory method or PluginBuilderFactory
050     * builder class.
051     */
052    public class PluginBuilder implements Builder<Object> {
053    
054        private static final Logger LOGGER = StatusLogger.getLogger();
055    
056        private final PluginType<?> pluginType;
057        private final Class<?> clazz;
058    
059        private Configuration configuration;
060        private Node node;
061        private LogEvent event;
062    
063        /**
064         * Constructs a PluginBuilder for a given PluginType.
065         *
066         * @param pluginType type of plugin to configure
067         */
068        public PluginBuilder(final PluginType<?> pluginType) {
069            this.pluginType = pluginType;
070            this.clazz = pluginType.getPluginClass();
071        }
072    
073        /**
074         * Specifies the Configuration to use for constructing the plugin instance.
075         *
076         * @param configuration the configuration to use.
077         * @return {@code this}
078         */
079        public PluginBuilder withConfiguration(final Configuration configuration) {
080            this.configuration = configuration;
081            return this;
082        }
083    
084        /**
085         * Specifies the Node corresponding to the plugin object that will be created.
086         *
087         * @param node the plugin configuration node to use.
088         * @return {@code this}
089         */
090        public PluginBuilder withConfigurationNode(final Node node) {
091            this.node = node;
092            return this;
093        }
094    
095        /**
096         * Specifies the LogEvent that may be used to provide extra context for string substitutions.
097         *
098         * @param event the event to use for extra information.
099         * @return {@code this}
100         */
101        public PluginBuilder forLogEvent(final LogEvent event) {
102            this.event = event;
103            return this;
104        }
105    
106        /**
107         * Builds the plugin object.
108         *
109         * @return the plugin object or {@code null} if there was a problem creating it.
110         */
111        @Override
112        public Object build() {
113            verify();
114            // first try to use a builder class if one is available
115            try {
116                LOGGER.debug("Building Plugin[name={}, class={}]. Searching for builder factory method...", pluginType.getElementName(),
117                        pluginType.getPluginClass().getName());
118                final Builder<?> builder = createBuilder(this.clazz);
119                if (builder != null) {
120                    injectFields(builder);
121                    final Object result = builder.build();
122                    LOGGER.debug("Built Plugin[name={}] OK from builder factory method.", pluginType.getElementName());
123                    return result;
124                }
125            } catch (final Exception e) {
126                LOGGER.error("Unable to inject fields into builder class for plugin type {}, element {}.", this.clazz,
127                    node.getName(), e);
128            }
129            // or fall back to factory method if no builder class is available
130            try {
131                LOGGER.debug("Still building Plugin[name={}, class={}]. Searching for factory method...",
132                        pluginType.getElementName(), pluginType.getPluginClass().getName());
133                final Method factory = findFactoryMethod(this.clazz);
134                final Object[] params = generateParameters(factory);
135                final Object plugin = factory.invoke(null, params);
136                LOGGER.debug("Built Plugin[name={}] OK from factory method.", pluginType.getElementName());
137                return plugin;
138            } catch (final Exception e) {
139                LOGGER.error("Unable to invoke factory method in class {} for element {}.", this.clazz, this.node.getName(),
140                    e);
141                return null;
142            }
143        }
144    
145        private void verify() {
146            Assert.requireNonNull(this.configuration, "No Configuration object was set.");
147            Assert.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.getGenericReturnType())) {
156                    ReflectionUtil.makeAccessible(method);
157                    @SuppressWarnings("unchecked")
158                    final Builder<?> builder = (Builder<?>) method.invoke(null);
159                    LOGGER.debug("Found builder factory method [{}]: {}.", method.getName(), method);
160                    return builder;
161                }
162            }
163            LOGGER.debug("No builder factory method found in class {}. Going to try finding a factory method instead.",
164                clazz.getName());
165            return null;
166        }
167    
168        private void injectFields(final Builder<?> builder) throws IllegalAccessException {
169            final Field[] fields = builder.getClass().getDeclaredFields();
170            AccessibleObject.setAccessible(fields, true);
171            final StringBuilder log = new StringBuilder();
172            boolean invalid = false;
173            for (final Field field : fields) {
174                log.append(log.length() == 0 ? "with params(" : ", ");
175                final Annotation[] annotations = field.getDeclaredAnnotations();
176                final String[] aliases = extractPluginAliases(annotations);
177                for (final Annotation a : annotations) {
178                    if (a instanceof PluginAliases) {
179                        continue; // already processed
180                    }
181                    final PluginVisitor<? extends Annotation> visitor =
182                        PluginVisitors.findVisitor(a.annotationType());
183                    if (visitor != null) {
184                        final Object value = visitor.setAliases(aliases)
185                            .setAnnotation(a)
186                            .setConversionType(field.getType())
187                            .setStrSubstitutor(configuration.getStrSubstitutor())
188                            .setMember(field)
189                            .visit(configuration, node, event, log);
190                        // don't overwrite default values if the visitor gives us no value to inject
191                        if (value != null) {
192                            field.set(builder, value);
193                        }
194                    }
195                }
196                final Collection<ConstraintValidator<?>> validators =
197                    ConstraintValidators.findValidators(annotations);
198                final Object value = field.get(builder);
199                for (final ConstraintValidator<?> validator : validators) {
200                    if (!validator.isValid(value)) {
201                        invalid = true;
202                    }
203                }
204            }
205            if (log.length() > 0) {
206                log.append(')');
207            }
208            LOGGER.debug("Calling build() on class {} for element {} {}", builder.getClass(), node.getName(),
209                log.toString());
210            if (invalid) {
211                throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid");
212            }
213            checkForRemainingAttributes();
214            verifyNodeChildrenUsed();
215        }
216    
217        private static Method findFactoryMethod(final Class<?> clazz) {
218            for (final Method method : clazz.getDeclaredMethods()) {
219                if (method.isAnnotationPresent(PluginFactory.class) &&
220                    Modifier.isStatic(method.getModifiers())) {
221                    LOGGER.debug("Found factory method [{}]: {}.", method.getName(), method);
222                    ReflectionUtil.makeAccessible(method);
223                    return method;
224                }
225            }
226            throw new IllegalStateException("No factory method found for class " + clazz.getName());
227        }
228    
229        private Object[] generateParameters(final Method factory) {
230            final StringBuilder log = new StringBuilder();
231            final Class<?>[] types = factory.getParameterTypes();
232            final Annotation[][] annotations = factory.getParameterAnnotations();
233            final Object[] args = new Object[annotations.length];
234            boolean invalid = false;
235            for (int i = 0; i < annotations.length; i++) {
236                log.append(log.length() == 0 ? "with params(" : ", ");
237                final String[] aliases = extractPluginAliases(annotations[i]);
238                for (final Annotation a : annotations[i]) {
239                    if (a instanceof PluginAliases) {
240                        continue; // already processed
241                    }
242                    final PluginVisitor<? extends Annotation> visitor = PluginVisitors.findVisitor(
243                        a.annotationType());
244                    if (visitor != null) {
245                        final Object value = visitor.setAliases(aliases)
246                            .setAnnotation(a)
247                            .setConversionType(types[i])
248                            .setStrSubstitutor(configuration.getStrSubstitutor())
249                            .setMember(factory)
250                            .visit(configuration, node, event, log);
251                        // don't overwrite existing values if the visitor gives us no value to inject
252                        if (value != null) {
253                            args[i] = value;
254                        }
255                    }
256                }
257                final Collection<ConstraintValidator<?>> validators =
258                    ConstraintValidators.findValidators(annotations[i]);
259                final Object value = args[i];
260                for (final ConstraintValidator<?> validator : validators) {
261                    if (!validator.isValid(value)) {
262                        invalid = true;
263                    }
264                }
265            }
266            if (log.length() > 0) {
267                log.append(')');
268            }
269            checkForRemainingAttributes();
270            verifyNodeChildrenUsed();
271            LOGGER.debug("Calling {} on class {} for element {} {}", factory.getName(), clazz.getName(), node.getName(),
272                log.toString());
273            if (invalid) {
274                throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid");
275            }
276            return args;
277        }
278    
279        private static String[] extractPluginAliases(final Annotation... parmTypes) {
280            String[] aliases = null;
281            for (final Annotation a : parmTypes) {
282                if (a instanceof PluginAliases) {
283                    aliases = ((PluginAliases) a).value();
284                }
285            }
286            return aliases;
287        }
288    
289        private void checkForRemainingAttributes() {
290            final Map<String, String> attrs = node.getAttributes();
291            if (!attrs.isEmpty()) {
292                final StringBuilder sb = new StringBuilder();
293                for (final String key : attrs.keySet()) {
294                    if (sb.length() == 0) {
295                        sb.append(node.getName());
296                        sb.append(" contains ");
297                        if (attrs.size() == 1) {
298                            sb.append("an invalid element or attribute ");
299                        } else {
300                            sb.append("invalid attributes ");
301                        }
302                    } else {
303                        sb.append(", ");
304                    }
305                    sb.append('"');
306                    sb.append(key);
307                    sb.append('"');
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    }