Add JSONLayout class

This layout will format the LoggingEvent information such the date,
logger, level, message, and location information, as well as the values
in the MDC and NDC, into valid JSON.  This is useful for consumption
by elasticsearch.
diff --git a/src/main/cpp/Makefile.am b/src/main/cpp/Makefile.am
index 6da89cb..236a9a6 100644
--- a/src/main/cpp/Makefile.am
+++ b/src/main/cpp/Makefile.am
@@ -72,6 +72,7 @@
         inputstreamreader.cpp \
         integer.cpp \
         integerpatternconverter.cpp \
+        jsonlayout.cpp \
         layout.cpp\
         level.cpp \
         levelmatchfilter.cpp \
diff --git a/src/main/cpp/jsonlayout.cpp b/src/main/cpp/jsonlayout.cpp
new file mode 100644
index 0000000..60bf5ef
--- /dev/null
+++ b/src/main/cpp/jsonlayout.cpp
@@ -0,0 +1,273 @@
+/*
+ * 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.
+ */
+
+#include <log4cxx/logstring.h>
+#include <log4cxx/jsonlayout.h>
+#include <log4cxx/spi/loggingevent.h>
+#include <log4cxx/level.h>
+#include <log4cxx/helpers/optionconverter.h>
+#include <log4cxx/helpers/iso8601dateformat.h>
+#include <log4cxx/helpers/stringhelper.h>
+
+#include <apr_time.h>
+#include <apr_strings.h>
+#include <string.h>
+
+using namespace log4cxx;
+using namespace log4cxx::helpers;
+using namespace log4cxx::spi;
+
+IMPLEMENT_LOG4CXX_OBJECT(JSONLayout)
+
+
+JSONLayout::JSONLayout() : locationInfo(false), dateFormat()
+{
+}
+
+
+void JSONLayout::setOption(const LogString& option, const LogString& value)
+{
+	if (StringHelper::equalsIgnoreCase(option,
+			LOG4CXX_STR("LOCATIONINFO"), LOG4CXX_STR("locationinfo")))
+	{
+		setLocationInfo(OptionConverter::toBoolean(value, false));
+	}
+}
+void JSONLayout::format(LogString& output,
+	const spi::LoggingEventPtr& event,
+	Pool& p) const
+{
+
+	output.append("{ ");
+
+	appendQuotedEscapedString(output, "timestamp");
+	output.append(": ");
+	LogString timestamp;
+	dateFormat.format(timestamp, event->getTimeStamp(), p);
+	appendQuotedEscapedString(output, timestamp);
+	output.append(", ");
+
+	appendQuotedEscapedString(output, "level");
+	output.append(": ");
+	LogString level;
+	event->getLevel()->toString(level);
+	appendQuotedEscapedString(output, level);
+	output.append(", ");
+
+	appendQuotedEscapedString(output, "logger");
+	output.append(": ");
+	appendQuotedEscapedString(output, event->getLoggerName());
+	output.append(", ");
+
+	appendQuotedEscapedString(output, "message");
+	output.append(": ");
+	appendQuotedEscapedString(output, event->getMessage());
+
+	appendSerializedMDC(output, event);
+
+	appendSerializedNDC(output, event);
+
+
+	if (locationInfo)
+	{
+		output.append(", ");
+		appendQuotedEscapedString(output, "location_info");
+		output.append(": { ");
+		const LocationInfo& locInfo = event->getLocationInformation();
+
+		appendQuotedEscapedString(output, "file");
+		output.append(": ");
+		LOG4CXX_DECODE_CHAR(fileName, locInfo.getFileName());
+		appendQuotedEscapedString(output, fileName);
+		output.append(", ");
+
+		appendQuotedEscapedString(output, "line");
+		output.append(": ");
+		LogString lineNumber;
+		StringHelper::toString(locInfo.getLineNumber(), p, lineNumber);
+		appendQuotedEscapedString(output, lineNumber);
+		output.append(", ");
+
+		appendQuotedEscapedString(output, "class");
+		output.append(": ");
+		appendQuotedEscapedString(output, locInfo.getClassName());
+		output.append(", ");
+
+		appendQuotedEscapedString(output, "method");
+		output.append(": ");
+		appendQuotedEscapedString(output, locInfo.getMethodName());
+
+		output.append(" } ");
+	}
+
+	output.append(" }");
+	output.append(LOG4CXX_EOL);
+
+}
+
+void JSONLayout::appendQuotedEscapedString(LogString& buf,
+	const LogString& input) const
+{
+	/* add leading quote */
+	buf.push_back(0x22);
+
+	logchar specialChars[] =
+	{
+		0x08,   /* \b backspace         */
+		0x09,   /* \t tab               */
+		0x0a,   /* \n newline           */
+		0x0c,   /* \f form feed         */
+		0x0d,   /* \r carriage return   */
+		0x22,   /* \" double quote      */
+		0x5c
+	}; /* \\ backslash         */
+
+
+	size_t start = 0;
+	size_t found = input.find_first_of(specialChars, start);
+
+	while (found != LogString::npos)
+	{
+		if (found > start)
+		{
+			buf.append(input, start, found - start);
+		}
+
+		switch (input[found])
+		{
+			case 0x08:
+				/* \b backspace */
+				buf.push_back(0x5c);
+				buf.push_back('b');
+				break;
+
+			case 0x09:
+				/* \t tab */
+				buf.push_back(0x5c);
+				buf.push_back('t');
+				break;
+
+			case 0x0a:
+				/* \n newline */
+				buf.push_back(0x5c);
+				buf.push_back('n');
+				break;
+
+			case 0x0c:
+				/* \f form feed */
+				buf.push_back(0x5c);
+				buf.push_back('f');
+				break;
+
+			case 0x0d:
+				/* \r carriage return */
+				buf.push_back(0x5c);
+				buf.push_back('r');
+				break;
+
+			case 0x22:
+				/* \" double quote */
+				buf.push_back(0x5c);
+				buf.push_back(0x22);
+				break;
+
+			case 0x5c:
+				/* \\ backslash */
+				buf.push_back(0x5c);
+				buf.push_back(0x5c);
+				break;
+
+			default:
+				buf.push_back(input[found]);
+				break;
+		}
+
+		start = found + 1;
+
+		if (found < input.size())
+		{
+			found = input.find_first_of(specialChars, start);
+		}
+		else
+		{
+			found = LogString::npos;
+		}
+	}
+
+	if (start < input.size())
+	{
+		buf.append(input, start, input.size() - start);
+	}
+
+	/* add trailing quote */
+	buf.push_back(0x22);
+
+	return;
+
+}
+
+void JSONLayout::appendSerializedMDC(LogString& buf,
+	const LoggingEventPtr& event) const
+{
+
+	LoggingEvent::KeySet keys = event->getMDCKeySet();
+
+	if (!keys.empty())
+	{
+		buf.append(", ");
+		appendQuotedEscapedString(buf, "context_map");
+		buf.append(": { ");
+
+		for (LoggingEvent::KeySet::iterator it = keys.begin();
+			it != keys.end(); ++it)
+		{
+			appendQuotedEscapedString(buf, *it);
+			buf.append(": ");
+			LogString value;
+			event->getMDC(*it, value);
+			appendQuotedEscapedString(buf, value);
+
+			/* if this isn't the last k:v pair, we need a comma */
+			if (it + 1 != keys.end())
+			{
+				buf.append(", ");
+			}
+		}
+
+		buf.append(" }");
+	}
+
+	return;
+
+}
+
+void JSONLayout::appendSerializedNDC(LogString& buf,
+	const LoggingEventPtr& event) const
+{
+
+	LogString ndcVal;
+
+	if (event->getNDC(ndcVal))
+	{
+		buf.append(", ");
+		appendQuotedEscapedString(buf, "context_stack");
+		buf.append(": [ ");
+		appendQuotedEscapedString(buf, ndcVal);
+		buf.append(" ]");
+	}
+}
+
diff --git a/src/main/include/log4cxx/Makefile.am b/src/main/include/log4cxx/Makefile.am
index 75ef01c..f629b45 100644
--- a/src/main/include/log4cxx/Makefile.am
+++ b/src/main/include/log4cxx/Makefile.am
@@ -35,6 +35,7 @@
     $(top_srcdir)/src/main/include/log4cxx/file.h \
     $(top_srcdir)/src/main/include/log4cxx/hierarchy.h \
     $(top_srcdir)/src/main/include/log4cxx/htmllayout.h \
