      1: <?php
      2: /**
      3:  * Licensed to the Apache Software Foundation (ASF) under one or more
      4:  * contributor license agreements. See the NOTICE file distributed with
      5:  * this work for additional information regarding copyright ownership.
      6:  * The ASF licenses this file to You under the Apache License, Version 2.0
      7:  * (the "License"); you may not use this file except in compliance with
      8:  * the License. You may obtain a copy of the License at
      9:  *
     10:  *     http://www.apache.org/licenses/LICENSE-2.0
     11:  *
     12:  * Unless required by applicable law or agreed to in writing, software
     13:  * distributed under the License is distributed on an "AS IS" BASIS,
     14:  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15:  * See the License for the specific language governing permissions and
     16:  * limitations under the License.
     17:  *
     18:  * @package log4php
     19:  */
     20:
     21: /**
     22:  * Most of the work of the {@link LoggerPatternLayout} class
     23:  * is delegated to the {@link LoggerPatternParser} class.
     24:  *
     25:  * <p>It is this class that parses conversion patterns and creates
     26:  * a chained list of {@link LoggerPatternConverter} converters.</p>
     27:  *
     28:  * @version $Revision: 1395467 $
     29:  * @package log4php
     30:  * @subpackage helpers
     31:  *
     32:  * @since 0.3
     33:  */
     34: class LoggerPatternParser {
     35:
     36:     /** Escape character for conversion words in the conversion pattern. */
     37:     const ESCAPE_CHAR = '%';
     38:
     39:     /** Maps conversion words to relevant converters. */
     40:     private $converterMap;
     41:
     42:     /** Conversion pattern used in layout. */
     43:     private $pattern;
     44:
     45:     /** Regex pattern used for parsing the conversion pattern. */
     46:     private $regex;
     47:
     48:     /**
     49:      * First converter in the chain.
     50:      * @var LoggerPatternConverter
     51:      */
     52:     private $head;
     53:
     54:     /** Last converter in the chain. */
     55:     private $tail;
     56:
     57:     public function __construct($pattern, $converterMap) {
     58:         $this->pattern = $pattern;
     59:         $this->converterMap = $converterMap;
     60:
     61:         // Construct the regex pattern
     62:         $this->regex =
     63:             '/' .                       // Starting regex pattern delimiter
     64:             self::ESCAPE_CHAR .         // Character which marks the start of the conversion pattern
     65:             '(?P<modifiers>[0-9.-]*)' . // Format modifiers (optional)
     66:             '(?P<word>[a-zA-Z]+)' .     // The conversion word
     67:             '(?P<option>{[^}]*})?' .    // Conversion option in braces (optional)
     68:             '/';                        // Ending regex pattern delimiter
     69:     }
     70:
     71:     /**
     72:      * Parses the conversion pattern string, converts it to a chain of pattern
     73:      * converters and returns the first converter in the chain.
     74:      *
     75:      * @return LoggerPatternConverter
     76:      */
     77:     public function parse() {
     78:
     79:         // Skip parsing if the pattern is empty
     80:         if (empty($this->pattern)) {
     81:             $this->addLiteral('');
     82:             return $this->head;
     83:         }
     84:
     85:         // Find all conversion words in the conversion pattern
     86:         $count = preg_match_all($this->regex, $this->pattern, $matches, PREG_OFFSET_CAPTURE);
     87:         if ($count === false) {
     88:             $error = error_get_last();
     89:             throw new LoggerException("Failed parsing layotut pattern: {$error['message']}");
     90:         }
     91:
     92:         $prevEnd = 0;
     93:
     94:         foreach($matches[0] as $key => $item) {
     95:
     96:             // Locate where the conversion command starts and ends
     97:             $length = strlen($item[0]);
     98:             $start = $item[1];
     99:             $end = $item[1] + $length;
    100:
    101:             // Find any literal expressions between matched commands
    102:             if ($start > $prevEnd) {
    103:                 $literal = substr($this->pattern, $prevEnd, $start - $prevEnd);
    104:                 $this->addLiteral($literal);
    105:             }
    106:
    107:             // Extract the data from the matched command
    108:             $word = !empty($matches['word'][$key]) ? $matches['word'][$key][0] : null;
    109:             $modifiers = !empty($matches['modifiers'][$key]) ? $matches['modifiers'][$key][0] : null;
    110:             $option = !empty($matches['option'][$key]) ? $matches['option'][$key][0] : null;
    111:
    112:             // Create a converter and add it to the chain
    113:             $this->addConverter($word, $modifiers, $option);
    114:
    115:             $prevEnd = $end;
    116:         }
    117:
    118:         // Add any trailing literals
    119:         if ($end < strlen($this->pattern)) {
    120:             $literal = substr($this->pattern, $end);
    121:             $this->addLiteral($literal);
    122:         }
    123:
    124:         return $this->head;
    125:     }
    126:
    127:     /**
    128:      * Adds a literal converter to the converter chain.
    129:      * @param string $string The string for the literal converter.
    130:      */
    131:     private function addLiteral($string) {
    132:         $converter = new LoggerPatternConverterLiteral($string);
    133:         $this->addToChain($converter);
    134:     }
    135:
    136:     /**
    137:      * Adds a non-literal converter to the converter chain.
    138:      *
    139:      * @param string $word The conversion word, used to determine which
    140:      *  converter will be used.
    141:      * @param string $modifiers Formatting modifiers.
    142:      * @param string $option Option to pass to the converter.
    143:      */
    144:     private function addConverter($word, $modifiers, $option) {
    145:         $formattingInfo = $this->parseModifiers($modifiers);
    146:         $option = trim($option, "{} ");
    147:
    148:         if (isset($this->converterMap[$word])) {
    149:             $converter = $this->getConverter($word, $formattingInfo, $option);
    150:             $this->addToChain($converter);
    151:         } else {
    152:             trigger_error("log4php: Invalid keyword '%$word' in converison pattern. Ignoring keyword.", E_USER_WARNING);
    153:         }
    154:     }
    155:
    156:     /**
    157:      * Determines which converter to use based on the conversion word. Creates
    158:      * an instance of the converter using the provided formatting info and
    159:      * option and returns it.
    160:      *
    161:      * @param string $word The conversion word.
    162:      * @param LoggerFormattingInfo $info Formatting info.
    163:      * @param string $option Converter option.
    164:      *
    165:      * @throws LoggerException
    166:      *
    167:      * @return LoggerPatternConverter
    168:      */
    169:     private function getConverter($word, $info, $option) {
    170:         if (!isset($this->converterMap[$word])) {
    171:             throw new LoggerException("Invalid keyword '%$word' in converison pattern. Ignoring keyword.");
    172:         }
    173:
    174:         $converterClass = $this->converterMap[$word];
    175:         if(!class_exists($converterClass)) {
    176:             throw new LoggerException("Class '$converterClass' does not exist.");
    177:         }
    178:
    179:         $converter = new $converterClass($info, $option);
    180:         if(!($converter instanceof LoggerPatternConverter)) {
    181:             throw new LoggerException("Class '$converterClass' is not an instance of LoggerPatternConverter.");
    182:         }
    183:
    184:         return $converter;
    185:     }
    186:
    187:     /** Adds a converter to the chain and updates $head and $tail pointers. */
    188:     private function addToChain(LoggerPatternConverter $converter) {
    189:         if (!isset($this->head)) {
    190:             $this->head = $converter;
    191:             $this->tail = $this->head;
    192:         } else {
    193:             $this->tail->next = $converter;
    194:             $this->tail = $this->tail->next;
    195:         }
    196:     }
    197:
    198:     /**
    199:      * Parses the formatting modifiers and produces the corresponding
    200:      * LoggerFormattingInfo object.
    201:      *
    202:      * @param string $modifier
    203:      * @return LoggerFormattingInfo
    204:      * @throws LoggerException
    205:      */
    206:     private function parseModifiers($modifiers) {
    207:         $info = new LoggerFormattingInfo();
    208:
    209:         // If no modifiers are given, return default values
    210:         if (empty($modifiers)) {
    211:             return $info;
    212:         }
    213:
    214:         // Validate
    215:         $pattern = '/^(-?[0-9]+)?\.?-?[0-9]+$/';
    216:         if (!preg_match($pattern, $modifiers)) {
    217:             trigger_error("log4php: Invalid modifier in conversion pattern: [$modifiers]. Ignoring modifier.", E_USER_WARNING);
    218:             return $info;
    219:         }
    220:
    221:         $parts = explode('.', $modifiers);
    222:
    223:         if (!empty($parts[0])) {
    224:             $minPart = (integer) $parts[0];
    225:             $info->min = abs($minPart);
    226:             $info->padLeft = ($minPart > 0);
    227:         }
    228:
    229:         if (!empty($parts[1])) {
    230:             $maxPart = (integer) $parts[1];
    231:             $info->max = abs($maxPart);
    232:             $info->trimLeft = ($maxPart < 0);
    233:         }
    234:
    235:         return $info;
    236:     }
    237: }
    238: