This is an automated email from the ASF dual-hosted git repository.

swebb2066 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4cxx.git


The following commit(s) were added to refs/heads/master by this push:
     new 5235de3d Add support for using SQLPrepare and bound parameters to the 
ODBC appender (#205)
5235de3d is described below

commit 5235de3d4f39e22e47a113d94052b83bc495ec74
Author: Stephen Webb <[email protected]>
AuthorDate: Tue Apr 25 12:53:12 2023 +1000

    Add support for using SQLPrepare and bound parameters to the ODBC appender 
(#205)
    
    * Document proposed configuration
    
    * Output the timestamp with the extra precision supported by the 
'datetime2' SQL type fields
    
    * Move new fields of ODBCAppenderPriv after existing fields (for ABI 
compatibility report)
    
    * Prevent faults when using a DSN intead of user and password on Linux
    
    ---------
    
    Co-authored-by: Stephen Webb <[email protected]>
    Co-authored-by: Robert Middleton <[email protected]>
---
 src/main/cpp/odbcappender.cpp                      | 303 +++++++++++++++++++--
 src/main/include/log4cxx/db/odbcappender.h         |  85 +++---
 .../include/log4cxx/private/odbcappender_priv.h    |  68 ++++-
 .../input/xml/odbcAppenderDSN-Log4cxxTest.xml      |  18 +-
 4 files changed, 404 insertions(+), 70 deletions(-)

diff --git a/src/main/cpp/odbcappender.cpp b/src/main/cpp/odbcappender.cpp
index 513debf3..7a2ed5ab 100644
--- a/src/main/cpp/odbcappender.cpp
+++ b/src/main/cpp/odbcappender.cpp
@@ -21,7 +21,22 @@
 #include <log4cxx/helpers/transcoder.h>
 #include <log4cxx/patternlayout.h>
 #include <apr_strings.h>
-#include <log4cxx/private/odbcappender_priv.h>
+#include <apr_time.h>
+#include <cmath> // std::pow
+
+#include <log4cxx/pattern/loggerpatternconverter.h>
+#include <log4cxx/pattern/classnamepatternconverter.h>
+#include <log4cxx/pattern/datepatternconverter.h>
+#include <log4cxx/pattern/filelocationpatternconverter.h>
+#include <log4cxx/pattern/fulllocationpatternconverter.h>
+#include <log4cxx/pattern/shortfilelocationpatternconverter.h>
+#include <log4cxx/pattern/linelocationpatternconverter.h>
+#include <log4cxx/pattern/messagepatternconverter.h>
+#include <log4cxx/pattern/methodlocationpatternconverter.h>
+#include <log4cxx/pattern/levelpatternconverter.h>
+#include <log4cxx/pattern/threadpatternconverter.h>
+#include <log4cxx/pattern/threadusernamepatternconverter.h>
+#include <log4cxx/pattern/ndcpatternconverter.h>
 
 #if !defined(LOG4CXX)
        #define LOG4CXX 1
@@ -32,13 +47,22 @@
                #include <windows.h>
        #endif
        #include <sqlext.h>
+#else
+       typedef void* SQLHSTMT;
 #endif
+#include <log4cxx/private/odbcappender_priv.h>
+#if defined(min)
+       #undef min
+#endif
+#include <cstring>
+#include <algorithm>
 
 
 using namespace log4cxx;
 using namespace log4cxx::helpers;
 using namespace log4cxx::db;
 using namespace log4cxx::spi;
+using namespace log4cxx::pattern;
 
 SQLException::SQLException(short fHandleType,
        void* hInput, const char* prolog,
@@ -103,6 +127,31 @@ ODBCAppender::~ODBCAppender()
        finalize();
 }
 
+#define RULES_PUT(spec, cls) \
+       specs.insert(PatternMap::value_type(LogString(LOG4CXX_STR(spec)), cls 
::newInstance))
+
+static PatternMap getFormatSpecifiers()
+{
+       PatternMap specs;
+       if (specs.empty())
+       {
+               RULES_PUT("logger", LoggerPatternConverter);
+               RULES_PUT("class", ClassNamePatternConverter);
+               RULES_PUT("time", DatePatternConverter);
+               RULES_PUT("shortfilename", ShortFileLocationPatternConverter);
+               RULES_PUT("fullfilename", FileLocationPatternConverter);
+               RULES_PUT("location", FullLocationPatternConverter);
+               RULES_PUT("line", LineLocationPatternConverter);
+               RULES_PUT("message", MessagePatternConverter);
+               RULES_PUT("method", MethodLocationPatternConverter);
+               RULES_PUT("level", LevelPatternConverter);
+               RULES_PUT("thread", ThreadPatternConverter);
+               RULES_PUT("threadname", ThreadUsernamePatternConverter);
+               RULES_PUT("ndc", NDCPatternConverter);
+       }
+       return specs;
+}
+
 void ODBCAppender::setOption(const LogString& option, const LogString& value)
 {
        if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("BUFFERSIZE"), 
LOG4CXX_STR("buffersize")))
@@ -127,16 +176,44 @@ void ODBCAppender::setOption(const LogString& option, 
const LogString& value)
        {
                setUser(value);
        }
+       else if (StringHelper::equalsIgnoreCase(option, 
LOG4CXX_STR("COLUMNMAPPING"), LOG4CXX_STR("columnmapping")))
+       {
+               _priv->mappedName.push_back(value);
+       }
        else
        {
                AppenderSkeleton::setOption(option, value);
        }
 }
 
