View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.logging.log4j.core.config.plugins.util;
19  
20  import java.lang.annotation.Annotation;
21  import java.lang.reflect.AccessibleObject;
22  import java.lang.reflect.Field;
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.lang.reflect.Modifier;
26  import java.util.Collection;
27  import java.util.List;
28  import java.util.Map;
29  
30  import org.apache.logging.log4j.Logger;
31  import org.apache.logging.log4j.core.LogEvent;
32  import org.apache.logging.log4j.core.config.Configuration;
33  import org.apache.logging.log4j.core.config.ConfigurationException;
34  import org.apache.logging.log4j.core.config.Node;
35  import org.apache.logging.log4j.core.config.plugins.PluginAliases;
36  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
37  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
38  import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator;
39  import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidators;
40  import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor;
41  import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitors;
42  import org.apache.logging.log4j.core.util.Assert;
43  import org.apache.logging.log4j.core.util.Builder;
44  import org.apache.logging.log4j.core.util.ReflectionUtil;
45  import org.apache.logging.log4j.core.util.TypeUtil;
46  import org.apache.logging.log4j.status.StatusLogger;
47  import org.apache.logging.log4j.util.Chars;
48  import org.apache.logging.log4j.util.StringBuilders;
49  
50  /**
51   * Builder class to instantiate and configure a Plugin object using a PluginFactory method or PluginBuilderFactory
52   * builder class.
53   */
54  public class PluginBuilder implements Builder<Object> {
55  
56      private static final Logger LOGGER = StatusLogger.getLogger();
57  
58      private final PluginType<?> pluginType;
59      private final Class<?> clazz;
60  
61      private Configuration configuration;
62      private Node node;
63      private LogEvent event;
64  
65      /**
66       * Constructs a PluginBuilder for a given PluginType.
67       *
68       * @param pluginType type of plugin to configure
69       */
70      public PluginBuilder(final PluginType<?> pluginType) {
71          this.pluginType = pluginType;
72          this.clazz = pluginType.getPluginClass();
73      }
74  
75      /**
76       * Specifies the Configuration to use for constructing the plugin instance.
77       *
78       * @param configuration the configuration to use.
79       * @return {@code this}
80       */
81      public PluginBuilder withConfiguration(final Configuration configuration) {
82          this.configuration = configuration;
83          return this;
84      }
85  
86      /**
87       * Specifies the Node corresponding to the plugin object that will be created.
88       *
89       * @param node the plugin configuration node to use.
90       * @return {@code this}
91       */
92      public PluginBuilder withConfigurationNode(final Node node) {
93          this.node = node;
94          return this;
95      }
96  
97      /**
98       * Specifies the LogEvent that may be used to provide extra context for string substitutions.
99       *
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 }