+    $(top_srcdir)/src/main/include/log4cxx/jsonlayout.h \
     $(top_srcdir)/src/main/include/log4cxx/layout.h \
     $(top_srcdir)/src/main/include/log4cxx/level.h \
     $(top_srcdir)/src/main/include/log4cxx/logger.h \
diff --git a/src/main/include/log4cxx/jsonlayout.h b/src/main/include/log4cxx/jsonlayout.h
new file mode 100644
index 0000000..c34725c
--- /dev/null
+++ b/src/main/include/log4cxx/jsonlayout.h
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+#ifndef _LOG4CXX_JSON_LAYOUT_H
+#define _LOG4CXX_JSON_LAYOUT_H
+
+#include <log4cxx/layout.h>
+#include <log4cxx/helpers/iso8601dateformat.h>
+#include <log4cxx/spi/loggingevent.h>
+
+
+
+namespace log4cxx
+{
+/**
+This layout outputs events in a JSON dictionary.
+*/
+class LOG4CXX_EXPORT JSONLayout : public Layout
+{
+	private:
+		// Print no location info by default
+		bool locationInfo; //= false
+
+		helpers::ISO8601DateFormat dateFormat;
+
+	protected:
+
+		void appendQuotedEscapedString(LogString& buf, const LogString& input) const;
+		void appendSerializedMDC(LogString& buf,
+			const spi::LoggingEventPtr& event) const;
+		void appendSerializedNDC(LogString& buf,
+			const spi::LoggingEventPtr& event) const;
+
+	public:
+		DECLARE_LOG4CXX_OBJECT(JSONLayout)
+		BEGIN_LOG4CXX_CAST_MAP()
+		LOG4CXX_CAST_ENTRY(JSONLayout)
+		LOG4CXX_CAST_ENTRY_CHAIN(Layout)
+		END_LOG4CXX_CAST_MAP()
+
+		JSONLayout();
+
+		/**
+		The <b>LocationInfo</b> option takes a boolean value. By
+		default, it is set to false which means there will be no location
+		information output by this layout. If the the option is set to
+		true, then the file name and line number of the statement
+		at the origin of the log statement will be output.
+		*/
+		inline void setLocationInfo(bool locationInfoFlag)
+		{
+			this->locationInfo = locationInfoFlag;
+		}
+
+		/**
+		Returns the current value of the <b>LocationInfo</b> option.
+		*/
+		inline bool getLocationInfo() const
+		{
+			return locationInfo;
+		}
+
+		/**
+		Returns the content type output by this layout, i.e "application/json".
+		*/
+		virtual LogString getContentType() const
+		{
+			return LOG4CXX_STR("application/json");
+		}
+
+		/**
+		No options to activate.
+		*/
+		virtual void activateOptions(log4cxx::helpers::Pool& /* p */) {}
+
+		/**
+		Set options
+		*/
+		virtual void setOption(const LogString& option, const LogString& value);
+
+		virtual void format(LogString& output,
+			const spi::LoggingEventPtr& event, log4cxx::helpers::Pool& pool) const;
+
+		/**
+		The JSON layout handles the throwable contained in logging
+		events. Hence, this method return <code>false</code>.  */
+		virtual bool ignoresThrowable() const
+		{
+			return false;
+		}
+
+}; // class JSONLayout
+LOG4CXX_PTR_DEF(JSONLayout);
+}  // namespace log4cxx
+
+
+#endif // _LOG4CXX_JSON_LAYOUT_H
diff --git a/src/test/cpp/Makefile.am b/src/test/cpp/Makefile.am
index 13b82ac..8b6c95d 100644
--- a/src/test/cpp/Makefile.am
+++ b/src/test/cpp/Makefile.am
@@ -168,6 +168,7 @@
     filetestcase.cpp \
     hierarchytest.cpp \
     hierarchythresholdtestcase.cpp \
+    jsonlayouttest.cpp \
     l7dtestcase.cpp \
     leveltestcase.cpp \
     logunit.cpp \
diff --git a/src/test/cpp/jsonlayouttest.cpp b/src/test/cpp/jsonlayouttest.cpp
new file mode 100644
index 0000000..239878e
--- /dev/null
+++ b/src/test/cpp/jsonlayouttest.cpp
@@ -0,0 +1,270 @@
+/*
+ * 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.
+ */
+
+#include "logunit.h"
+#include <log4cxx/logger.h>
+#include <log4cxx/jsonlayout.h>
+#include <log4cxx/ndc.h>
+#include <log4cxx/mdc.h>
+
+#include <iostream>
+#include <log4cxx/helpers/stringhelper.h>
+
+
+using namespace log4cxx;
+using namespace log4cxx::helpers;
+using namespace log4cxx::spi;
+
+#if defined(__LOG4CXX_FUNC__)
+	#undef __LOG4CXX_FUNC__
+	#define __LOG4CXX_FUNC__ "X::X()"
+#else
+	#error __LOG4CXX_FUNC__ expected to be defined
+#endif
+/**
+ * Test for JSONLayout.
+ *
+ */
+LOGUNIT_CLASS(JSONLayoutTest), public JSONLayout
+{
+	LOGUNIT_TEST_SUITE(JSONLayoutTest);
+	LOGUNIT_TEST(testGetContentType);
+	LOGUNIT_TEST(testIgnoresThrowable);
+	LOGUNIT_TEST(testAppendQuotedEscapedStringWithPrintableChars);
+	LOGUNIT_TEST(testAppendQuotedEscapedStringWithControlChars);
+	LOGUNIT_TEST(testAppendSerializedMDC);
+	LOGUNIT_TEST(testAppendSerializedNDC);
+	LOGUNIT_TEST(testFormat);
+	LOGUNIT_TEST(testGetSetLocationInfo);
+	LOGUNIT_TEST_SUITE_END();
+
+
+public:
+	/**
+	 * Clear MDC and NDC before test.
+	 */
+	void setUp()
+	{
+		NDC::clear();
+		MDC::clear();
+	}
+
+	/**
+	 * Clear MDC and NDC after test.
+	 */
+	void tearDown()
+	{
+		setUp();
+	}
+
+
+public:
+	/**
+	 * Tests getContentType.
+	 */
+	void testGetContentType()
+	{
+		LogString expected(LOG4CXX_STR("application/json"));
+		LogString actual(JSONLayout().getContentType());
+		LOGUNIT_ASSERT(expected == actual);
+	}
+
+	/**
+	 * Tests ignoresThrowable.
+	 */
+	void testIgnoresThrowable()
+	{
+		LOGUNIT_ASSERT_EQUAL(false, JSONLayout().ignoresThrowable());
+	}
+
+	/**
+	 * Tests appendQuotedEscapedString with printable characters.
+	 */
+	void testAppendQuotedEscapedStringWithPrintableChars()
+	{
+
+		LogString s1("foo");                  /*  foo */
+		LogString s2;
+		appendQuotedEscapedString(s2, s1);    /*  "foo"  */
+		LOGUNIT_ASSERT_EQUAL("\"foo\"", s2);
+
+		LogString s3;
+		appendQuotedEscapedString(s3, s2);    /*  "\"foo\""  */
+		LOGUNIT_ASSERT_EQUAL("\"\\\"foo\\\"\"", s3);
+
+		LogString t1("bar\"baz");             /*  bar"baz */
+		LogString t2;
+		appendQuotedEscapedString(t2, t1);    /*  "bar\"baz"  */
+		LOGUNIT_ASSERT_EQUAL("\"bar\\\"baz\"", t2);
+
+		LogString t3;
+		appendQuotedEscapedString(t3, t2);    /*  "\"bar\\\"baz\""    */
+		LOGUNIT_ASSERT_EQUAL("\"\\\"bar\\\\\\\"baz\\\"\"", t3);
+	}
+
+	/**
+	 * Tests appendQuotedEscapedString with control characters.
+	 */
+	void testAppendQuotedEscapedStringWithControlChars()
+	{
+
+		LogString bs = {0x08};
+		LogString bs_expected = {0x22, 0x5c, 'b', 0x22};      /* "\b" */
+		LogString bs_escaped;
+		appendQuotedEscapedString(bs_escaped, bs);
+		LOGUNIT_ASSERT_EQUAL(bs_expected, bs_escaped);
+
+		LogString tab = {0x09};
+		LogString tab_expected = {0x22, 0x5c, 't', 0x22};     /* "\t" */
+		LogString tab_escaped;
+		appendQuotedEscapedString(tab_escaped, tab);
+		LOGUNIT_ASSERT_EQUAL(tab_expected, tab_escaped);
+
+		LogString newline = {0x0a};
+		LogString newline_expected = {0x22, 0x5c, 'n', 0x22}; /* "\n" */
+		LogString newline_escaped;
+		appendQuotedEscapedString(newline_escaped, newline);
+		LOGUNIT_ASSERT_EQUAL(newline_expected, newline_escaped);
+
+		LogString ff = {0x0c};
+		LogString ff_expected = {0x22, 0x5c, 'f', 0x22};      /* "\f" */
+		LogString ff_escaped;
+		appendQuotedEscapedString(ff_escaped, ff);
+		LOGUNIT_ASSERT_EQUAL(ff_expected, ff_escaped);
+
+		LogString cr = {0x0d};
+		LogString cr_expected = {0x22, 0x5c, 'r', 0x22};      /* "\r" */
+		LogString cr_escaped;
+		appendQuotedEscapedString(cr_escaped, cr);
+		LOGUNIT_ASSERT_EQUAL(cr_expected, cr_escaped);
+
+	}
+
+	/**
+	 * Tests appendSerializedMDC.
+	 */
+	void testAppendSerializedMDC()
+	{
+
+		LoggingEventPtr event1 = new LoggingEvent(LOG4CXX_STR("Logger"),
+			Level::getInfo(),
+			LOG4CXX_STR("A message goes here."),
+			LOG4CXX_LOCATION);
+
+		MDC::put("key1", "value1");
+		MDC::put("key2", "value2");
+
+		LogString output1;
+		LogString expected1 = ", \"context_map\": { "
+			"\"key1\": \"value1\", \"key2\": \"value2\" }";
+
+		appendSerializedMDC(output1, event1);
+
+		LOGUNIT_ASSERT_EQUAL(expected1, output1);
+
+	}
+
+
+	/**
+	 * Tests appendSerializedNDC.
+	 */
+	void testAppendSerializedNDC()
+	{
+
+		LoggingEventPtr event1 = new LoggingEvent(LOG4CXX_STR("Logger"),
+			Level::getInfo(),
+			LOG4CXX_STR("A message goes here."),
+			LOG4CXX_LOCATION);
+
+		NDC::push("one");
+		NDC::push("two");
+		NDC::push("three");
+
+		LogString output1;
+		LogString expected1 = ", \"context_stack\": [ \"one two three\" ]";
+
+		appendSerializedNDC(output1, event1);
+
+		LOGUNIT_ASSERT_EQUAL(expected1, output1);
+
+
+	}
+
+
+	/**
+	 * Tests format.
+	 */
+	void testFormat()
+	{
+
+		Pool p;
+
+		LoggingEventPtr event1 = new LoggingEvent(LOG4CXX_STR("Logger"),
+			Level::getInfo(),
+			LOG4CXX_STR("A message goes here."),
+			LOG4CXX_LOCATION);
+
+		LogString timestamp;
+		helpers::ISO8601DateFormat dateFormat;
+		dateFormat.format(timestamp, event1->getTimeStamp(), p);
+
+		NDC::push("one");
+		NDC::push("two");
+		NDC::push("three");
+
+		MDC::put("key1", "value1");
+		MDC::put("key2", "value2");
+
+		LogString output1;
+		LogString expected1;
+
+		expected1.append("{ \"timestamp\": \"")
+		.append(timestamp)
+		.append("\", ")
+		.append("\"level\": \"INFO\", ")
+		.append("\"logger\": \"Logger\", ")
+		.append("\"message\": \"A message goes here.\"");
+
+		appendSerializedMDC(expected1, event1);
+		appendSerializedNDC(expected1, event1);
+
+		expected1.append(" }\n");
+
+		format(output1, event1, p);
+
+		LOGUNIT_ASSERT_EQUAL(expected1, output1);
+
+	}
+
+	/**
+	 * Tests getLocationInfo and setLocationInfo.
+	 */
+	void testGetSetLocationInfo()
+	{
+		JSONLayout layout;
+		LOGUNIT_ASSERT_EQUAL(false, layout.getLocationInfo());
+		layout.setLocationInfo(true);
+		LOGUNIT_ASSERT_EQUAL(true, layout.getLocationInfo());
+		layout.setLocationInfo(false);
+		LOGUNIT_ASSERT_EQUAL(false, layout.getLocationInfo());
+	}
+
+};
+
+
+LOGUNIT_TEST_SUITE_REGISTRATION(JSONLayoutTest);
+