From 6370372f5a04cdec6598fa8199b762ce33fa4d40 Mon Sep 17 00:00:00 2001 From: Mikolaj Izdebski Date: Wed, 2 Feb 2022 19:37:17 +0100 Subject: [PATCH] Fix CVE-2022-23305 JDBCAppender --- .../apache/log4j/helpers/PatternParser.java | 944 +++++++++--------- .../org/apache/log4j/jdbc/JDBCAppender.java | 715 ++++++------- .../apache/log4j/jdbc/JdbcPatternParser.java | 115 +++ 3 files changed, 942 insertions(+), 832 deletions(-) create mode 100644 src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java diff --git a/src/main/java/org/apache/log4j/helpers/PatternParser.java b/src/main/java/org/apache/log4j/helpers/PatternParser.java index 0d3ead67..4b169a2d 100644 --- a/src/main/java/org/apache/log4j/helpers/PatternParser.java +++ b/src/main/java/org/apache/log4j/helpers/PatternParser.java @@ -30,541 +30,503 @@ import java.util.Arrays; // Reinhard Deschler /** - Most of the work of the {@link org.apache.log4j.PatternLayout} class - is delegated to the PatternParser class. - -

It is this class that parses conversion patterns and creates - a chained list of {@link OptionConverter OptionConverters}. - - @author James P. Cakalic - @author Ceki Gülcü - @author Anders Kristensen - - @since 0.8.2 -*/ + * Most of the work of the {@link org.apache.log4j.PatternLayout} class is + * delegated to the PatternParser class. + * + *

