1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.logging.log4j.audit;
18
19 import java.lang.annotation.Annotation;
20 import java.lang.reflect.InvocationHandler;
21 import java.lang.reflect.Method;
22 import java.lang.reflect.Proxy;
23 import java.util.*;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ConcurrentMap;
26
27 import org.apache.commons.lang3.StringUtils;
28 import org.apache.logging.log4j.LogManager;
29 import org.apache.logging.log4j.Logger;
30 import org.apache.logging.log4j.ThreadContext;
31 import org.apache.logging.log4j.audit.annotation.Constraint;
32 import org.apache.logging.log4j.audit.annotation.Constraints;
33 import org.apache.logging.log4j.audit.annotation.MaxLength;
34 import org.apache.logging.log4j.audit.annotation.RequestContext;
35 import org.apache.logging.log4j.audit.annotation.RequestContextConstraints;
36 import org.apache.logging.log4j.audit.annotation.Required;
37 import org.apache.logging.log4j.audit.exception.AuditException;
38 import org.apache.logging.log4j.audit.util.NamingUtils;
39 import org.apache.logging.log4j.audit.exception.ConstraintValidationException;
40 import org.apache.logging.log4j.catalog.api.plugins.ConstraintPlugins;
41 import org.apache.logging.log4j.message.StructuredDataMessage;
42
43 import static org.apache.logging.log4j.catalog.api.util.StringUtils.appendNewline;
44
45
46
47
48
49 public class LogEventFactory {
50
51 private static final Logger logger = LogManager.getLogger(LogEventFactory.class);
52
53 private static final AuditLogger AUDIT_LOGGER = new AuditLogger();
54
55 private static final int DEFAULT_MAX_LENGTH = 32;
56
57 private static final AuditExceptionHandler DEFAULT_HANDLER = (message, ex) -> {
58 throw new AuditException("Error logging event " + message.getId().getName(), ex);
59 };
60
61 private static final AuditExceptionHandler NOOP_EXCEPTION_HANDLER = (message, ex) -> {
62 };
63
64 private static AuditExceptionHandler defaultExceptionHandler = DEFAULT_HANDLER;
65
66 private static ConcurrentMap<Class<?>, List<Property>> classMap = new ConcurrentHashMap<>();
67
68 private static ConstraintPlugins constraintPlugins = ConstraintPlugins.getInstance();
69
70 public static void setDefaultHandler(AuditExceptionHandler exceptionHandler) {
71 defaultExceptionHandler = (exceptionHandler == null) ? NOOP_EXCEPTION_HANDLER : exceptionHandler;
72 }
73
74 static void resetDefaultHandler() {
75 defaultExceptionHandler = DEFAULT_HANDLER;
76 }
77
78
79
80
81
82
83
84 @SuppressWarnings("unchecked")
85 public static <T extends AuditEvent> T getEvent(Class<T> intrface) {
86
87 Class<?>[] interfaces = new Class<?>[] { intrface };
88
89 AuditMessage msg = buildAuditMessage(intrface);
90 AuditEvent audit = (AuditEvent) Proxy.newProxyInstance(intrface.getClassLoader(), interfaces,
91 new AuditProxy(msg, intrface, defaultExceptionHandler));
92
93 return (T) audit;
94 }
95
96 private static <T> int getMaxLength(Class<T> intrface) {
97 MaxLength maxLength = intrface.getAnnotation(MaxLength.class);
98 return maxLength == null ? DEFAULT_MAX_LENGTH : maxLength.value();
99 }
100
101 private static AuditMessage buildAuditMessage(Class<?> intrface) {
102 String eventId = NamingUtils.lowerFirst(intrface.getSimpleName());
103 int msgLength = getMaxLength(intrface);
104 return new AuditMessage(eventId, msgLength);
105 }
106
107
108
109
110
111
112
113
114 public static void logEvent(Class<?> intrface, Map<String, String> properties) {
115 logEvent(intrface, properties, DEFAULT_HANDLER);
116 }
117
118
119
120
121
122
123
124
125 public static void logEvent(Class<?> intrface, Map<String, String> properties, AuditExceptionHandler handler) {
126 AuditMessage msg = buildAuditMessage(intrface);
127
128 if (properties != null) {
129 for (Map.Entry<String, String> entry : properties.entrySet()) {
130 msg.put(entry.getKey(), entry.getValue());
131 }
132 }
133
134 validateEvent(intrface, msg);
135 logEvent(msg, handler);
136 }
137
138 private static void validateEvent(Class<?> intrface, AuditMessage msg) {
139 StringBuilder errors = new StringBuilder();
140 validateContextConstraints(intrface, errors);
141
142 List<Property> props = getProperties(intrface);
143 Map<String, Property> propertyMap = new HashMap<>();
144
145 for (Property property : props) {
146 propertyMap.put(property.name, property);
147 if (property.isRequired && !msg.containsKey(property.name)) {
148 if (errors.length() > 0) {
149 errors.append("\n");
150 }
151 errors.append("Required attribute ").append(property.name).append(" is missing from ").append(msg.getId().getName());
152 }
153 if (msg.containsKey(property.name)) {
154 validateConstraints(false, property.constraints, property.name, msg, errors);
155 }
156 }
157
158 msg.forEach((key, value) -> {
159 if (!propertyMap.containsKey(key)) {
160 if (errors.length() > 0) {
161 errors.append("Attribute ").append(key).append(" is not defined for ").append(msg.getId().getName());
162 }
163 }
164 });
165
166 if (errors.length() > 0) {
167 throw new ConstraintValidationException(errors.toString());
168 }
169 }
170
171
172
173
174
175
176 public static void logEvent(AuditMessage msg, AuditExceptionHandler handler) {
177 runMessageAction(() -> AUDIT_LOGGER.logEvent(msg), msg, handler);
178 }
179
180 private static void runMessageAction(Runnable action, AuditMessage msg, AuditExceptionHandler handler) {
181 try {
182 action.run();
183 } catch (Throwable ex) {
184 if (handler == null) {
185 handler = defaultExceptionHandler;
186 }
187 handler.handleException(msg, ex);
188 }
189 }
190
191 public static List<String> getPropertyNames(String className) {
192 Class<?> intrface = getClass(className);
193 List<String> names;
194 if (intrface != null) {
195 List<Property> props = getProperties(intrface);
196 names = new ArrayList<>(props.size());
197 for (Property prop : props) {
198 names.add(prop.name);
199 }
200 } else {
201 names = new ArrayList<>();
202 }
203 return names;
204 }
205
206 private static List<Property> getProperties(Class<?> intrface) {
207 List<Property> props = classMap.get(intrface);
208 if (props != null) {
209 return props;
210 }
211 props = new ArrayList<>();
212 Method[] methods = intrface.getMethods();
213 boolean isCompletionStatus = false;
214 for (Method method : methods) {
215 if (method.getName().startsWith("set") && !method.getName().equals("setAuditExceptionHandler")) {
216 if (method.getName().equals("setCompletionStatus")) {
217 isCompletionStatus = true;
218 }
219 String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
220 Annotation[] annotations = method.getDeclaredAnnotations();
221 List<Constraint> constraints = new ArrayList<>();
222 boolean isRequired = false;
223 for (Annotation annotation : annotations) {
224 if (annotation instanceof Constraint) {
225 constraints.add((Constraint) annotation);
226 }
227 if (annotation instanceof Required) {
228 isRequired = true;
229 }
230 }
231 props.add(new Property(name, isRequired, constraints));
232 }
233 }
234 if (!isCompletionStatus) {
235 props.add(new Property("completionStatus", false, new ArrayList<>()));
236 }
237
238 classMap.putIfAbsent(intrface, props);
239 return classMap.get(intrface);
240 }
241
242 private static Class<?> getClass(String className) {
243 try {
244 Class<?> intrface = Class.forName(className);
245 if (AuditEvent.class.isAssignableFrom(intrface)) {
246 return intrface;
247 }
248 logger.error(className + " is not an AuditEvent");
249 } catch (ClassNotFoundException cnfe) {
250 logger.error("Unable to locate class {}", className);
251 }
252 return null;
253 }
254
255 private static class AuditProxy implements InvocationHandler {
256
257 private final AuditMessage msg;
258 private final Class<?> intrface;
259 private AuditExceptionHandler auditExceptionHandler;
260
261 AuditProxy(AuditMessage msg, Class<?> intrface, AuditExceptionHandler auditExceptionHandler) {
262 this.msg = msg;
263 this.intrface = intrface;
264 this.auditExceptionHandler = auditExceptionHandler;
265 }
266
267 public AuditMessage getMessage() {
268 return msg;
269 }
270
271 @Override
272 public Object invoke(Object o, Method method, Object[] objects) {
273 if (method.getName().equals("toString") && method.getParameterCount() == 0) {
274 return msg.toString();
275 }
276
277 if (method.getName().equals("logEvent")) {
278
279 runMessageAction(() -> validateEvent(intrface, msg), msg, auditExceptionHandler);
280
281 logEvent(msg, auditExceptionHandler);
282 return null;
283 }
284
285 if (method.getName().equals("setCompletionStatus")) {
286 if (objects == null || objects[0] == null) {
287 throw new IllegalArgumentException("Missing completion status");
288 }
289 String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
290 msg.put(name, objects[0].toString());
291 return null;
292 }
293
294 if (method.getName().equals("setAuditExceptionHandler")) {
295 if (objects == null || objects[0] == null) {
296 auditExceptionHandler = NOOP_EXCEPTION_HANDLER;
297 } else if (objects[0] instanceof AuditExceptionHandler) {
298 auditExceptionHandler = (AuditExceptionHandler) objects[0];
299 } else {
300 throw new IllegalArgumentException(objects[0] + " is not an " + AuditExceptionHandler.class.getName());
301 }
302 return null;
303 }
304
305 if (method.getName().startsWith("set")) {
306 runMessageAction(() -> setProperty(method, objects), msg, auditExceptionHandler);
307 return null;
308 }
309
310 return null;
311 }
312
313 @SuppressWarnings("unchecked")
314 private void setProperty(Method method, Object[] objects) {
315 String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
316 if (objects == null || objects[0] == null) {
317 throw new IllegalArgumentException("No value to be set for " + name);
318 }
319
320 StringBuilder errors = new StringBuilder();
321 Annotation[] annotations = method.getDeclaredAnnotations();
322 for (Annotation annotation : annotations) {
323 if (annotation instanceof Constraints) {
324 Constraints constraints = (Constraints) annotation;
325 validateConstraints(false, constraints.value(), name, objects[0].toString(),
326 errors);
327 } else if (annotation instanceof Constraint) {
328 Constraint constraint = (Constraint) annotation;
329 constraintPlugins.validateConstraint(false, constraint.constraintType(),
330 name, objects[0].toString(), constraint.constraintValue(), errors);
331 }
332 }
333 if (errors.length() > 0) {
334 throw new ConstraintValidationException(errors.toString());
335 }
336
337 String result;
338 if (objects[0] instanceof List) {
339 result = StringUtils.join(objects, ", ");
340 } else if (objects[0] instanceof Map) {
341 StructuredDataMessage extra = new StructuredDataMessage(name, null, null);
342 extra.putAll((Map) objects[0]);
343 msg.addContent(name, extra);
344 return;
345 } else {
346 result = objects[0].toString();
347 }
348
349 msg.put(name, result);
350 }
351 }
352
353 private static void validateConstraints(boolean isRequestContext, Constraint[] constraints, String name,
354 AuditMessage msg, StringBuilder errors) {
355 String value = isRequestContext ? ThreadContext.get(name) : msg.get(name);
356 validateConstraints(isRequestContext, constraints, name, value, errors);
357 }
358
359 private static void validateConstraints(boolean isRequestContext, Constraint[] constraints, String name,
360 String value, StringBuilder errors) {
361 for (Constraint constraint : constraints) {
362 constraintPlugins.validateConstraint(isRequestContext, constraint.constraintType(), name, value,
363 constraint.constraintValue(), errors);
364 }
365 }
366
367 private static void validateContextConstraints(Class<?> intrface, StringBuilder buffer) {
368 RequestContextConstraints reqCtxConstraints = intrface.getAnnotation(RequestContextConstraints.class);
369 if (reqCtxConstraints != null) {
370 for (RequestContext ctx : reqCtxConstraints.value()) {
371 validateContextConstraint(ctx, buffer);
372 }
373 } else {
374 RequestContext ctx = intrface.getAnnotation(RequestContext.class);
375 validateContextConstraint(ctx, buffer);
376 }
377 }
378
379 private static void validateContextConstraint(RequestContext constraint, StringBuilder errors) {
380 if (constraint == null) {
381
382 return;
383 }
384
385 String value = ThreadContext.get(constraint.key());
386 if (value != null) {
387 validateConstraints(true, constraint.constraints(), constraint.key(), value, errors);
388 } else if (constraint.required()) {
389 appendNewline(errors);
390 errors.append("ThreadContext does not contain required key ").append(constraint.key());
391 }
392 }
393
394 private static boolean isBlank(String value) {
395 return value != null && value.length() > 0;
396 }
397
398 private static class Property {
399 private final String name;
400 private final boolean isRequired;
401 private final Constraint[] constraints;
402
403 public Property(String name, boolean isRequired, List<Constraint> constraints) {
404 this.name = name;
405 this.constraints = constraints.toArray(new Constraint[constraints.size()]);
406 this.isRequired = isRequired;
407 }
408 }
409
410 }