View Javadoc

1   package org.asteriskjava.config;
2   
3   import java.io.BufferedReader;
4   import java.io.FileReader;
5   import java.io.IOException;
6   import java.nio.CharBuffer;
7   import java.util.*;
8   
9   /**
10   * Reads and parses Asterisk configuration files.
11   * <p/>
12   * See Asterisk's <code>main/config.c</code>.
13   * <p/>
14   * Client code is not supposed to use this class.
15   *
16   * @author srt
17   * @version $Id$
18   */
19  public class ConfigFileReader
20  {
21      private static final int MAX_LINE_LENGTH = 8192;
22      private static char COMMENT_META = ';';
23      private static char COMMENT_TAG = '-';
24      private static final Set<Class<?>> WARNING_CLASSES;
25  
26      static
27      {
28          WARNING_CLASSES = new HashSet<Class<?>>();
29          WARNING_CLASSES.add(MissingEqualSignException.class);
30          WARNING_CLASSES.add(UnknownDirectiveException.class);
31          WARNING_CLASSES.add(MissingDirectiveParameterException.class);
32      }
33  
34      private StringBuilder commentBlock;
35      protected final Map<String, Category> categories;
36      private final List<ConfigParseException> warnings;
37  
38      protected Category currentCategory;
39      private int currentCommentLevel = 0;
40  
41      public ConfigFileReader()
42      {
43          this.commentBlock = new StringBuilder();
44          this.categories = new LinkedHashMap<String, Category>();
45          this.warnings = new ArrayList<ConfigParseException>();
46      }
47  
48      public ConfigFile readFile(String configfile) throws IOException, ConfigParseException
49      {
50          final ConfigFile result;
51          final BufferedReader reader;
52  
53          reader = new BufferedReader(new FileReader(configfile));
54          try
55          {
56              readFile(configfile, reader);
57          }
58          finally
59          {
60              try
61              {
62                  reader.close();
63              }
64              catch (Exception e) // NOPMD
65              {
66                  // ignore
67              }
68          }
69  
70          result = new ConfigFileImpl(configfile, new TreeMap<String, Category>(categories));
71  
72  
73          return result;
74      }
75  
76      void reset()
77      {
78          commentBlock = new StringBuilder();
79          categories.clear();
80          warnings.clear();
81          currentCategory = null;
82          currentCommentLevel = 0;
83      }
84  
85      Collection<Category> getCategories()
86      {
87          return categories.values();
88      }
89  
90      public Collection<ConfigParseException> getWarnings()
91      {
92          return new ArrayList<ConfigParseException>(warnings);
93      }
94  
95      void readFile(String configfile, BufferedReader reader) throws IOException, ConfigParseException
96      {
97          String line;
98          int lineno = 0;
99          CharBuffer buffer = CharBuffer.allocate(MAX_LINE_LENGTH);
100 
101         reset();
102         while ((line = reader.readLine()) != null)
103         {
104             lineno++;
105             buffer.clear();
106             buffer.put(line);
107             buffer.put("\n");
108             buffer.flip();
109 
110             processLine(configfile, lineno, buffer);
111         }
112     }
113 
114     ConfigElement processLine(String configfile, int lineno, CharBuffer buffer) throws ConfigParseException
115     {
116         char c;
117         final StringBuilder processLineBuilder;
118         final StringBuilder lineCommentBuilder;
119 
120         processLineBuilder = new StringBuilder(MAX_LINE_LENGTH);
121         lineCommentBuilder = new StringBuilder(MAX_LINE_LENGTH);
122         buffer.mark();
123 
124         while (buffer.hasRemaining())
125         {
126             final int position;
127 
128             position = buffer.position();
129             c = buffer.get();
130             //System.out.println(position + ": " + c);
131 
132             if (c == COMMENT_META)
133             {
134                 if (position >= 1 && buffer.get(position - 1) == '\\')
135                 {
136                     /* Escaped semicolons aren't comments. */
137                 } // NOPMD
138                 else if (buffer.remaining() >= 3
139                         && buffer.get(position + 1) == COMMENT_TAG
140                         && buffer.get(position + 2) == COMMENT_TAG
141                         && buffer.get(position + 3) != COMMENT_TAG)
142                 {
143                     /* Meta-Comment start detected ";--" */
144 
145                     currentCommentLevel++;
146                     //System.out.println("Comment start, new level: " + currentCommentLevel);
147 
148                     if (!inComment())
149                     {
150                         commentBlock.append(";--");
151                         buffer.position(position + 3);
152                         buffer.mark();
153                         continue;
154                     }
155                 }
156                 else if (inComment()
157                         && position >= 2
158                         && buffer.get(position - 1) == COMMENT_TAG
159                         && buffer.get(position - 2) == COMMENT_TAG)
160                 {
161                     /* Meta-Comment end detected */
162 
163                     currentCommentLevel--;
164 
165                     if (!inComment())
166                     {
167                         buffer.reset();
168 
169                         //int commentLength = (position + 1) - buffer.position();
170 
171                         //buffer.reset();
172                         //for (int i = 0; i < commentLength; i++)
173                         //{
174                         //    commentBlock.append(buffer.get());
175                         //}
176 
177                         commentBlock.append(c);
178                         //System.out.println("Comment end at " + position + ": '" + commentBlock.toString() + "'");
179 
180                         buffer.position(position + 1);
181                         buffer.compact();
182                         buffer.flip();
183 
184                         //System.out.println("Buffer compacted");
185                         continue;
186                     }
187                 }
188                 else
189                 {
190                     if (!inComment())
191                     {
192                         /* If ; is found, and we are not nested in a comment, we immediately stop all comment processing */
193                         //System.out.println("Found ; while not in comment");
194                         while (buffer.hasRemaining())
195                         {
196                             lineCommentBuilder.append(buffer.get());
197                         }
198                         break;
199                     }
200                     else
201                     {
202                         /* Found ';' while in comment */
203                     } // NOPMD
204                 }
205             }
206 
207 
208             if (inComment())
209             {
210                 commentBlock.append(c);
211             }
212             else
213             {
214                 //System.out.println("Added '" + c + "' to processLine");
215                 processLineBuilder.append(c);
216             }
217         }
218 
219         String processLineString;
220         String lineCommentString;
221         ConfigElement configElement;
222 
223         processLineString = processLineBuilder.toString().trim();
224         lineCommentString = lineCommentBuilder.toString().trim();
225 
226         //System.out.println("process line: '" + processLineString + "'");
227         if (processLineString.length() == 0)
228         {
229             if (lineCommentString.length() != 0)
230             {
231                 commentBlock.append(";");
232                 commentBlock.append(lineCommentString);
233             }
234             if (!inComment())
235             {
236                 commentBlock.append("\n");
237             }
238             return null;
239         }
240 
241         try
242         {
243             configElement = processTextLine(configfile, lineno, processLineString);
244         }
245         catch (ConfigParseException e)
246         {
247             // some parsing exceptions are treated as warnings by Asterisk, we mirror this behavior.
248             if (WARNING_CLASSES.contains(e.getClass()))
249             {
250                 warnings.add(e);
251                 return null;
252             }
253             else
254             {
255                 throw e;
256             }
257         }
258 
259         if (lineCommentString.length() != 0)
260         {
261             configElement.setComment(lineCommentString);
262         }
263 
264         if (commentBlock.length() != 0)
265         {
266             configElement.setPreComment(commentBlock.toString());
267             commentBlock.delete(0, commentBlock.length());
268         }
269 
270         return configElement;
271     }
272 
273     boolean inComment()
274     {
275         return currentCommentLevel != 0;
276     }
277 
278     protected ConfigElement processTextLine(String configfile, int lineno, String line) throws ConfigParseException
279     {
280         ConfigElement configElement;
281 
282         if (line.charAt(0) == '[') // A category header
283         {
284             configElement = parseCategoryHeader(configfile, lineno, line);
285         }
286         else if (line.charAt(0) == '#') // a directive - #include or #exec
287         {
288             configElement = parseDirective(configfile, lineno, line);
289         }
290         else // just a line
291         {
292             if (currentCategory == null)
293             {
294                 throw new ConfigParseException(configfile, lineno,
295                         "parse error: No category context for line %d of %s", lineno, configfile);
296             }
297 
298             configElement = parseVariable(configfile, lineno, line);
299             currentCategory.addElement(configElement);
300         }
301 
302         return configElement;
303     }
304 
305     protected Category parseCategoryHeader(String configfile, int lineno, String line) throws ConfigParseException
306     {
307         final Category category;
308         final String name;
309         final int nameEndPos;
310 
311         /* format is one of the following:
312         * [foo]        define a new category named 'foo'
313         * [foo](!)     define a new template category named 'foo'
314         * [foo](+)     append to category 'foo', error if foo does not exist.
315         * [foo](a)     define a new category and inherit from template a.
316         *              You can put a comma-separated list of templates and '!' and '+'
317         *              between parentheses, with obvious meaning.
318         */
319 
320         nameEndPos = line.indexOf(']');
321         if (nameEndPos == -1)
322         {
323             throw new ConfigParseException(configfile, lineno,
324                     "parse error: no closing ']', line %d of %s", lineno, configfile);
325         }
326         name = line.substring(1, nameEndPos);
327         category = new Category(configfile, lineno, name);
328 
329         /* Handle options or categories to inherit from if available */
330         if (line.length() > nameEndPos + 1 && line.charAt(nameEndPos + 1) == '(')
331         {
332             final String[] options;
333             final String optionsString;
334             final int optionsEndPos;
335 
336             optionsString = line.substring(nameEndPos + 1);
337             optionsEndPos = optionsString.indexOf(')');
338             if (optionsEndPos == -1)
339             {
340                 throw new ConfigParseException(configfile, lineno,
341                         "parse error: no closing ')', line %d of %s", lineno, configfile);
342             }
343 
344             options = optionsString.substring(1, optionsEndPos).split(",");
345             for (String cur : options)
346             {
347                 if ("!".equals(cur)) // category template
348                 {
349                     category.markAsTemplate();
350                 }
351                 else if ("+".equals(cur)) // category addition
352                 {
353                     final Category categoryToAddTo;
354 
355                     categoryToAddTo = categories.get(name);
356                     if (categoryToAddTo == null)
357                     {
358                         throw new ConfigParseException(configfile, lineno,
359                                 "Category addition requested, but category '%s' does not exist, line %d of %s",
360                                 name, lineno, configfile);
361                     }
362 
363                     //todo implement category addition
364                     //category = categoryToAddTo;
365                 }
366                 else
367                 {
368                     final Category baseCategory;
369 
370                     baseCategory = categories.get(cur);
371                     if (baseCategory == null)
372                     {
373                         throw new ConfigParseException(configfile, lineno,
374                                 "Inheritance requested, but category '%s' does not exist, line %d of %s",
375                                 cur, lineno, configfile);
376                     }
377                     inheritCategory(category, baseCategory);
378                 }
379             }
380         }
381 
382         appendCategory(category);
383         return category;
384     }
385 
386     ConfigDirective parseDirective(String configfile, int lineno, String line) throws ConfigParseException
387     {
388         ConfigDirective directive;
389         final StringBuilder nameBuilder;
390         final StringBuilder paramBuilder;
391         String name = null;
392         String param;
393 
394         nameBuilder = new StringBuilder();
395         paramBuilder = new StringBuilder();
396         for (int i = 1; i < line.length(); i++)
397         {
398             final char c;
399 
400             c = line.charAt(i);
401             if (name == null)
402             {
403                 nameBuilder.append(c);
404                 if (Character.isWhitespace(c) || i + 1 == line.length())
405                 {
406                     name = nameBuilder.toString().trim();
407                 }
408             }
409             else
410             {
411                 paramBuilder.append(c);
412             }
413         }
414 
415         param = paramBuilder.toString().trim();
416 
417         if (param.length() != 0 &&
418                 (param.charAt(0) == '"' || param.charAt(0) == '<' || param.charAt(0) == '>'))
419         {
420             param = param.substring(1);
421         }
422 
423         int endPos = param.length() - 1;
424         if (param.length() != 0 &&
425                 (param.charAt(endPos) == '"' || param.charAt(endPos) == '<' || param.charAt(endPos) == '>'))
426         {
427             param = param.substring(0, endPos);
428         }
429 
430         if ("exec".equalsIgnoreCase(name))
431         {
432             directive = new ExecDirective(configfile, lineno, param);
433         }
434         else if ("include".equalsIgnoreCase(name))
435         {
436             directive = new IncludeDirective(configfile, lineno, param);
437         }
438         else
439         {
440             throw new UnknownDirectiveException(configfile, lineno,
441                     "Unknown directive '%s' at line %d of %s", name, lineno, configfile);
442         }
443 
444         if (param.length() == 0)
445         {
446             throw new MissingDirectiveParameterException(configfile, lineno,
447                     "Directive '#%s' needs an argument (%s) at line %d of %s",
448                     name.toLowerCase(Locale.US),
449                     "include".equalsIgnoreCase(name) ? "filename" : "/path/to/executable",
450                     lineno,
451                     configfile);
452         }
453 
454         return directive;
455     }
456 
457     protected ConfigVariable parseVariable(String configfile, int lineno, String line) throws ConfigParseException
458     {
459         int pos;
460         String name;
461         String value;
462 
463         pos = line.indexOf('=');
464         if (pos == -1)
465         {
466             throw new MissingEqualSignException(configfile, lineno,
467                     "No '=' (equal sign) in line %d of %s", lineno, configfile);
468         }
469 
470         name = line.substring(0, pos).trim();
471 
472         // Ignore > in =>
473         if (line.length() > pos + 1 && line.charAt(pos + 1) == '>')
474         {
475             pos++;
476         }
477 
478         value = (line.length() > pos + 1) ? line.substring(pos + 1).trim() : "";
479         return new ConfigVariable(configfile, lineno, name, value);
480     }
481 
482     void inheritCategory(Category category, Category baseCategory)
483     {
484         category.addBaseCategory(baseCategory);
485     }
486 
487     void appendCategory(Category category)
488     {
489         categories.put(category.getName(), category);
490         currentCategory = category;
491     }
492 }