Merge branch 'master' of https://github.com/apache/logging-log4j-audit into LOG4J2-2421
diff --git a/log4j-audit/log4j-audit-api/src/main/java/org/apache/logging/log4j/audit/AbstractEventLogger.java b/log4j-audit/log4j-audit-api/src/main/java/org/apache/logging/log4j/audit/AbstractEventLogger.java
index ee203d6..5de2844 100644
--- a/log4j-audit/log4j-audit-api/src/main/java/org/apache/logging/log4j/audit/AbstractEventLogger.java
+++ b/log4j-audit/log4j-audit-api/src/main/java/org/apache/logging/log4j/audit/AbstractEventLogger.java
@@ -29,9 +29,7 @@
 import org.apache.logging.log4j.catalog.api.plugins.ConstraintPlugins;
 import org.apache.logging.log4j.message.StructuredDataMessage;
 
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 import static java.util.Collections.*;
 
@@ -126,8 +124,7 @@
                     missingAttributes.append(name);
                 } else {
                     if (attr.getConstraints() != null && attr.getConstraints().size() > 0) {
-                        Constraint[] constraints = attr.getConstraints().toArray(new Constraint[attr.getConstraints().size()]);
-                        validateConstraints(false, constraints, name, attributes.get(name), errors);
+                        validateConstraints(false, attr.getConstraints(), name, attributes.get(name), errors);
                     }
                 }
             }
@@ -165,8 +162,7 @@
         }
 
         List<String> reqCtxAttrs = catalogManager.getRequiredContextAttributes(eventName, event.getCatalogId());
