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: