You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by gg...@apache.org on 2018/01/27 18:46:37 UTC

[3/6] logging-log4j2 git commit: [LOG4J2-2188] Split off JPA support into a new module log4j-jpa.

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverter.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverter.java b/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverter.java
new file mode 100644
index 0000000..bbec319
--- /dev/null
+++ b/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverter.java
@@ -0,0 +1,238 @@
+/*
+ * 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.core.appender.db.jpa.converter;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.persistence.AttributeConverter;
+import javax.persistence.Converter;
+
+import org.apache.logging.log4j.util.LoaderUtil;
+import org.apache.logging.log4j.util.Strings;
+
+/**
+ * A JPA 2.1 attribute converter for {@link Throwable}s in {@link org.apache.logging.log4j.core.LogEvent}s. This
+ * converter is capable of converting both to and from {@link String}s.
+ */
+@Converter(autoApply = false)
+public class ThrowableAttributeConverter implements AttributeConverter<Throwable, String> {
+    private static final int CAUSED_BY_STRING_LENGTH = 10;
+
+    private static final Field THROWABLE_CAUSE;
+
+    private static final Field THROWABLE_MESSAGE;
+
+    static {
+        try {
+            THROWABLE_CAUSE = Throwable.class.getDeclaredField("cause");
+            THROWABLE_CAUSE.setAccessible(true);
+            THROWABLE_MESSAGE = Throwable.class.getDeclaredField("detailMessage");
+            THROWABLE_MESSAGE.setAccessible(true);
+        } catch (final NoSuchFieldException e) {
+            throw new IllegalStateException("Something is wrong with java.lang.Throwable.", e);
+        }
+    }
+
+    @Override
+    public String convertToDatabaseColumn(final Throwable throwable) {
+        if (throwable == null) {
+            return null;
+        }
+
+        final StringBuilder builder = new StringBuilder();
+        this.convertThrowable(builder, throwable);
+        return builder.toString();
+    }
+
+    private void convertThrowable(final StringBuilder builder, final Throwable throwable) {
+        builder.append(throwable.toString()).append('\n');
+        for (final StackTraceElement element : throwable.getStackTrace()) {
+            builder.append("\tat ").append(element).append('\n');
+        }
+        if (throwable.getCause() != null) {
+            builder.append("Caused by ");
+            this.convertThrowable(builder, throwable.getCause());
+        }
+    }
+
+    @Override
+    public Throwable convertToEntityAttribute(final String s) {
+        if (Strings.isEmpty(s)) {
+            return null;
+        }
+
+        final List<String> lines = Arrays.asList(s.split("(\n|\r\n)"));
+        return this.convertString(lines.listIterator(), false);
+    }
+
+    private Throwable convertString(final ListIterator<String> lines, final boolean removeCausedBy) {
+        String firstLine = lines.next();
+        if (removeCausedBy) {
+            firstLine = firstLine.substring(CAUSED_BY_STRING_LENGTH);
+        }
+        final int colon = firstLine.indexOf(":");
+        String throwableClassName;
+        String message = null;
+        if (colon > 1) {
+            throwableClassName = firstLine.substring(0, colon);
+            if (firstLine.length() > colon + 1) {
+                message = firstLine.substring(colon + 1).trim();
+            }
+        } else {
+            throwableClassName = firstLine;
+        }
+
+        final List<StackTraceElement> stackTrace = new ArrayList<>();
+        Throwable cause = null;
+        while (lines.hasNext()) {
+            final String line = lines.next();
+
+            if (line.startsWith("Caused by ")) {
+                lines.previous();
+                cause = convertString(lines, true);
+                break;
+            }
+
+            stackTrace.add(
+                    StackTraceElementAttributeConverter.convertString(line.trim().substring(3).trim())
+            );
+        }
+
+        return this.getThrowable(throwableClassName, message, cause,
+                stackTrace.toArray(new StackTraceElement[stackTrace.size()]));
+    }
+
+    private Throwable getThrowable(final String throwableClassName, final String message, final Throwable cause,
+                                   final StackTraceElement[] stackTrace) {
+        try {
+            @SuppressWarnings("unchecked")
+            final Class<Throwable> throwableClass = (Class<Throwable>) LoaderUtil.loadClass(throwableClassName);
+
+            if (!Throwable.class.isAssignableFrom(throwableClass)) {
+                return null;
+            }
+
+            Throwable throwable;
+            if (message != null && cause != null) {
+                throwable = this.getThrowable(throwableClass, message, cause);
+                if (throwable == null) {
+                    throwable = this.getThrowable(throwableClass, cause);
+                    if (throwable == null) {
+                        throwable = this.getThrowable(throwableClass, message);
+                        if (throwable == null) {
+                            throwable = this.getThrowable(throwableClass);
+                            if (throwable != null) {
+                                THROWABLE_MESSAGE.set(throwable, message);
+                                THROWABLE_CAUSE.set(throwable, cause);
+                            }
+                        } else {
+                            THROWABLE_CAUSE.set(throwable, cause);
+                        }
+                    } else {
+                        THROWABLE_MESSAGE.set(throwable, message);
+                    }
+                }
+            } else if (cause != null) {
+                throwable = this.getThrowable(throwableClass, cause);
+                if (throwable == null) {
+                    throwable = this.getThrowable(throwableClass);
+                    if (throwable != null) {
+                        THROWABLE_CAUSE.set(throwable, cause);
+                    }
+                }
+            } else if (message != null) {
+                throwable = this.getThrowable(throwableClass, message);
+                if (throwable == null) {
+                    throwable = this.getThrowable(throwableClass);
+                    if (throwable != null) {
+                        THROWABLE_MESSAGE.set(throwable, cause);
+                    }
+                }
+            } else {
+                throwable = this.getThrowable(throwableClass);
+            }
+
+            if (throwable == null) {
+                return null;
+            }
+            throwable.setStackTrace(stackTrace);
+            return throwable;
+        } catch (final Exception e) {
+            return null;
+        }
+    }
+
+    private Throwable getThrowable(final Class<Throwable> throwableClass, final String message, final Throwable cause) {
+        try {
+            @SuppressWarnings("unchecked")
+            final
+            Constructor<Throwable>[] constructors = (Constructor<Throwable>[]) throwableClass.getConstructors();
+            for (final Constructor<Throwable> constructor : constructors) {
+                final Class<?>[] parameterTypes = constructor.getParameterTypes();
+                if (parameterTypes.length == 2) {
+                    if (String.class == parameterTypes[0] && Throwable.class.isAssignableFrom(parameterTypes[1])) {
+                        return constructor.newInstance(message, cause);
+                    } else if (String.class == parameterTypes[1] &&
+                            Throwable.class.isAssignableFrom(parameterTypes[0])) {
+                        return constructor.newInstance(cause, message);
+                    }
+                }
+            }
+            return null;
+        } catch (final Exception e) {
+            return null;
+        }
+    }
+
+    private Throwable getThrowable(final Class<Throwable> throwableClass, final Throwable cause) {
+        try {
+            @SuppressWarnings("unchecked")
+            final
+            Constructor<Throwable>[] constructors = (Constructor<Throwable>[]) throwableClass.getConstructors();
+            for (final Constructor<Throwable> constructor : constructors) {
+                final Class<?>[] parameterTypes = constructor.getParameterTypes();
+                if (parameterTypes.length == 1 && Throwable.class.isAssignableFrom(parameterTypes[0])) {
+                    return constructor.newInstance(cause);
+                }
+            }
+            return null;
+        } catch (final Exception e) {
+            return null;
+        }
+    }
+
+    private Throwable getThrowable(final Class<Throwable> throwableClass, final String message) {
+        try {
+            return throwableClass.getConstructor(String.class).newInstance(message);
+        } catch (final Exception e) {
+            return null;
+        }
+    }
+
+    private Throwable getThrowable(final Class<Throwable> throwableClass) {
+        try {
+            return throwableClass.newInstance();
+        } catch (final Exception e) {
+            return null;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/package-info.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/package-info.java b/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/package-info.java
new file mode 100644
index 0000000..2245fa7
--- /dev/null
+++ b/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+// Lines too long...
+//CHECKSTYLE:OFF
+/**
+ * The converters in this package implement the JPA 2.1 mechanism for converting non-standard types to and from
+ * database fields. Most of these types are capable of two-way conversion and can be used to both persist and retrieve
+ * entities. The
+ * {@link org.apache.logging.log4j.core.appender.db.jpa.converter.ContextMapAttributeConverter ContextMapAttributeConverter}
+ * and {@link org.apache.logging.log4j.core.appender.db.jpa.converter.ContextStackAttributeConverter ContextStackAttributeConverter}
+ * only support persistence and not retrieval, persisting the type as a simple string. You can use the
+ * {@link org.apache.logging.log4j.core.appender.db.jpa.converter.ContextMapJsonAttributeConverter ContextMapJsonAttributeConverter}
+ * and {@link org.apache.logging.log4j.core.appender.db.jpa.converter.ContextStackJsonAttributeConverter ContextStackJsonAttributeConverter}
+ * instead, which require the Jackson Data Processor dependency to also be on your class path.
+ */
+//CHECKSTYLE:ON
+package org.apache.logging.log4j.core.appender.db.jpa.converter;
+

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/package-info.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/package-info.java b/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/package-info.java
new file mode 100644
index 0000000..3434faa
--- /dev/null
+++ b/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+/**
+ * The JPA Appender supports writing log events to a relational database using the Java Persistence API. You will need
+ * a JDBC driver on your classpath for the database you wish to log to. You will also need the Java Persistence API 2.1
+ * and your JPA provider of choice on the class path; these Maven dependencies are optional and will not automatically
+ * be added to your classpath.
+ */
+package org.apache.logging.log4j.core.appender.db.jpa;

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/site/manual/index.md
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/site/manual/index.md b/log4j-jpa/src/site/manual/index.md
new file mode 100644
index 0000000..235dfb8
--- /dev/null
+++ b/log4j-jpa/src/site/manual/index.md
@@ -0,0 +1,33 @@
+<!-- vim: set syn=markdown : -->
+<!--
+    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.
+-->
+
+# Log4j Java Persistence API module
+
+As of Log4j 2.11.0, JPA support has moved from the existing module logj-core to the new module log4j-jpa.
+
+## Requirements
+
+This module was introduced in Log4j 2.11.0 and requires the javax.persistence API.
+
+Some features may require optional
+[dependencies](../runtime-dependencies.html). These dependencies are specified in the
+documentation for those features.
+
+Some Log4j features require external dependencies.
+See the [Dependency Tree](dependencies.html#Dependency_Tree)
+for the exact list of JAR files needed for these features.

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/site/site.xml
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/site/site.xml b/log4j-jpa/src/site/site.xml
new file mode 100644
index 0000000..6d4cddc
--- /dev/null
+++ b/log4j-jpa/src/site/site.xml
@@ -0,0 +1,52 @@
+<!--
+ 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.
+
+-->
+<project name="Log4j Core"
+         xmlns="http://maven.apache.org/DECORATION/1.4.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/DECORATION/1.4.0 http://maven.apache.org/xsd/decoration-1.4.0.xsd">
+  <body>
+    <links>
+      <item name="Apache" href="http://www.apache.org/" />
+      <item name="Logging Services" href="http://logging.apache.org/"/>
+      <item name="Log4j" href="../index.html"/>
+    </links>
+
+    <!-- Component-specific reports -->
+    <menu ref="reports"/>
+
+	<!-- Overall Project Info -->
+    <menu name="Log4j Project Information" img="icon-info-sign">
+      <item name="Dependencies" href="../dependencies.html" />
+      <item name="Dependency Convergence" href="../dependency-convergence.html" />
+      <item name="Dependency Management" href="../dependency-management.html" />
+      <item name="Project Team" href="../team-list.html" />
+      <item name="Mailing Lists" href="../mail-lists.html" />
+      <item name="Issue Tracking" href="../issue-tracking.html" />
+      <item name="Project License" href="../license.html" />
+      <item name="Source Repository" href="../source-repository.html" />
+      <item name="Project Summary" href="../project-summary.html" />
+    </menu>
+
+    <menu name="Log4j Project Reports" img="icon-cog">
+      <item name="Changes Report" href="../changes-report.html" />
+      <item name="JIRA Report" href="../jira-report.html" />
+      <item name="Surefire Report" href="../surefire-report.html" />
+      <item name="RAT Report" href="../rat-report.html" />
+    </menu>
+  </body>
+</project>

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/AbstractJpaAppenderTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/AbstractJpaAppenderTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/AbstractJpaAppenderTest.java
new file mode 100644
index 0000000..6ff3c14
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/AbstractJpaAppenderTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.core.appender.db.jpa;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import static org.junit.Assert.*;
+
+@Category(Appenders.Jpa.class)
+public abstract class AbstractJpaAppenderTest {
+    private final String databaseType;
+    private Connection connection;
+
+    public AbstractJpaAppenderTest(final String databaseType) {
+        this.databaseType = databaseType;
+    }
+
+    protected abstract Connection setUpConnection() throws SQLException;
+
+    public void setUp(final String configFileName) throws SQLException {
+        this.connection = this.setUpConnection();
+
+        System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY,
+                "org/apache/logging/log4j/core/appender/db/jpa/" + configFileName);
+        PropertiesUtil.getProperties().reload();
+        final LoggerContext context = LoggerContext.getContext(false);
+        if (context.getConfiguration() instanceof DefaultConfiguration) {
+            context.reconfigure();
+        }
+        StatusLogger.getLogger().reset();
+    }
+
+    public void tearDown() throws SQLException {
+        final LoggerContext context = LoggerContext.getContext(false);
+        try {
+            String appenderName = "databaseAppender";
+            final Appender appender = context.getConfiguration().getAppender(appenderName);
+            assertNotNull("The appender '" + appenderName + "' should not be null.", appender);
+            assertTrue("The appender should be a JpaAppender.", appender instanceof JpaAppender);
+            ((JpaAppender) appender).getManager().close();
+        } finally {
+            System.clearProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY);
+            PropertiesUtil.getProperties().reload();
+            context.reconfigure();
+            StatusLogger.getLogger().reset();
+
+            try (Statement statement = this.connection.createStatement();) {
+                statement.execute("SHUTDOWN");
+            }
+
+            this.connection.close();
+        }
+    }
+
+    @Test
+    public void testBaseJpaEntityAppender() throws SQLException {
+        try {
+            this.setUp("log4j2-" + this.databaseType + "-jpa-base.xml");
+
+            final RuntimeException exception = new RuntimeException("Hello, world!");
+            final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            final PrintWriter writer = new PrintWriter(outputStream);
+            exception.printStackTrace(writer);
+            writer.close();
+            final String stackTrace = outputStream.toString().replace("\r\n", "\n");
+
+            final long millis = System.currentTimeMillis();
+
+            final Logger logger1 = LogManager.getLogger(this.getClass().getName() + ".testBaseJpaEntityAppender");
+            final Logger logger2 = LogManager.getLogger(this.getClass().getName() + ".testBaseJpaEntityAppenderAgain");
+            logger1.info("Test my message 01.");
+            logger1.error("This is another message 02.", exception);
+            logger2.warn("A final warning has been issued.");
+
+            final Statement statement = this.connection.createStatement();
+            final ResultSet resultSet = statement.executeQuery("SELECT * FROM jpaBaseLogEntry ORDER BY id");
+
+            assertTrue("There should be at least one row.", resultSet.next());
+
+            long date = resultSet.getTimestamp("eventDate").getTime();
+            assertTrue("The date should be later than pre-logging (1).", date >= millis);
+            assertTrue("The date should be earlier than now (1).", date <= System.currentTimeMillis());
+            assertEquals("The level column is not correct (1).", "INFO", resultSet.getString("level"));
+            assertEquals("The logger column is not correct (1).", logger1.getName(), resultSet.getString("logger"));
+            assertEquals("The message column is not correct (1).", "Test my message 01.",
+                    resultSet.getString("message"));
+            assertNull("The exception column is not correct (1).", resultSet.getString("exception"));
+
+            assertTrue("There should be at least two rows.", resultSet.next());
+
+            date = resultSet.getTimestamp("eventDate").getTime();
+            assertTrue("The date should be later than pre-logging (2).", date >= millis);
+            assertTrue("The date should be earlier than now (2).", date <= System.currentTimeMillis());
+            assertEquals("The level column is not correct (2).", "ERROR", resultSet.getString("level"));
+            assertEquals("The logger column is not correct (2).", logger1.getName(), resultSet.getString("logger"));
+            assertEquals("The message column is not correct (2).", "This is another message 02.",
+                    resultSet.getString("message"));
+            assertEquals("The exception column is not correct (2).", stackTrace, resultSet.getString("exception"));
+
+            assertTrue("There should be three rows.", resultSet.next());
+
+            date = resultSet.getTimestamp("eventDate").getTime();
+            assertTrue("The date should be later than pre-logging (3).", date >= millis);
+            assertTrue("The date should be earlier than now (3).", date <= System.currentTimeMillis());
+            assertEquals("The level column is not correct (3).", "WARN", resultSet.getString("level"));
+            assertEquals("The logger column is not correct (3).", logger2.getName(), resultSet.getString("logger"));
+            assertEquals("The message column is not correct (3).", "A final warning has been issued.",
+                    resultSet.getString("message"));
+            assertNull("The exception column is not correct (3).", resultSet.getString("exception"));
+
+            assertFalse("There should not be four rows.", resultSet.next());
+        } finally {
+            this.tearDown();
+        }
+    }
+
+    @Test
+    public void testBasicJpaEntityAppender() throws SQLException {
+        try {
+            this.setUp("log4j2-" + this.databaseType + "-jpa-basic.xml");
+
+            final Error exception = new Error("Goodbye, cruel world!");
+            final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            final PrintWriter writer = new PrintWriter(outputStream);
+            exception.printStackTrace(writer);
+            writer.close();
+            final String stackTrace = outputStream.toString().replace("\r\n", "\n");
+
+            final long millis = System.currentTimeMillis();
+
+            final Logger logger1 = LogManager.getLogger(this.getClass().getName() + ".testBasicJpaEntityAppender");
+            final Logger logger2 = LogManager.getLogger(this.getClass().getName() + ".testBasicJpaEntityAppenderAgain");
+            logger1.debug("Test my debug 01.");
+            logger1.warn("This is another warning 02.", exception);
+            logger2.fatal("A fatal warning has been issued.");
+
+            final Statement statement = this.connection.createStatement();
+            final ResultSet resultSet = statement.executeQuery("SELECT * FROM jpaBasicLogEntry ORDER BY id");
+
+            assertTrue("There should be at least one row.", resultSet.next());
+
+            long date = resultSet.getLong("timemillis");
+            assertTrue("The date should be later than pre-logging (1).", date >= millis);
+            assertTrue("The date should be earlier than now (1).", date <= System.currentTimeMillis());
+            assertEquals("The level column is not correct (1).", "DEBUG", resultSet.getString("level"));
+            assertEquals("The logger column is not correct (1).", logger1.getName(), resultSet.getString("loggerName"));
+            assertEquals("The message column is not correct (1).", "Test my debug 01.",
+                    resultSet.getString("message"));
+            assertNull("The exception column is not correct (1).", resultSet.getString("thrown"));
+
+            assertTrue("There should be at least two rows.", resultSet.next());
+
+            date = resultSet.getLong("timemillis");
+            assertTrue("The date should be later than pre-logging (2).", date >= millis);
+            assertTrue("The date should be earlier than now (2).", date <= System.currentTimeMillis());
+            assertEquals("The level column is not correct (2).", "WARN", resultSet.getString("level"));
+            assertEquals("The logger column is not correct (2).", logger1.getName(), resultSet.getString("loggerName"));
+            assertEquals("The message column is not correct (2).", "This is another warning 02.",
+                    resultSet.getString("message"));
+            assertEquals("The exception column is not correct (2).", stackTrace, resultSet.getString("thrown"));
+
+            assertTrue("There should be three rows.", resultSet.next());
+
+            date = resultSet.getLong("timemillis");
+            assertTrue("The date should be later than pre-logging (3).", date >= millis);
+            assertTrue("The date should be earlier than now (3).", date <= System.currentTimeMillis());
+            assertEquals("The level column is not correct (3).", "FATAL", resultSet.getString("level"));
+            assertEquals("The logger column is not correct (3).", logger2.getName(), resultSet.getString("loggerName"));
+            assertEquals("The message column is not correct (3).", "A fatal warning has been issued.",
+                    resultSet.getString("message"));
+            assertNull("The exception column is not correct (3).", resultSet.getString("thrown"));
+
+            assertFalse("There should not be four rows.", resultSet.next());
+        } finally {
+            this.tearDown();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/JpaH2AppenderTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/JpaH2AppenderTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/JpaH2AppenderTest.java
new file mode 100644
index 0000000..055dd03
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/JpaH2AppenderTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.core.appender.db.jpa;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.util.Strings;
+import org.junit.experimental.categories.Category;
+
+@Category(Appenders.Jpa.class)
+public class JpaH2AppenderTest extends AbstractJpaAppenderTest {
+    private static final String USER_ID = "sa";
+    private static final String PASSWORD = Strings.EMPTY;
+
+    public JpaH2AppenderTest() {
+        super("h2");
+    }
+
+    @Override
+    protected Connection setUpConnection() throws SQLException {
+        final Connection connection = DriverManager.getConnection("jdbc:h2:mem:Log4j", USER_ID, PASSWORD);
+
+        try (Statement statement = connection.createStatement()) {
+            statement.executeUpdate("CREATE TABLE jpaBaseLogEntry ( "
+                    + "id INTEGER IDENTITY, eventDate DATETIME, level NVARCHAR(10), logger NVARCHAR(255), "
+                    + "message NVARCHAR(1024), exception NVARCHAR(1048576)" + " )");
+        }
+
+        try (Statement statement = connection.createStatement()) {
+            statement.executeUpdate("CREATE TABLE jpaBasicLogEntry ( "
+                    + "id INTEGER IDENTITY, timemillis BIGINT, nanoTime BIGINT, level NVARCHAR(10), loggerName NVARCHAR(255), "
+                    + "message NVARCHAR(1024), thrown NVARCHAR(1048576), contextMapJson NVARCHAR(1048576),"
+                    + "loggerFQCN NVARCHAR(1024), contextStack NVARCHAR(1048576), marker NVARCHAR(255), source NVARCHAR(2048),"
+                    + "threadId BIGINT, threadName NVARCHAR(255), threadPriority INTEGER" + " )");
+        }
+
+        return connection;
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/JpaHsqldbAppenderTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/JpaHsqldbAppenderTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/JpaHsqldbAppenderTest.java
new file mode 100644
index 0000000..3e22d96
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/JpaHsqldbAppenderTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.core.appender.db.jpa;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.util.Strings;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import static org.junit.Assert.*;
+
+@Category(Appenders.Jpa.class)
+public class JpaHsqldbAppenderTest extends AbstractJpaAppenderTest {
+    private static final String USER_ID = "sa";
+    private static final String PASSWORD = Strings.EMPTY;
+
+    public JpaHsqldbAppenderTest() {
+        super("hsqldb");
+    }
+
+    @Override
+    protected Connection setUpConnection() throws SQLException {
+        final Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:Log4j", USER_ID, PASSWORD);
+
+        try (Statement statement = connection.createStatement()) {
+            statement.executeUpdate("CREATE TABLE jpaBaseLogEntry ( "
+                    + "id INTEGER IDENTITY, eventDate DATETIME, level VARCHAR(10), logger VARCHAR(255), "
+                    + "message VARCHAR(1024), exception VARCHAR(1048576)" + " )");
+        }
+
+        try (Statement statement = connection.createStatement()) {
+            statement.executeUpdate("CREATE TABLE jpaBasicLogEntry ( "
+                    + "id INTEGER IDENTITY, timemillis BIGINT, nanoTime BIGINT, level VARCHAR(10), loggerName VARCHAR(255), "
+                    + "message VARCHAR(1024), thrown VARCHAR(1048576), contextMapJson VARCHAR(1048576),"
+                    + "loggerFQCN VARCHAR(1024), contextStack VARCHAR(1048576), marker VARCHAR(255), source VARCHAR(2048),"
+                    + "threadId BIGINT, threadName NVARCHAR(255), threadPriority INTEGER" + " )");
+        }
+
+        return connection;
+    }
+
+    @Test
+    public void testNoEntityClassName() {
+        final JpaAppender appender = JpaAppender.createAppender("name", null, null, null, null, "jpaAppenderTestUnit");
+
+        assertNull("The appender should be null.", appender);
+    }
+
+    @Test
+    public void testNoPersistenceUnitName() {
+        final JpaAppender appender = JpaAppender.createAppender("name", null, null, null, TestBaseEntity.class.getName(),
+                null);
+
+        assertNull("The appender should be null.", appender);
+    }
+
+    @Test
+    public void testBadEntityClassName() {
+        final JpaAppender appender = JpaAppender.createAppender("name", null, null, null, "com.foo.Bar",
+                "jpaAppenderTestUnit");
+
+        assertNull("The appender should be null.", appender);
+    }
+
+    @Test
+    public void testNonLogEventEntity() {
+        final JpaAppender appender = JpaAppender.createAppender("name", null, null, null, Object.class.getName(),
+                "jpaAppenderTestUnit");
+
+        assertNull("The appender should be null.", appender);
+    }
+
+    @Test
+    public void testBadConstructorEntity01() {
+        final JpaAppender appender = JpaAppender.createAppender("name", null, null, null,
+                BadConstructorEntity1.class.getName(), "jpaAppenderTestUnit");
+
+        assertNull("The appender should be null.", appender);
+    }
+
+    @Test
+    public void testBadConstructorEntity02() {
+        final JpaAppender appender = JpaAppender.createAppender("name", null, null, null,
+                BadConstructorEntity2.class.getName(), "jpaAppenderTestUnit");
+
+        assertNull("The appender should be null.", appender);
+    }
+
+    @SuppressWarnings("unused")
+    public static class BadConstructorEntity1 extends TestBaseEntity {
+        private static final long serialVersionUID = 1L;
+
+        public BadConstructorEntity1(final LogEvent wrappedEvent) {
+            super(wrappedEvent);
+        }
+    }
+
+    @SuppressWarnings("unused")
+    public static class BadConstructorEntity2 extends TestBaseEntity {
+        private static final long serialVersionUID = 1L;
+
+        public BadConstructorEntity2() {
+            super(null);
+        }
+
+        public BadConstructorEntity2(final LogEvent wrappedEvent, final String badParameter) {
+            super(wrappedEvent);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/LogEventEntityTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/LogEventEntityTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/LogEventEntityTest.java
new file mode 100644
index 0000000..ed59b53
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/LogEventEntityTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.core.appender.db.jpa;
+
+import java.util.Map;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.ThreadContext.ContextStack;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.impl.ThrowableProxy;
+import org.apache.logging.log4j.message.Message;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class LogEventEntityTest {
+
+    @Test
+    public void testToImmutable_AbstractLogEventWrapperEntity() {
+        final LogEvent logEvent = new AbstractLogEventWrapperEntity() {
+
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public Map<String, String> getContextMap() {
+                return null;
+            }
+
+            @Override
+            public ContextStack getContextStack() {
+                return null;
+            }
+
+            @Override
+            public Level getLevel() {
+                return null;
+            }
+
+            @Override
+            public String getLoggerFqcn() {
+                return null;
+            }
+
+            @Override
+            public String getLoggerName() {
+                return null;
+            }
+
+            @Override
+            public Marker getMarker() {
+                return null;
+            }
+
+            @Override
+            public Message getMessage() {
+                return null;
+            }
+
+            @Override
+            public long getNanoTime() {
+                return 0;
+            }
+
+            @Override
+            public StackTraceElement getSource() {
+                return null;
+            }
+
+            @Override
+            public long getThreadId() {
+                return 0;
+            }
+
+            @Override
+            public String getThreadName() {
+                return null;
+            }
+
+            @Override
+            public int getThreadPriority() {
+                return 0;
+            }
+
+            @Override
+            public Throwable getThrown() {
+                return null;
+            }
+
+            @Override
+            public ThrowableProxy getThrownProxy() {
+                return null;
+            }
+
+            @Override
+            public long getTimeMillis() {
+                return 0;
+            }
+        };
+        Assert.assertNotSame(logEvent, logEvent.toImmutable());
+    }
+
+    @Test
+    public void testToImmutable_TestBaseEntity() {
+        final LogEvent logEvent = new TestBaseEntity();
+        Assert.assertNotSame(logEvent, logEvent.toImmutable());
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/TestBaseEntity.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/TestBaseEntity.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/TestBaseEntity.java
new file mode 100644
index 0000000..5d4473e
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/TestBaseEntity.java
@@ -0,0 +1,181 @@
+/*
+ * 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.core.appender.db.jpa;
+
+import java.util.Date;
+import java.util.Map;
+
+import javax.persistence.Basic;
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+import javax.persistence.Transient;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.db.jpa.converter.LevelAttributeConverter;
+import org.apache.logging.log4j.core.appender.db.jpa.converter.MessageAttributeConverter;
+import org.apache.logging.log4j.core.appender.db.jpa.converter.ThrowableAttributeConverter;
+import org.apache.logging.log4j.core.impl.ThrowableProxy;
+import org.apache.logging.log4j.message.Message;
+
+@Entity
+@Table(name = "jpaBaseLogEntry")
+public class TestBaseEntity extends AbstractLogEventWrapperEntity {
+    private static final long serialVersionUID = 1L;
+
+    private long id = 0L;
+
+    public TestBaseEntity() {
+        super();
+    }
+
+    public TestBaseEntity(final LogEvent wrappedEvent) {
+        super(wrappedEvent);
+    }
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    public long getId() {
+        return this.id;
+    }
+
+    public void setId(final long id) {
+        this.id = id;
+    }
+
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name = "eventDate")
+    public Date getEventDate() {
+        return new Date(this.getTimeMillis());
+    }
+
+    public void setEventDate(final Date date) {
+        // this entity is write-only
+    }
+
+    @Override
+    @Convert(converter = LevelAttributeConverter.class)
+    @Column(name = "level")
+    public Level getLevel() {
+        return this.getWrappedEvent().getLevel();
+    }
+
+    @Override
+    @Basic
+    @Column(name = "logger")
+    public String getLoggerName() {
+        return this.getWrappedEvent().getLoggerName();
+    }
+
+    @Override
+    @Transient
+    public StackTraceElement getSource() {
+        return this.getWrappedEvent().getSource();
+    }
+
+    @Override
+    @Convert(converter = MessageAttributeConverter.class)
+    @Column(name = "message")
+    public Message getMessage() {
+        return this.getWrappedEvent().getMessage();
+    }
+
+    @Override
+    @Transient
+    public Marker getMarker() {
+        return this.getWrappedEvent().getMarker();
+    }
+
+    @Override
+    @Transient
+    public long getThreadId() {
+        return this.getWrappedEvent().getThreadId();
+    }
+
+    @Override
+    @Transient
+    public String getThreadName() {
+        return this.getWrappedEvent().getThreadName();
+    }
+
+    @Override
+    @Transient
+    public int getThreadPriority() {
+        return this.getWrappedEvent().getThreadPriority();
+    }
+
+    @Override
+    @Transient
+    public long getTimeMillis() {
+        return this.getWrappedEvent().getTimeMillis();
+    }
+
+    @Override
+    @Transient
+    public long getNanoTime() {
+        return this.getWrappedEvent().getNanoTime();
+    }
+
+    @Override
+    @Convert(converter = ThrowableAttributeConverter.class)
+    @Column(name = "exception")
+    public Throwable getThrown() {
+        return this.getWrappedEvent().getThrown();
+    }
+
+    @Override
+    @Transient
+    public ThrowableProxy getThrownProxy() {
+        return this.getWrappedEvent().getThrownProxy();
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    @Transient
+    public Map<String, String> getContextMap() {
+        return this.getWrappedEvent().getContextMap();
+    }
+
+    @Override
+    @Transient
+    public ReadOnlyStringMap getContextData() {
+        return this.getWrappedEvent().getContextData();
+    }
+
+    @Override
+    @Transient
+    public ThreadContext.ContextStack getContextStack() {
+        return this.getWrappedEvent().getContextStack();
+    }
+
+    @Override
+    @Transient
+    public String getLoggerFqcn() {
+        return this.getWrappedEvent().getLoggerFqcn();
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/TestBasicEntity.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/TestBasicEntity.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/TestBasicEntity.java
new file mode 100644
index 0000000..b804078
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/TestBasicEntity.java
@@ -0,0 +1,65 @@
+/*
+ * 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.core.appender.db.jpa;
+
+import java.util.Map;
+
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.db.jpa.converter.ContextMapJsonAttributeConverter;
+
+@Entity
+@Table(name = "jpaBasicLogEntry")
+@SuppressWarnings("unused")
+public class TestBasicEntity extends BasicLogEventEntity {
+    private static final long serialVersionUID = 1L;
+
+    private long id = 0L;
+
+    public TestBasicEntity() {
+        super();
+    }
+
+    public TestBasicEntity(final LogEvent wrapped) {
+        super(wrapped);
+    }
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    public long getId() {
+        return this.id;
+    }
+
+    public void setId(final long id) {
+        this.id = id;
+    }
+
+    @Override
+    @Convert(converter = ContextMapJsonAttributeConverter.class)
+    @Column(name = "contextMapJson")
+    public Map<String, String> getContextMap() {
+        return super.getContextMap();
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextDataAttributeConverterTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextDataAttributeConverterTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextDataAttributeConverterTest.java
new file mode 100644
index 0000000..41306a1
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextDataAttributeConverterTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.core.appender.db.jpa.converter;
+
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import static org.junit.Assert.*;
+
+@Category(Appenders.Jpa.class)
+public class ContextDataAttributeConverterTest {
+    private ContextDataAttributeConverter converter;
+
+    @Before
+    public void setUp() {
+        this.converter = new ContextDataAttributeConverter();
+    }
+
+    @Test
+    public void testConvertToDatabaseColumn01() {
+        final StringMap map = new SortedArrayStringMap();
+        map.putValue("test1", "another1");
+        map.putValue("key2", "value2");
+
+        assertEquals("The converted value is not correct.", map.toString(),
+                this.converter.convertToDatabaseColumn(map));
+    }
+
+    @Test
+    public void testConvertToDatabaseColumn02() {
+        final StringMap map = new SortedArrayStringMap();
+        map.putValue("someKey", "coolValue");
+        map.putValue("anotherKey", "testValue");
+        map.putValue("myKey", "yourValue");
+
+        assertEquals("The converted value is not correct.", map.toString(),
+                this.converter.convertToDatabaseColumn(map));
+    }
+
+    @Test
+    public void testConvertNullToDatabaseColumn() {
+        assertNull("The converted value should be null.", this.converter.convertToDatabaseColumn(null));
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testConvertToEntityAttribute() {
+        this.converter.convertToEntityAttribute(null);
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextDataJsonAttributeConverterTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextDataJsonAttributeConverterTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextDataJsonAttributeConverterTest.java
new file mode 100644
index 0000000..161a23e
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextDataJsonAttributeConverterTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.core.appender.db.jpa.converter;
+
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import static org.junit.Assert.*;
+
+@Category(Appenders.Jpa.class)
+public class ContextDataJsonAttributeConverterTest {
+    private ContextDataJsonAttributeConverter converter;
+
+    @Before
+    public void setUp() {
+        this.converter = new ContextDataJsonAttributeConverter();
+    }
+
+    @Test
+    public void testConvert01() {
+        final StringMap map = new SortedArrayStringMap();
+        map.putValue("test1", "another1");
+        map.putValue("key2", "value2");
+
+        final String converted = this.converter.convertToDatabaseColumn(map);
+
+        assertNotNull("The converted value should not be null.", converted);
+
+        final ReadOnlyStringMap reversed = this.converter.convertToEntityAttribute(converted);
+
+        assertNotNull("The reversed value should not be null.", reversed);
+        assertEquals("The reversed value is not correct.", map, reversed);
+    }
+
+    @Test
+    public void testConvert02() {
+        final StringMap map = new SortedArrayStringMap();
+        map.putValue("someKey", "coolValue");
+        map.putValue("anotherKey", "testValue");
+        map.putValue("myKey", "yourValue");
+
+        final String converted = this.converter.convertToDatabaseColumn(map);
+
+        assertNotNull("The converted value should not be null.", converted);
+
+        final ReadOnlyStringMap reversed = this.converter.convertToEntityAttribute(converted);
+
+        assertNotNull("The reversed value should not be null.", reversed);
+        assertEquals("The reversed value is not correct.", map, reversed);
+    }
+
+    @Test
+    public void testConvertNullToDatabaseColumn() {
+        assertNull("The converted value should be null.", this.converter.convertToDatabaseColumn(null));
+    }
+
+    @Test
+    public void testConvertNullOrBlankToEntityAttribute() {
+        assertNull("The converted attribute should be null (1).", this.converter.convertToEntityAttribute(null));
+        assertNull("The converted attribute should be null (2).", this.converter.convertToEntityAttribute(""));
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextMapAttributeConverterTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextMapAttributeConverterTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextMapAttributeConverterTest.java
new file mode 100644
index 0000000..e755d7f
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextMapAttributeConverterTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.core.appender.db.jpa.converter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.logging.log4j.categories.Appenders;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import static org.junit.Assert.*;
+
+@Category(Appenders.Jpa.class)
+public class ContextMapAttributeConverterTest {
+    private ContextMapAttributeConverter converter;
+
+    @Before
+    public void setUp() {
+        this.converter = new ContextMapAttributeConverter();
+    }
+
+    @Test
+    public void testConvertToDatabaseColumn01() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("test1", "another1");
+        map.put("key2", "value2");
+
+        assertEquals("The converted value is not correct.", map.toString(),
+                this.converter.convertToDatabaseColumn(map));
+    }
+
+    @Test
+    public void testConvertToDatabaseColumn02() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("someKey", "coolValue");
+        map.put("anotherKey", "testValue");
+        map.put("myKey", "yourValue");
+
+        assertEquals("The converted value is not correct.", map.toString(),
+                this.converter.convertToDatabaseColumn(map));
+    }
+
+    @Test
+    public void testConvertNullToDatabaseColumn() {
+        assertNull("The converted value should be null.", this.converter.convertToDatabaseColumn(null));
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testConvertToEntityAttribute() {
+        this.converter.convertToEntityAttribute(null);
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextMapJsonAttributeConverterTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextMapJsonAttributeConverterTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextMapJsonAttributeConverterTest.java
new file mode 100644
index 0000000..07af863
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextMapJsonAttributeConverterTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.core.appender.db.jpa.converter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.logging.log4j.categories.Appenders;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import static org.junit.Assert.*;
+
+@Category(Appenders.Jpa.class)
+public class ContextMapJsonAttributeConverterTest {
+    private ContextMapJsonAttributeConverter converter;
+
+    @Before
+    public void setUp() {
+        this.converter = new ContextMapJsonAttributeConverter();
+    }
+
+    @Test
+    public void testConvert01() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("test1", "another1");
+        map.put("key2", "value2");
+
+        final String converted = this.converter.convertToDatabaseColumn(map);
+
+        assertNotNull("The converted value should not be null.", converted);
+
+        final Map<String, String> reversed = this.converter.convertToEntityAttribute(converted);
+
+        assertNotNull("The reversed value should not be null.", reversed);
+        assertEquals("The reversed value is not correct.", map, reversed);
+    }
+
+    @Test
+    public void testConvert02() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("someKey", "coolValue");
+        map.put("anotherKey", "testValue");
+        map.put("myKey", "yourValue");
+
+        final String converted = this.converter.convertToDatabaseColumn(map);
+
+        assertNotNull("The converted value should not be null.", converted);
+
+        final Map<String, String> reversed = this.converter.convertToEntityAttribute(converted);
+
+        assertNotNull("The reversed value should not be null.", reversed);
+        assertEquals("The reversed value is not correct.", map, reversed);
+    }
+
+    @Test
+    public void testConvertNullToDatabaseColumn() {
+        assertNull("The converted value should be null.", this.converter.convertToDatabaseColumn(null));
+    }
+
+    @Test
+    public void testConvertNullOrBlankToEntityAttribute() {
+        assertNull("The converted attribute should be null (1).", this.converter.convertToEntityAttribute(null));
+        assertNull("The converted attribute should be null (2).", this.converter.convertToEntityAttribute(""));
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextStackAttributeConverterTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextStackAttributeConverterTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextStackAttributeConverterTest.java
new file mode 100644
index 0000000..57ae0a7
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextStackAttributeConverterTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.core.appender.db.jpa.converter;
+
+import java.util.Arrays;
+
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.spi.MutableThreadContextStack;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import static org.junit.Assert.*;
+
+@Category(Appenders.Jpa.class)
+public class ContextStackAttributeConverterTest {
+    private ContextStackAttributeConverter converter;
+
+    @Before
+    public void setUp() {
+        this.converter = new ContextStackAttributeConverter();
+    }
+
+    @Test
+    public void testConvertToDatabaseColumn01() {
+        final ThreadContext.ContextStack stack = new MutableThreadContextStack(
+                Arrays.asList("value1", "another2"));
+
+        assertEquals("The converted value is not correct.", "value1\nanother2",
+                this.converter.convertToDatabaseColumn(stack));
+    }
+
+    @Test
+    public void testConvertToDatabaseColumn02() {
+        final ThreadContext.ContextStack stack = new MutableThreadContextStack(
+                Arrays.asList("key1", "value2", "my3"));
+
+        assertEquals("The converted value is not correct.",
+                "key1\nvalue2\nmy3",
+                this.converter.convertToDatabaseColumn(stack));
+    }
+
+    @Test
+    public void testConvertNullToDatabaseColumn() {
+        assertNull("The converted value should be null.", this.converter.convertToDatabaseColumn(null));
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testConvertToEntityAttribute() {
+        this.converter.convertToEntityAttribute(null);
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextStackJsonAttributeConverterTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextStackJsonAttributeConverterTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextStackJsonAttributeConverterTest.java
new file mode 100644
index 0000000..0fae006
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ContextStackJsonAttributeConverterTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.core.appender.db.jpa.converter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.junit.ThreadContextStackRule;
+import org.apache.logging.log4j.spi.MutableThreadContextStack;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(Appenders.Jpa.class)
+public class ContextStackJsonAttributeConverterTest {
+    private ContextStackJsonAttributeConverter converter;
+
+    @Rule
+    public final ThreadContextStackRule threadContextRule = new ThreadContextStackRule(); 
+
+    @Before
+    public void setUp() {
+        this.converter = new ContextStackJsonAttributeConverter();
+    }
+
+    @Test
+    public void testConvert01() {
+        final ThreadContext.ContextStack stack = new MutableThreadContextStack(
+                Arrays.asList("value1", "another2"));
+
+        final String converted = this.converter.convertToDatabaseColumn(stack);
+
+        assertNotNull("The converted value should not be null.", converted);
+
+        final ThreadContext.ContextStack reversed = this.converter
+                .convertToEntityAttribute(converted);
+
+        assertNotNull("The reversed value should not be null.", reversed);
+        assertEquals("The reversed value is not correct.", stack.asList(),
+                reversed.asList());
+    }
+
+    @Test
+    public void testConvert02() {
+        final ThreadContext.ContextStack stack = new MutableThreadContextStack(
+                Arrays.asList("key1", "value2", "my3"));
+
+        final String converted = this.converter.convertToDatabaseColumn(stack);
+
+        assertNotNull("The converted value should not be null.", converted);
+
+        final ThreadContext.ContextStack reversed = this.converter
+                .convertToEntityAttribute(converted);
+
+        assertNotNull("The reversed value should not be null.", reversed);
+        assertEquals("The reversed value is not correct.", stack.asList(),
+                reversed.asList());
+    }
+
+    @Test
+    public void testConvertNullToDatabaseColumn() {
+        assertNull("The converted value should be null.", this.converter.convertToDatabaseColumn(null));
+    }
+
+    @Test
+    public void testConvertNullOrBlankToEntityAttribute() {
+        assertNull("The converted attribute should be null (1).", this.converter.convertToEntityAttribute(null));
+        assertNull("The converted attribute should be null (2).", this.converter.convertToEntityAttribute(""));
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/MarkerAttributeConverterTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/MarkerAttributeConverterTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/MarkerAttributeConverterTest.java
new file mode 100644
index 0000000..334bf1e
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/MarkerAttributeConverterTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.core.appender.db.jpa.converter;
+
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.MarkerManager;
+import org.apache.logging.log4j.categories.Appenders;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import static org.junit.Assert.*;
+
+@Category(Appenders.Jpa.class)
+public class MarkerAttributeConverterTest {
+    private MarkerAttributeConverter converter;
+
+    @Before
+    public void setUp() {
+        this.converter = new MarkerAttributeConverter();
+    }
+
+    @Test
+    public void testConvert01() {
+        final Marker marker = MarkerManager.getMarker("testConvert01");
+
+        final String converted = this.converter.convertToDatabaseColumn(marker);
+
+        assertNotNull("The converted value should not be null.", converted);
+        assertEquals("The converted value is not correct.", "testConvert01", converted);
+
+        final Marker reversed = this.converter.convertToEntityAttribute(converted);
+
+        assertNotNull("The reversed value should not be null.", reversed);
+        assertEquals("The reversed value is not correct.", "testConvert01", marker.getName());
+    }
+
+    @Test
+    public void testConvert02() {
+        Marker marker = MarkerManager.getMarker("anotherConvert02").setParents(MarkerManager.getMarker("finalConvert03"));
+        marker = MarkerManager.getMarker("testConvert02").setParents(marker);
+
+        final String converted = this.converter.convertToDatabaseColumn(marker);
+
+        assertNotNull("The converted value should not be null.", converted);
+        assertEquals("The converted value is not correct.", "testConvert02[ anotherConvert02[ finalConvert03 ] ]",
+                converted);
+
+        final Marker reversed = this.converter.convertToEntityAttribute(converted);
+
+        assertNotNull("The reversed value should not be null.", reversed);
+        assertEquals("The reversed value is not correct.", "testConvert02", marker.getName());
+        final Marker[] parents = marker.getParents();
+        assertNotNull("The first parent should not be null.", parents);
+        assertNotNull("The first parent should not be null.", parents[0]);
+        assertEquals("The first parent is not correct.", "anotherConvert02", parents[0].getName());
+        assertNotNull("The second parent should not be null.", parents[0].getParents());
+        assertNotNull("The second parent should not be null.", parents[0].getParents()[0]);
+        assertEquals("The second parent is not correct.", "finalConvert03", parents[0].getParents()[0].getName());
+    }
+
+    @Test
+    public void testConvertNullToDatabaseColumn() {
+        assertNull("The converted value should be null.", this.converter.convertToDatabaseColumn(null));
+    }
+
+    @Test
+    public void testConvertNullOrBlankToEntityAttribute() {
+        assertNull("The converted attribute should be null (1).", this.converter.convertToEntityAttribute(null));
+        assertNull("The converted attribute should be null (2).", this.converter.convertToEntityAttribute(""));
+    }
+}

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/df10e02a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/MessageAttributeConverterTest.java
----------------------------------------------------------------------
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/MessageAttributeConverterTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/MessageAttributeConverterTest.java
new file mode 100644
index 0000000..e665dd6
--- /dev/null
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/MessageAttributeConverterTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.core.appender.db.jpa.converter;
+
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import static org.junit.Assert.*;
+
+@Category(Appenders.Jpa.class)
+public class MessageAttributeConverterTest {
+    private static final StatusLogger LOGGER = StatusLogger.getLogger();
+
+    private MessageAttributeConverter converter;
+
+    @Before
+    public void setUp() {
+        this.converter = new MessageAttributeConverter();
+    }
+
+    @Test
+    public void testConvert01() {
+        final Message message = LOGGER.getMessageFactory().newMessage("Message #{} said [{}].", 3, "Hello");
+
+        final String converted = this.converter.convertToDatabaseColumn(message);
+
+        assertNotNull("The converted value should not be null.", converted);
+        assertEquals("The converted value is not correct.", "Message #3 said [Hello].", converted);
+
+        final Message reversed = this.converter.convertToEntityAttribute(converted);
+
+        assertNotNull("The reversed value should not be null.", reversed);
+        assertEquals("The reversed value is not correct.", "Message #3 said [Hello].", reversed.getFormattedMessage());
+    }
+
+    @Test
+    public void testConvertNullToDatabaseColumn() {
+        assertNull("The converted value should be null.", this.converter.convertToDatabaseColumn(null));
+    }
+
+    @Test
+    public void testConvertNullOrBlankToEntityAttribute() {
+        assertNull("The converted attribute should be null (1).", this.converter.convertToEntityAttribute(null));
+        assertNull("The converted attribute should be null (2).", this.converter.convertToEntityAttribute(""));
+    }
+}