PSJIP - sip.conf to res_sip.conf script
[asterisk/asterisk.git] / contrib / scripts / sip_to_res_sip / astconfigparser.py
1 import re
2
3 from astdicts import OrderedDict
4 from astdicts import MultiOrderedDict
5
6 def merge_values(left, right, key):
7     """Merges values from right into left."""
8     if isinstance(left, list):
9         vals0 = left
10     else: # assume dictionary
11         vals0 = left[key] if key in left else []
12     vals1 = right[key] if key in right else []
13
14     return vals0 + [i for i in vals1 if i not in vals0]
15
16 ###############################################################################
17
18 class Section(MultiOrderedDict):
19     """A Section is a MultiOrderedDict itself that maintains a list of
20        key/value options.  However, in the case of an Asterisk config
21        file a section may have other defaults sections that is can pull
22        data from (i.e. templates).  So when an option is looked up by key
23        it first checks the base section and if not found looks in the
24        added default sections. If not found at that point then a 'KeyError'
25        exception is raised.
26     """
27     count = 0
28
29     def __init__(self, defaults=None, templates=None):
30         MultiOrderedDict.__init__(self)
31         # track an ordered id of sections
32         Section.count += 1
33         self.id = Section.count
34         self._defaults = [] if defaults is None else defaults
35         self._templates = [] if templates is None else templates
36
37     def __cmp__(self, other):
38         return cmp(self.id, other.id)
39
40     def get(self, key, from_self=True, from_templates=True, from_defaults=True):
41         if from_self and key in self:
42             return MultiOrderedDict.__getitem__(self, key)
43
44         if from_templates:
45             if self in self._templates:
46                 return []
47             for t in self._templates:
48                 try:
49                     # fail if not found on the search - doing it this way
50                     # allows template's templates to be searched.
51                     return t.get(key, True, from_templates, from_defaults)
52                 except KeyError:
53                     pass
54
55         if from_defaults:
56             for d in self._defaults:
57                 try:
58                     return d.get(key, True, from_templates, from_defaults)
59                 except KeyError:
60                     pass
61
62         raise KeyError(key)
63
64     def __getitem__(self, key):
65         """Get the value for the given key. If it is not found in the 'self'
66            then check inside templates and defaults before declaring raising
67            a KeyError exception.
68         """
69         return self.get(key)
70
71     def keys(self, self_only=False):
72         res = MultiOrderedDict.keys(self)
73         if self_only:
74             return res
75
76         for d in self._templates:
77             for key in d.keys():
78                 if key not in res:
79                     res.append(key)
80
81         for d in self._defaults:
82             for key in d.keys():
83                 if key not in res:
84                     res.append(key)
85         return res
86
87     def add_defaults(self, defaults):
88         defaults.sort()
89         for i in defaults:
90             self._defaults.insert(0, i)
91
92     def add_templates(self, templates):
93         templates.sort(reverse=True);
94         self._templates.extend(templates)
95
96     def get_merged(self, key):
97         """Return a list of values for a given key merged from default(s)"""
98         # first merge key/values from defaults together
99         merged = []
100         for i in reversed(self._defaults):
101             if not merged:
102                 merged = i
103                 continue
104             merged = merge_values(merged, i, key)
105
106         for i in reversed(self._templates):
107             if not merged:
108                 merged = i
109                 continue
110             merged = merge_values(merged, i, key)
111
112         # then merge self in
113         return merge_values(merged, self, key)
114
115 ###############################################################################
116
117 COMMENT = ';'
118 COMMENT_START = ';--'
119 COMMENT_END = '--;'
120
121 DEFAULTSECT = 'general'
122
123 def remove_comment(line, is_comment):
124     """Remove any commented elements from the line."""
125     if not line: return line, is_comment
126
127     if is_comment:
128         part = line.partition(COMMENT_END)
129         if part[1]:
130             # found multi-line comment end check string after it
131             return remove_comment(part[2], False)
132         return "", True
133
134     part = line.partition(COMMENT_START)
135     if part[1]:
136         # found multi-line comment start check string before
137         # it to make sure there wasn't an eol comment in it
138         has_comment = part[0].partition(COMMENT)
139         if has_comment[1]:
140             # eol comment found return anything before it
141             return has_comment[0], False
142
143         # check string after it to see if the comment ends
144         line, is_comment = remove_comment(part[2], True)
145         if is_comment:
146             # return possible string data before comment
147             return part[0].strip(), True
148
149         # otherwise it was an embedded comment so combine
150         return ''.join([part[0].strip(), ' ', line]).rstrip(), False
151
152     # check for eol comment
153     return line.partition(COMMENT)[0].strip(), False
154
155 def try_include(line):
156     """Checks to see if the given line is an include.  If so return the
157        included filename, otherwise None.
158     """
159     if not line.startswith('#'):
160         return None
161
162     # it is an include - get file name
163     try:
164         return line[line.index('"') + 1:line.rindex('"')]
165     except ValueError:
166         print "Invalid include - could not parse filename."
167         return None
168
169 def try_section(line):
170     """Checks to see if the given line is a section. If so return the section
171        name, otherwise return 'None'.
172     """
173     # leading spaces were stripped when checking for comments
174     if not line.startswith('['):
175         return None, False, []
176
177     section, delim, templates = line.partition(']')
178     if not templates:
179         return section[1:], False, []
180
181     # strip out the parens and parse into an array
182     templates = templates.replace('(', "").replace(')', "").split(',')
183     # go ahead and remove extra whitespace
184     templates = [i.strip() for i in templates]
185     try:
186         templates.remove('!')
187         return section[1:], True, templates
188     except:
189         return section[1:], False, templates
190
191 def try_option(line):
192     """Parses the line as an option, returning the key/value pair."""
193     data = re.split('=>?', line)
194     # should split in two (key/val), but either way use first two elements
195     return data[0].rstrip(), data[1].lstrip()
196
197 ###############################################################################
198
199 def find_value(sections, key):
200     """Given a list of sections, try to find value(s) for the given key."""
201     # always start looking in the last one added
202     sections.sort(reverse=True);
203     for s in sections:
204         try:
205             # try to find in section and section's templates
206             return s.get(key, from_defaults=False)
207         except KeyError:
208             pass
209
210     # wasn't found in sections or a section's templates so check in defaults
211     for s in sections:
212         try:
213             # try to find in section's defaultsects
214             return s.get(key, from_self=False, from_templates=False)
215         except KeyError:
216             pass
217
218     raise KeyError(key)
219
220 def find_dict(mdicts, key, val):
221     """Given a list of mult-dicts, return the multi-dict that contains
222        the given key/value pair."""
223
224     def found(d):
225         return key in d and val in d[key]
226
227     try:
228         return [d for d in mdicts if found(d)][0]
229     except IndexError:
230         raise LookupError("Dictionary not located for key = %s, value = %s"
231                           % (key, val))
232
233 def get_sections(parser, key, attr='_sections', searched=None):
234     if searched is None:
235         searched = []
236     if parser is None or parser in searched:
237         return []
238
239     try:
240         sections = getattr(parser, attr)
241         res = sections[key] if key in sections else []
242         searched.append(parser)
243         return res + get_sections(parser._includes, key, attr, searched) \
244             + get_sections(parser._parent, key, attr, searched)
245     except:
246         # assume ordereddict of parsers
247         res = []
248         for p in parser.itervalues():
249             res.extend(get_sections(p, key, attr, searched))
250         return res
251
252 def get_defaults(parser, key):
253     return get_sections(parser, key, '_defaults')
254
255 def write_dicts(file, mdicts):
256     for section, sect_list in mdicts.iteritems():
257         # every section contains a list of dictionaries
258         for sect in sect_list:
259             file.write("[%s]\n" % section)
260             for key, val_list in sect.iteritems():
261                 # every value is also a list
262                 for v in val_list:
263                     key_val = key
264                     if v is not None:
265                         key_val += " = " + str(v)
266                         file.write("%s\n" % (key_val))
267             file.write("\n")
268
269 ###############################################################################
270
271 class MultiOrderedConfigParser:
272     def __init__(self, parent=None):
273         self._parent = parent
274         self._defaults = MultiOrderedDict()
275         self._sections = MultiOrderedDict()
276         self._includes = OrderedDict()
277
278     def defaults(self):
279         return self._defaults
280
281     def default(self, key):
282         """Retrieves a list of dictionaries for a default section."""
283         return get_defaults(self, key)
284
285     def add_default(self, key, template_keys=None):
286         """Adds a default section to defaults, returning the
287            default Section object.
288         """
289         if template_keys is None:
290             template_keys = []
291         return self.add_section(key, template_keys, self._defaults)
292
293     def sections(self):
294         return self._sections
295
296     def section(self, key):
297         """Retrieves a list of dictionaries for a section."""
298         return get_sections(self, key)
299
300     def add_section(self, key, template_keys=None, mdicts=None):
301         if template_keys is None:
302             template_keys = []
303         if mdicts is None:
304             mdicts = self._sections
305         res = Section()
306         for t in template_keys:
307             res.add_templates(get_defaults(self, t))
308         res.add_defaults(get_defaults(self, DEFAULTSECT))
309         mdicts.insert(0, key, res)
310         return res
311
312     def includes(self):
313         return self._includes
314
315     def add_include(self, filename, parser=None):
316         if filename in self._includes:
317             return self._includes[filename]
318
319         self._includes[filename] = res = \
320              MultiOrderedConfigParser(self) if parser is None else parser
321         return res;
322
323     def get(self, section, key):
324         """Retrieves the list of values from a section for a key."""
325         try:
326             # search for the value in the list of sections
327             return find_value(self.section(section), key)
328         except KeyError:
329             pass
330
331         try:
332             # section may be a default section so, search
333             # for the value in the list of defaults
334             return find_value(self.default(section), key)
335         except KeyError:
336             raise LookupError("key %r not found for section %r"
337                               % (key, section))
338
339     def set(self, section, key, val):
340         """Sets an option in the given section."""
341         # TODO - set in multiple sections? (for now set in first)
342         # TODO - set in both sections and defaults?
343         if section in self._sections:
344             self.section(section)[0][key] = val
345         else:
346             self.defaults(section)[0][key] = val
347
348     def read(self, filename):
349         try:
350             with open(filename, 'rt') as file:
351                 self._read(file, filename)
352         except IOError:
353             print "Could not open file ", filename, " for reading"
354
355     def _read(self, file, filename):
356         is_comment = False # used for multi-lined comments
357         for line in file:
358             line, is_comment = remove_comment(line, is_comment)
359             if not line:
360                 # line was empty or was a comment
361                 continue
362
363             include_name = try_include(line)
364             if include_name:
365                 parser = self.add_include(include_name)
366                 parser.read(include_name)
367                 continue
368
369             section, is_template, templates = try_section(line)
370             if section:
371                 if section == DEFAULTSECT or is_template:
372                     sect = self.add_default(section, templates)
373                 else:
374                     sect = self.add_section(section, templates)
375                 continue
376
377             key, val = try_option(line)
378             sect[key] = val
379
380     def write(self, f):
381         try:
382             for key, val in self._includes.iteritems():
383                 val.write(key)
384                 f.write('#include "%s"\n' % key)
385
386             f.write('\n')
387             write_dicts(f, self._defaults)
388             write_dicts(f, self._sections)
389         except:
390             try:
391                 with open(f, 'wt') as fp:
392                     self.write(fp)
393             except IOError:
394                 print "Could not open file ", f, " for writing"