+ * It is this class that parses conversion patterns and creates a chained list + * of {@link OptionConverter OptionConverters}. + * + * @author James P. Cakalic + * @author Ceki Gülcü + * @author Anders Kristensen + * + * @since 0.8.2 + */ public class PatternParser { - private static final char ESCAPE_CHAR = '%'; - - private static final int LITERAL_STATE = 0; - private static final int CONVERTER_STATE = 1; - private static final int DOT_STATE = 3; - private static final int MIN_STATE = 4; - private static final int MAX_STATE = 5; - - static final int FULL_LOCATION_CONVERTER = 1000; - static final int METHOD_LOCATION_CONVERTER = 1001; - static final int CLASS_LOCATION_CONVERTER = 1002; - static final int LINE_LOCATION_CONVERTER = 1003; - static final int FILE_LOCATION_CONVERTER = 1004; - - static final int RELATIVE_TIME_CONVERTER = 2000; - static final int THREAD_CONVERTER = 2001; - static final int LEVEL_CONVERTER = 2002; - static final int NDC_CONVERTER = 2003; - static final int MESSAGE_CONVERTER = 2004; - - int state; - protected StringBuffer currentLiteral = new StringBuffer(32); - protected int patternLength; - protected int i; - PatternConverter head; - PatternConverter tail; - protected FormattingInfo formattingInfo = new FormattingInfo(); - protected String pattern; - - public - PatternParser(String pattern) { - this.pattern = pattern; - patternLength = pattern.length(); - state = LITERAL_STATE; - } - - private - void addToList(PatternConverter pc) { - if(head == null) { - head = tail = pc; - } else { - tail.next = pc; - tail = pc; + private static final char LEFT_BRACKET = '{'; + private static final char RIGHT_BRACKET = '}'; + private static final char N_CHAR = 'n'; + private static final char DOT_CHAR = '.'; + private static final char DASH_CHAR = '-'; + private static final char ESCAPE_CHAR = '%'; + + private static final int LITERAL_STATE = 0; + private static final int CONVERTER_STATE = 1; + private static final int DOT_STATE = 3; + private static final int MIN_STATE = 4; + private static final int MAX_STATE = 5; + + static final int FULL_LOCATION_CONVERTER = 1000; + static final int METHOD_LOCATION_CONVERTER = 1001; + static final int CLASS_LOCATION_CONVERTER = 1002; + static final int LINE_LOCATION_CONVERTER = 1003; + static final int FILE_LOCATION_CONVERTER = 1004; + + static final int RELATIVE_TIME_CONVERTER = 2000; + static final int THREAD_CONVERTER = 2001; + static final int LEVEL_CONVERTER = 2002; + static final int NDC_CONVERTER = 2003; + static final int MESSAGE_CONVERTER = 2004; + + int state; + protected StringBuffer currentLiteral = new StringBuffer(32); + protected int patternLength; + protected int i; + PatternConverter head; + PatternConverter tail; + protected FormattingInfo formattingInfo = new FormattingInfo(); + protected String pattern; + + public PatternParser(String pattern) { + this.pattern = pattern; + patternLength = pattern.length(); + state = LITERAL_STATE; } - } - - protected - String extractOption() { - if((i < patternLength) && (pattern.charAt(i) == '{')) { - int end = pattern.indexOf('}', i); - if (end > i) { - String r = pattern.substring(i + 1, end); - i = end+1; - return r; - } - } - return null; - } - - - /** - The option is expected to be in decimal and positive. In case of - error, zero is returned. */ - protected - int extractPrecisionOption() { - String opt = extractOption(); - int r = 0; - if(opt != null) { - try { - r = Integer.parseInt(opt); - if(r <= 0) { - LogLog.error( - "Precision option (" + opt + ") isn't a positive integer."); - r = 0; + + private void addToList(PatternConverter pc) { + if (head == null) { + head = tail = pc; + } else { + tail.next = pc; + tail = pc; } - } - catch (NumberFormatException e) { - LogLog.error("Category option \""+opt+"\" not a decimal integer.", e); - } } - return r; - } - - public - PatternConverter parse() { - char c; - i = 0; - while(i < patternLength) { - c = pattern.charAt(i++); - switch(state) { - case LITERAL_STATE: - // In literal state, the last char is always a literal. - if(i == patternLength) { - currentLiteral.append(c); - continue; - } - if(c == ESCAPE_CHAR) { - // peek at the next char. - switch(pattern.charAt(i)) { - case ESCAPE_CHAR: - currentLiteral.append(c); - i++; // move pointer - break; - case 'n': - currentLiteral.append(Layout.LINE_SEP); - i++; // move pointer - break; - default: - if(currentLiteral.length() != 0) { - addToList(new LiteralPatternConverter( - currentLiteral.toString())); - //LogLog.debug("Parsed LITERAL converter: \"" - // +currentLiteral+"\"."); - } - currentLiteral.setLength(0); - currentLiteral.append(c); // append % - state = CONVERTER_STATE; - formattingInfo.reset(); - } - } - else { - currentLiteral.append(c); - } - break; - case CONVERTER_STATE: - currentLiteral.append(c); - switch(c) { - case '-': - formattingInfo.leftAlign = true; - break; - case '.': - state = DOT_STATE; - break; - default: - if(c >= '0' && c <= '9') { - formattingInfo.min = c - '0'; - state = MIN_STATE; - } - else - finalizeConverter(c); - } // switch - break; - case MIN_STATE: - currentLiteral.append(c); - if(c >= '0' && c <= '9') - formattingInfo.min = formattingInfo.min*10 + (c - '0'); - else if(c == '.') - state = DOT_STATE; - else { - finalizeConverter(c); - } - break; - case DOT_STATE: - currentLiteral.append(c); - if(c >= '0' && c <= '9') { - formattingInfo.max = c - '0'; - state = MAX_STATE; - } - else { - LogLog.error("Error occured in position "+i - +".\n Was expecting digit, instead got char \""+c+"\"."); - state = LITERAL_STATE; - } - break; - case MAX_STATE: - currentLiteral.append(c); - if(c >= '0' && c <= '9') - formattingInfo.max = formattingInfo.max*10 + (c - '0'); - else { - finalizeConverter(c); - state = LITERAL_STATE; + + protected String extractOption() { + if ((i < patternLength) && (pattern.charAt(i) == LEFT_BRACKET)) { + int end = pattern.indexOf(RIGHT_BRACKET, i); + if (end > i) { + String r = pattern.substring(i + 1, end); + i = end + 1; + return r; + } } - break; - } // switch - } // while - if(currentLiteral.length() != 0) { - addToList(new LiteralPatternConverter(currentLiteral.toString())); - //LogLog.debug("Parsed LITERAL converter: \""+currentLiteral+"\"."); + return null; } - return head; - } - - protected - void finalizeConverter(char c) { - PatternConverter pc = null; - switch(c) { - case 'c': - pc = new CategoryPatternConverter(formattingInfo, - extractPrecisionOption()); - //LogLog.debug("CATEGORY converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 'C': - pc = new ClassNamePatternConverter(formattingInfo, - extractPrecisionOption()); - //LogLog.debug("CLASS_NAME converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 'd': - String dateFormatStr = AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT; - DateFormat df; - String dOpt = extractOption(); - if(dOpt != null) - dateFormatStr = dOpt; - - if(dateFormatStr.equalsIgnoreCase( - AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT)) - df = new ISO8601DateFormat(); - else if(dateFormatStr.equalsIgnoreCase( - AbsoluteTimeDateFormat.ABS_TIME_DATE_FORMAT)) - df = new AbsoluteTimeDateFormat(); - else if(dateFormatStr.equalsIgnoreCase( - AbsoluteTimeDateFormat.DATE_AND_TIME_DATE_FORMAT)) - df = new DateTimeDateFormat(); - else { - try { - df = new SimpleDateFormat(dateFormatStr); - } - catch (IllegalArgumentException e) { - LogLog.error("Could not instantiate SimpleDateFormat with " + - dateFormatStr, e); - df = (DateFormat) OptionConverter.instantiateByClassName( - "org.apache.log4j.helpers.ISO8601DateFormat", - DateFormat.class, null); + + /** + * The option is expected to be in decimal and positive. In case of error, zero + * is returned. + */ + protected int extractPrecisionOption() { + String opt = extractOption(); + int r = 0; + if (opt != null) { + try { + r = Integer.parseInt(opt); + if (r <= 0) { + LogLog.error("Precision option (" + opt + ") isn't a positive integer."); + r = 0; + } + } catch (NumberFormatException e) { + LogLog.error("Category option \"" + opt + "\" not a decimal integer.", e); + } } - } - pc = new DatePatternConverter(formattingInfo, df); - //LogLog.debug("DATE converter {"+dateFormatStr+"}."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 'F': - pc = new LocationPatternConverter(formattingInfo, - FILE_LOCATION_CONVERTER); - //LogLog.debug("File name converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 'l': - pc = new LocationPatternConverter(formattingInfo, - FULL_LOCATION_CONVERTER); - //LogLog.debug("Location converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 'L': - pc = new LocationPatternConverter(formattingInfo, - LINE_LOCATION_CONVERTER); - //LogLog.debug("LINE NUMBER converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 'm': - pc = new BasicPatternConverter(formattingInfo, MESSAGE_CONVERTER); - //LogLog.debug("MESSAGE converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 'M': - pc = new LocationPatternConverter(formattingInfo, - METHOD_LOCATION_CONVERTER); - //LogLog.debug("METHOD converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 'p': - pc = new BasicPatternConverter(formattingInfo, LEVEL_CONVERTER); - //LogLog.debug("LEVEL converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 'r': - pc = new BasicPatternConverter(formattingInfo, - RELATIVE_TIME_CONVERTER); - //LogLog.debug("RELATIVE time converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - case 't': - pc = new BasicPatternConverter(formattingInfo, THREAD_CONVERTER); - //LogLog.debug("THREAD converter."); - //formattingInfo.dump(); - currentLiteral.setLength(0); - break; - /*case 'u': - if(i < patternLength) { - char cNext = pattern.charAt(i); - if(cNext >= '0' && cNext <= '9') { - pc = new UserFieldPatternConverter(formattingInfo, cNext - '0'); - LogLog.debug("USER converter ["+cNext+"]."); - formattingInfo.dump(); - currentLiteral.setLength(0); - i++; + return r; + } + + public PatternConverter parse() { + char c; + i = 0; + while (i < patternLength) { + c = pattern.charAt(i++); + switch (state) { + case LITERAL_STATE: + // In literal state, the last char is always a literal. + if (i == patternLength) { + currentLiteral.append(c); + continue; + } + if (c == ESCAPE_CHAR) { + // peek at the next char. + switch (pattern.charAt(i)) { + case ESCAPE_CHAR: + currentLiteral.append(c); + i++; // move pointer + break; + case N_CHAR: + currentLiteral.append(Layout.LINE_SEP); + i++; // move pointer + break; + default: + if (currentLiteral.length() != 0) { + addToList(new LiteralPatternConverter(currentLiteral.toString())); + // LogLog.debug("Parsed LITERAL converter: \"" + // +currentLiteral+"\"."); + } + currentLiteral.setLength(0); + currentLiteral.append(c); // append % + state = CONVERTER_STATE; + formattingInfo.reset(); + } + } else { + currentLiteral.append(c); + } + break; + case CONVERTER_STATE: + currentLiteral.append(c); + switch (c) { + case DASH_CHAR: + formattingInfo.leftAlign = true; + break; + case DOT_CHAR: + state = DOT_STATE; + break; + default: + if (c >= '0' && c <= '9') { + formattingInfo.min = c - '0'; + state = MIN_STATE; + } else + finalizeConverter(c); + } // switch + break; + case MIN_STATE: + currentLiteral.append(c); + if (c >= '0' && c <= '9') + formattingInfo.min = formattingInfo.min * 10 + (c - '0'); + else if (c == '.') + state = DOT_STATE; + else { + finalizeConverter(c); + } + break; + case DOT_STATE: + currentLiteral.append(c); + if (c >= '0' && c <= '9') { + formattingInfo.max = c - '0'; + state = MAX_STATE; + } else { + LogLog.error("Error occured in position " + i + ".\n Was expecting digit, instead got char \"" + c + + "\"."); + state = LITERAL_STATE; + } + break; + case MAX_STATE: + currentLiteral.append(c); + if (c >= '0' && c <= '9') + formattingInfo.max = formattingInfo.max * 10 + (c - '0'); + else { + finalizeConverter(c); + state = LITERAL_STATE; + } + break; + } // switch + } // while + if (currentLiteral.length() != 0) { + addToList(new LiteralPatternConverter(currentLiteral.toString())); + // LogLog.debug("Parsed LITERAL converter: \""+currentLiteral+"\"."); } - else - LogLog.error("Unexpected char" +cNext+" at position "+i); - } - break;*/ - case 'x': - pc = new BasicPatternConverter(formattingInfo, NDC_CONVERTER); - //LogLog.debug("NDC converter."); - currentLiteral.setLength(0); - break; - case 'X': - String xOpt = extractOption(); - pc = new MDCPatternConverter(formattingInfo, xOpt); - currentLiteral.setLength(0); - break; - default: - LogLog.error("Unexpected char [" +c+"] at position "+i - +" in conversion patterrn."); - pc = new LiteralPatternConverter(currentLiteral.toString()); - currentLiteral.setLength(0); + return head; } - addConverter(pc); - } - - protected - void addConverter(PatternConverter pc) { - currentLiteral.setLength(0); - // Add the pattern converter to the list. - addToList(pc); - // Next pattern is assumed to be a literal. - state = LITERAL_STATE; - // Reset formatting info - formattingInfo.reset(); - } - - // --------------------------------------------------------------------- - // PatternConverters - // --------------------------------------------------------------------- - - private static class BasicPatternConverter extends PatternConverter { - int type; - - BasicPatternConverter(FormattingInfo formattingInfo, int type) { - super(formattingInfo); - this.type = type; + protected void finalizeConverter(char c) { + PatternConverter pc = null; + switch (c) { + case 'c': + pc = new CategoryPatternConverter(formattingInfo, extractPrecisionOption()); + // LogLog.debug("CATEGORY converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 'C': + pc = new ClassNamePatternConverter(formattingInfo, extractPrecisionOption()); + // LogLog.debug("CLASS_NAME converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 'd': + String dateFormatStr = AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT; + DateFormat df; + String dOpt = extractOption(); + if (dOpt != null) + dateFormatStr = dOpt; + + if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT)) + df = new ISO8601DateFormat(); + else if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.ABS_TIME_DATE_FORMAT)) + df = new AbsoluteTimeDateFormat(); + else if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.DATE_AND_TIME_DATE_FORMAT)) + df = new DateTimeDateFormat(); + else { + try { + df = new SimpleDateFormat(dateFormatStr); + } catch (IllegalArgumentException e) { + LogLog.error("Could not instantiate SimpleDateFormat with " + dateFormatStr, e); + df = (DateFormat) OptionConverter.instantiateByClassName( + "org.apache.log4j.helpers.ISO8601DateFormat", DateFormat.class, null); + } + } + pc = new DatePatternConverter(formattingInfo, df); + // LogLog.debug("DATE converter {"+dateFormatStr+"}."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 'F': + pc = new LocationPatternConverter(formattingInfo, FILE_LOCATION_CONVERTER); + // LogLog.debug("File name converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 'l': + pc = new LocationPatternConverter(formattingInfo, FULL_LOCATION_CONVERTER); + // LogLog.debug("Location converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 'L': + pc = new LocationPatternConverter(formattingInfo, LINE_LOCATION_CONVERTER); + // LogLog.debug("LINE NUMBER converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 'm': + pc = new BasicPatternConverter(formattingInfo, MESSAGE_CONVERTER); + // LogLog.debug("MESSAGE converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 'M': + pc = new LocationPatternConverter(formattingInfo, METHOD_LOCATION_CONVERTER); + // LogLog.debug("METHOD converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 'p': + pc = new BasicPatternConverter(formattingInfo, LEVEL_CONVERTER); + // LogLog.debug("LEVEL converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 'r': + pc = new BasicPatternConverter(formattingInfo, RELATIVE_TIME_CONVERTER); + // LogLog.debug("RELATIVE time converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + case 't': + pc = new BasicPatternConverter(formattingInfo, THREAD_CONVERTER); + // LogLog.debug("THREAD converter."); + // formattingInfo.dump(); + currentLiteral.setLength(0); + break; + /* + * case 'u': if(i < patternLength) { char cNext = pattern.charAt(i); if(cNext >= + * '0' && cNext <= '9') { pc = new UserFieldPatternConverter(formattingInfo, + * cNext - '0'); LogLog.debug("USER converter ["+cNext+"]."); + * formattingInfo.dump(); currentLiteral.setLength(0); i++; } else + * LogLog.error("Unexpected char" +cNext+" at position "+i); } break; + */ + case 'x': + pc = new BasicPatternConverter(formattingInfo, NDC_CONVERTER); + // LogLog.debug("NDC converter."); + currentLiteral.setLength(0); + break; + case 'X': + String xOpt = extractOption(); + pc = new MDCPatternConverter(formattingInfo, xOpt); + currentLiteral.setLength(0); + break; + default: + LogLog.error("Unexpected char [" + c + "] at position " + i + " in conversion patterrn."); + pc = new LiteralPatternConverter(currentLiteral.toString()); + currentLiteral.setLength(0); + } + + addConverter(pc); } - public - String convert(LoggingEvent event) { - switch(type) { - case RELATIVE_TIME_CONVERTER: - return (Long.toString(event.timeStamp - LoggingEvent.getStartTime())); - case THREAD_CONVERTER: - return event.getThreadName(); - case LEVEL_CONVERTER: - return event.getLevel().toString(); - case NDC_CONVERTER: - return event.getNDC(); - case MESSAGE_CONVERTER: { - return event.getRenderedMessage(); - } - default: return null; - } + protected void addConverter(PatternConverter pc) { + currentLiteral.setLength(0); + // Add the pattern converter to the list. + addToList(pc); + // Next pattern is assumed to be a literal. + state = LITERAL_STATE; + // Reset formatting info + formattingInfo.reset(); } - } - private static class LiteralPatternConverter extends PatternConverter { - private String literal; + // --------------------------------------------------------------------- + // PatternConverters + // --------------------------------------------------------------------- - LiteralPatternConverter(String value) { - literal = value; - } + private static class BasicPatternConverter extends PatternConverter { + int type; - public - final - void format(StringBuffer sbuf, LoggingEvent event) { - sbuf.append(literal); - } + BasicPatternConverter(FormattingInfo formattingInfo, int type) { + super(formattingInfo); + this.type = type; + } - public - String convert(LoggingEvent event) { - return literal; + public String convert(LoggingEvent event) { + switch (type) { + case RELATIVE_TIME_CONVERTER: + return (Long.toString(event.timeStamp - LoggingEvent.getStartTime())); + case THREAD_CONVERTER: + return event.getThreadName(); + case LEVEL_CONVERTER: + return event.getLevel().toString(); + case NDC_CONVERTER: + return event.getNDC(); + case MESSAGE_CONVERTER: { + return event.getRenderedMessage(); + } + default: + return null; + } + } } - } - private static class DatePatternConverter extends PatternConverter { - private DateFormat df; - private Date date; + private static class LiteralPatternConverter extends PatternConverter { + private String literal; - DatePatternConverter(FormattingInfo formattingInfo, DateFormat df) { - super(formattingInfo); - date = new Date(); - this.df = df; - } + LiteralPatternConverter(String value) { + literal = value; + } - public - String convert(LoggingEvent event) { - date.setTime(event.timeStamp); - String converted = null; - try { - converted = df.format(date); - } - catch (Exception ex) { - LogLog.error("Error occured while converting date.", ex); - } - return converted; + public final void format(StringBuffer sbuf, LoggingEvent event) { + sbuf.append(literal); + } + + public String convert(LoggingEvent event) { + return literal; + } } - } - private static class MDCPatternConverter extends PatternConverter { - private String key; + private static class DatePatternConverter extends PatternConverter { + private DateFormat df; + private Date date; - MDCPatternConverter(FormattingInfo formattingInfo, String key) { - super(formattingInfo); - this.key = key; - } + DatePatternConverter(FormattingInfo formattingInfo, DateFormat df) { + super(formattingInfo); + date = new Date(); + this.df = df; + } - public - String convert(LoggingEvent event) { - if (key == null) { - StringBuffer buf = new StringBuffer("{"); - Map properties = event.getProperties(); - if (properties.size() > 0) { - Object[] keys = properties.keySet().toArray(); - Arrays.sort(keys); - for (int i = 0; i < keys.length; i++) { - buf.append('{'); - buf.append(keys[i]); - buf.append(','); - buf.append(properties.get(keys[i])); - buf.append('}'); - } - } - buf.append('}'); - return buf.toString(); - } else { - Object val = event.getMDC(key); - if(val == null) { - return null; - } else { - return val.toString(); - } - } + public String convert(LoggingEvent event) { + date.setTime(event.timeStamp); + String converted = null; + try { + converted = df.format(date); + } catch (Exception ex) { + LogLog.error("Error occured while converting date.", ex); + } + return converted; + } } - } + private static class MDCPatternConverter extends PatternConverter { + private String key; - private class LocationPatternConverter extends PatternConverter { - int type; + MDCPatternConverter(FormattingInfo formattingInfo, String key) { + super(formattingInfo); + this.key = key; + } - LocationPatternConverter(FormattingInfo formattingInfo, int type) { - super(formattingInfo); - this.type = type; + public String convert(LoggingEvent event) { + if (key == null) { + StringBuffer buf = new StringBuffer("{"); + Map properties = event.getProperties(); + if (properties.size() > 0) { + Object[] keys = properties.keySet().toArray(); + Arrays.sort(keys); + for (int i = 0; i < keys.length; i++) { + buf.append('{'); + buf.append(keys[i]); + buf.append(','); + buf.append(properties.get(keys[i])); + buf.append('}'); + } + } + buf.append('}'); + return buf.toString(); + } else { + Object val = event.getMDC(key); + if (val == null) { + return null; + } else { + return val.toString(); + } + } + } } - public - String convert(LoggingEvent event) { - LocationInfo locationInfo = event.getLocationInformation(); - switch(type) { - case FULL_LOCATION_CONVERTER: - return locationInfo.fullInfo; - case METHOD_LOCATION_CONVERTER: - return locationInfo.getMethodName(); - case LINE_LOCATION_CONVERTER: - return locationInfo.getLineNumber(); - case FILE_LOCATION_CONVERTER: - return locationInfo.getFileName(); - default: return null; - } - } - } + private class LocationPatternConverter extends PatternConverter { + int type; - private static abstract class NamedPatternConverter extends PatternConverter { - int precision; + LocationPatternConverter(FormattingInfo formattingInfo, int type) { + super(formattingInfo); + this.type = type; + } - NamedPatternConverter(FormattingInfo formattingInfo, int precision) { - super(formattingInfo); - this.precision = precision; + public String convert(LoggingEvent event) { + LocationInfo locationInfo = event.getLocationInformation(); + switch (type) { + case FULL_LOCATION_CONVERTER: + return locationInfo.fullInfo; + case METHOD_LOCATION_CONVERTER: + return locationInfo.getMethodName(); + case LINE_LOCATION_CONVERTER: + return locationInfo.getLineNumber(); + case FILE_LOCATION_CONVERTER: + return locationInfo.getFileName(); + default: + return null; + } + } } - abstract - String getFullyQualifiedName(LoggingEvent event); - - public - String convert(LoggingEvent event) { - String n = getFullyQualifiedName(event); - if(precision <= 0) - return n; - else { - int len = n.length(); - - // We substract 1 from 'len' when assigning to 'end' to avoid out of - // bounds exception in return r.substring(end+1, len). This can happen if - // precision is 1 and the category name ends with a dot. - int end = len -1 ; - for(int i = precision; i > 0; i--) { - end = n.lastIndexOf('.', end-1); - if(end == -1) - return n; + private static abstract class NamedPatternConverter extends PatternConverter { + int precision; + + NamedPatternConverter(FormattingInfo formattingInfo, int precision) { + super(formattingInfo); + this.precision = precision; + } + + abstract String getFullyQualifiedName(LoggingEvent event); + + public String convert(LoggingEvent event) { + String n = getFullyQualifiedName(event); + if (precision <= 0) + return n; + else { + int len = n.length(); + + // We substract 1 from 'len' when assigning to 'end' to avoid out of + // bounds exception in return r.substring(end+1, len). This can happen if + // precision is 1 and the category name ends with a dot. + int end = len - 1; + for (int i = precision; i > 0; i--) { + end = n.lastIndexOf('.', end - 1); + if (end == -1) + return n; + } + return n.substring(end + 1, len); + } } - return n.substring(end+1, len); - } } - } - private class ClassNamePatternConverter extends NamedPatternConverter { + private class ClassNamePatternConverter extends NamedPatternConverter { - ClassNamePatternConverter(FormattingInfo formattingInfo, int precision) { - super(formattingInfo, precision); - } + ClassNamePatternConverter(FormattingInfo formattingInfo, int precision) { + super(formattingInfo, precision); + } - String getFullyQualifiedName(LoggingEvent event) { - return event.getLocationInformation().getClassName(); + String getFullyQualifiedName(LoggingEvent event) { + return event.getLocationInformation().getClassName(); + } } - } - private class CategoryPatternConverter extends NamedPatternConverter { + private class CategoryPatternConverter extends NamedPatternConverter { - CategoryPatternConverter(FormattingInfo formattingInfo, int precision) { - super(formattingInfo, precision); - } + CategoryPatternConverter(FormattingInfo formattingInfo, int precision) { + super(formattingInfo, precision); + } - String getFullyQualifiedName(LoggingEvent event) { - return event.getLoggerName(); + String getFullyQualifiedName(LoggingEvent event) { + return event.getLoggerName(); + } } - } } - diff --git a/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java b/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java index ad35f657..6ba9b66c 100644 --- a/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java +++ b/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java @@ -18,381 +18,414 @@ package org.apache.log4j.jdbc; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; -import java.util.Iterator; import org.apache.log4j.PatternLayout; +import org.apache.log4j.helpers.LogLog; import org.apache.log4j.spi.ErrorCode; import org.apache.log4j.spi.LoggingEvent; - /** - The JDBCAppender provides for sending log events to a database. - -

WARNING: This version of JDBCAppender - is very likely to be completely replaced in the future. Moreoever, - it does not log exceptions. - -

Each append call adds to an ArrayList buffer. When - the buffer is filled each log event is placed in a sql statement - (configurable) and executed. - - BufferSize, db URL, User, & Password are - configurable options in the standard log4j ways. - -

The setSql(String sql) sets the SQL statement to be - used for logging -- this statement is sent to a - PatternLayout (either created automaticly by the - appender or added by the user). Therefore by default all the - conversion patterns in PatternLayout can be used - inside of the statement. (see the test cases for examples) - -

Overriding the {@link #getLogStatement} method allows more - explicit control of the statement used for logging. - -

For use as a base class: - -

- - @author Kevin Steppe (ksteppe@pacbell.net) - -*/ -public class JDBCAppender extends org.apache.log4j.AppenderSkeleton - implements org.apache.log4j.Appender { - - /** - * URL of the DB for default connection handling - */ - protected String databaseURL = "jdbc:odbc:myDB"; - - /** - * User to connect as for default connection handling - */ - protected String databaseUser = "me"; - - /** - * User to use for default connection handling - */ - protected String databasePassword = "mypassword"; - - /** - * Connection used by default. The connection is opened the first time it - * is needed and then held open until the appender is closed (usually at - * garbage collection). This behavior is best modified by creating a - * sub-class and overriding the getConnection and - * closeConnection methods. - */ - protected Connection connection = null; - - /** - * Stores the string given to the pattern layout for conversion into a SQL - * statement, eg: insert into LogTable (Thread, Class, Message) values - * ("%t", "%c", "%m"). - * - * Be careful of quotes in your messages! - * - * Also see PatternLayout. - */ - protected String sqlStatement = ""; - - /** - * size of LoggingEvent buffer before writting to the database. - * Default is 1. - */ - protected int bufferSize = 1; - - /** - * ArrayList holding the buffer of Logging Events. - */ - protected ArrayList buffer; - - /** - * Helper object for clearing out the buffer - */ - protected ArrayList removes; - - private boolean locationInfo = false; - - public JDBCAppender() { - super(); - buffer = new ArrayList(bufferSize); - removes = new ArrayList(bufferSize); - } - - /** - * Gets whether the location of the logging request call - * should be captured. - * - * @since 1.2.16 - * @return the current value of the LocationInfo option. - */ - public boolean getLocationInfo() { - return locationInfo; - } - - /** - * The LocationInfo option takes a boolean value. By default, it is - * set to false which means there will be no effort to extract the location - * information related to the event. As a result, the event that will be - * ultimately logged will likely to contain the wrong location information - * (if present in the log format). - *

- *

- * Location information extraction is comparatively very slow and should be - * avoided unless performance is not a concern. - *

- * @since 1.2.16 - * @param flag true if location information should be extracted. - */ - public void setLocationInfo(final boolean flag) { - locationInfo = flag; - } - - - /** - * Adds the event to the buffer. When full the buffer is flushed. - */ - public void append(LoggingEvent event) { - event.getNDC(); - event.getThreadName(); - // Get a copy of this thread's MDC. - event.getMDCCopy(); - if (locationInfo) { - event.getLocationInformation(); - } - event.getRenderedMessage(); - event.getThrowableStrRep(); - buffer.add(event); - - if (buffer.size() >= bufferSize) - flushBuffer(); - } - - /** - * By default getLogStatement sends the event to the required Layout 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 String getLogStatement(LoggingEvent event) { - return getLayout().format(event); - } - - /** - * - * Override this to provide an alertnate 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 JDBCAppender which is - * accessed in an override of this method. - * */ - protected void execute(String sql) throws SQLException { - - Connection con = null; - Statement stmt = null; - - try { - con = getConnection(); - - stmt = con.createStatement(); - stmt.executeUpdate(sql); - } finally { - if(stmt != null) { - stmt.close(); - } - closeConnection(con); + * The JDBCAppender provides for sending log events to a database. + * + *

+ * WARNING: This version of JDBCAppender is very likely + * to be completely replaced in the future. Moreoever, it does not log + * exceptions. + * + *

+ * Each append call adds to an ArrayList buffer. When the buffer is + * filled each log event is placed in a sql statement (configurable) and + * executed. + * + * BufferSize, db URL, User, & Password are + * configurable options in the standard log4j ways. + * + *

+ * The setSql(String sql) sets the SQL statement to be used for + * logging -- this statement is sent to a PatternLayout (either + * created automatically by the appender or added by the user). Therefore by + * default all the conversion patterns in PatternLayout can be used + * inside of the statement. (see the test cases for examples) + * + *

+ * Overriding the {@link #getLogStatement} method allows more explicit control + * of the statement used for logging. + * + *

+ * For use as a base class: + * + *

+ * + * @author Kevin Steppe + * (ksteppe@pacbell.net) + * + */ +public class JDBCAppender extends org.apache.log4j.AppenderSkeleton implements org.apache.log4j.Appender { + +// private final Boolean secureSqlReplacement = Boolean +// .parseBoolean(System.getProperty("org.apache.log4j.jdbc.JDBCAppender.secure_jdbc_replacement", "true")); + + private static final IllegalArgumentException ILLEGAL_PATTERN_FOR_SECURE_EXEC = new IllegalArgumentException( + "Only org.apache.log4j.PatternLayout is supported for now due to CVE-2022-23305"); + + /** + * URL of the DB for default connection handling + */ + protected String databaseURL = "jdbc:odbc:myDB"; + + /** + * User to connect as for default connection handling + */ + protected String databaseUser = "me"; + + /** + * User to use for default connection handling + */ + protected String databasePassword = "mypassword"; + + /** + * Connection used by default. The connection is opened the first time it is + * needed and then held open until the appender is closed (usually at garbage + * collection). This behavior is best modified by creating a sub-class and + * overriding the getConnection and closeConnection + * methods. + */ + protected Connection connection = null; + + /** + * Stores the string given to the pattern layout for conversion into a SQL + * statement, eg: insert into LogTable (Thread, Class, Message) values ("%t", + * "%c", "%m"). + * + * Be careful of quotes in your messages! + * + * Also see PatternLayout. + */ + protected String sqlStatement = ""; + + private JdbcPatternParser preparedStatementParser; + /** + * size of LoggingEvent buffer before writting to the database. Default is 1. + */ + protected int bufferSize = 1; + + /** + * ArrayList holding the buffer of Logging Events. + */ + protected ArrayList buffer; + + private boolean locationInfo = false; + + private boolean isActive = false; + + public JDBCAppender() { + super(); + buffer = new ArrayList(bufferSize); } - //System.out.println("Execute: " + sql); - } - - - /** - * Override this to return the connection to a pool, or to clean up the - * resource. - * - * The default behavior holds a single connection open until the appender - * is closed (typically when garbage collected). - */ - protected void closeConnection(Connection con) { - } - - /** - * Override this to link with your connection pooling system. - * - * By default this creates a single connection which is held open - * until the object is garbage collected. - */ - protected Connection getConnection() throws SQLException { - if (!DriverManager.getDrivers().hasMoreElements()) - setDriver("sun.jdbc.odbc.JdbcOdbcDriver"); - - if (connection == null) { - connection = DriverManager.getConnection(databaseURL, databaseUser, - databasePassword); - } - - return connection; - } - - /** - * Closes the appender, flushing the buffer first then closing the default - * connection if it is open. - */ - public void close() - { - flushBuffer(); - - try { - if (connection != null && !connection.isClosed()) - connection.close(); - } catch (SQLException e) { - errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE); + @Override + public void activateOptions() { + + if (getSql() == null || getSql().trim().length() == 0) { + LogLog.error("JDBCAppender.sql parameter is mandatory. Skipping all futher processing"); + isActive = false; + return; + } + + LogLog.debug("JDBCAppender constructing internal pattern parser"); + preparedStatementParser = new JdbcPatternParser(getSql()); + isActive = true; } - this.closed = true; - } - - /** - * loops through the buffer of LoggingEvents, gets a - * sql string from getLogStatement() and sends it to execute(). - * Errors are sent to the errorHandler. - * - * If a statement fails the LoggingEvent stays in the buffer! - */ - public void flushBuffer() { - //Do the actual logging - removes.ensureCapacity(buffer.size()); - for (Iterator i = buffer.iterator(); i.hasNext();) { - LoggingEvent logEvent = (LoggingEvent)i.next(); - try { - String sql = getLogStatement(logEvent); - execute(sql); - } - catch (SQLException e) { - errorHandler.error("Failed to excute sql", e, - ErrorCode.FLUSH_FAILURE); - } finally { - removes.add(logEvent); - } + + /** + * Gets whether the location of the logging request call should be captured. + * + * @since 1.2.16 + * @return the current value of the LocationInfo option. + */ + public boolean getLocationInfo() { + return locationInfo; } - - // remove from the buffer any events that were reported - buffer.removeAll(removes); - - // clear the buffer of reported events - removes.clear(); - } - - - /** closes the appender before disposal */ - public void finalize() { - close(); - } - - - /** - * JDBCAppender requires a layout. - * */ - public boolean requiresLayout() { - return true; - } - - - /** - * - */ - public void setSql(String s) { - sqlStatement = s; - if (getLayout() == null) { - this.setLayout(new PatternLayout(s)); + + /** + * The LocationInfo option takes a boolean value. By default, it is set + * to false which means there will be no effort to extract the location + * information related to the event. As a result, the event that will be + * ultimately logged will likely to contain the wrong location information (if + * present in the log format). + *

+ *

+ * Location information extraction is comparatively very slow and should be + * avoided unless performance is not a concern. + *

+ * + * @since 1.2.16 + * @param flag true if location information should be extracted. + */ + public void setLocationInfo(final boolean flag) { + locationInfo = flag; } - else { - ((PatternLayout)getLayout()).setConversionPattern(s); + + /** + * Adds the event to the buffer. When full the buffer is flushed. + */ + public void append(LoggingEvent event) { + if (!isActive) { + return; + } + event.getNDC(); + event.getThreadName(); + // Get a copy of this thread's MDC. + event.getMDCCopy(); + if (locationInfo) { + event.getLocationInformation(); + } + event.getRenderedMessage(); + event.getThrowableStrRep(); + buffer.add(event); + + if (buffer.size() >= bufferSize) + flushBuffer(); } - } + /** + * By default getLogStatement sends the event to the required Layout 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 String getLogStatement(LoggingEvent event) { + return getLayout().format(event); + } - /** - * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m") - */ - public String getSql() { - return sqlStatement; - } + /** + * + * Override this to provide an alertnate 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 + * JDBCAppender which is accessed in an override of this method. + */ + protected void execute(String sql) throws SQLException { + + Connection con = null; + Statement stmt = null; + + try { + con = getConnection(); + + stmt = con.createStatement(); + stmt.executeUpdate(sql); + } finally { + if (stmt != null) { + stmt.close(); + } + closeConnection(con); + } + + // System.out.println("Execute: " + sql); + } + /** + * Override this to return the connection to a pool, or to clean up the + * resource. + * + * The default behavior holds a single connection open until the appender is + * closed (typically when garbage collected). + */ + protected void closeConnection(Connection con) { + } - public void setUser(String user) { - databaseUser = user; - } + /** + * Override this to link with your connection pooling system. + * + * By default this creates a single connection which is held open until the + * object is garbage collected. + */ + protected Connection getConnection() throws SQLException { + if (!DriverManager.getDrivers().hasMoreElements()) + setDriver("sun.jdbc.odbc.JdbcOdbcDriver"); + + if (connection == null) { + connection = DriverManager.getConnection(databaseURL, databaseUser, databasePassword); + } + + return connection; + } + /** + * Closes the appender, flushing the buffer first then closing the default + * connection if it is open. + */ + public void close() { + flushBuffer(); + + try { + if (connection != null && !connection.isClosed()) + connection.close(); + } catch (SQLException e) { + errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE); + } + this.closed = true; + } - public void setURL(String url) { - databaseURL = url; - } + /** + * loops through the buffer of LoggingEvents, gets a sql string from + * getLogStatement() and sends it to execute(). Errors are sent to the + * errorHandler. + * + * If a statement fails the LoggingEvent stays in the buffer! + */ + public void flushBuffer() { + if (buffer.isEmpty()) { + return; + } + flushBufferSecure(); + } + private void flushBufferSecure() { + // Prepare events that we will store to the DB + ArrayList removes = new ArrayList(buffer); + buffer.removeAll(removes); + + if (layout.getClass() != PatternLayout.class) { + errorHandler.error("Failed to convert pattern " + layout + " to SQL", ILLEGAL_PATTERN_FOR_SECURE_EXEC, + ErrorCode.MISSING_LAYOUT); + return; + } + Connection con = null; + boolean useBatch = removes.size() > 1; + try { + con = getConnection(); + PreparedStatement ps = con.prepareStatement(preparedStatementParser.getParameterizedSql()); + safelyInsertIntoDB(removes, useBatch, ps); + } catch (SQLException e) { + errorHandler.error("Failed to append messages sql", e, ErrorCode.FLUSH_FAILURE); + } finally { + if (con != null) { + closeConnection(con); + } + } + + } + + private void safelyInsertIntoDB(ArrayList removes, boolean useBatch, PreparedStatement ps) + throws SQLException { + try { + for (LoggingEvent logEvent : removes) { + try { + preparedStatementParser.setParameters(ps, logEvent); + if (useBatch) { + ps.addBatch(); + } + } catch (SQLException e) { + errorHandler.error("Failed to append parameters", e, ErrorCode.FLUSH_FAILURE); + } + } + if (useBatch) { + ps.executeBatch(); + } else { + ps.execute(); + } + } finally { + try { + ps.close(); + } catch (SQLException ignored) { + } + } + } - public void setPassword(String password) { - databasePassword = password; - } + /** closes the appender before disposal */ + public void finalize() { + close(); + } + /** + * JDBCAppender requires a layout. + */ + public boolean requiresLayout() { + return true; + } - public void setBufferSize(int newBufferSize) { - bufferSize = newBufferSize; - buffer.ensureCapacity(bufferSize); - removes.ensureCapacity(bufferSize); - } + /** + * + */ + public void setSql(String s) { + sqlStatement = s; + if (getLayout() == null) { + this.setLayout(new PatternLayout(s)); + } else { + ((PatternLayout) getLayout()).setConversionPattern(s); + } + } + /** + * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m") + */ + public String getSql() { + return sqlStatement; + } - public String getUser() { - return databaseUser; - } + public void setUser(String user) { + databaseUser = user; + } + public void setURL(String url) { + databaseURL = url; + } - public String getURL() { - return databaseURL; - } + public void setPassword(String password) { + databasePassword = password; + } + public void setBufferSize(int newBufferSize) { + bufferSize = newBufferSize; + buffer.ensureCapacity(bufferSize); + } - public String getPassword() { - return databasePassword; - } + public String getUser() { + return databaseUser; + } + public String getURL() { + return databaseURL; + } - public int getBufferSize() { - return bufferSize; - } + public String getPassword() { + return databasePassword; + } + public int getBufferSize() { + return bufferSize; + } - /** - * Ensures that the given driver class has been loaded for sql connection - * creation. - */ - public void setDriver(String driverClass) { - try { - Class.forName(driverClass); - } catch (Exception e) { - errorHandler.error("Failed to load driver", e, - ErrorCode.GENERIC_FAILURE); + /** + * Ensures that the given driver class has been loaded for sql connection + * creation. + */ + public void setDriver(String driverClass) { + try { + Class.forName(driverClass); + } catch (Exception e) { + errorHandler.error("Failed to load driver", e, ErrorCode.GENERIC_FAILURE); + } } - } } - diff --git a/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java b/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java new file mode 100644 index 00000000..d6807dee --- /dev/null +++ b/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java @@ -0,0 +1,115 @@ +/* + * 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.log4j.jdbc; + +import org.apache.log4j.helpers.PatternConverter; +import org.apache.log4j.helpers.PatternParser; +import org.apache.log4j.spi.LoggingEvent; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class JdbcPatternParser { + private static final String QUESTION_MARK = "?"; + + private static final char PERCENT_CHAR = '%'; + + private final static Pattern STRING_LITERAL_PATTERN = Pattern.compile("'((?>[^']|'')+)'"); + // NOTE: capturing group work seem to work just as well. + //private final static Pattern STRING_LITERAL_PATTERN = Pattern.compile("'(([^']|'')+)'"); + + private String parameterizedSql; + final private List patternStringRepresentationList = new ArrayList(); + final private List args = new ArrayList(); + + JdbcPatternParser(String insertString) { + init(insertString); + } + + public String getParameterizedSql() { + return parameterizedSql; + } + + public List getUnmodifiablePatternStringRepresentationList() { + return Collections.unmodifiableList(patternStringRepresentationList); + } + + @Override + public String toString() { + return "JdbcPatternParser{sql=" + parameterizedSql + ",args=" + patternStringRepresentationList + "}"; + } + + /** + * Converts '....' literals into bind variables in JDBC. + */ + private void init(String insertString) { + if (insertString == null) { + throw new IllegalArgumentException("Null pattern"); + } + + Matcher m = STRING_LITERAL_PATTERN.matcher(insertString); + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String matchedStr = m.group(1); + if (matchedStr.indexOf(PERCENT_CHAR) == -1) { + replaceWithMatchedStr(m, sb); + } else { + // Replace with bind + replaceWithBind(m, sb, matchedStr); + } + } + m.appendTail(sb); + this.parameterizedSql = sb.toString(); + } + + private void replaceWithMatchedStr(Matcher m, StringBuffer sb) { + // Just literal, append it as is + m.appendReplacement(sb, "'$1'"); + } + + private void replaceWithBind(Matcher m, StringBuffer sb, String matchedStr) { + m.appendReplacement(sb, QUESTION_MARK); + // We will use prepared statements, so we don't need to escape quotes. + // And we assume the users had 'That''s a string with quotes' in their configs. + matchedStr = matchedStr.replaceAll("''", "'"); + patternStringRepresentationList.add(matchedStr); + args.add(new PatternParser(matchedStr).parse()); + } + + public void setParameters(PreparedStatement ps, LoggingEvent logEvent) throws SQLException { + for (int i = 0; i < args.size(); i++) { + final PatternConverter head = args.get(i); + String value = buildValueStr(logEvent, head); + ps.setString(i + 1, value); + } + } + + private String buildValueStr(LoggingEvent logEvent, final PatternConverter head) { + StringBuffer buffer = new StringBuffer(); + PatternConverter c = head; + while (c != null) { + c.format(buffer, logEvent); + c = c.next; + } + return buffer.toString(); + } +} -- 2.33.1