You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by gg...@apache.org on 2022/02/06 14:27:12 UTC

[logging-log4j2] branch release-2.x updated: Add more missing code and tests from Log4j 1.2.17.

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

ggregory pushed a commit to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/release-2.x by this push:
     new bbfd378  Add more missing code and tests from Log4j 1.2.17.
bbfd378 is described below

commit bbfd3788242ae4f88622227ade5c210681b5198a
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Sun Feb 6 09:27:07 2022 -0500

    Add more missing code and tests from Log4j 1.2.17.
    
    2 tests are ignored for now.
---
 .../main/java/org/apache/log4j/PatternLayout.java  |  76 ++-
 .../org/apache/log4j/helpers/FormattingInfo.java   |  39 ++
 .../org/apache/log4j/helpers/OptionConverter.java  |   3 +-
 .../org/apache/log4j/helpers/PatternConverter.java | 111 ++++
 .../org/apache/log4j/helpers/PatternParser.java    | 570 +++++++++++++++++++++
 .../src/test/java/org/apache/log4j/LayoutTest.java | 168 ++++++
 .../org/apache/log4j/PropertyConfiguratorTest.java |   2 +-
 .../org/apache/log4j/helpers/DateLayoutTest.java   | 288 +++++++++++
 .../log4j/helpers/OptionConverterTestCase.java     | 177 +++++++
 .../log4j/helpers/PatternParserTestCase.java       | 131 +++++
 .../test/java/org/apache/log4j/util/Compare.java   | 150 ++++++
 .../src/test/java/org/apache/log4j/xml/XLevel.java |  74 +++
 src/changes/changes.xml                            |   9 +
 13 files changed, 1794 insertions(+), 4 deletions(-)

diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/PatternLayout.java b/log4j-1.2-api/src/main/java/org/apache/log4j/PatternLayout.java
index e250700..da6f87f 100644
--- a/log4j-1.2-api/src/main/java/org/apache/log4j/PatternLayout.java
+++ b/log4j-1.2-api/src/main/java/org/apache/log4j/PatternLayout.java
@@ -16,8 +16,9 @@
  */
 package org.apache.log4j;
 
+import org.apache.log4j.helpers.PatternConverter;
+import org.apache.log4j.helpers.PatternParser;
 import org.apache.log4j.spi.LoggingEvent;
-import org.apache.logging.log4j.util.Strings;
 
 /**
  *
@@ -35,6 +36,17 @@ public class PatternLayout extends Layout {
      */
     public final static String TTCC_CONVERSION_PATTERN = "%r [%t] %p %c %x - %m%n";
 
+    protected final int BUF_SIZE = 256;
+
+    protected final int MAX_CAPACITY = 1024;
+
+    // output buffer appended to when format() is invoked
+    private StringBuffer sbuf = new StringBuffer(BUF_SIZE);
+
+    private String pattern;
+
+    private PatternConverter head;
+
     /**
      * Constructs a PatternLayout using the DEFAULT_LAYOUT_PATTERN.
      *
@@ -44,17 +56,77 @@ public class PatternLayout extends Layout {
         this(DEFAULT_CONVERSION_PATTERN);
     }
 
+    /**
+     * Constructs a PatternLayout using the supplied conversion pattern.
+     */
     public PatternLayout(final String pattern) {
+        this.pattern = pattern;
+        head = createPatternParser((pattern == null) ? DEFAULT_CONVERSION_PATTERN : pattern).parse();
+    }
 
+    /**
+     * Does not do anything as options become effective
+     */
+    public void activateOptions() {
+        // nothing to do.
+    }
+
+    /**
+     * Returns PatternParser used to parse the conversion string. Subclasses may override this to return a subclass of
+     * PatternParser which recognize custom conversion characters.
+     *
+     * @since 0.9.0
+     */
+    protected PatternParser createPatternParser(final String pattern) {
+        return new PatternParser(pattern);
     }
 
+    /**
+     * Produces a formatted string as specified by the conversion pattern.
+     */
     @Override
     public String format(final LoggingEvent event) {
-        return Strings.EMPTY;
+        // Reset working stringbuffer
+        if (sbuf.capacity() > MAX_CAPACITY) {
+            sbuf = new StringBuffer(BUF_SIZE);
+        } else {
+            sbuf.setLength(0);
+        }
+
+        PatternConverter c = head;
+
+        while (c != null) {
+            c.format(sbuf, event);
+            c = c.next;
+        }
+        return sbuf.toString();
+    }
+
+    /**
+     * Returns the value of the <b>ConversionPattern</b> option.
+     */
+    public String getConversionPattern() {
+        return pattern;
     }
 
+    /**
+     * The PatternLayout does not handle the throwable contained within {@link LoggingEvent LoggingEvents}. Thus, it returns
+     * <code>true</code>.
+     *
+     * @since 0.8.4
+     */
     @Override
     public boolean ignoresThrowable() {
         return true;
     }
+
+    /**
+     * Set the <b>ConversionPattern</b> option. This is the string which controls formatting and consists of a mix of
+     * literal content and conversion specifiers.
+     */
+    public void setConversionPattern(final String conversionPattern) {
+        pattern = conversionPattern;
+        head = createPatternParser(conversionPattern).parse();
+    }
+
 }
diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/FormattingInfo.java b/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/FormattingInfo.java
new file mode 100644
index 0000000..7b62660
--- /dev/null
+++ b/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/FormattingInfo.java
@@ -0,0 +1,39 @@
+/*
+ * 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.helpers;
+
+/**
+ * FormattingInfo instances contain the information obtained when parsing formatting modifiers in conversion modifiers.
+ * 
+ * @since 0.8.2
+ */
+public class FormattingInfo {
+    int min = -1;
+    int max = 0x7FFFFFFF;
+    boolean leftAlign = false;
+
+    void dump() {
+        LogLog.debug("min=" + min + ", max=" + max + ", leftAlign=" + leftAlign);
+    }
+
+    void reset() {
+        min = -1;
+        max = 0x7FFFFFFF;
+        leftAlign = false;
+    }
+}
diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/OptionConverter.java b/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/OptionConverter.java
index e4030f1..ad52737 100644
--- a/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/OptionConverter.java
+++ b/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/OptionConverter.java
@@ -30,6 +30,7 @@ import org.apache.log4j.spi.Configurator;
 import org.apache.log4j.spi.LoggerRepository;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.LoaderUtil;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.Strings;
@@ -52,7 +53,7 @@ public class OptionConverter {
     static char DELIM_STOP = '}';
     static int DELIM_START_LEN = 2;
     static int DELIM_STOP_LEN = 1;
-    private static final Logger LOGGER = LogManager.getLogger(OptionConverter.class);
+    private static final Logger LOGGER = StatusLogger.getLogger();
 
     private static final CharMap[] charMap = new CharMap[] {
         new CharMap('n', '\n'),
diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/PatternConverter.java b/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/PatternConverter.java
new file mode 100644
index 0000000..2b46db7
--- /dev/null
+++ b/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/PatternConverter.java
@@ -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.
+ */
+
+package org.apache.log4j.helpers;
+
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+
+   <p>PatternConverter is an abtract class that provides the
+   formatting functionality that derived classes need.
+
+   <p>Conversion specifiers in a conversion patterns are parsed to
+   individual PatternConverters. Each of which is responsible for
+   converting a logging event in a converter specific manner.
+
+   @author <a href="mailto:cakalijp@Maritz.com">James P. Cakalic</a>
+   @author Ceki G&uuml;lc&uuml;
+
+   @since 0.8.2
+ */
+public abstract class PatternConverter {
+  public PatternConverter next;
+  int min = -1;
+  int max = 0x7FFFFFFF;
+  boolean leftAlign = false;
+
+  protected
+  PatternConverter() {  }
+  
+  protected
+  PatternConverter(FormattingInfo fi) {
+    min = fi.min;
+    max = fi.max;
+    leftAlign = fi.leftAlign;
+  }
+
+  /**
+     Derived pattern converters must override this method in order to
+     convert conversion specifiers in the correct way.
+  */
+  abstract
+  protected
+  String convert(LoggingEvent event);
+
+  /**
+     A template method for formatting in a converter specific way.
+   */
+  public
+  void format(StringBuffer sbuf, LoggingEvent e) {
+    String s = convert(e);
+
+    if(s == null) {
+      if(0 < min)
+	spacePad(sbuf, min);
+      return;
+    }
+
+    int len = s.length();
+
+    if(len > max)
+      sbuf.append(s.substring(len-max));
+    else if(len < min) {
+      if(leftAlign) {	
+	sbuf.append(s);
+	spacePad(sbuf, min-len);
+      }
+      else {
+	spacePad(sbuf, min-len);
+	sbuf.append(s);
+      }
+    }
+    else
+      sbuf.append(s);
+  }	
+
+  static String[] SPACES = {" ", "  ", "    ", "        ", //1,2,4,8 spaces
+			    "                ", // 16 spaces
+			    "                                " }; // 32 spaces
+
+  /**
+     Fast space padding method.
+  */
+  public
+  void spacePad(StringBuffer sbuf, int length) {
+    while(length >= 32) {
+      sbuf.append(SPACES[5]);
+      length -= 32;
+    }
+    
+    for(int i = 4; i >= 0; i--) {	
+      if((length & (1<<i)) != 0) {
+	sbuf.append(SPACES[i]);
+      }
+    }
+  }
+}
diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/PatternParser.java b/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/PatternParser.java
new file mode 100644
index 0000000..0d3ead6
--- /dev/null
+++ b/log4j-1.2-api/src/main/java/org/apache/log4j/helpers/PatternParser.java
@@ -0,0 +1,570 @@
+/*
+ * 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.helpers;
+
+import org.apache.log4j.Layout;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.LocationInfo;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.Arrays;
+
+// Contributors:   Nelson Minar <(n...@monkey.org>
+//                 Igor E. Poteryaev <ja...@mail.ru>
+//                 Reinhard Deschler <re...@web.de>
+
+/**
+   Most of the work of the {@link org.apache.log4j.PatternLayout} class
+   is delegated to the PatternParser class.
+
+   <p>It is this class that parses conversion patterns and creates
+   a chained list of {@link OptionConverter OptionConverters}.
+
+   @author <a href=mailto:"cakalijp@Maritz.com">James P. Cakalic</a>
+   @author Ceki G&uuml;lc&uuml;
+   @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;
+    }
+  }
+
+  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;
+	}
+      }
+      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;
+	}
+	break;
+      } // switch
+    } // while
+    if(currentLiteral.length() != 0) {
+      addToList(new LiteralPatternConverter(currentLiteral.toString()));
+      //LogLog.debug("Parsed LITERAL converter: \""+currentLiteral+"\".");
+    }
+    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);
+	}
+      }
+      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);
+  }
+
+  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;
+    }
+
+    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 LiteralPatternConverter extends PatternConverter {
+    private String literal;
+
+    LiteralPatternConverter(String value) {
+      literal = value;
+    }
+
+    public
+    final
+    void format(StringBuffer sbuf, LoggingEvent event) {
+      sbuf.append(literal);
+    }
+
+    public
+    String convert(LoggingEvent event) {
+      return literal;
+    }
+  }
+
+  private static class DatePatternConverter extends PatternConverter {
+    private DateFormat df;
+    private Date date;
+
+    DatePatternConverter(FormattingInfo formattingInfo, DateFormat df) {
+      super(formattingInfo);
+      date = new Date();
+      this.df = df;
+    }
+
+    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;
+
+    MDCPatternConverter(FormattingInfo formattingInfo, String key) {
+      super(formattingInfo);
+      this.key = key;
+    }
+
+    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();
+        }
+      }
+    }
+  }
+
+
+  private class LocationPatternConverter extends PatternConverter {
+    int type;
+
+    LocationPatternConverter(FormattingInfo formattingInfo, int type) {
+      super(formattingInfo);
+      this.type = type;
+    }
+
+    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 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);
+      }
+    }
+  }
+
+  private class ClassNamePatternConverter extends NamedPatternConverter {
+
+    ClassNamePatternConverter(FormattingInfo formattingInfo, int precision) {
+      super(formattingInfo, precision);
+    }
+
+    String getFullyQualifiedName(LoggingEvent event) {
+      return event.getLocationInformation().getClassName();
+    }
+  }
+
+  private class CategoryPatternConverter extends NamedPatternConverter {
+
+    CategoryPatternConverter(FormattingInfo formattingInfo, int precision) {
+      super(formattingInfo, precision);
+    }
+
+    String getFullyQualifiedName(LoggingEvent event) {
+      return event.getLoggerName();
+    }
+  }
+}
+
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/LayoutTest.java b/log4j-1.2-api/src/test/java/org/apache/log4j/LayoutTest.java
new file mode 100644
index 0000000..991c3e9
--- /dev/null
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/LayoutTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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;
+
+import org.apache.log4j.spi.LoggingEvent;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for Layout.
+ *
+ */
+public class LayoutTest extends TestCase {
+
+    /**
+     * Concrete Layout class for tests.
+     */
+    private static final class MockLayout extends Layout {
+        /**
+         * @{inheritDoc}
+         */
+        public void activateOptions() {
+        }
+
+        /**
+         * @{inheritDoc}
+         */
+        public String format(final LoggingEvent event) {
+            return "Mock";
+        }
+
+        /**
+         * @{inheritDoc}
+         */
+        public boolean ignoresThrowable() {
+            return true;
+        }
+    }
+
+    /**
+     * Expected content type.
+     */
+    private final String contentType;
+
+    /**
+     * Expected value for ignoresThrowable.
+     */
+    private final boolean ignoresThrowable;
+
+    /**
+     * Expected value for header.
+     */
+    private final String header;
+
+    /**
+     * Expected value for footer.
+     */
+    private final String footer;
+
+    /**
+     * Construct a new instance of LayoutTest.
+     * 
+     * @param testName test name.
+     */
+    public LayoutTest(final String testName) {
+        super(testName);
+        contentType = "text/plain";
+        ignoresThrowable = true;
+        header = null;
+        footer = null;
+    }
+
+    /**
+     * Constructor for use by derived tests.
+     * 
+     * @param testName name of test.
+     * @param expectedContentType expected value for getContentType().
+     * @param expectedIgnoresThrowable expected value for ignoresThrowable().
+     * @param expectedHeader expected value for getHeader().
+     * @param expectedFooter expected value for getFooter().
+     */
+    protected LayoutTest(final String testName, final String expectedContentType, final boolean expectedIgnoresThrowable, final String expectedHeader,
+        final String expectedFooter) {
+        super(testName);
+        contentType = expectedContentType;
+        ignoresThrowable = expectedIgnoresThrowable;
+        header = expectedHeader;
+        footer = expectedFooter;
+    }
+
+    /**
+     * Creates layout for test.
+     * 
+     * @return new instance of Layout.
+     */
+    protected Layout createLayout() {
+        return new MockLayout();
+    }
+
+    /**
+     * Tests format.
+     * 
+     * @throws Exception derived tests, particular XMLLayoutTest, may throw exceptions.
+     */
+    public void testFormat() throws Exception {
+        Logger logger = Logger.getLogger("org.apache.log4j.LayoutTest");
+        LoggingEvent event = new LoggingEvent("org.apache.log4j.Logger", logger, Level.INFO, "Hello, World", null);
+        String result = createLayout().format(event);
+        assertEquals("Mock", result);
+    }
+
+    /**
+     * Tests getContentType.
+     */
+    public void testGetContentType() {
+        assertEquals(contentType, createLayout().getContentType());
+    }
+
+    /**
+     * Tests getFooter.
+     */
+    public void testGetFooter() {
+        assertEquals(footer, createLayout().getFooter());
+    }
+
+    /**
+     * Tests getHeader.
+     */
+    public void testGetHeader() {
+        assertEquals(header, createLayout().getHeader());
+    }
+
+    /**
+     * Tests ignoresThrowable.
+     */
+    public void testIgnoresThrowable() {
+        assertEquals(ignoresThrowable, createLayout().ignoresThrowable());
+    }
+
+    /**
+     * Tests Layout.LINE_SEP.
+     */
+    public void testLineSep() {
+        assertEquals(System.getProperty("line.separator"), Layout.LINE_SEP);
+    }
+
+    /**
+     * Tests Layout.LINE_SEP.
+     */
+    public void testLineSepLen() {
+        assertEquals(Layout.LINE_SEP.length(), Layout.LINE_SEP_LEN);
+    }
+}
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/PropertyConfiguratorTest.java b/log4j-1.2-api/src/test/java/org/apache/log4j/PropertyConfiguratorTest.java
index 26812ec..347bfd0 100644
--- a/log4j-1.2-api/src/test/java/org/apache/log4j/PropertyConfiguratorTest.java
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/PropertyConfiguratorTest.java
@@ -406,7 +406,7 @@ public class PropertyConfiguratorTest {
         assertFalse(file.exists());
     }
 
-    void validateNested() {
+    public void validateNested() {
         final Logger logger = Logger.getLogger("org.apache.log4j.PropertyConfiguratorTest");
         final String appenderName = "ROLLING";
         // Appender OK
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/helpers/DateLayoutTest.java b/log4j-1.2-api/src/test/java/org/apache/log4j/helpers/DateLayoutTest.java
new file mode 100644
index 0000000..0573e78
--- /dev/null
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/helpers/DateLayoutTest.java
@@ -0,0 +1,288 @@
+/*
+ * 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.helpers;
+
+import org.apache.log4j.Layout;
+import org.apache.log4j.LayoutTest;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+
+import java.util.TimeZone;
+import java.util.Calendar;
+
+/**
+ * Tests {@link DateLayout}.
+ */
+public class DateLayoutTest extends LayoutTest {
+
+    /**
+     * Construct a new instance of LayoutTest.
+     * 
+     * @param testName test name.
+     */
+    public DateLayoutTest(final String testName) {
+        super(testName);
+    }
+
+    /**
+     * Constructor for use by derived tests.
+     * 
+     * @param testName name of test.
+     * @param expectedContentType expected value for getContentType().
+     * @param expectedIgnoresThrowable expected value for ignoresThrowable().
+     * @param expectedHeader expected value for getHeader().
+     * @param expectedFooter expected value for getFooter().
+     */
+    protected DateLayoutTest(final String testName, final String expectedContentType, final boolean expectedIgnoresThrowable, final String expectedHeader,
+        final String expectedFooter) {
+        super(testName, expectedContentType, expectedIgnoresThrowable, expectedHeader, expectedFooter);
+    }
+
+    /**
+     * @{inheritDoc}
+     */
+    protected Layout createLayout() {
+        return new MockLayout();
+    }
+
+    /**
+     * Tests DateLayout.NULL_DATE_FORMAT constant.
+     */
+    public void testNullDateFormat() {
+        assertEquals("NULL", DateLayout.NULL_DATE_FORMAT);
+    }
+
+    /**
+     * Tests DateLayout.RELATIVE constant.
+     */
+    public void testRelativeTimeDateFormat() {
+        assertEquals("RELATIVE", DateLayout.RELATIVE_TIME_DATE_FORMAT);
+    }
+
+    /**
+     * Tests DateLayout.DATE_FORMAT_OPTION constant.
+     * 
+     * @deprecated since constant is deprecated
+     */
+    public void testDateFormatOption() {
+        assertEquals("DateFormat", DateLayout.DATE_FORMAT_OPTION);
+    }
+
+    /**
+     * Tests DateLayout.TIMEZONE_OPTION constant.
+     * 
+     * @deprecated since constant is deprecated
+     */
+    public void testTimeZoneOption() {
+        assertEquals("TimeZone", DateLayout.TIMEZONE_OPTION);
+    }
+
+    /**
+     * Tests getOptionStrings().
+     * 
+     * @deprecated since getOptionStrings is deprecated.
+     *
+     */
+    public void testGetOptionStrings() {
+        String[] options = ((DateLayout) createLayout()).getOptionStrings();
+        assertEquals(2, options.length);
+    }
+
+    /**
+     * Tests setting DateFormat through setOption method.
+     * 
+     * @deprecated since setOption is deprecated.
+     */
+    public void testSetOptionDateFormat() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setOption("dAtefOrmat", "foobar");
+        assertEquals("FOOBAR", layout.getDateFormat());
+    }
+
+    /**
+     * Tests setting TimeZone through setOption method.
+     * 
+     * @deprecated since setOption is deprecated.
+     */
+    public void testSetOptionTimeZone() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setOption("tImezOne", "+05:00");
+        assertEquals("+05:00", layout.getTimeZone());
+    }
+
+    /**
+     * Tests setDateFormat.
+     */
+    public void testSetDateFormat() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat("ABSOLUTE");
+        assertEquals("ABSOLUTE", layout.getDateFormat());
+    }
+
+    /**
+     * Tests setTimeZone.
+     */
+    public void testSetTimeZone() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setTimeZone("+05:00");
+        assertEquals("+05:00", layout.getTimeZone());
+    }
+
+    /**
+     * Tests 2 parameter setDateFormat with null.
+     */
+    public void testSetDateFormatNull() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat((String) null, null);
+    }
+
+    /**
+     * Tests 2 parameter setDateFormat with "NULL".
+     */
+    public void testSetDateFormatNullString() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat("NuLL", null);
+    }
+
+    /**
+     * Tests 2 parameter setDateFormat with "RELATIVE".
+     */
+    public void testSetDateFormatRelative() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat("rElatIve", TimeZone.getDefault());
+    }
+
+    /**
+     * Tests 2 parameter setDateFormat with "ABSOLUTE".
+     */
+    public void testSetDateFormatAbsolute() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat("aBsolUte", TimeZone.getDefault());
+    }
+
+    /**
+     * Tests 2 parameter setDateFormat with "DATETIME".
+     */
+    public void testSetDateFormatDateTime() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat("dAte", TimeZone.getDefault());
+    }
+
+    /**
+     * Tests 2 parameter setDateFormat with "ISO8601".
+     */
+    public void testSetDateFormatISO8601() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat("iSo8601", TimeZone.getDefault());
+    }
+
+    /**
+     * Tests 2 parameter setDateFormat with "HH:mm:ss".
+     */
+    public void testSetDateFormatSimple() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat("HH:mm:ss", TimeZone.getDefault());
+    }
+
+    /**
+     * Tests activateOptions.
+     */
+    public void testActivateOptions() {
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat("HH:mm:ss");
+        layout.setTimeZone("+05:00");
+        layout.activateOptions();
+    }
+
+    /**
+     * Tests setDateFormat(DateFormat, TimeZone).
+     */
+    public void testSetDateFormatWithFormat() {
+        DateFormat format = new SimpleDateFormat("HH:mm");
+        DateLayout layout = (DateLayout) createLayout();
+        layout.setDateFormat(format, TimeZone.getDefault());
+    }
+
+    /**
+     * Tests IS08601DateFormat class.
+     * 
+     * @deprecated since ISO8601DateFormat is deprecated
+     */
+    public void testISO8601Format() {
+        DateFormat format = new ISO8601DateFormat();
+        Calendar calendar = Calendar.getInstance();
+        calendar.clear();
+        calendar.set(1970, 0, 1, 0, 0, 0);
+        String actual = format.format(calendar.getTime());
+        assertEquals("1970-01-01 00:00:00,000", actual);
+    }
+
+    /**
+     * Tests DateTimeDateFormat class.
+     * 
+     * @deprecated since DateTimeDateFormat is deprecated
+     */
+    public void testDateTimeFormat() {
+        DateFormat format = new DateTimeDateFormat();
+        Calendar calendar = Calendar.getInstance();
+        calendar.clear();
+        calendar.set(1970, 0, 1, 0, 0, 0);
+        String actual = format.format(calendar.getTime());
+        SimpleDateFormat df = new SimpleDateFormat("dd MMM yyyy HH:mm:ss,SSS");
+        String expected = df.format(calendar.getTime());
+        assertEquals(expected, actual);
+    }
+
+    /**
+     * Concrete Layout class for tests.
+     */
+    private static final class MockLayout extends DateLayout {
+        /**
+         * Create new instance of MockLayout.
+         */
+        public MockLayout() {
+            //
+            // checks that protected fields are properly initialized
+            assertNotNull(pos);
+            assertNotNull(date);
+            assertNull(dateFormat);
+        }
+
+        /**
+         * @{inheritDoc}
+         */
+        public String format(final LoggingEvent event) {
+            return "Mock";
+        }
+
+        /**
+         * @{inheritDoc}
+         */
+        public void activateOptions() {
+        }
+
+        /**
+         * @{inheritDoc}
+         */
+        public boolean ignoresThrowable() {
+            return true;
+        }
+    }
+}
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/helpers/OptionConverterTestCase.java b/log4j-1.2-api/src/test/java/org/apache/log4j/helpers/OptionConverterTestCase.java
new file mode 100644
index 0000000..83ded20
--- /dev/null
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/helpers/OptionConverterTestCase.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Log4j uses the JUnit framework for internal unit testing. JUnit
+// is available from "http://www.junit.org".
+
+package org.apache.log4j.helpers;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.PropertyConfiguratorTest;
+import org.apache.log4j.xml.XLevel;
+import org.junit.Ignore;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Test variable substitution code.
+ * 
+ * @since 1.0
+ */
+@Ignore("WIP")
+public class OptionConverterTestCase extends TestCase {
+
+    Properties props;
+
+    public OptionConverterTestCase(String name) {
+        super(name);
+    }
+
+    public void setUp() {
+        props = new Properties();
+        props.put("TOTO", "wonderful");
+        props.put("key1", "value1");
+        props.put("key2", "value2");
+        // Log4J will NPE without this:
+        props.put("line.separator", System.getProperty("line.separator"));
+        // Log4J will throw an Error without this:
+        props.put("java.home", System.getProperty("java.home"));
+        System.setProperties(props);
+
+    }
+
+    public void tearDown() {
+        props = null;
+        LogManager.resetConfiguration();
+    }
+
+    public void varSubstTest1() {
+        String r;
+
+        r = OptionConverter.substVars("hello world.", null);
+        assertEquals("hello world.", r);
+
+        r = OptionConverter.substVars("hello ${TOTO} world.", null);
+
+        assertEquals("hello wonderful world.", r);
+    }
+
+    public void varSubstTest2() {
+        String r;
+
+        r = OptionConverter.substVars("Test2 ${key1} mid ${key2} end.", null);
+        assertEquals("Test2 value1 mid value2 end.", r);
+    }
+
+    public void varSubstTest3() {
+        String r;
+
+        r = OptionConverter.substVars("Test3 ${unset} mid ${key1} end.", null);
+        assertEquals("Test3  mid value1 end.", r);
+    }
+
+    public void varSubstTest4() {
+        String val = "Test4 ${incomplete ";
+        try {
+            OptionConverter.substVars(val, null);
+        } catch (IllegalArgumentException e) {
+            String errorMsg = e.getMessage();
+            // System.out.println('['+errorMsg+']');
+            assertEquals('"' + val + "\" has no closing brace. Opening brace at position 6.", errorMsg);
+        }
+    }
+
+    public void varSubstTest5() {
+        Properties props = new Properties();
+        props.put("p1", "x1");
+        props.put("p2", "${p1}");
+        String res = OptionConverter.substVars("${p2}", props);
+        System.out.println("Result is [" + res + "].");
+        assertEquals(res, "x1");
+    }
+
+    /**
+     * Tests configuring Log4J from an InputStream.
+     * 
+     * @since 1.2.17
+     */
+    public void testInputStream() throws IOException {
+        File file = new File("src/test/resources/log4j1-1.2.17/input/filter1.properties");
+        assertTrue(file.exists());
+        try (FileInputStream inputStream = new FileInputStream(file)) {
+            OptionConverter.selectAndConfigure(inputStream, null, LogManager.getLoggerRepository());
+        }
+        new PropertyConfiguratorTest().validateNested();
+    }
+
+    public void toLevelTest1() {
+        String val = "INFO";
+        Level p = OptionConverter.toLevel(val, null);
+        assertEquals(p, Level.INFO);
+    }
+
+    public void toLevelTest2() {
+        String val = "INFO#org.apache.log4j.xml.XLevel";
+        Level p = OptionConverter.toLevel(val, null);
+        assertEquals(p, Level.INFO);
+    }
+
+    public void toLevelTest3() {
+        String val = "TRACE#org.apache.log4j.xml.XLevel";
+        Level p = OptionConverter.toLevel(val, null);
+        assertEquals(p, XLevel.TRACE);
+    }
+
+    public void toLevelTest4() {
+        String val = "TR#org.apache.log4j.xml.XLevel";
+        Level p = OptionConverter.toLevel(val, null);
+        assertEquals(p, null);
+    }
+
+    public void toLevelTest5() {
+        String val = "INFO#org.apache.log4j.xml.TOTO";
+        Level p = OptionConverter.toLevel(val, null);
+        assertEquals(p, null);
+    }
+
+    public static Test suite() {
+        TestSuite suite = new TestSuite();
+        suite.addTest(new OptionConverterTestCase("varSubstTest5"));
+        suite.addTest(new OptionConverterTestCase("varSubstTest1"));
+        suite.addTest(new OptionConverterTestCase("varSubstTest2"));
+        suite.addTest(new OptionConverterTestCase("varSubstTest3"));
+        suite.addTest(new OptionConverterTestCase("varSubstTest4"));
+
+        suite.addTest(new OptionConverterTestCase("testInputStream"));
+
+        suite.addTest(new OptionConverterTestCase("toLevelTest1"));
+        suite.addTest(new OptionConverterTestCase("toLevelTest2"));
+        suite.addTest(new OptionConverterTestCase("toLevelTest3"));
+        suite.addTest(new OptionConverterTestCase("toLevelTest4"));
+        suite.addTest(new OptionConverterTestCase("toLevelTest5"));
+        return suite;
+    }
+
+}
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/helpers/PatternParserTestCase.java b/log4j-1.2-api/src/test/java/org/apache/log4j/helpers/PatternParserTestCase.java
new file mode 100644
index 0000000..e91cc0f
--- /dev/null
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/helpers/PatternParserTestCase.java
@@ -0,0 +1,131 @@
+/*
+ * 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.helpers;
+
+import org.apache.log4j.Appender;
+import org.apache.log4j.FileAppender;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.MDC;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.util.Compare;
+import org.junit.Ignore;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Test case for helpers/PatternParser.java. Tests the various conversion patterns supported by PatternParser. This test
+ * class tests PatternParser via the PatternLayout class which uses it.
+ */
+@Ignore("WIP")
+public class PatternParserTestCase extends TestCase {
+
+    static String OUTPUT_FILE = "target/PatternParser";
+    static String WITNESS_FILE = "target/witness/PatternParser";
+
+    static String msgPattern = "%m%n";
+
+    public static Test suite() {
+        TestSuite suite = new TestSuite();
+        suite.addTest(new PatternParserTestCase("mdcPattern"));
+        return suite;
+    }
+    Logger root;
+
+    Logger logger;
+
+    public PatternParserTestCase(String name) {
+        super(name);
+    }
+
+    /**
+     * Test case for MDC conversion pattern.
+     */
+    public void mdcPattern() throws Exception {
+
+        String mdcMsgPattern1 = "%m : %X%n";
+        String mdcMsgPattern2 = "%m : %X{key1}%n";
+        String mdcMsgPattern3 = "%m : %X{key2}%n";
+        String mdcMsgPattern4 = "%m : %X{key3}%n";
+        String mdcMsgPattern5 = "%m : %X{key1},%X{key2},%X{key3}%n";
+
+        // set up appender
+        PatternLayout layout = new PatternLayout(msgPattern);
+        Appender appender = new FileAppender(layout, OUTPUT_FILE + "_mdc", false);
+
+        // set appender on root and set level to debug
+        root.addAppender(appender);
+        root.setLevel(Level.DEBUG);
+
+        // output starting message
+        root.debug("starting mdc pattern test");
+
+        layout.setConversionPattern(mdcMsgPattern1);
+        root.debug("empty mdc, no key specified in pattern");
+
+        layout.setConversionPattern(mdcMsgPattern2);
+        root.debug("empty mdc, key1 in pattern");
+
+        layout.setConversionPattern(mdcMsgPattern3);
+        root.debug("empty mdc, key2 in pattern");
+
+        layout.setConversionPattern(mdcMsgPattern4);
+        root.debug("empty mdc, key3 in pattern");
+
+        layout.setConversionPattern(mdcMsgPattern5);
+        root.debug("empty mdc, key1, key2, and key3 in pattern");
+
+        MDC.put("key1", "value1");
+        MDC.put("key2", "value2");
+
+        layout.setConversionPattern(mdcMsgPattern1);
+        root.debug("filled mdc, no key specified in pattern");
+
+        layout.setConversionPattern(mdcMsgPattern2);
+        root.debug("filled mdc, key1 in pattern");
+
+        layout.setConversionPattern(mdcMsgPattern3);
+        root.debug("filled mdc, key2 in pattern");
+
+        layout.setConversionPattern(mdcMsgPattern4);
+        root.debug("filled mdc, key3 in pattern");
+
+        layout.setConversionPattern(mdcMsgPattern5);
+        root.debug("filled mdc, key1, key2, and key3 in pattern");
+
+        MDC.remove("key1");
+        MDC.remove("key2");
+
+        layout.setConversionPattern(msgPattern);
+        root.debug("finished mdc pattern test");
+
+        assertTrue(Compare.compare(OUTPUT_FILE + "_mdc", WITNESS_FILE + "_mdc"));
+    }
+
+    public void setUp() {
+        root = Logger.getRootLogger();
+        root.removeAllAppenders();
+    }
+
+    public void tearDown() {
+        root.getLoggerRepository().resetConfiguration();
+    }
+
+}
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/util/Compare.java b/log4j-1.2-api/src/test/java/org/apache/log4j/util/Compare.java
new file mode 100644
index 0000000..b65b9e9
--- /dev/null
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/util/Compare.java
@@ -0,0 +1,150 @@
+/*
+ * 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.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class Compare {
+
+    static final int B1_NULL = -1;
+    static final int B2_NULL = -2;
+
+    public static boolean compare(final Class testClass, final String file1, final String file2) throws IOException {
+        try (final BufferedReader in1 = new BufferedReader(new FileReader(file1));
+            final BufferedReader in2 = new BufferedReader(new InputStreamReader(open(testClass, file2)))) {
+            return compare(testClass, file1, file2, in1, in2);
+        }
+    }
+
+    public static boolean compare(final Class testClass, final String file1, final String file2, final BufferedReader in1, final BufferedReader in2)
+        throws IOException {
+
+        String s1;
+        int lineCounter = 0;
+
+        while ((s1 = in1.readLine()) != null) {
+            lineCounter++;
+
+            final String s2 = in2.readLine();
+
+            if (!s1.equals(s2)) {
+                System.out.println("Files [" + file1 + "] and [" + file2 + "] differ on line " + lineCounter);
+                System.out.println("One reads:  [" + s1 + "].");
+                System.out.println("Other reads:[" + s2 + "].");
+                outputFile(testClass, file1);
+                outputFile(testClass, file2);
+
+                return false;
+            }
+        }
+
+        // the second file is longer
+        if (in2.read() != -1) {
+            System.out.println("File [" + file2 + "] longer than file [" + file1 + "].");
+            outputFile(testClass, file1);
+            outputFile(testClass, file2);
+
+            return false;
+        }
+
+        return true;
+    }
+
+    static public boolean compare(final String file1, final String file2) throws FileNotFoundException, IOException {
+        try (final BufferedReader in1 = new BufferedReader(new FileReader(file1)); final BufferedReader in2 = new BufferedReader(new FileReader(file2))) {
+
+            String s1;
+            int lineCounter = 0;
+            while ((s1 = in1.readLine()) != null) {
+                lineCounter++;
+                final String s2 = in2.readLine();
+                if (!s1.equals(s2)) {
+                    System.out.println("Files [" + file1 + "] and [" + file2 + "] differ on line " + lineCounter);
+                    System.out.println("One reads:  [" + s1 + "].");
+                    System.out.println("Other reads:[" + s2 + "].");
+                    return false;
+                }
+            }
+
+            // the second file is longer
+            if (in2.read() != -1) {
+                System.out.println("File [" + file2 + "] longer than file [" + file1 + "].");
+                return false;
+            }
+
+            return true;
+        }
+    }
+
+    private static final InputStream open(final Class testClass, final String fileName) throws IOException {
+        String resourceName = fileName;
+        if (fileName.startsWith("witness/")) {
+            resourceName = fileName.substring(fileName.lastIndexOf('/') + 1);
+        }
+        InputStream is = testClass.getResourceAsStream(resourceName);
+        if (is == null) {
+            final File file = new File(fileName);
+            if (file.exists()) {
+                is = new FileInputStream(file);
+            } else {
+                throw new FileNotFoundException("Resource " + resourceName + " not found");
+            }
+        }
+        return is;
+    }
+
+    /**
+     *
+     * Prints file on the console.
+     *
+     */
+    private static void outputFile(final Class testClass, final String file) throws IOException {
+        try (final InputStream is = open(testClass, file); final BufferedReader in1 = new BufferedReader(new InputStreamReader(is))) {
+
+            String s1;
+            int lineCounter = 0;
+            System.out.println("--------------------------------");
+            System.out.println("Contents of " + file + ":");
+
+            while ((s1 = in1.readLine()) != null) {
+                lineCounter++;
+                System.out.print(lineCounter);
+
+                if (lineCounter < 10) {
+                    System.out.print("   : ");
+                } else if (lineCounter < 100) {
+                    System.out.print("  : ");
+                } else if (lineCounter < 1000) {
+                    System.out.print(" : ");
+                } else {
+                    System.out.print(": ");
+                }
+
+                System.out.println(s1);
+            }
+        }
+    }
+
+}
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/xml/XLevel.java b/log4j-1.2-api/src/test/java/org/apache/log4j/xml/XLevel.java
new file mode 100644
index 0000000..b0503b7
--- /dev/null
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/xml/XLevel.java
@@ -0,0 +1,74 @@
+/*
+ * 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.xml;
+
+import org.apache.log4j.Level;
+
+/**
+ * This class introduces a new level level called TRACE. TRACE has lower level than DEBUG.
+ */
+public class XLevel extends Level {
+    private static final long serialVersionUID = 7288304330257085144L;
+
+    public static final int TRACE_INT = Level.DEBUG_INT - 1;
+    public static final int LETHAL_INT = Level.FATAL_INT + 1;
+
+    private static String TRACE_STR = "TRACE";
+    private static String LETHAL_STR = "LETHAL";
+
+    public static final XLevel TRACE = new XLevel(TRACE_INT, TRACE_STR, 7);
+    public static final XLevel LETHAL = new XLevel(LETHAL_INT, LETHAL_STR, 0);
+
+    public static Level toLevel(final int i) throws IllegalArgumentException {
+        switch (i) {
+        case TRACE_INT:
+            return XLevel.TRACE;
+        case LETHAL_INT:
+            return XLevel.LETHAL;
+        }
+        return Level.toLevel(i);
+    }
+
+    /**
+     * Convert the string passed as argument to a level. If the conversion fails, then this method returns {@link #TRACE}.
+     */
+    public static Level toLevel(final String sArg) {
+        return toLevel(sArg, XLevel.TRACE);
+    }
+
+    public static Level toLevel(final String sArg, final Level defaultValue) {
+
+        if (sArg == null) {
+            return defaultValue;
+        }
+        final String stringVal = sArg.toUpperCase();
+
+        if (stringVal.equals(TRACE_STR)) {
+            return XLevel.TRACE;
+        } else if (stringVal.equals(LETHAL_STR)) {
+            return XLevel.LETHAL;
+        }
+
+        return Level.toLevel(sArg, defaultValue);
+    }
+
+    protected XLevel(final int level, final String strLevel, final int syslogEquiv) {
+        super(level, strLevel, syslogEquiv);
+    }
+
+}
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 2a78d4b..c59773a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -184,6 +184,15 @@
       <action dev="ggregory" type="fix" due-to="Gary Gregory">
         Log4j 1.2 bridge missing UtilLoggingLevel.
       </action>
+      <action dev="ggregory" type="fix" due-to="Gary Gregory">
+        Log4j 1.2 bridge missing FormattingInfo.
+      </action>
+      <action dev="ggregory" type="fix" due-to="Gary Gregory">
+        Log4j 1.2 bridge missing PatternConverter.
+      </action>
+      <action dev="ggregory" type="fix" due-to="Gary Gregory">
+        Log4j 1.2 bridge missing PatternParser.
+      </action>
       <action dev="ggregory" type="fix">
         JndiManager reverts to 2.17.0 behavior: Read the system property for each call.
       </action>