a6d88c7b75341aa52e635037b9c8c116e89ad52b
[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     elif line.startswith(";"):
207          # if the line is actually a comment just ignore it all
208          line = ""
209
210     return line.replace("\\", "").strip(), False
211
212 def try_include(line):
213     """
214     Checks to see if the given line is an include.  If so return the
215     included filename, otherwise None.
216     """
217
218     match = re.match('^#include\s*[<"]?(.*)[>"]?$', line)
219     return match.group(1) if match else None
220
221
222 def try_section(line):
223     """
224     Checks to see if the given line is a section. If so return the section
225     name, otherwise return 'None'.
226     """
227     # leading spaces were stripped when checking for comments
228     if not line.startswith('['):
229         return None, False, []
230
231     section, delim, templates = line.partition(']')
232     if not templates:
233         return section[1:], False, []
234
235     # strip out the parens and parse into an array
236     templates = templates.replace('(', "").replace(')', "").split(',')
237     # go ahead and remove extra whitespace
238     templates = [i.strip() for i in templates]
239     try:
240         templates.remove('!')
241         return section[1:], True, templates
242     except:
243         return section[1:], False, templates
244
245
246 def try_option(line):
247     """Parses the line as an option, returning the key/value pair."""
248     data = re.split('=>?', line, 1)
249     # should split in two (key/val), but either way use first two elements
250     return data[0].rstrip(), data[1].lstrip()
251
252 ###############################################################################
253
254
255 def find_dict(mdicts, key, val):
256     """
257     Given a list of mult-dicts, return the multi-dict that contains
258     the given key/value pair.
259     """
260
261     def found(d):
262         return key in d and val in d[key]
263
264     try:
265         return [d for d in mdicts if found(d)][0]
266     except IndexError:
267         raise LookupError("Dictionary not located for key = %s, value = %s"
268                           % (key, val))
269
270
271 def write_dicts(config_file, mdicts):
272     """Write the contents of the mdicts to the specified config file"""
273     for section, sect_list in mdicts.iteritems():
274         # every section contains a list of dictionaries
275         for sect in sect_list:
276             config_file.write("[%s]\n" % section)
277             for key, val_list in sect.iteritems():
278                 # every value is also a list
279                 for v in val_list:
280                     key_val = key
281                     if v is not None:
282                         key_val += " = " + str(v)
283                         config_file.write("%s\n" % (key_val))
284             config_file.write("\n")
285
286 ###############################################################################
287
288
289 class MultiOrderedConfigParser:
290     def __init__(self, parent=None):
291         self._parent = parent
292         self._defaults = MultiOrderedDict()
293         self._sections = MultiOrderedDict()
294         self._includes = OrderedDict()
295
296     def find_value(self, sections, key):
297         """Given a list of sections, try to find value(s) for the given key."""
298         # always start looking in the last one added
299         sections.sort(reverse=True)
300         for s in sections:
301             try:
302                 # try to find in section and section's templates
303                 return s.get(key, from_defaults=False)
304             except KeyError:
305                 pass
306
307         # wasn't found in sections or a section's templates so check in
308         # defaults
309         for s in sections:
310             try:
311                 # try to find in section's defaultsects
312                 return s.get(key, from_self=False, from_templates=False)
313             except KeyError:
314                 pass
315
316         raise KeyError(key)
317
318     def defaults(self):
319         return self._defaults
320
321     def default(self, key):
322         """Retrieves a list of dictionaries for a default section."""
323         return self.get_defaults(key)
324
325     def add_default(self, key, template_keys=None):
326         """
327         Adds a default section to defaults, returning the
328         default Section object.
329         """
330         if template_keys is None:
331             template_keys = []
332         return self.add_section(key, template_keys, self._defaults)
333
334     def sections(self):
335         return self._sections
336
337     def section(self, key):
338         """Retrieves a list of dictionaries for a section."""
339         return self.get_sections(key)
340
341     def get_sections(self, key, attr='_sections', searched=None):
342         """
343         Retrieve a list of sections that have values for the given key.
344         The attr parameter can be used to control what part of the parser
345         to retrieve values from.
346         """
347         if searched is None:
348             searched = []
349         if self in searched:
350             return []
351
352         sections = getattr(self, attr)
353         res = sections[key] if key in sections else []
354         searched.append(self)
355         if self._includes:
356             res.extend(list(itertools.chain(*[
357                 incl.get_sections(key, attr, searched)
358                 for incl in self._includes.itervalues()])))
359         if self._parent:
360             res += self._parent.get_sections(key, attr, searched)
361         return res
362
363     def get_defaults(self, key):
364         """
365         Retrieve a list of defaults that have values for the given key.
366         """
367         return self.get_sections(key, '_defaults')
368
369     def add_section(self, key, template_keys=None, mdicts=None):
370         """
371         Create a new section in the configuration. The name of the
372         new section is the 'key' parameter.
373         """
374         if template_keys is None:
375             template_keys = []
376         if mdicts is None:
377             mdicts = self._sections
378         res = Section()
379         for t in template_keys:
380             res.add_templates(self.get_defaults(t))
381         res.add_defaults(self.get_defaults(DEFAULTSECT))
382         mdicts.insert(0, key, res)
383         return res
384
385     def includes(self):
386         return self._includes
387
388     def add_include(self, filename, parser=None):
389         """
390         Add a new #include file to the configuration.
391         """
392         if filename in self._includes:
393             return self._includes[filename]
394
395         self._includes[filename] = res = \
396             MultiOrderedConfigParser(self) if parser is None else parser
397         return res
398
399     def get(self, section, key):
400         """Retrieves the list of values from a section for a key."""
401         try:
402             # search for the value in the list of sections
403             return self.find_value(self.section(section), key)
404         except KeyError:
405             pass
406
407         try:
408             # section may be a default section so, search
409             # for the value in the list of defaults
410             return self.find_value(self.default(section), key)
411         except KeyError:
412             raise LookupError("key %r not found for section %r"
413                               % (key, section))
414
415     def multi_get(self, section, key_list):
416         """
417         Retrieves the list of values from a section for a list of keys.
418         This method is intended to be used for equivalent keys. Thus, as soon
419         as any match is found for any key in the key_list, the match is
420         returned. This does not concatenate the lookups of all of the keys
421         together.
422         """
423         for i in key_list:
424             try:
425                 return self.get(section, i)
426             except LookupError:
427                 pass
428
429         # Making it here means all lookups failed.
430         raise LookupError("keys %r not found for section %r" %
431                           (key_list, section))
432
433     def set(self, section, key, val):
434         """Sets an option in the given section."""
435         # TODO - set in multiple sections? (for now set in first)
436         # TODO - set in both sections and defaults?
437         if section in self._sections:
438             self.section(section)[0][key] = val
439         else:
440             self.defaults(section)[0][key] = val
441
442     def read(self, filename, sect=None):
443         """Parse configuration information from a file"""
444         try:
445             with open(filename, 'rt') as config_file:
446                 self._read(config_file, sect)
447         except IOError:
448             print "Could not open file ", filename, " for reading"
449
450     def _read(self, config_file, sect):
451         """Parse configuration information from the config_file"""
452         is_comment = False  # used for multi-lined comments
453         for line in config_file:
454             line, is_comment = remove_comment(line, is_comment)
455             if not line:
456                 # line was empty or was a comment
457                 continue
458
459             include_name = try_include(line)
460             if include_name:
461                 parser = self.add_include(include_name)
462                 parser.read(include_name, sect)
463                 continue
464
465             section, is_template, templates = try_section(line)
466             if section:
467                 if section == DEFAULTSECT or is_template:
468                     sect = self.add_default(section, templates)
469                 else:
470                     sect = self.add_section(section, templates)
471                 continue
472
473             key, val = try_option(line)
474             if sect is None:
475                 raise Exception("Section not defined before assignment")
476             sect[key] = val
477
478     def write(self, config_file):
479         """Write configuration information out to a file"""
480         try:
481             for key, val in self._includes.iteritems():
482                 val.write(key)
483                 config_file.write('#include "%s"\n' % key)
484
485             config_file.write('\n')
486             write_dicts(config_file, self._defaults)
487             write_dicts(config_file, self._sections)
488         except:
489             try:
490                 with open(config_file, 'wt') as fp:
491                     self.write(fp)
492             except IOError:
493                 print "Could not open file ", config_file, " for writing"