-
-        if (reqCtxAttrs != null) {
+        if (reqCtxAttrs != null && !reqCtxAttrs.isEmpty()) {
             StringBuilder sb = new StringBuilder();
             for (String attr : reqCtxAttrs) {
                 if (!ThreadContext.containsKey(attr)) {
@@ -181,22 +177,37 @@
                         " is missing required RequestContextMapping values for " + sb.toString());
             }
         }
+
         Map<String, Attribute> reqCtxAttributes = catalogManager.getRequestContextAttributes();
-        for (Map.Entry<String, String> entry : ThreadContext.getImmutableContext().entrySet()) {
+        for (Map.Entry<String, Attribute> entry : reqCtxAttributes.entrySet()) {
+            Attribute attribute = entry.getValue();
+            String attr = entry.getKey();
+            if (attribute.isRequired() && !ThreadContext.containsKey(attr)) {
+                if (errors.length() > 0) {
+                    errors.append(", ");
+                }
+                errors.append(attr);
+            }
+        }
+        if (errors.length() > 0) {
+            throw new AuditException("Event " + eventName +
+                                             " is missing required Thread Context values for " + errors.toString());
+        }
+
+        for (Map.Entry<String, Attribute> entry : reqCtxAttributes.entrySet()) {
             Attribute attribute = reqCtxAttributes.get(entry.getKey());
-            if (attribute == null) {
+            if (!ThreadContext.containsKey(entry.getKey())) {
                 continue;
             }
             Set<Constraint> constraintList = attribute.getConstraints();
             if (constraintList != null && constraintList.size() > 0) {
-                Constraint[] constraints =
-                        attribute.getConstraints().toArray(new Constraint[attribute.getConstraints().size()]);
-                validateConstraints(true, constraints, entry.getKey(), ThreadContext.get(entry.getKey()), errors);
+                validateConstraints(true, constraintList, entry.getKey(), ThreadContext.get(entry.getKey()), errors);
             }
         }
         if (errors.length() > 0) {
             throw new AuditException("Event " + eventName + " has incorrect data in the Thread Context: " + errors.toString());
         }
+
         msg.putAll(attributes);
         try {
             logEvent(msg);
@@ -209,7 +220,7 @@
         }
     }
 
-    private static void validateConstraints(boolean isRequestContext, Constraint[] constraints, String name,
+    private static void validateConstraints(boolean isRequestContext, Collection<Constraint> constraints, String name,
                                             String value, StringBuilder errors) {
         for (Constraint constraint : constraints) {
             constraintPlugins.validateConstraint(isRequestContext, constraint.getConstraintType().getName(), name, value,
diff --git a/log4j-audit/log4j-audit-api/src/test/java/org/apache/logging/log4j/audit/AuditLoggerTest.java b/log4j-audit/log4j-audit-api/src/test/java/org/apache/logging/log4j/audit/AuditLoggerTest.java
index 3cfca75..5d925d5 100644
--- a/log4j-audit/log4j-audit-api/src/test/java/org/apache/logging/log4j/audit/AuditLoggerTest.java
+++ b/log4j-audit/log4j-audit-api/src/test/java/org/apache/logging/log4j/audit/AuditLoggerTest.java
@@ -77,13 +77,16 @@
     @Before
     public void before() {
         app.clear();
+        ThreadContext.clearMap();
     }
 
     @Test
     public void testAuditLogger() throws Exception {
         auditLogger = buildAuditLogger(catalogReader);
 
+        ThreadContext.put("accountNumber", "12345");
         ThreadContext.put("companyId", "12345");
+        ThreadContext.put("userId", "JohnDoe");
         ThreadContext.put("ipAddress", "127.0.0.1");
         ThreadContext.put("environment", "dev");
         ThreadContext.put("product", "TestProduct");
@@ -110,7 +113,18 @@
     }
 
     @Test(expected = AuditException.class)
-    public void testBadAttribute() throws Exception {
+    public void testMissingRequestContextAttribute() throws Exception {
+        auditLogger = buildAuditLogger(catalogReader);
+
+        Map<String, String> properties = new HashMap<String, String>();
+        properties.put("toAccount", "123456");
+        properties.put("fromAccount", "111111");
+        properties.put("amount", "111.55");
+        auditLogger.logEvent("transfer", properties);
+    }
+
+    @Test(expected = AuditException.class)
+    public void testMissingEventAttribute() throws Exception {
         auditLogger = buildAuditLogger(catalogReader);
 
         ThreadContext.put("companyId", "12345");
diff --git a/log4j-audit/log4j-audit-api/src/test/java/org/apache/logging/log4j/audit/TransferTest.java b/log4j-audit/log4j-audit-api/src/test/java/org/apache/logging/log4j/audit/TransferTest.java
index 716bb58..7b1cd47 100644
--- a/log4j-audit/log4j-audit-api/src/test/java/org/apache/logging/log4j/audit/TransferTest.java
+++ b/log4j-audit/log4j-audit-api/src/test/java/org/apache/logging/log4j/audit/TransferTest.java
@@ -43,7 +43,7 @@
 public class TransferTest extends BaseEventTest {
 
     @Test(expected = ConstraintValidationException.class)
-    public void testValidationFailure() {
+    public void testValidationFailureForMissingRequestContextAttribute() {
         Transfer transfer = LogEventFactory.getEvent(Transfer.class);
         ThreadContext.put("companyId", "12345");
         ThreadContext.put("ipAddress", "127.0.0.1");
@@ -58,6 +58,23 @@
         fail("Should have thrown an AuditException");
     }
 
+    @Test(expected = ConstraintValidationException.class)
+    public void testValidationFailureForMissingEventAttribute() {
+        Transfer transfer = LogEventFactory.getEvent(Transfer.class);
+        ThreadContext.put("accountNumber", "12345");
+        ThreadContext.put("companyId", "12345");
+        ThreadContext.put("userId", "JohnDoe");
+        ThreadContext.put("ipAddress", "127.0.0.1");
+        ThreadContext.put("environment", "dev");
+        ThreadContext.put("product", "TestProduct");
+        ThreadContext.put("timeZone", "America/Phoenix");
+        ThreadContext.put("loginId", "TestUser");
+        transfer.setToAccount(123456);
+        transfer.setFromAccount(111111);
+        transfer.logEvent();
+        fail("Should have thrown an AuditException");
+    }
+
     @Test
     public void testAuditClass() {
         Transfer transfer = LogEventFactory.getEvent(Transfer.class);
@@ -95,7 +112,7 @@
     }
 
     @Test(expected = ConstraintValidationException.class)
-    public void testAuditLogException() {
+    public void testAuditLogWithMissingRequestContextAttribute() {
         ThreadContext.put("userId", "JohnDoe");
         ThreadContext.put("ipAddress", "127.0.0.1");
         ThreadContext.put("environment", "dev");
@@ -109,6 +126,21 @@
         LogEventFactory.logEvent(Transfer.class, properties);
     }
 
+    @Test(expected = ConstraintValidationException.class)
+    public void testAuditLogWithMissingEventAttribute() {
+        ThreadContext.put("accountNumber", "12345");
+        ThreadContext.put("userId", "JohnDoe");
+        ThreadContext.put("ipAddress", "127.0.0.1");
+        ThreadContext.put("environment", "dev");
+        ThreadContext.put("product", "TestProduct");
+        ThreadContext.put("timeZone", "America/Phoenix");
+        ThreadContext.put("loginId", "TestUser");
+        Map<String, String> properties = new HashMap<>();
+        properties.put("toAccount", "123456");
+        properties.put("fromAccount", "111111");
+        LogEventFactory.logEvent(Transfer.class, properties);
+    }
+
     @Test
     public void testAuditLog() {
         ThreadContext.put("accountNumber", "12345");