blob: 36d761f61824a31e66e88a0a4f846116b0102aec [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache license, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the license for the specific language governing permissions and
* limitations under the license.
*/
package org.apache.logging.log4j.audit;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.audit.annotation.Constraint;
import org.apache.logging.log4j.audit.annotation.Constraints;
import org.apache.logging.log4j.audit.annotation.EventName;
import org.apache.logging.log4j.audit.annotation.MaxLength;
import org.apache.logging.log4j.audit.annotation.RequestContext;
import org.apache.logging.log4j.audit.annotation.RequestContextConstraints;
import org.apache.logging.log4j.audit.annotation.Required;
import org.apache.logging.log4j.audit.exception.AuditException;
import org.apache.logging.log4j.audit.exception.ConstraintValidationException;
import org.apache.logging.log4j.audit.util.NamingUtils;
import org.apache.logging.log4j.catalog.api.plugins.ConstraintPlugins;
import org.apache.logging.log4j.message.StructuredDataMessage;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static org.apache.logging.log4j.catalog.api.util.StringUtils.appendNewline;
/**
* Handles logging generated Events. Every Event extends the AuditProxy, which handles construction of the
* Event and logging of the Event.
*/
public class LogEventFactory {
private static final Logger logger = LogManager.getLogger(LogEventFactory.class);
private static final AuditLogger AUDIT_LOGGER = new AuditLogger();
private static final int DEFAULT_MAX_LENGTH = 32;
private static final AuditExceptionHandler DEFAULT_HANDLER = (message, ex) -> {
throw new AuditException("Error logging event " + message.getId().getName(), ex);
};
private static final AuditExceptionHandler NOOP_EXCEPTION_HANDLER = (message, ex) -> {
};
private static AuditExceptionHandler defaultExceptionHandler = DEFAULT_HANDLER;
private static final ConcurrentMap<Class<?>, List<Property>> classMap = new ConcurrentHashMap<>();
private static final ConstraintPlugins constraintPlugins = ConstraintPlugins.getInstance();
public static void setDefaultHandler(AuditExceptionHandler exceptionHandler) {
defaultExceptionHandler = (exceptionHandler == null) ? NOOP_EXCEPTION_HANDLER : exceptionHandler;
}
static void resetDefaultHandler() {
defaultExceptionHandler = DEFAULT_HANDLER;
}
/**
* Constructs an Event object from its interface.
* @param intrface The Event interface.
* @param <T> The Event type.
* @return Returns an instance of the Event.
*/
@SuppressWarnings("unchecked")
public static <T extends AuditEvent> T getEvent(Class<T> intrface) {
Class<?>[] interfaces = new Class<?>[] { intrface };
AuditMessage msg = buildAuditMessage(intrface);
AuditEvent audit = (AuditEvent) Proxy.newProxyInstance(intrface.getClassLoader(), interfaces,
new AuditProxy(msg, intrface, defaultExceptionHandler));
return (T) audit;
}
private static String getEventName(Class<?> intrface) {
EventName eventName = intrface.getAnnotation(EventName.class);
return eventName != null ? eventName.value() : NamingUtils.lowerFirst(intrface.getSimpleName());
}
private static <T> int getMaxLength(Class<T> intrface) {
MaxLength maxLength = intrface.getAnnotation(MaxLength.class);
return maxLength == null ? DEFAULT_MAX_LENGTH : maxLength.value();
}
private static AuditMessage buildAuditMessage(Class<?> intrface) {
String eventName = getEventName(intrface);
int msgLength = getMaxLength(intrface);
return new AuditMessage(eventName, msgLength);
}
/**
*
* This method is used to construct and AuditMessage from a set of properties and the Event interface
* that represents the event being audited using the default error handler.
* @param intrface The Event interface.
* @param properties The properties to be included in the event.
*/
public static void logEvent(Class<?> intrface, Map<String, String> properties) {
logEvent(intrface, properties, DEFAULT_HANDLER);
}
/**
* This method is used to construct and AuditMessage from a set of properties and the Event interface
* that represents the event being audited.
* @param intrface The Event interface.
* @param properties The properties to be included in the event.
* @param handler Class that gets control when an exception occurs logging the event.
*/
public static void logEvent(Class<?> intrface, Map<String, String> properties, AuditExceptionHandler handler) {
AuditMessage msg = buildAuditMessage(intrface);
if (properties != null) {
for (Map.Entry<String, String> entry : properties.entrySet()) {
msg.put(entry.getKey(), entry.getValue());
}
}
validateEvent(intrface, msg);
logEvent(msg, handler);
}
private static void validateEvent(Class<?> intrface, AuditMessage msg) {
StringBuilder errors = new StringBuilder();
validateContextConstraints(intrface, errors);
List<Property> props = getProperties(intrface);
Map<String, Property> propertyMap = new HashMap<>();
for (Property property : props) {
propertyMap.put(property.name, property);
if (property.isRequired && !msg.containsKey(property.name)) {
if (errors.length() > 0) {
errors.append("\n");
}
errors.append("Required attribute ").append(property.name).append(" is missing from ").append(msg.getId().getName());
}
if (msg.containsKey(property.name)) {
validateConstraints(false, property.constraints, property.name, msg, errors);
}
}
msg.forEach((key, value) -> {
if (!propertyMap.containsKey(key)) {
if (errors.length() > 0) {
errors.append("Attribute ").append(key).append(" is not defined for ").append(msg.getId().getName());
}
}
});
if (errors.length() > 0) {
throw new ConstraintValidationException(errors.toString());
}
}
/**
* Used to Log the actual AuditMessage.
* @param msg The AuditMessage.
* @param handler Class that gets control when an exception occurs logging the event.
*/
public static void logEvent(AuditMessage msg, AuditExceptionHandler handler) {
runMessageAction(() -> AUDIT_LOGGER.logEvent(msg), msg, handler);
}
private static void runMessageAction(Runnable action, AuditMessage msg, AuditExceptionHandler handler) {
try {
action.run();
} catch (Throwable ex) {
if (handler == null) {
handler = defaultExceptionHandler;
}
handler.handleException(msg, ex);
}
}
public static List<String> getPropertyNames(String className) {
Class<?> intrface = getClass(className);
List<String> names;
if (intrface != null) {
List<Property> props = getProperties(intrface);
names = new ArrayList<>(props.size());
for (Property prop : props) {
names.add(prop.name);
}
} else {
names = new ArrayList<>();
}
return names;
}
private static List<Property> getProperties(Class<?> intrface) {
List<Property> props = classMap.get(intrface);
if (props != null) {
return props;
}
props = new ArrayList<>();
Method[] methods = intrface.getMethods();
boolean isCompletionStatus = false;
for (Method method : methods) {
if (method.getName().startsWith("set") && !method.getName().equals("setAuditExceptionHandler")) {
if (method.getName().equals("setCompletionStatus")) {
isCompletionStatus = true;
}
String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
Annotation[] annotations = method.getDeclaredAnnotations();
List<Constraint> constraints = new ArrayList<>();
boolean isRequired = false;
for (Annotation annotation : annotations) {
if (annotation instanceof Constraint) {
constraints.add((Constraint) annotation);
}
if (annotation instanceof Required) {
isRequired = true;
}
}
props.add(new Property(name, isRequired, constraints));
}
}
if (!isCompletionStatus) {
props.add(new Property("completionStatus", false, new ArrayList<>()));
}
classMap.putIfAbsent(intrface, props);
return classMap.get(intrface);
}
private static Class<?> getClass(String className) {
try {
Class<?> intrface = Class.forName(className);
if (AuditEvent.class.isAssignableFrom(intrface)) {
return intrface;
}
logger.error(className + " is not an AuditEvent");
} catch (ClassNotFoundException cnfe) {
logger.error("Unable to locate class {}", className);
}
return null;
}
private static class AuditProxy implements InvocationHandler {
private final AuditMessage msg;
private final Class<?> intrface;
private AuditExceptionHandler auditExceptionHandler;
AuditProxy(AuditMessage msg, Class<?> intrface, AuditExceptionHandler auditExceptionHandler) {
this.msg = msg;
this.intrface = intrface;
this.auditExceptionHandler = auditExceptionHandler;
}
public AuditMessage getMessage() {
return msg;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) {
if (method.getName().equals("toString") && method.getParameterCount() == 0) {
return msg.toString();
}
if (method.getName().equals("logEvent")) {
runMessageAction(() -> validateEvent(intrface, msg), msg, auditExceptionHandler);
logEvent(msg, auditExceptionHandler);
return null;
}
if (method.getName().equals("setCompletionStatus")) {
if (objects == null || objects[0] == null) {
throw new IllegalArgumentException("Missing completion status");
}
String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
msg.put(name, objects[0].toString());
return null;
}
if (method.getName().equals("setAuditExceptionHandler")) {
if (objects == null || objects[0] == null) {
auditExceptionHandler = NOOP_EXCEPTION_HANDLER;
} else if (objects[0] instanceof AuditExceptionHandler) {
auditExceptionHandler = (AuditExceptionHandler) objects[0];
} else {
throw new IllegalArgumentException(objects[0] + " is not an " + AuditExceptionHandler.class.getName());
}
return null;
}
if (method.getName().startsWith("set")) {
runMessageAction(() -> setProperty(method, objects), msg, auditExceptionHandler);
return null;
}
return null;
}
@SuppressWarnings("unchecked")
private void setProperty(Method method, Object[] objects) {
String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
if (objects == null || objects[0] == null) {
throw new IllegalArgumentException("No value to be set for " + name);
}
StringBuilder errors = new StringBuilder();
Annotation[] annotations = method.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof Constraints) {
Constraints constraints = (Constraints) annotation;
validateConstraints(false, constraints.value(), name, objects[0].toString(),
errors);
} else if (annotation instanceof Constraint) {
Constraint constraint = (Constraint) annotation;
constraintPlugins.validateConstraint(false, constraint.constraintType(),
name, objects[0].toString(), constraint.constraintValue(), errors);
}
}
if (errors.length() > 0) {
throw new ConstraintValidationException(errors.toString());
}
String result;
if (objects[0] instanceof List) {
result = StringUtils.join(objects, ", ");
} else if (objects[0] instanceof Map) {
StructuredDataMessage extra = new StructuredDataMessage(name, null, null);
extra.putAll((Map) objects[0]);
msg.addContent(name, extra);
return;
} else {
result = objects[0].toString();
}
msg.put(name, result);
}
}
private static void validateConstraints(boolean isRequestContext, Constraint[] constraints, String name,
AuditMessage msg, StringBuilder errors) {
String value = isRequestContext ? ThreadContext.get(name) : msg.get(name);
validateConstraints(isRequestContext, constraints, name, value, errors);
}
private static void validateConstraints(boolean isRequestContext, Constraint[] constraints, String name,
String value, StringBuilder errors) {
for (Constraint constraint : constraints) {
constraintPlugins.validateConstraint(isRequestContext, constraint.constraintType(), name, value,
constraint.constraintValue(), errors);
}
}
private static void validateContextConstraints(Class<?> intrface, StringBuilder buffer) {
RequestContextConstraints reqCtxConstraints = intrface.getAnnotation(RequestContextConstraints.class);
if (reqCtxConstraints != null) {
for (RequestContext ctx : reqCtxConstraints.value()) {
validateContextConstraint(ctx, buffer);
}
} else {
RequestContext ctx = intrface.getAnnotation(RequestContext.class);
validateContextConstraint(ctx, buffer);
}
}
private static void validateContextConstraint(RequestContext constraint, StringBuilder errors) {
if (constraint == null) {
// the request context is not mandatory
return;
}
String value = ThreadContext.get(constraint.key());
if (value != null) {
validateConstraints(true, constraint.constraints(), constraint.key(), value, errors);
} else if (constraint.required()) {
appendNewline(errors);
errors.append("ThreadContext does not contain required key ").append(constraint.key());
}
}
private static boolean isBlank(String value) {
return value != null && value.length() > 0;
}
private static class Property {
private final String name;
private final boolean isRequired;
private final Constraint[] constraints;
public Property(String name, boolean isRequired, List<Constraint> constraints) {
this.name = name;
this.constraints = constraints.toArray(new Constraint[0]);
this.isRequired = isRequired;
}
}
}