46f4fb484b284ddae24410f6587c8c2a117c50a0
[asterisk/asterisk.git] / contrib / scripts / sip_to_pjsip / astconfigparser.py
1 """
2 Copyright (C) 2016, Digium, Inc.
3
4 This program is free software, distributed under the terms of
5 the GNU General Public License Version 2.
6 """
7
8 import re
9 import itertools
10
11 from astdicts import OrderedDict
12 from astdicts import MultiOrderedDict
13
14
15 def merge_values(left, right, key):
16     """Merges values from right into left."""
17     if isinstance(left, list):
18         vals0 = left
19     else:  # assume dictionary
20         vals0 = left[key] if key in left else []
21     vals1 = right[key] if key in right else []
22
23     return vals0 + [i for i in vals1 if i not in vals0]
24
25 ###############################################################################
26
27
28 class Section(MultiOrderedDict):
29     """
30     A Section is a MultiOrderedDict itself that maintains a list of
31     key/value options.  However, in the case of an Asterisk config
32     file a section may have other defaults sections that is can pull
33     data from (i.e. templates).  So when an option is looked up by key
34     it first checks the base section and if not found looks in the
35     added default sections. If not found at that point then a 'KeyError'
36     exception is raised.
37     """
38     count = 0
39
40     def __init__(self, defaults=None, templates=None):
41         MultiOrderedDict.__init__(self)
42         # track an ordered id of sections
43         Section.count += 1
44         self.id = Section.count
45         self._defaults = [] if defaults is None else defaults
46         self._templates = [] if templates is None else templates
47
48     def __cmp__(self, other):
49         """
50         Use self.id as means of determining equality
51         """
52         return cmp(self.id, other.id)
53
54     def __eq__(self, other):
55         """
56         Use self.id as means of determining equality
57         """
58         return self.id == other.id
59
60     def get(self, key, from_self=True, from_templates=True,
61             from_defaults=True):
62         """
63         Get the values corresponding to a given key. The parameters to this
64         function form a hierarchy that determines priority of the search.
65         from_self takes priority over from_templates, and from_templates takes
66         priority over from_defaults.
67
68         Parameters:
69         from_self - If True, search within the given section.
70         from_templates - If True, search in this section's templates.
71         from_defaults - If True, search within this section's defaults.
72         """
73         if from_self and key in self:
74             return MultiOrderedDict.__getitem__(self, key)
75
76         if from_templates:
77             if self in self._templates:
78                 return []
79             for t in self._templates:
80                 try:
81                     # fail if not found on the search - doing it this way
82                     # allows template's templates to be searched.
83                     return t.get(key, True, from_templates, from_defaults)
84                 except KeyError:
85                     pass
86
87         if from_defaults:
88             for d in self._defaults:
89                 try:
90                     return d.get(key, True, from_templates, from_defaults)
91                 except KeyError:
92                     pass
93
94         raise KeyError(key)
95
96     def __getitem__(self, key):
97         """
98         Get the value for the given key. If it is not found in the 'self'
99         then check inside templates and defaults before declaring raising
100         a KeyError exception.
101         """
102         return self.get(key)
103
104     def keys(self, self_only=False):
105         """
106         Get the keys from this section. If self_only is True, then
107         keys from this section's defaults and templates are not
108         included in the returned value
109         """
110         res = MultiOrderedDict.keys(self)
111         if self_only:
112             return res
113
114         for d in self._templates:
115             for key in d.keys():
116                 if key not in res:
117                     res.append(key)
118
119         for d in self._defaults:
120             for key in d.keys():
121                 if key not in res:
122                     res.append(key)
123         return res
124
125     def add_defaults(self, defaults):
126         """
127         Add a list of defaults to the section. Defaults are
128         sections such as 'general'
129         """
130         defaults.sort()
131         for i in defaults:
132             self._defaults.insert(0, i)
133
134     def add_templates(self, templates):
135         """
136         Add a list of templates to the section.
137         """
138         templates.sort()
139         for i in templates:
140             self._templates.insert(0, i)
141
142     def get_merged(self, key):
143         """Return a list of values for a given key merged from default(s)"""
144         # first merge key/values from defaults together
145         merged = []
146         for i in reversed(self._defaults):
147             if not merged:
148                 merged = i
149                 continue
150             merged = merge_values(merged, i, key)
151
152         for i in reversed(self._templates):
153             if not merged:
154                 merged = i
155                 continue
156             merged = merge_values(merged, i, key)
157
158         # then merge self in
159         return merge_values(merged, self, key)
160
161 ###############################################################################
162
163 COMMENT = ';'
164 COMMENT_START = ';--'
165 COMMENT_END = '--;'
166
167 DEFAULTSECT = 'general'
168
169
170 def remove_comment(line, is_comment):
171     """Remove any commented elements from the line."""
172     if not line:
173         return line, is_comment
174
175     if is_comment:
176         part = line.partition(COMMENT_END)
177         if part[1]:
178             # found multi-line comment end check string after it
179             return remove_comment(part[2], False)
180         return "", True
181
182     part = line.partition(COMMENT_START)
183     if part[1]:
184         # found multi-line comment start check string before
185         # it to make sure there wasn't an eol comment in it
186         has_comment = part[0].partition(COMMENT)
187         if has_comment[1]:
188             # eol comment found return anything before it
189             return has_comment[0], False
190
191         # check string after it to see if the comment ends
192         line, is_comment = remove_comment(part[2], True)
193         if is_comment:
194             # return possible string data before comment
195             return part[0].strip(), True
196
197         # otherwise it was an embedded comment so combine
198         return ''.join([part[0].strip(), ' ', line]).rstrip(), False
199
200     # find the first occurence of a comment that is not escaped
201     match = re.match(r'.*?([^\\];)', line)
202
203     if match:
204          # the end of where the real string is is where the comment starts
205          line = line[0:(match.end()-1)]
206
207     return line.replace("\\", "").strip(), False
208
209 def try_include(line):
210     """
211     Checks to see if the given line is an include.  If so return the
212     included filename, otherwise None.
213     """
214
215     match = re.match('^#include\s*[<"]?(.*)[>"]?$', line)
216     return match.group(1) if match else None
217
218
219 def try_section(line):
220     """
221     Checks to see if the given line is a section. If so return the section
222     name, otherwise return 'None'.
223     """
224     # leading spaces were stripped when checking for comments
225     if not line.startswith('['):
226         return None, False, []
227
228     section, delim, templates = line.partition(']')
229     if not templates:
230         return section[1:], False, []
231
232     # strip out the parens and parse into an array
233     templates = templates.replace('(', "").replace(')', "").split(',')
234     # go ahead and remove extra whitespace
235     templates = [i.strip() for i in templates]
236     try:
237         templates.remove('!')
238         return section[1:], True, templates
239     except:
240         return section[1:], False, templates
241
242
243 def try_option(line):
244     """Parses the line as an option, returning the key/value pair."""
245     data = re.split('=>?', line, 1)
246     # should split in two (key/val), but either way use first two elements
247     return data[0].rstrip(), data[1].lstrip()
248
249 ###############################################################################
250
251
252 def find_dict(mdicts, key, val):
253     """
254     Given a list of mult-dicts, return the multi-dict that contains
255     the given key/value pair.
256     """
257
258     def found(d):
259         return key in d and val in d[key]
260
261     try:
262         return [d for d in mdicts if found(d)][0]
263     except IndexError:
264         raise LookupError("Dictionary not located for key = %s, value = %s"
265                           % (key, val))
266
267
268 def write_dicts(config_file, mdicts):
269     """Write the contents of the mdicts to the specified config file"""
270     for section, sect_list in mdicts.iteritems():
271         # every section contains a list of dictionaries
272         for sect in sect_list:
273             config_file.write("[%s]\n" % section)
274             for key, val_list in sect.iteritems():
275                 # every value is also a list
276                 for v in val_list:
277                     key_val = key
278                     if v is not None:
279                         key_val += " = " + str(v)
280                         config_file.write("%s\n" % (key_val))
281             config_file.write("\n")
282
283 ###############################################################################
284
285
286 class MultiOrderedConfigParser:
287     def __init__(self, parent=None):
288         self._parent = parent
289         self._defaults = MultiOrderedDict()
290         self._sections = MultiOrderedDict()
291         self._includes = OrderedDict()
292
293     def find_value(self, sections, key):
294         """Given a list of sections, try to find value(s) for the given key."""
295         # always start looking in the last one added
296         sections.sort(reverse=True)
297         for s in sections:
298             try:
299                 # try to find in section and section's templates
300                 return s.get(key, from_defaults=False)
301             except KeyError:
302                 pass
303
304         # wasn't found in sections or a section's templates so check in
305         # defaults
306         for s in sections:
307             try:
308                 # try to find in section's defaultsects
309                 return s.get(key, from_self=False, from_templates=False)
310             except KeyError:
311                 pass
312
313         raise KeyError(key)
314
315     def defaults(self):
316         return self._defaults
317
318     def default(self, key):
319         """Retrieves a list of dictionaries for a default section."""
320         return self.get_defaults(key)
321
322     def add_default(self, key, template_keys=None):
323         """
324         Adds a default section to defaults, returning the
325         default Section object.
326         """
327         if template_keys is None:
328             template_keys = []
329         return self.add_section(key, template_keys, self._defaults)
330
331     def sections(self):
332         return self._sections
333
334     def section(self, key):
335         """Retrieves a list of dictionaries for a section."""
336         return self.get_sections(key)
337
338     def get_sections(self, key, attr='_sections', searched=None):
339         """
340         Retrieve a list of sections that have values for the given key.
341         The attr parameter can be used to control what part of the parser
342         to retrieve values from.
343         """
344         if searched is None:
345             searched = []
346         if self in searched:
347             return []
348
349         sections = getattr(self, attr)
350         res = sections[key] if key in sections else []
351         searched.append(self)
352         if self._includes:
353             res.extend(list(itertools.chain(*[
354                 incl.get_sections(key, attr, searched)
355                 for incl in self._includes.itervalues()])))
356         if self._parent:
357             res += self._parent.get_sections(key, attr, searched)
358         return res
359
360     def get_defaults(self, key):
361         """
362         Retrieve a list of defaults that have values for the given key.
363         """
364         return self.get_sections(key, '_defaults')
365
366     def add_section(self, key, template_keys=None, mdicts=None):
367         """
368         Create a new section in the configuration. The name of the
369         new section is the 'key' parameter.
370         """
371         if template_keys is None:
372             template_keys = []
373         if mdicts is None:
374             mdicts = self._sections
375         res = Section()
376         for t in template_keys:
377             res.add_templates(self.get_defaults(t))
378         res.add_defaults(self.get_defaults(DEFAULTSECT))
379         mdicts.insert(0, key, res)
380         return res
381
382     def includes(self):
383         return self._includes
384
385     def add_include(self, filename, parser=None):
386         """
387         Add a new #include file to the configuration.
388         """
389         if filename in self._includes:
390             return self._includes[filename]
391
392         self._includes[filename] = res = \
393             MultiOrderedConfigParser(self) if parser is None else parser
394         return res
395
396     def get(self, section, key):
397         """Retrieves the list of values from a section for a key."""
398         try:
399             # search for the value in the list of sections
400             return self.find_value(self.section(section), key)
401         except KeyError:
402             pass
403
404         try:
405             # section may be a default section so, search
406             # for the value in the list of defaults
407             return self.find_value(self.default(section), key)
408         except KeyError:
409             raise LookupError("key %r not found for section %r"
410                               % (key, section))
411
412     def multi_get(self, section, key_list):
413         """
414         Retrieves the list of values from a section for a list of keys.
415         This method is intended to be used for equivalent keys. Thus, as soon
416         as any match is found for any key in the key_list, the match is
417         returned. This does not concatenate the lookups of all of the keys
418         together.
419         """
420         for i in key_list:
421             try:
422                 return self.get(section, i)
423             except LookupError:
424                 pass
425
426         # Making it here means all lookups failed.
427         raise LookupError("keys %r not found for section %r" %
428                           (key_list, section))
429
430     def set(self, section, key, val):
431         """Sets an option in the given section."""
432         # TODO - set in multiple sections? (for now set in first)
433         # TODO - set in both sections and defaults?
434         if section in self._sections:
435             self.section(section)[0][key] = val
436         else:
437             self.defaults(section)[0][key] = val
438
439     def read(self, filename, sect=None):
440         """Parse configuration information from a file"""
441         try:
442             with open(filename, 'rt') as config_file:
443                 self._read(config_file, sect)
444         except IOError:
445             print "Could not open file ", filename, " for reading"
446
447     def _read(self, config_file, sect):
448         """Parse configuration information from the config_file"""
449         is_comment = False  # used for multi-lined comments
450         for line in config_file:
451             line, is_comment = remove_comment(line, is_comment)
452             if not line:
453                 # line was empty or was a comment
454                 continue
455
456             include_name = try_include(line)
457             if include_name:
458                 parser = self.add_include(include_name)
459                 parser.read(include_name, sect)
460                 continue
461
462             section, is_template, templates = try_section(line)
463             if section:
464                 if section == DEFAULTSECT or is_template:
465                     sect = self.add_default(section, templates)
466                 else:
467                     sect = self.add_section(section, templates)
468                 continue
469
470             key, val = try_option(line)
471             if sect is None:
472                 raise Exception("Section not defined before assignment")
473             sect[key] = val
474
475     def write(self, config_file):
476         """Write configuration information out to a file"""
477         try:
478             for key, val in self._includes.iteritems():
479                 val.write(key)
480                 config_file.write('#include "%s"\n' % key)
481
482             config_file.write('\n')
483             write_dicts(config_file, self._defaults)
484             write_dicts(config_file, self._sections)
485         except:
486             try:
487                 with open(config_file, 'wt') as fp:
488                     self.write(fp)
489             except IOError:
490                 print "Could not open file ", config_file, " for writing"