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