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