+//* Does ODBCAppender require a layout?
+
+bool ODBCAppender::requiresLayout() const
+{
+       return _priv->parameterValue.empty();
+}
+
 void ODBCAppender::activateOptions(log4cxx::helpers::Pool&)
 {
 #if !LOG4CXX_HAVE_ODBC
        LogLog::error(LOG4CXX_STR("Can not activate ODBCAppender unless 
compiled with ODBC support."));
+#else
+       auto specs = getFormatSpecifiers();
+       for (auto& name : _priv->mappedName)
+       {
+               auto pItem = specs.find(StringHelper::toLowerCase(name));
+               if (specs.end() == pItem)
+                       LogLog::error(name + LOG4CXX_STR(" is not a supported 
ColumnMapping value"));
+               else
+               {
+                       ODBCAppenderPriv::DataBinding paramData{ 0, 0, 0, 0, 0 
};
+                       std::vector<LogString> options;
+                       if (LOG4CXX_STR("time") == pItem->first)
+                               options.push_back(LOG4CXX_STR("yyyy-MM-dd 
HH:mm:ss.SSSSSS"));
+                       paramData.converter = 
log4cxx::cast<LoggingEventPatternConverter>((pItem->second)(options));
+                       _priv->parameterValue.push_back(paramData);
+               }
+       }
 #endif
 }
 
@@ -176,16 +253,21 @@ void ODBCAppender::execute(const LogString& sql, 
log4cxx::helpers::Pool& p)
 
                if (ret < 0)
                {
-                       throw SQLException( SQL_HANDLE_DBC, con, "Failed to 
allocate sql handle.", p);
+                       throw SQLException( SQL_HANDLE_DBC, con, "Failed to 
allocate sql handle", p);
                }
 
+#if LOG4CXX_LOGCHAR_IS_WCHAR
+               ret = SQLExecDirectW(stmt, (SQLWCHAR*)sql.c_str(), SQL_NTS);
+#elif LOG4CXX_LOGCHAR_IS_UTF8
+               ret = SQLExecDirectA(stmt, (SQLCHAR*)sql.c_str(), SQL_NTS);
+#else
                SQLWCHAR* wsql;
                encode(&wsql, sql, p);
                ret = SQLExecDirectW(stmt, wsql, SQL_NTS);
-
+#endif
                if (ret < 0)
                {
-                       throw SQLException(SQL_HANDLE_STMT, stmt, "Failed to 
execute sql statement.", p);
+                       throw SQLException(SQL_HANDLE_STMT, stmt, "Failed to 
execute sql statement", p);
                }
        }
        catch (SQLException&)
@@ -211,10 +293,6 @@ void ODBCAppender::closeConnection(ODBCAppender::SQLHDBC 
/* con */)
 {
 }
 
-
-
-
-
 ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 {
 #if LOG4CXX_HAVE_ODBC
@@ -226,7 +304,7 @@ ODBCAppender::SQLHDBC 
ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 
                if (ret < 0)
                {
-                       SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to 
allocate SQL handle.", p);
+                       SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to 
allocate SQL handle", p);
                        _priv->env = SQL_NULL_HENV;
                        throw ex;
                }
@@ -235,7 +313,7 @@ ODBCAppender::SQLHDBC 
ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 
                if (ret < 0)
                {
-                       SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to 
set odbc version.", p);
+                       SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to 
set odbc version", p);
                        SQLFreeHandle(SQL_HANDLE_ENV, _priv->env);
                        _priv->env = SQL_NULL_HENV;
                        throw ex;
@@ -248,16 +326,18 @@ ODBCAppender::SQLHDBC 
ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 
                if (ret < 0)
                {
-                       SQLException ex(SQL_HANDLE_DBC, _priv->connection, 
"Failed to allocate sql handle.", p);
+                       SQLException ex(SQL_HANDLE_DBC, _priv->connection, 
"Failed to allocate sql handle", p);
                        _priv->connection = SQL_NULL_HDBC;
                        throw ex;
                }
 
 
-               SQLWCHAR* wURL, *wUser, *wPwd;
+               SQLWCHAR* wURL, *wUser = nullptr, *wPwd = nullptr;
                encode(&wURL, _priv->databaseURL, p);
-               encode(&wUser, _priv->databaseUser, p);
-               encode(&wPwd, _priv->databasePassword, p);
+               if (!_priv->databaseUser.empty())
+                       encode(&wUser, _priv->databaseUser, p);
+               if (!_priv->databasePassword.empty())
+                       encode(&wPwd, _priv->databasePassword, p);
 
                ret = SQLConnectW( _priv->connection,
                                wURL, SQL_NTS,
@@ -267,7 +347,7 @@ ODBCAppender::SQLHDBC 
ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 
                if (ret < 0)
                {
-                       SQLException ex(SQL_HANDLE_DBC, _priv->connection, 
"Failed to connect to database.", p);
+                       SQLException ex(SQL_HANDLE_DBC, _priv->connection, 
"Failed to connect to database", p);
                        SQLFreeHandle(SQL_HANDLE_DBC, _priv->connection);
                        _priv->connection = SQL_NULL_HDBC;
                        throw ex;
@@ -316,14 +396,203 @@ void ODBCAppender::close()
        _priv->closed = true;
 }
 
+#if LOG4CXX_HAVE_ODBC
+void ODBCAppender::ODBCAppenderPriv::setPreparedStatement(SQLHDBC con, Pool& p)
+{
+       auto ret = SQLAllocHandle( SQL_HANDLE_STMT, con, 
&this->preparedStatement);
+       if (ret < 0)
+       {
+               throw SQLException( SQL_HANDLE_DBC, con, "Failed to allocate 
statement handle.", p);
+       }
+
+#if LOG4CXX_LOGCHAR_IS_WCHAR
+       ret = SQLPrepareW(this->preparedStatement, 
(SQLWCHAR*)this->sqlStatement.c_str(), SQL_NTS);
+#elif LOG4CXX_LOGCHAR_IS_UTF8
+       ret = SQLPrepareA(this->preparedStatement, 
(SQLCHAR*)this->sqlStatement.c_str(), SQL_NTS);
+#else
+       SQLWCHAR* wsql;
+       encode(&wsql, this->sqlStatement, p);
+       ret = SQLPrepareW(this->preparedStatement, wsql, SQL_NTS);
+#endif
+       if (ret < 0)
+       {
+               throw SQLException(SQL_HANDLE_STMT, this->preparedStatement, 
"Failed to prepare sql statement.", p);
+       }
+
+       int parameterNumber = 0;
+       for (auto& item : this->parameterValue)
+       {
+               ++parameterNumber;
+               SQLSMALLINT  targetType;
+               SQLULEN      targetMaxCharCount;
+               SQLSMALLINT  decimalDigits;
+               SQLSMALLINT  nullable;
+               auto ret = SQLDescribeParam
+                       ( this->preparedStatement
+                       , parameterNumber
+                       , &targetType
+                       , &targetMaxCharCount
+                       , &decimalDigits
+                       , &nullable
+                       );
+               if (ret < 0)
+               {
+                       throw SQLException(SQL_HANDLE_STMT, 
this->preparedStatement, "Failed to describe parameter", p);
+               }
+               if (SQL_CHAR == targetType || SQL_VARCHAR == targetType || 
SQL_LONGVARCHAR == targetType)
+               {
+                       item.paramType = SQL_C_CHAR;
+                       item.paramMaxCharCount = targetMaxCharCount;
+                       item.paramValueSize = 
(SQLINTEGER)(item.paramMaxCharCount) * sizeof(char) + sizeof(char);
+                       item.paramValue = 
(SQLPOINTER)p.palloc(item.paramValueSize + sizeof(char));
+               }
+               else if (SQL_WCHAR == targetType || SQL_WVARCHAR == targetType 
|| SQL_WLONGVARCHAR == targetType)
+               {
+                       item.paramType = SQL_C_WCHAR;
+                       item.paramMaxCharCount = targetMaxCharCount;
+                       item.paramValueSize = (SQLINTEGER)(targetMaxCharCount) 
* sizeof(wchar_t) + sizeof(wchar_t);
+                       item.paramValue = 
(SQLPOINTER)p.palloc(item.paramValueSize + sizeof(wchar_t));
+               }
+               else if (SQL_TYPE_TIMESTAMP == targetType || SQL_TYPE_DATE == 
targetType || SQL_TYPE_TIME == targetType
+                       || SQL_DATETIME == targetType)
+               {
+                       item.paramType = SQL_C_TYPE_TIMESTAMP;
+                       item.paramMaxCharCount = decimalDigits;
+                       item.paramValueSize = sizeof(SQL_TIMESTAMP_STRUCT);
+                       item.paramValue = 
(SQLPOINTER)p.palloc(item.paramValueSize);
+               }
+               else
+               {
+                       if (SQL_INTEGER != targetType)
+                       {
+                               LogString msg(LOG4CXX_STR("Unexpected 
targetType ("));
+                               helpers::StringHelper::toString(targetType, p, 
msg);
+                               msg += LOG4CXX_STR(") at parameter ");
+                               
helpers::StringHelper::toString(parameterNumber, p, msg);
+                               msg += LOG4CXX_STR(" while preparing SQL");
+                               LogLog::warn(msg);
+                       }
+                       item.paramMaxCharCount = 30;
+#if LOG4CXX_LOGCHAR_IS_UTF8
+                       item.paramType = SQL_C_CHAR;
+                       item.paramValueSize = 
(SQLINTEGER)(item.paramMaxCharCount) * sizeof(char);
+                       item.paramValue = 
(SQLPOINTER)p.palloc(item.paramValueSize + sizeof(char));
+#else
+                       item.paramType = SQL_C_WCHAR;
+                       item.paramValueSize = 
(SQLINTEGER)(item.paramMaxCharCount) * sizeof(wchar_t);
+                       item.paramValue = 
(SQLPOINTER)p.palloc(item.paramValueSize + sizeof(wchar_t));
+#endif
+               }
+               item.strLen_or_Ind = SQL_NTS;
+               ret = SQLBindParameter
+                       ( this->preparedStatement
+                       , parameterNumber
+                       , SQL_PARAM_INPUT
+                       , item.paramType  // ValueType
+                       , targetType
+                       , targetMaxCharCount
+                       , decimalDigits
+                       , item.paramValue
+                       , item.paramValueSize
+                       , &item.strLen_or_Ind
+                       );
+               if (ret < 0)
+               {
+                       throw SQLException(SQL_HANDLE_STMT, 
this->preparedStatement, "Failed to bind parameter", p);
+               }
+       }
+}
+
+void ODBCAppender::ODBCAppenderPriv::setParameterValues(const 
spi::LoggingEventPtr& event, Pool& p)
+{
+       for (auto& item : this->parameterValue)
+       {
+               if (!item.paramValue || item.paramValueSize <= 0)
+                       ;
+               else if (SQL_C_WCHAR == item.paramType)
+               {
+                       LogString sbuf;
+                       item.converter->format(event, sbuf, p);
+#if LOG4CXX_LOGCHAR_IS_WCHAR_T
+                       std::wstring& tmp = sbuf;
+#else
+                       std::wstring tmp;
+                       Transcoder::encode(sbuf, tmp);
+#endif
+                       auto dst = (wchar_t*)item.paramValue;
+                       auto charCount = 
std::min(size_t(item.paramMaxCharCount), tmp.size());
+                       auto copySize = std::min(size_t(item.paramValueSize - 
1), charCount * sizeof(wchar_t));
+                       std::memcpy(dst, tmp.data(), copySize);
+                       dst[copySize / sizeof(wchar_t)] = 0;
+               }
+               else if (SQL_C_CHAR == item.paramType)
+               {
+                       LogString sbuf;
+                       item.converter->format(event, sbuf, p);
+#if LOG4CXX_LOGCHAR_IS_UTF8
+                       std::string& tmp = sbuf;
+#else
+                       std::string tmp;
+                       Transcoder::encode(sbuf, tmp);
+#endif
+                       auto dst = (char*)item.paramValue;
+                       auto sz = std::min(size_t(item.paramMaxCharCount), 
tmp.size());
+                       auto copySize = std::min(size_t(item.paramValueSize - 
1), sz * sizeof(char));
+                       std::memcpy(dst, tmp.data(), copySize);
+                       dst[copySize] = 0;
+               }
+               else if (SQL_C_TYPE_TIMESTAMP == item.paramType)
+               {
+                       apr_time_exp_t exploded;
+                       apr_status_t stat = this->timeZone->explode(&exploded, 
event->getTimeStamp());
+                       if (stat == APR_SUCCESS)
+                       {
+                               auto dst = 
(SQL_TIMESTAMP_STRUCT*)item.paramValue;
+                               dst->year = 1900 + exploded.tm_year;
+                               dst->month = 1 + exploded.tm_mon;
+                               dst->day = exploded.tm_mday;
+                               dst->hour = exploded.tm_hour;
+                               dst->minute = exploded.tm_min;
+                               dst->second = exploded.tm_sec;
+                               // Prevent '[ODBC SQL Server Driver]Datetime 
field overflow' by rounding to the target field precision
+                               int roundingExponent = 6 - 
(int)item.paramMaxCharCount;
+                               if (0 < roundingExponent)
+                               {
+                                       int roundingDivisor = (int)std::pow(10, 
roundingExponent);
+                                       dst->fraction = 1000 * roundingDivisor 
* ((exploded.tm_usec + roundingDivisor / 2) / roundingDivisor);
+                               }
+                               else
+                                       dst->fraction = 1000 * exploded.tm_usec;
+                       }
+               }
+       }
+}
+#endif
+
 void ODBCAppender::flushBuffer(Pool& p)
 {
        for (auto& logEvent : _priv->buffer)
        {
                try
                {
-                       auto sql = getLogStatement(logEvent, p);
-                       execute(sql, p);
+                       if (!_priv->parameterValue.empty())
+                       {
+#if LOG4CXX_HAVE_ODBC
+                               if (0 == _priv->preparedStatement)
+                                       
_priv->setPreparedStatement(getConnection(p), p);
+                               _priv->setParameterValues(logEvent, p);
+                               auto ret = SQLExecute(_priv->preparedStatement);
+                               if (ret < 0)
+                               {
+                                       throw SQLException(SQL_HANDLE_STMT, 
_priv->preparedStatement, "Failed to execute prepared statement", p);
+                               }
+#endif
+                       }
+                       else
+                       {
+                               auto sql = getLogStatement(logEvent, p);
+                               execute(sql, p);
+                       }
                }
                catch (SQLException& e)
                {
diff --git a/src/main/include/log4cxx/db/odbcappender.h 
b/src/main/include/log4cxx/db/odbcappender.h
index aa85aaf6..60db2ccb 100644
--- a/src/main/include/log4cxx/db/odbcappender.h
+++ b/src/main/include/log4cxx/db/odbcappender.h
@@ -47,15 +47,25 @@ class LOG4CXX_EXPORT SQLException : public 
log4cxx::helpers::Exception
 /**
 The ODBCAppender sends log events to a database.
 
-<p>Each append call adds to an <code>ArrayList</code> buffer.  When
-the buffer is filled each log event is placed in a sql statement
-(which is configured in the <b>sql</b> element or the attached PatternLayout) 
and executed.
-
-The SQL insert statement pattern must be provided.
-The SQL statement can be specified in the Log4cxx configuration file
-either using the <b>sql</b> parameter element
-or by attaching a PatternLayout layout element.
-  
+<p>Each append call adds the spi::LoggingEvent to a buffer.
+When the buffer is full, values are extracted from each spi::LoggingEvent
+and the sql insert statement executed.
+
+The SQL insert statement pattern must be provided
+either in the Log4cxx configuration file
+using the <b>sql</b> parameter element
+or programatically by calling the <code>setSql(String sql)</code> method.
+
+If no <b>ColumnMapping</b> element is provided in the configuration file
+the sql statement is assumed to be a PatternLayout layout.
+In this case all the conversion patterns in PatternLayout
+can be used inside of the statement. (see the test cases for examples)
+
+If the <b>sql</b> element is not provided
+and no <b>ColumnMapping</b> element is provided
+the attached a PatternLayout layout element
+is assumed to contain the sql statement.
+
 The following <b>param</b> elements are optional:
 - one of <b>DSN</b>, <b>URL</b>, <b>ConnectionString</b> -
   The <b>serverName</b> parameter value in the <a 
href="https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlconnect-function";>SQLConnect</a>
 call.
@@ -67,16 +77,22 @@ The following <b>param</b> elements are optional:
   Delay executing the sql until this many logging events are available.
   One by default, meaning an sql statement is executed
   whenever a logging event is appended.
-
-<p>The <code>setSql(String sql)</code> sets the SQL statement to be
-used for logging -- this statement is sent to a
-<code>PatternLayout</code> (either created automaticly by the
-appender or added by the user).  Therefore by default all the
-conversion patterns in <code>PatternLayout</code> can be used
-inside of the statement.  (see the test cases for examples)
-
-<p>Overriding the {@link #getLogStatement} method allows more
-explicit control of the statement used for logging.
+- <b>ColumnMapping</b> -
+  One element for each "?" in the <b>sql</b> statement
+  in a sequence corresponding to the columns in the insert statement.
+  The following values are supported:
+  - logger
+  - level
+  - thread
+  - threadname
+  - time
+  - shortfilename
+  - fullfilename
+  - line
+  - class
+  - method
+  - message
+  - ndc
 
 <p>For use as a base class:
 
@@ -92,18 +108,21 @@ you override getConnection make sure to implement
 generated.  Typically this would return the connection to the
 pool it came from.
 
-<li>Override getLogStatement to
-produce specialized or dynamic statements. The default uses the
-sql option value.
-
 </ul>
 
 An example configuration that writes to the data source named "LoggingDSN" is:
 ~~~{.xml}
 <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/";>
-appender name="SqlAppender" class="ODBCAppender">
+<appender name="SqlAppender" class="ODBCAppender">
  <param name="DSN" value="LoggingDSN"/>
- <param name="sql" value="INSERT INTO 
[SomeDatabaseName].[SomeUserName].[SomeTableName] 
([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) 
VALUES ('%t', '%c','%d{dd MMM yyyy HH:mm:ss.SSS}','%p','%f','%L','%m{'}')" />
+ <param name="sql" value="INSERT INTO 
[SomeDatabaseName].[SomeUserName].[SomeTableName] 
([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) 
VALUES (?,?,?,?,?,?,?)" />
+ <param name="ColumnMapping" value="thread"/>
+ <param name="ColumnMapping" value="logger"/>
+ <param name="ColumnMapping" value="time"/>
+ <param name="ColumnMapping" value="level"/>
+ <param name="ColumnMapping" value="shortfilename"/>
+ <param name="ColumnMapping" value="line"/>
+ <param name="ColumnMapping" value="message"/>
 </appender>
 <appender name="ASYNC" class="AsyncAppender">
   <param name="BufferSize" value="1000"/>
@@ -150,21 +169,18 @@ class LOG4CXX_EXPORT ODBCAppender : public 
AppenderSkeleton
                */
                void append(const spi::LoggingEventPtr& event, helpers::Pool&) 
override;
 
+       protected:
                /**
-               * By default getLogStatement sends the event to the required 
Layout object.
+               * Sends the event to the attached PatternLayout object.
                * The layout will format the given pattern into a workable SQL 
string.
                *
-               * Overriding this provides direct access to the LoggingEvent
-               * when constructing the logging statement.
-               *
                */
-       protected:
                LogString getLogStatement(const spi::LoggingEventPtr& event,
                        helpers::Pool& p) const;
 
                /**
                *
-               * Override this to provide an alertnate method of getting
+               * Override this to provide an alternate method of getting
                * connections (such as caching).  One method to fix this is to 
open
                * connections at the start of flushBuffer() and close them at 
the
                * end.  I use a connection pool outside of ODBCAppender which is
@@ -207,12 +223,9 @@ class LOG4CXX_EXPORT ODBCAppender : public AppenderSkeleton
                virtual void flushBuffer(log4cxx::helpers::Pool& p);
 
                /**
-               * ODBCAppender requires a layout.
+               * Does this appender require a layout?
                * */
-               bool requiresLayout() const override
-               {
-                       return true;
-               }
+               bool requiresLayout() const override;
 
                /**
                * Set pre-formated statement eg: insert into LogTable (msg) 
values ("%m")
diff --git a/src/main/include/log4cxx/private/odbcappender_priv.h 
b/src/main/include/log4cxx/private/odbcappender_priv.h
index 4a78ae55..de519582 100644
--- a/src/main/include/log4cxx/private/odbcappender_priv.h
+++ b/src/main/include/log4cxx/private/odbcappender_priv.h
@@ -19,9 +19,26 @@
 #define LOG4CXX_ODBCAPPENDER_PRIV
 
 #include <log4cxx/db/odbcappender.h>
+#include <log4cxx/pattern/loggingeventpatternconverter.h>
 #include "appenderskeleton_priv.h"
 
-#include <list>
+#if !defined(LOG4CXX)
+       #define LOG4CXX 1
+#endif
+#include <log4cxx/private/log4cxx_private.h>
+#if LOG4CXX_HAVE_ODBC
+       #if defined(WIN32) || defined(_WIN32)
+               #include <windows.h>
+       #endif
+       #include <sqlext.h>
+#else
+       typedef void* SQLHSTMT;
+       typedef void* SQLPOINTER;
+       typedef uint64_t SQLULEN;
+       typedef int64_t SQLLEN;
+       typedef long SQLINTEGER;
+       typedef short SQLSMALLINT;
+#endif
 
 namespace log4cxx
 {
@@ -30,11 +47,14 @@ namespace db
 
 struct ODBCAppender::ODBCAppenderPriv : public 
AppenderSkeleton::AppenderSkeletonPrivate
 {
-       ODBCAppenderPriv() :
-               AppenderSkeletonPrivate(),
-               connection(nullptr),
-               env(nullptr),
-               bufferSize(1) {}
+       ODBCAppenderPriv()
+               : AppenderSkeletonPrivate()
+               , connection(0)
+               , env(0)
+               , preparedStatement(0)
+               , bufferSize(1)
+               , timeZone(helpers::TimeZone::getDefault())
+               {}
 
        /**
        * URL of the DB for default connection handling
@@ -58,20 +78,13 @@ struct ODBCAppender::ODBCAppenderPriv : public 
AppenderSkeleton::AppenderSkeleto
        * sub-class and overriding the <code>getConnection</code> and
        * <code>closeConnection</code> methods.
        */
-       log4cxx::db::ODBCAppender::SQLHDBC connection;
-       log4cxx::db::ODBCAppender::SQLHENV env;
+       SQLHDBC connection;
+       SQLHENV env;
 
        /**
        * Stores the string given to the pattern layout for conversion into a 
SQL
-       * statement, eg: insert into LogTable (Thread, File, Message) values
-       * ("%t", "%F", "%m")
-       *
-       * Be careful of quotes in your messages!
-       *
-       * Also see PatternLayout.
        */
        LogString sqlStatement;
-
        /**
        * size of LoggingEvent buffer before writing to the database.
        * Default is 1.
@@ -82,6 +95,31 @@ struct ODBCAppender::ODBCAppenderPriv : public 
AppenderSkeleton::AppenderSkeleto
        * ArrayList holding the buffer of Logging Events.
        */
        std::vector<spi::LoggingEventPtr> buffer;
+
+       /** Provides timestamp components
+       */
+       helpers::TimeZonePtr timeZone;
+
+       /**
+       * The prepared statement handle and the bound column names, converters 
and buffers
+       */
+       SQLHSTMT preparedStatement;
+       struct DataBinding
+       {
+               using ConverterPtr = pattern::LoggingEventPatternConverterPtr;
+               ConverterPtr converter;
+               SQLSMALLINT  paramType;
+               SQLULEN      paramMaxCharCount;
+               SQLPOINTER   paramValue;
+               SQLINTEGER   paramValueSize;
+               SQLLEN       strLen_or_Ind;
+       };
+       std::vector<LogString>   mappedName;
+       std::vector<DataBinding> parameterValue;
+#if LOG4CXX_HAVE_ODBC
+       void setPreparedStatement(SQLHDBC con, helpers::Pool& p);
+       void setParameterValues(const spi::LoggingEventPtr& event, 
helpers::Pool& p);
+#endif
 };
 
 }
diff --git a/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml 
b/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml
index ff5bcbe2..8873ab40 100644
--- a/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml
+++ b/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml
@@ -25,16 +25,30 @@
     <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/>
   </layout>
 </appender>
-<appender name="SqlAppender" class="ODBCAppender">
+<appender name="DirectAppender" class="ODBCAppender">
  <param name="DSN" value="Log4cxxTest"/>
  <param name="sql" value="INSERT INTO [ApplicationLogs].[dbo].[UnitTestLog] 
([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) 
VALUES ('%t', '%c','%d{dd MMM yyyy HH:mm:ss.SSS}','%p','%f','%L','%m{'}')" />
  <!--param name="User" value="dbo"/-->
  <!--param name="Password" value="myLog4cxxTestPassword"/-->
 </appender>
+<appender name="PreparedAppender" class="ODBCAppender">
+ <param name="DSN" value="Log4cxxTest"/>
+ <param name="sql" value="INSERT INTO [ApplicationLogs].[dbo].[UnitTestLog] 
([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) 
VALUES (?,?,?,?,?,?,?)" />
+ <param name="ColumnMapping" value="thread"/>
+ <param name="ColumnMapping" value="logger"/>
+ <param name="ColumnMapping" value="time"/>
+ <param name="ColumnMapping" value="level"/>
+ <param name="ColumnMapping" value="shortfilename"/>
+ <param name="ColumnMapping" value="line"/>
+ <param name="ColumnMapping" value="message"/>
+ <!--param name="User" value="dbo"/-->
+ <!--param name="User" value="dbo"/-->
+ <!--param name="Password" value="myLog4cxxTestPassword"/-->
+</appender>
 <appender name="ASYNC" class="AsyncAppender">
   <param name="BufferSize" value="1000"/>
   <param name="Blocking" value="false"/>
-  <appender-ref ref="SqlAppender"/>
+  <appender-ref ref="PreparedAppender"/>
 </appender>
 <logger name="DB">
   <level value="INFO" />

Reply via email to