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  import java.util.Objects;
30  
31  import org.apache.logging.log4j.Logger;
32  import org.apache.logging.log4j.core.LogEvent;
33  import org.apache.logging.log4j.core.config.Configuration;
34  import org.apache.logging.log4j.core.config.ConfigurationException;
35  import org.apache.logging.log4j.core.config.Node;
36  import org.apache.logging.log4j.core.config.plugins.PluginAliases;
37  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
38  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
39  import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator;
40  import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidators;
41  import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor;
42  import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitors;
43  import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
44  import org.apache.logging.log4j.core.util.Builder;
45  import org.apache.logging.log4j.core.util.ReflectionUtil;
46  import org.apache.logging.log4j.core.util.TypeUtil;
47  import org.apache.logging.log4j.status.StatusLogger;
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={}].", pluginType.getElementName(),
119                     pluginType.getPluginClass().getName());
120             final Builder<?> builder = createBuilder(this.clazz);
121             if (builder != null) {
122                 injectFields(builder);
123                 return builder.build();
124             }
125         } catch (final ConfigurationException e) { // LOG4J2-1908
126             LOGGER.error("Could not create plugin of type {} for element {}", this.clazz, node.getName(), e);
127             return null; // no point in trying the factory method
128         } catch (final Throwable t) {
129             LOGGER.error("Could not create plugin of type {} for element {}: {}",
130                     this.clazz, node.getName(),
131                     (t instanceof InvocationTargetException ? ((InvocationTargetException) t).getCause() : t).toString(), t);
132         }
133         // or fall back to factory method if no builder class is available
134         try {
135             final Method factory = findFactoryMethod(this.clazz);
136             final Object[] params = generateParameters(factory);
137             return factory.invoke(null, params);
138         } catch (final Throwable t) {
139             LOGGER.error("Unable to invoke factory method in {} for element {}: {}",
140                     this.clazz, this.node.getName(),
141                     (t instanceof InvocationTargetException ? ((InvocationTargetException) t).getCause() : t).toString(), t);
142             return null;
143         }
144     }
145 
146     private void verify() {
147         Objects.requireNonNull(this.configuration, "No Configuration object was set.");
148         Objects.requireNonNull(this.node, "No Node object was set.");
149     }
150 
151     private static Builder<?> createBuilder(final Class<?> clazz)
152         throws InvocationTargetException, IllegalAccessException {
153         for (final Method method : clazz.getDeclaredMethods()) {
154             if (method.isAnnotationPresent(PluginBuilderFactory.class) &&
155                 Modifier.isStatic(method.getModifiers()) &&
156                 TypeUtil.isAssignable(Builder.class, method.getReturnType())) {
157                 ReflectionUtil.makeAccessible(method);
158                 return (Builder<?>) method.invoke(null);
159             }
160         }
161         return null;
162     }
163 
164     private void injectFields(final Builder<?> builder) throws IllegalAccessException {
165         final List<Field> fields = TypeUtil.getAllDeclaredFields(builder.getClass());
166         AccessibleObject.setAccessible(fields.toArray(new Field[] {}), true);
167         final StringBuilder log = new StringBuilder();
168         boolean invalid = false;
169         String reason = "";
170         for (final Field field : fields) {
171             log.append(log.length() == 0 ? simpleName(builder) + "(" : ", ");
172             final Annotation[] annotations = field.getDeclaredAnnotations();
173             final String[] aliases = extractPluginAliases(annotations);
174             for (final Annotation a : annotations) {
175                 if (a instanceof PluginAliases) {
176                     continue; // already processed
177                 }
178                 final PluginVisitor<? extends Annotation> visitor =
179                     PluginVisitors.findVisitor(a.annotationType());
180                 if (visitor != null) {
181                     final Object value = visitor.setAliases(aliases)
182                         .setAnnotation(a)
183                         .setConversionType(field.getType())
184                         .setStrSubstitutor(event == null
185                                 ? new ConfigurationStrSubstitutor(configuration.getStrSubstitutor())
186                                 : configuration.getStrSubstitutor())
187                         .setMember(field)
188                         .visit(configuration, node, event, log);
189                     // don't overwrite default values if the visitor gives us no value to inject
190                     if (value != null) {
191                         field.set(builder, value);
192                     }
193                 }
194             }
195             final Collection<ConstraintValidator<?>> validators =
196                 ConstraintValidators.findValidators(annotations);
197             final Object value = field.get(builder);
198             for (final ConstraintValidator<?> validator : validators) {
199                 if (!validator.isValid(field.getName(), value)) {
200                     invalid = true;
201                     if (!reason.isEmpty()) {
202                         reason += ", ";
203                     }
204                     reason += "field '" + field.getName() + "' has invalid value '" + value + "'";
205                 }
206             }
207         }
208         log.append(log.length() == 0 ? builder.getClass().getSimpleName() + "()" : ")");
209         LOGGER.debug(log.toString());
210         if (invalid) {
211             throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid: " + reason);
212         }
213         checkForRemainingAttributes();
214         verifyNodeChildrenUsed();
215     }
216 
217     /**
218      * {@code object.getClass().getSimpleName()} returns {@code Builder}, when we want {@code PatternLayout$Builder}.
219      */
220     private static String simpleName(final Object object) {
221         if (object == null) {
222             return "null";
223         }
224         final String cls = object.getClass().getName();
225         final int index = cls.lastIndexOf('.');
226         return index < 0 ? cls : cls.substring(index + 1);
227     }
228 
229     private static Method findFactoryMethod(final Class<?> clazz) {
230         for (final Method method : clazz.getDeclaredMethods()) {
231             if (method.isAnnotationPresent(PluginFactory.class) &&
232                 Modifier.isStatic(method.getModifiers())) {
233                 ReflectionUtil.makeAccessible(method);
234                 return method;
235             }
236         }
237         throw new IllegalStateException("No factory method found for class " + clazz.getName());
238     }
239 
240     private Object[] generateParameters(final Method factory) {
241         final StringBuilder log = new StringBuilder();
242         final Class<?>[] types = factory.getParameterTypes();
243         final Annotation[][] annotations = factory.getParameterAnnotations();
244         final Object[] args = new Object[annotations.length];
245         boolean invalid = false;
246         for (int i = 0; i < annotations.length; i++) {
247             log.append(log.length() == 0 ? factory.getName() + "(" : ", ");
248             final String[] aliases = extractPluginAliases(annotations[i]);
249             for (final Annotation a : annotations[i]) {
250                 if (a instanceof PluginAliases) {
251                     continue; // already processed
252                 }
253                 final PluginVisitor<? extends Annotation> visitor = PluginVisitors.findVisitor(
254                     a.annotationType());
255                 if (visitor != null) {
256                     final Object value = visitor.setAliases(aliases)
257                         .setAnnotation(a)
258                         .setConversionType(types[i])
259                         .setStrSubstitutor(event == null
260                                 ? new ConfigurationStrSubstitutor(configuration.getStrSubstitutor())
261                                 : configuration.getStrSubstitutor())
262                         .setMember(factory)
263                         .visit(configuration, node, event, log);
264                     // don't overwrite existing values if the visitor gives us no value to inject
265                     if (value != null) {
266                         args[i] = value;
267                     }
268                 }
269             }
270             final Collection<ConstraintValidator<?>> validators =
271                 ConstraintValidators.findValidators(annotations[i]);
272             final Object value = args[i];
273             final String argName = "arg[" + i + "](" + simpleName(value) + ")";
274             for (final ConstraintValidator<?> validator : validators) {
275                 if (!validator.isValid(argName, value)) {
276                     invalid = true;
277                 }
278             }
279         }
280         log.append(log.length() == 0 ? factory.getName() + "()" : ")");
281         checkForRemainingAttributes();
282         verifyNodeChildrenUsed();
283         LOGGER.debug(log.toString());
284         if (invalid) {
285             throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid");
286         }
287         return args;
288     }
289 
290     private static String[] extractPluginAliases(final Annotation... parmTypes) {
291         String[] aliases = null;
292         for (final Annotation a : parmTypes) {
293             if (a instanceof PluginAliases) {
294                 aliases = ((PluginAliases) a).value();
295             }
296         }
297         return aliases;
298     }
299 
300     private void checkForRemainingAttributes() {
301         final Map<String, String> attrs = node.getAttributes();
302         if (!attrs.isEmpty()) {
303             final StringBuilder sb = new StringBuilder();
304             for (final String key : attrs.keySet()) {
305                 if (sb.length() == 0) {
306                     sb.append(node.getName());
307                     sb.append(" contains ");
308                     if (attrs.size() == 1) {
309                         sb.append("an invalid element or attribute ");
310                     } else {
311                         sb.append("invalid attributes ");
312                     }
313                 } else {
314                     sb.append(", ");
315                 }
316                 StringBuilders.appendDqValue(sb, key);
317             }
318             LOGGER.error(sb.toString());
319         }
320     }
321 
322     private void verifyNodeChildrenUsed() {
323         final List<Node> children = node.getChildren();
324         if (!(pluginType.isDeferChildren() || children.isEmpty())) {
325             for (final Node child : children) {
326                 final String nodeType = node.getType().getElementName();
327                 final String start = nodeType.equals(node.getName()) ? node.getName() : nodeType + ' ' + node.getName();
328                 LOGGER.error("{} has no parameter that matches element {}", start, child.getName());
329             }
330         }
331     }
332 }