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.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.StringBuilders;
48  
49  /**
50   * Builder class to instantiate and configure a Plugin object using a PluginFactory method or PluginBuilderFactory
51   * builder class.
52   */
53  public class PluginBuilder implements Builder<Object> {
54  
55      private static final Logger LOGGER = StatusLogger.getLogger();
56  
57      private final PluginType<?> pluginType;
58      private final Class<?> clazz;
59  
60      private Configuration configuration;
61      private Node node;
62      private LogEvent event;
63  
64      /**
65       * Constructs a PluginBuilder for a given PluginType.
66       *
67       * @param pluginType type of plugin to configure
68       */
69      public PluginBuilder(final PluginType<?> pluginType) {
70          this.pluginType = pluginType;
71          this.clazz = pluginType.getPluginClass();
72      }
73  
74      /**
75       * Specifies the Configuration to use for constructing the plugin instance.
76       *
77       * @param configuration the configuration to use.
78       * @return {@code this}
79       */
80      public PluginBuilder withConfiguration(final Configuration configuration) {
81          this.configuration = configuration;
82          return this;
83      }
84  
85      /**
86       * Specifies the Node corresponding to the plugin object that will be created.
87       *
88       * @param node the plugin configuration node to use.
89       * @return {@code this}
90       */
91      public PluginBuilder withConfigurationNode(final Node node) {
92          this.node = node;
93          return this;
94      }
95  
96      /**
97       * Specifies the LogEvent that may be used to provide extra context for string substitutions.
98       *
99       * @param event the event to use for extra information.
100      * @return {@code this}
101      */
102     public PluginBuilder forLogEvent(final LogEvent event) {
103         this.event = event;
104         return this;
105     }
106 
107     /**
108      * Builds the plugin object.
109      *
110      * @return the plugin object or {@code null} if there was a problem creating it.
111      */
112     @Override
113     public Object build() {
114         verify();
115         // first try to use a builder class if one is available
116         try {
117             LOGGER.debug("Building Plugin[name={}, class={}].", pluginType.getElementName(),
118                     pluginType.getPluginClass().getName());
119             final Builder<?> builder = createBuilder(this.clazz);
120             if (builder != null) {
121                 injectFields(builder);
122                 return builder.build();
123             }
124         } catch (final ConfigurationException e) { // LOG4J2-1908
125             LOGGER.error("Could not create plugin of type {} for element {}", this.clazz, node.getName(), e);
126             return null; // no point in trying the factory method
127         } catch (final Exception e) {
128             LOGGER.error("Could not create plugin of type {} for element {}: {}",
129                     this.clazz, node.getName(),
130                     (e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e).toString(), e);
131         }
132         // or fall back to factory method if no builder class is available
133         try {
134             final Method factory = findFactoryMethod(this.clazz);
135             final Object[] params = generateParameters(factory);
136             return factory.invoke(null, params);
137         } catch (final Exception e) {
138             LOGGER.error("Unable to invoke factory method in {} for element {}: {}",
139                     this.clazz, this.node.getName(),
140                     (e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e).toString(), e);
141             return null;
142         }
143     }
144 
145     private void verify() {
146         Objects.requireNonNull(this.configuration, "No Configuration object was set.");
147         Objects.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.getReturnType())) {
156                 ReflectionUtil.makeAccessible(method);
157                 return (Builder<?>) method.invoke(null);
158             }
159         }
160         return null;
161     }
162 
163     private void injectFields(final Builder<?> builder) throws IllegalAccessException {
164         final List<Field> fields = TypeUtil.getAllDeclaredFields(builder.getClass());
165         AccessibleObject.setAccessible(fields.toArray(new Field[] {}), true);
166         final StringBuilder log = new StringBuilder();
167         boolean invalid = false;
168         String reason = "";
169         for (final Field field : fields) {
170             log.append(log.length() == 0 ? simpleName(builder) + "(" : ", ");
171             final Annotation[] annotations = field.getDeclaredAnnotations();
172             final String[] aliases = extractPluginAliases(annotations);
173             for (final Annotation a : annotations) {
174                 if (a instanceof PluginAliases) {
175                     continue; // already processed
176                 }
177                 final PluginVisitor<? extends Annotation> visitor =
178                     PluginVisitors.findVisitor(a.annotationType());
179                 if (visitor != null) {
180                     final Object value = visitor.setAliases(aliases)
181                         .setAnnotation(a)
182                         .setConversionType(field.getType())
183                         .setStrSubstitutor(configuration.getStrSubstitutor())
184                         .setMember(field)
185                         .visit(configuration, node, event, log);
186                     // don't overwrite default values if the visitor gives us no value to inject
187                     if (value != null) {
188                         field.set(builder, value);
189                     }
190                 }
191             }
192             final Collection<ConstraintValidator<?>> validators =
193                 ConstraintValidators.findValidators(annotations);
194             final Object value = field.get(builder);
195             for (final ConstraintValidator<?> validator : validators) {
196                 if (!validator.isValid(field.getName(), value)) {
197                     invalid = true;
198                     if (!reason.isEmpty()) {
199                         reason += ", ";
200                     }
201                     reason += "field '" + field.getName() + "' has invalid value '" + value + "'";
202                 }
203             }
204         }
205         log.append(log.length() == 0 ? builder.getClass().getSimpleName() + "()" : ")");
206         LOGGER.debug(log.toString());
207         if (invalid) {
208             throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid: " + reason);
209         }
210         checkForRemainingAttributes();
211         verifyNodeChildrenUsed();
212     }
213 
214     /**
215      * {@code object.getClass().getSimpleName()} returns {@code Builder}, when we want {@code PatternLayout$Builder}.
216      */
217     private static String simpleName(final Object object) {
218         if (object == null) {
219             return "null";
220         }
221         final String cls = object.getClass().getName();
222         final int index = cls.lastIndexOf('.');
223         return index < 0 ? cls : cls.substring(index + 1);
224     }
225 
226     private static Method findFactoryMethod(final Class<?> clazz) {
227         for (final Method method : clazz.getDeclaredMethods()) {
228             if (method.isAnnotationPresent(PluginFactory.class) &&
229                 Modifier.isStatic(method.getModifiers())) {
230                 ReflectionUtil.makeAccessible(method);
231                 return method;
232             }
233         }
234         throw new IllegalStateException("No factory method found for class " + clazz.getName());
235     }
236 
237     private Object[] generateParameters(final Method factory) {
238         final StringBuilder log = new StringBuilder();
239         final Class<?>[] types = factory.getParameterTypes();
240         final Annotation[][] annotations = factory.getParameterAnnotations();
241         final Object[] args = new Object[annotations.length];
242         boolean invalid = false;
243         for (int i = 0; i < annotations.length; i++) {
244             log.append(log.length() == 0 ? factory.getName() + "(" : ", ");
245             final String[] aliases = extractPluginAliases(annotations[i]);
246             for (final Annotation a : annotations[i]) {
247                 if (a instanceof PluginAliases) {
248                     continue; // already processed
249                 }
250                 final PluginVisitor<? extends Annotation> visitor = PluginVisitors.findVisitor(
251                     a.annotationType());
252                 if (visitor != null) {
253                     final Object value = visitor.setAliases(aliases)
254                         .setAnnotation(a)
255                         .setConversionType(types[i])
256                         .setStrSubstitutor(configuration.getStrSubstitutor())
257                         .setMember(factory)
258                         .visit(configuration, node, event, log);
259                     // don't overwrite existing values if the visitor gives us no value to inject
260                     if (value != null) {
261                         args[i] = value;
262                     }
263                 }
264             }
265             final Collection<ConstraintValidator<?>> validators =
266                 ConstraintValidators.findValidators(annotations[i]);
267             final Object value = args[i];
268             final String argName = "arg[" + i + "](" + simpleName(value) + ")";
269             for (final ConstraintValidator<?> validator : validators) {
270                 if (!validator.isValid(argName, value)) {
271                     invalid = true;
272                 }
273             }
274         }
275         log.append(log.length() == 0 ? factory.getName() + "()" : ")");
276         checkForRemainingAttributes();
277         verifyNodeChildrenUsed();
278         LOGGER.debug(log.toString());
279         if (invalid) {
280             throw new ConfigurationException("Arguments given for element " + node.getName() + " are invalid");
281         }
282         return args;
283     }
284 
285     private static String[] extractPluginAliases(final Annotation... parmTypes) {
286         String[] aliases = null;
287         for (final Annotation a : parmTypes) {
288             if (a instanceof PluginAliases) {
289                 aliases = ((PluginAliases) a).value();
290             }
291         }
292         return aliases;
293     }
294 
295     private void checkForRemainingAttributes() {
296         final Map<String, String> attrs = node.getAttributes();
297         if (!attrs.isEmpty()) {
298             final StringBuilder sb = new StringBuilder();
299             for (final String key : attrs.keySet()) {
300                 if (sb.length() == 0) {
301                     sb.append(node.getName());
302                     sb.append(" contains ");
303                     if (attrs.size() == 1) {
304                         sb.append("an invalid element or attribute ");
305                     } else {
306                         sb.append("invalid attributes ");
307                     }
308                 } else {
309                     sb.append(", ");
310                 }
311                 StringBuilders.appendDqValue(sb, key);
312             }
313             LOGGER.error(sb.toString());
314         }
315     }
316 
317     private void verifyNodeChildrenUsed() {
318         final List<Node> children = node.getChildren();
319         if (!(pluginType.isDeferChildren() || children.isEmpty())) {
320             for (final Node child : children) {
321                 final String nodeType = node.getType().getElementName();
322                 final String start = nodeType.equals(node.getName()) ? node.getName() : nodeType + ' ' + node.getName();
323                 LOGGER.error("{} has no parameter that matches element {}", start, child.getName());
324             }
325         }
326     }
327 }