new file mode 100644
--- /dev/null
+++ b/library/log4php/helpers/LoggerPatternParser.php
@@ -0,0 +1,237 @@
+<?php
+/**
+ * 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 log4php
+ */
+
+/**
+ * Most of the work of the {@link LoggerPatternLayout} class
+ * is delegated to the {@link LoggerPatternParser} class.
+ *
+ * <p>It is this class that parses conversion patterns and creates
+ * a chained list of {@link LoggerPatternConverter} converters.</p>
+ *
+ * @version $Revision: 1395467 $
+ * @package log4php
+ * @subpackage helpers
+ *
+ * @since 0.3
+ */
+class LoggerPatternParser {
+
+ /** Escape character for conversion words in the conversion pattern. */
+ const ESCAPE_CHAR = '%';
+
+ /** Maps conversion words to relevant converters. */
+ private $converterMap;
+
+ /** Conversion pattern used in layout. */
+ private $pattern;
+
+ /** Regex pattern used for parsing the conversion pattern. */
+ private $regex;
+
+ /**
+ * First converter in the chain.
+ * @var LoggerPatternConverter
+ */
+ private $head;
+
+ /** Last converter in the chain. */
+ private $tail;
+
+ public function __construct($pattern, $converterMap) {
+ $this->pattern = $pattern;
+ $this->converterMap = $converterMap;
+
+ // Construct the regex pattern
+ $this->regex =
+ '/' . // Starting regex pattern delimiter
+ self::ESCAPE_CHAR . // Character which marks the start of the conversion pattern
+ '(?P<modifiers>[0-9.-]*)' . // Format modifiers (optional)
+ '(?P<word>[a-zA-Z]+)' . // The conversion word
+ '(?P<option>{[^}]*})?' . // Conversion option in braces (optional)
+ '/'; // Ending regex pattern delimiter
+ }
+
+ /**
+ * Parses the conversion pattern string, converts it to a chain of pattern
+ * converters and returns the first converter in the chain.
+ *
+ * @return LoggerPatternConverter
+ */
+ public function parse() {
+
+ // Skip parsing if the pattern is empty
+ if (empty($this->pattern)) {
+ $this->addLiteral('');
+ return $this->head;
+ }
+
+ // Find all conversion words in the conversion pattern
+ $count = preg_match_all($this->regex, $this->pattern, $matches, PREG_OFFSET_CAPTURE);
+ if ($count === false) {
+ $error = error_get_last();
+ throw new LoggerException("Failed parsing layotut pattern: {$error['message']}");
+ }
+
+ $prevEnd = 0;
+
+ foreach ($matches[0] as $key => $item) {
+
+ // Locate where the conversion command starts and ends
+ $length = strlen($item[0]);
+ $start = $item[1];
+ $end = $item[1] + $length;
+
+ // Find any literal expressions between matched commands
+ if ($start > $prevEnd) {
+ $literal = substr($this->pattern, $prevEnd, $start - $prevEnd);
+ $this->addLiteral($literal);
+ }
+
+ // Extract the data from the matched command
+ $word = !empty($matches['word'][$key]) ? $matches['word'][$key][0] : null;
+ $modifiers = !empty($matches['modifiers'][$key]) ? $matches['modifiers'][$key][0] : null;
+ $option = !empty($matches['option'][$key]) ? $matches['option'][$key][0] : null;
+
+ // Create a converter and add it to the chain
+ $this->addConverter($word, $modifiers, $option);
+
+ $prevEnd = $end;
+ }
+
+ // Add any trailing literals
+ if ($end < strlen($this->pattern)) {
+ $literal = substr($this->pattern, $end);
+ $this->addLiteral($literal);
+ }
+
+ return $this->head;
+ }
+
+ /**
+ * Adds a literal converter to the converter chain.
+ * @param string $string The string for the literal converter.
+ */
+ private function addLiteral($string) {
+ $converter = new LoggerPatternConverterLiteral($string);
+ $this->addToChain($converter);
+ }
+
+ /**
+ * Adds a non-literal converter to the converter chain.
+ *
+ * @param string $word The conversion word, used to determine which
+ * converter will be used.
+ * @param string $modifiers Formatting modifiers.
+ * @param string $option Option to pass to the converter.
+ */
+ private function addConverter($word, $modifiers, $option) {
+ $formattingInfo = $this->parseModifiers($modifiers);
+ $option = trim($option, "{} ");
+
+ if (isset($this->converterMap[$word])) {
+ $converter = $this->getConverter($word, $formattingInfo, $option);
+ $this->addToChain($converter);
+ } else {
+ trigger_error("log4php: Invalid keyword '%$word' in converison pattern. Ignoring keyword.", E_USER_WARNING);
+ }
+ }
+
+ /**
+ * Determines which converter to use based on the conversion word. Creates
+ * an instance of the converter using the provided formatting info and
+ * option and returns it.
+ *
+ * @param string $word The conversion word.
+ * @param LoggerFormattingInfo $info Formatting info.
+ * @param string $option Converter option.
+ *
+ * @throws LoggerException
+ *
+ * @return LoggerPatternConverter
+ */
+ private function getConverter($word, $info, $option) {
+ if (!isset($this->converterMap[$word])) {
+ throw new LoggerException("Invalid keyword '%$word' in converison pattern. Ignoring keyword.");
+ }
+
+ $converterClass = $this->converterMap[$word];
+ if (!class_exists($converterClass)) {
+ throw new LoggerException("Class '$converterClass' does not exist.");
+ }
+
+ $converter = new $converterClass($info, $option);
+ if (!($converter instanceof LoggerPatternConverter)) {
+ throw new LoggerException("Class '$converterClass' is not an instance of LoggerPatternConverter.");
+ }
+
+ return $converter;
+ }
+
+ /** Adds a converter to the chain and updates $head and $tail pointers. */
+ private function addToChain(LoggerPatternConverter $converter) {
+ if (!isset($this->head)) {
+ $this->head = $converter;
+ $this->tail = $this->head;
+ } else {
+ $this->tail->next = $converter;
+ $this->tail = $this->tail->next;
+ }
+ }
+
+ /**
+ * Parses the formatting modifiers and produces the corresponding
+ * LoggerFormattingInfo object.
+ *
+ * @param string $modifier
+ * @return LoggerFormattingInfo
+ * @throws LoggerException
+ */
+ private function parseModifiers($modifiers) {
+ $info = new LoggerFormattingInfo();
+
+ // If no modifiers are given, return default values
+ if (empty($modifiers)) {
+ return $info;
+ }
+
+ // Validate
+ $pattern = '/^(-?[0-9]+)?\.?-?[0-9]+$/';
+ if (!preg_match($pattern, $modifiers)) {
+ trigger_error("log4php: Invalid modifier in conversion pattern: [$modifiers]. Ignoring modifier.", E_USER_WARNING);
+ return $info;
+ }
+
+ $parts = explode('.', $modifiers);
+
+ if (!empty($parts[0])) {
+ $minPart = (integer)$parts[0];
+ $info->min = abs($minPart);
+ $info->padLeft = ($minPart > 0);
+ }
+
+ if (!empty($parts[1])) {
+ $maxPart = (integer)$parts[1];
+ $info->max = abs($maxPart);
+ $info->trimLeft = ($maxPart < 0);
+ }
+
+ return $info;
+ }
+}