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 018package org.apache.logging.log4j.core.config.plugins.util; 019 020import java.lang.annotation.Annotation; 021import java.lang.reflect.AccessibleObject; 022import java.lang.reflect.Field; 023import java.lang.reflect.InvocationTargetException; 024import java.lang.reflect.Method; 025import java.lang.reflect.Modifier; 026import java.util.Collection; 027import java.util.List; 028import java.util.Map; 029import java.util.Objects; 030 031import org.apache.logging.log4j.Logger; 032import org.apache.logging.log4j.core.LogEvent; 033import org.apache.logging.log4j.core.config.Configuration; 034import org.apache.logging.log4j.core.config.ConfigurationException; 035import org.apache.logging.log4j.core.config.Node; 036import org.apache.logging.log4j.core.config.plugins.PluginAliases; 037import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; 038import org.apache.logging.log4j.core.config.plugins.PluginFactory; 039import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator; 040import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidators; 041import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor; 042import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitors; 043import org.apache.logging.log4j.core.util.Builder; 044import org.apache.logging.log4j.core.util.ReflectionUtil; 045import org.apache.logging.log4j.core.util.TypeUtil; 046import org.apache.logging.log4j.status.StatusLogger; 047import org.apache.logging.log4j.util.StringBuilders; 048 049/** 050 * Builder class to instantiate and configure a Plugin object using a PluginFactory method or PluginBuilderFactory 051 * builder class. 052 */ 053public class PluginBuilder implements Builder<Object> { 054 055 private static final Logger LOGGER = StatusLogger.getLogger(); 056 057 private final PluginType<?> pluginType; 058 private final Class<?> clazz; 059 060 private Configuration configuration; 061 private Node node; 062 private LogEvent event; 063 064 /** 065 * Constructs a PluginBuilder for a given PluginType. 066 * 067 * @param pluginType type of plugin to configure 068 */ 069 public PluginBuilder(final PluginType<?> pluginType) { 070 this.pluginType = pluginType; 071 this.clazz = pluginType.getPluginClass(); 072 } 073 074 /** 075 * Specifies the Configuration to use for constructing the plugin instance. 076 * 077 * @param configuration the configuration to use. 078 * @return {@code this} 079 */ 080 public PluginBuilder withConfiguration(final Configuration configuration) { 081 this.configuration = configuration; 082 return this; 083 } 084 085 /** 086 * Specifies the Node corresponding to the plugin object that will be created. 087 * 088 * @param node the plugin configuration node to use. 089 * @return {@code this} 090 */ 091 public PluginBuilder withConfigurationNode(final Node node) { 092 this.node = node; 093 return this; 094 } 095 096 /** 097 * Specifies the LogEvent that may be used to provide extra context for string substitutions. 098 * 099 * @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}