Improve the g_file_make_symbolic_link docs
[platform/upstream/glib.git] / gio / gsettings-schema-convert
1 #!/usr/bin/env python
2 # vim: set ts=4 sw=4 et: coding=UTF-8
3 #
4 # Copyright (c) 2010, Novell, Inc.
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
19 # USA.
20 #
21 # Authors: Vincent Untz <vuntz@gnome.org>
22
23 # TODO: add alias support for choices
24 #       choices: 'this-is-an-alias' = 'real', 'other', 'real'
25 # TODO: we don't support migrating a pair from a gconf schema. It has yet to be
26 #       seen in real-world usage, though.
27
28 import os
29 import sys
30
31 import optparse
32
33 try:
34     from lxml import etree as ET
35 except ImportError:
36     try:
37         from xml.etree import cElementTree as ET
38     except ImportError:
39         import cElementTree as ET
40
41
42 GSETTINGS_SIMPLE_SCHEMA_INDENT = '  '
43 TYPES_FOR_CHOICES = [ 's' ]
44 TYPES_FOR_RANGE = [ 'y', 'n', 'q', 'i', 'u', 'x', 't', 'h', 'd' ]
45
46
47 ######################################
48
49
50 def is_schema_id_valid(id):
51     # FIXME: there's currently no restriction on what an id should contain,
52     # but there might be some later on
53     return True
54
55
56 def is_key_name_valid(name):
57     # FIXME: we could check that name is valid ([-a-z0-9], no leading/trailing
58     # -, no leading digit, 32 char max). Note that we don't want to validate
59     # the key when converting from gconf, though, since gconf keys use
60     # underscores.
61     return True
62
63
64 def are_choices_valid(choices):
65     # FIXME: we could check that all values have the same type with GVariant
66     return True
67
68
69 def is_range_valid(minmax):
70     # FIXME: we'll be able to easily check min < max once we can convert the
71     # values with GVariant
72     return True
73
74
75 ######################################
76
77
78 class GSettingsSchemaConvertException(Exception):
79     pass
80
81
82 ######################################
83
84
85 class GSettingsSchemaRoot:
86
87     def __init__(self):
88         self.gettext_domain = None
89         self.schemas = []
90
91     def get_simple_string(self):
92         need_empty_line = False
93         result = ''
94
95         for schema in self.schemas:
96             if need_empty_line:
97                 result += '\n'
98             result += schema.get_simple_string()
99             if result:
100                 need_empty_line = True
101
102         # Only put the gettext domain if we have some content
103         if result and self.gettext_domain:
104             result = 'gettext-domain: %s\n\n%s' % (self.gettext_domain, result)
105
106         return result
107
108     def get_xml_node(self):
109         schemalist_node = ET.Element('schemalist')
110         if self.gettext_domain:
111             schemalist_node.set('gettext-domain', self.gettext_domain)
112         for schema in self.schemas:
113             for schema_node in schema.get_xml_nodes():
114                 schemalist_node.append(schema_node)
115         return schemalist_node
116
117
118 ######################################
119
120
121 class GSettingsSchema:
122
123     def __init__(self):
124         self.id = None
125         self.path = None
126         # only set when this schema is a child
127         self.name = None
128
129         self.gettext_domain = None
130         self.children = []
131         self.keys = []
132
133     def get_simple_string(self, current_indent = '', parent_path = ''):
134         if not self.children and not self.keys:
135             return ''
136
137         content = self._get_simple_string_for_content(current_indent)
138         if not content:
139             return ''
140
141         if self.name:
142             id = 'child %s' % self.name
143             force_empty_line = False
144         else:
145             id = 'schema %s' % self.id
146             force_empty_line = True
147
148         result = ''
149         result += '%s%s:\n' % (current_indent, id)
150         result += self._get_simple_string_for_attributes(current_indent, parent_path, force_empty_line)
151         result += content
152
153         return result
154
155     def _get_simple_string_for_attributes(self, current_indent, parent_path, force_empty_line):
156         need_empty_line = force_empty_line
157         result = ''
158
159         if self.gettext_domain:
160             result += '%sgettext-domain: %s\n' % (current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT, self.gettext_domain)
161             need_empty_line = True
162         if self.path and (not parent_path or (self.path != '%s%s/' % (parent_path, self.name))):
163             result += '%spath: %s\n' % (current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT, self.path)
164             need_empty_line = True
165         if need_empty_line:
166             result += '\n'
167
168         return result
169
170     def _get_simple_string_for_content(self, current_indent):
171         need_empty_line = False
172         result = ''
173
174         for key in self.keys:
175             result += key.get_simple_string(current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT)
176             need_empty_line = True
177
178         for child in self.children:
179             if need_empty_line:
180                 result += '\n'
181             result += child.get_simple_string(current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT, self.path)
182             if result:
183                 need_empty_line = True
184
185         return result
186
187     def get_xml_nodes(self):
188         if not self.children and not self.keys:
189             return []
190
191         (node, children_nodes) = self._get_xml_nodes_for_content()
192         if node is None:
193             return []
194
195         node.set('id', self.id)
196         if self.path:
197             node.set('path', self.path)
198
199         nodes = [ node ]
200         nodes.extend(children_nodes)
201
202         return nodes
203
204     def _get_xml_nodes_for_content(self):
205         if not self.keys and not self.children:
206             return (None, None)
207
208         children_nodes = []
209
210         schema_node = ET.Element('schema')
211         if self.gettext_domain:
212             schema_node.set('gettext-domain', self.gettext_domain)
213
214         for key in self.keys:
215             key_node = key.get_xml_node()
216             schema_node.append(key_node)
217         for child in self.children:
218             child_nodes = child.get_xml_nodes()
219             children_nodes.extend(child_nodes)
220             child_node = ET.SubElement(schema_node, 'child')
221             if not child.name:
222                 raise GSettingsSchemaConvertException('Internal error: child being processed with no schema id.')
223             child_node.set('name', child.name)
224             child_node.set('schema', '%s' % child.id)
225
226         return (schema_node, children_nodes)
227
228
229 ######################################
230
231
232 class GSettingsSchemaKey:
233
234     def __init__(self):
235         self.name = None
236         self.type = None
237         self.default = None
238         self.typed_default = None
239         self.l10n = None
240         self.l10n_context = None
241         self.summary = None
242         self.description = None
243         self.choices = None
244         self.range = None
245
246     def fill(self, name, type, default, typed_default, l10n, l10n_context, summary, description, choices, range):
247         self.name = name
248         self.type = type
249         self.default = default
250         self.typed_default = typed_default
251         self.l10n = l10n
252         self.l10n_context = l10n_context
253         self.summary = summary
254         self.description = description
255         self.choices = choices
256         self.range = range
257
258     def _has_range_choices(self):
259         return self.choices is not None and self.type in TYPES_FOR_CHOICES
260
261     def _has_range_minmax(self):
262         return self.range is not None and len(self.range) == 2 and self.type in TYPES_FOR_RANGE
263
264     def get_simple_string(self, current_indent):
265         # FIXME: kill this when we'll have python bindings for GVariant. Right
266         # now, every simple format schema we'll generate has to have an
267         # explicit type since we can't guess the type later on when converting
268         # to XML.
269         self.typed_default = '@%s %s' % (self.type, self.default)
270
271         result = ''
272         result += '%skey %s = %s\n' % (current_indent, self.name, self.typed_default or self.default)
273         current_indent += GSETTINGS_SIMPLE_SCHEMA_INDENT
274         if self.l10n:
275             l10n = self.l10n
276             if self.l10n_context:
277                 l10n += ' %s' % self.l10n_context
278             result += '%sl10n: %s\n' % (current_indent, l10n)
279         if self.summary:
280             result += '%ssummary: %s\n' % (current_indent, self.summary)
281         if self.description:
282             result += '%sdescription: %s\n' % (current_indent, self.description)
283         if self._has_range_choices():
284             result += '%schoices: %s\n' % (current_indent, ', '.join(self.choices))
285         elif self._has_range_minmax():
286             result += '%srange: %s\n' % (current_indent, '%s..%s' % (self.range[0] or '', self.range[1] or ''))
287         return result
288
289     def get_xml_node(self):
290         key_node = ET.Element('key')
291         key_node.set('name', self.name)
292         key_node.set('type', self.type)
293         default_node = ET.SubElement(key_node, 'default')
294         default_node.text = self.default
295         if self.l10n:
296             default_node.set('l10n', self.l10n)
297             if self.l10n_context:
298                 default_node.set('context', self.l10n_context)
299         if self.summary:
300             summary_node = ET.SubElement(key_node, 'summary')
301             summary_node.text = self.summary
302         if self.description:
303             description_node = ET.SubElement(key_node, 'description')
304             description_node.text = self.description
305         if self._has_range_choices():
306             choices_node = ET.SubElement(key_node, 'choices')
307             for choice in self.choices:
308                 choice_node = ET.SubElement(choices_node, 'choice')
309                 choice_node.set('value', choice)
310         elif self._has_range_minmax():
311             (min, max) = self.range
312             range_node = ET.SubElement(key_node, 'range')
313             min_node = ET.SubElement(range_node, 'min')
314             if min:
315                 min_node.text = min
316             max_node = ET.SubElement(range_node, 'max')
317             if max:
318                 max_node.text = max
319         return key_node
320
321
322 ######################################
323
324
325 class SimpleSchemaParser:
326
327     allowed_tokens = {
328       ''               : [ 'gettext-domain', 'schema' ],
329       'gettext-domain' : [ ],
330       'schema'         : [ 'gettext-domain', 'path', 'child', 'key' ],
331       'path'           : [ ],
332       'child'          : [ 'gettext-domain', 'child', 'key' ],
333       'key'            : [ 'l10n', 'summary', 'description', 'choices', 'range' ],
334       'l10n'           : [ ],
335       'summary'        : [ ],
336       'description'    : [ ],
337       'choices'        : [ ],
338       'range'          : [ ]
339     }
340
341     allowed_separators = [ ':', '=' ]
342
343     def __init__(self, file):
344         self.file = file
345
346         self.root = GSettingsSchemaRoot()
347
348         # this is just a convenient helper to remove the leading indentation
349         # that should be common to all lines
350         self.leading_indent = None
351
352         self.indent_stack = []
353         self.token_stack = []
354         self.object_stack = [ self.root ]
355
356         self.previous_token = None
357         self.current_token = None
358         self.unparsed_line = ''
359
360     def _eat_indent(self):
361         line = self.unparsed_line
362         i = 0
363         buf = ''
364         previous_max_index = len(self.indent_stack) - 1
365         index = -1
366
367         while i < len(line) - 1 and line[i].isspace():
368             buf += line[i]
369             i += 1
370             if previous_max_index > index:
371                 if buf == self.indent_stack[index + 1]:
372                     buf = ''
373                     index += 1
374                     continue
375                 elif self.indent_stack[index + 1].startswith(buf):
376                     continue
377                 else:
378                     raise GSettingsSchemaConvertException('Inconsistent indentation.')
379             else:
380                 continue
381
382         if buf and previous_max_index > index:
383             raise GSettingsSchemaConvertException('Inconsistent indentation.')
384         elif buf and previous_max_index <= index:
385             self.indent_stack.append(buf)
386         elif previous_max_index > index:
387             self.indent_stack = self.indent_stack[:index + 1]
388
389         self.unparsed_line = line[i:]
390
391     def _parse_word(self):
392         line = self.unparsed_line
393         i = 0
394         while i < len(line) and not line[i].isspace() and not line[i] in self.allowed_separators:
395             i += 1
396         self.unparsed_line = line[i:]
397         return line[:i]
398
399     def _word_to_token(self, word):
400         lower = word.lower()
401         if lower and lower in self.allowed_tokens.keys():
402             return lower
403         raise GSettingsSchemaConvertException('\'%s\' is not a valid token.' % lower)
404
405     def _token_allow_separator(self):
406         return self.current_token in [ 'gettext-domain', 'path', 'l10n', 'summary', 'description', 'choices', 'range' ]
407
408     def _parse_id_without_separator(self):
409         line = self.unparsed_line
410         if line[-1] in self.allowed_separators:
411             line = line[:-1].strip()
412         if not is_schema_id_valid(line):
413             raise GSettingsSchemaConvertException('\'%s\' is not a valid schema id.' % line)
414
415         self.unparsed_line = ''
416         return line
417
418     def _parse_key(self):
419         line = self.unparsed_line
420
421         split = False
422         for separator in self.allowed_separators:
423             items = line.split(separator)
424             if len(items) == 2:
425                 split = True
426                 break
427
428         if not split:
429             raise GSettingsSchemaConvertException('Key \'%s\' cannot be parsed.' % line)
430
431         name = items[0].strip()
432         if not is_key_name_valid(name):
433             raise GSettingsSchemaConvertException('\'%s\' is not a valid key name.' % name)
434
435         type = ''
436         value = items[1].strip()
437         if value[0] == '@':
438             i = 1
439             while not value[i].isspace():
440                 i += 1
441             type = value[1:i]
442             value = value[i:].strip()
443             if not value:
444                 raise GSettingsSchemaConvertException('No value specified for key \'%s\' (\'%s\').' % (name, line))
445
446         self.unparsed_line = ''
447
448         object = GSettingsSchemaKey()
449         object.name = name
450         object.type = type
451         object.default = value
452
453         return object
454
455     def _parse_l10n(self):
456         line = self.unparsed_line
457
458         items = [ item.strip() for item in line.split(' ', 1) if item.strip() ]
459         if not items:
460             self.unparsed_line = ''
461             return (None, None)
462         if len(items) == 1:
463             self.unparsed_line = ''
464             return (items[0], None)
465         if len(items) == 2:
466             self.unparsed_line = ''
467             return (items[0], items[1])
468
469         raise GSettingsSchemaConvertException('Internal error: more items than expected for localization \'%s\'.' % line)
470
471     def _parse_choices(self, object):
472         if object.type not in TYPES_FOR_CHOICES:
473             raise GSettingsSchemaConvertException('Key \'%s\' of type \'%s\' cannot have choices.' % (object.name, object.type))
474
475         line = self.unparsed_line
476         choices = [ item.strip() for item in line.split(',') ]
477         if not are_choices_valid(choices):
478             raise GSettingsSchemaConvertException('\'%s\' is not a valid choice.' % line)
479
480         self.unparsed_line = ''
481         return choices
482
483     def _parse_range(self, object):
484         if object.type not in TYPES_FOR_RANGE:
485             raise GSettingsSchemaConvertException('Key \'%s\' of type \'%s\' cannot have a range.' % (object.name, object.type))
486
487         line = self.unparsed_line
488         minmax = [ item.strip() for item in line.split('..') ]
489
490         if len(minmax) != 2:
491             raise GSettingsSchemaConvertException('Range \'%s\' cannot be parsed.' % line)
492         if not is_range_valid(minmax):
493             raise GSettingsSchemaConvertException('\'%s\' is not a valid range.' % line)
494
495         self.unparsed_line = ''
496         return tuple(minmax)
497
498     def parse_line(self, line):
499         # make sure that lines with only spaces are ignored and considered as
500         # empty lines
501         self.unparsed_line = line.rstrip()
502
503         # ignore empty line
504         if not self.unparsed_line:
505             return
506
507         # look at the indentation to know where we should be
508         self._eat_indent()
509         if self.leading_indent is None:
510             self.leading_indent = len(self.indent_stack)
511
512         # ignore comments
513         if self.unparsed_line[0] == '#':
514             return
515
516         word = self._parse_word()
517         if self.current_token:
518             self.previous_token = self.current_token
519         self.current_token = self._word_to_token(word)
520         self.unparsed_line = self.unparsed_line.lstrip()
521
522         allow_separator = self._token_allow_separator()
523         if len(self.unparsed_line) > 0 and self.unparsed_line[0] in self.allowed_separators:
524             if allow_separator:
525                 self.unparsed_line = self.unparsed_line[1:].lstrip()
526             else:
527                 raise GSettingsSchemaConvertException('Separator \'%s\' is not allowed after \'%s\'.' % (self.unparsed_line[0], self.current_token))
528
529         new_level = len(self.indent_stack) - self.leading_indent
530         old_level = len(self.token_stack)
531
532         if new_level > old_level + 1:
533             raise GSettingsSchemaConvertException('Internal error: stacks not in sync.')
534         elif new_level <= old_level:
535             self.token_stack = self.token_stack[:new_level]
536             # we always have the root
537             self.object_stack = self.object_stack[:new_level + 1]
538
539         if new_level == 0:
540             parent_token = ''
541         else:
542             parent_token = self.token_stack[-1]
543
544         # there's new indentation, but no token is allowed under the previous
545         # one
546         if new_level == old_level + 1 and self.previous_token != parent_token:
547             raise GSettingsSchemaConvertException('\'%s\' is not allowed under \'%s\'.' % (self.current_token, self.previous_token))
548
549         if not self.current_token in self.allowed_tokens[parent_token]:
550             if parent_token:
551                 error = '\'%s\' is not allowed under \'%s\'.' % (self.current_token, parent_token)
552             else:
553                 error = '\'%s\' is not allowed at the root level.' % self.current_token
554             raise GSettingsSchemaConvertException(error)
555
556         current_object = self.object_stack[-1]
557
558         new_object = None
559         if self.current_token == 'gettext-domain':
560             current_object.gettext_domain = self.unparsed_line
561         elif self.current_token == 'schema':
562             name = self._parse_id_without_separator()
563             new_object = GSettingsSchema()
564             new_object.id = name
565             current_object.schemas.append(new_object)
566         elif self.current_token == 'path':
567             current_object.path = self.unparsed_line
568         elif self.current_token == 'child':
569             if not isinstance(current_object, GSettingsSchema):
570                 raise GSettingsSchemaConvertException('Internal error: child being processed with no parent schema.')
571             name = self._parse_id_without_separator()
572             new_object = GSettingsSchema()
573             new_object.id = '%s.%s' % (current_object.id, name)
574             if current_object.path:
575                 new_object.path = '%s%s/' % (current_object.path, name)
576             new_object.name = name
577             current_object.children.append(new_object)
578         elif self.current_token == 'key':
579             new_object =  self._parse_key()
580             current_object.keys.append(new_object)
581         elif self.current_token == 'l10n':
582             (current_object.l10n, current_object.l10n_context) = self._parse_l10n()
583         elif self.current_token == 'summary':
584             current_object.summary = self.unparsed_line
585         elif self.current_token == 'description':
586             current_object.description = self.unparsed_line
587         elif self.current_token == 'choices':
588             current_object.choices = self._parse_choices(current_object)
589         elif self.current_token == 'range':
590             current_object.range = self._parse_range(current_object)
591
592         if new_object:
593             self.token_stack.append(self.current_token)
594             self.object_stack.append(new_object)
595
596     def parse(self):
597         f = open(self.file, 'r')
598         lines = [ line[:-1] for line in f.readlines() ]
599         f.close()
600
601         try:
602             current_line_nb = 0
603             for line in lines:
604                 current_line_nb += 1
605                 self.parse_line(line)
606         except GSettingsSchemaConvertException, e:
607             raise GSettingsSchemaConvertException('%s:%s: %s' % (os.path.basename(self.file), current_line_nb, e))
608
609         return self.root
610
611
612 ######################################
613
614
615 class XMLSchemaParser:
616
617     def __init__(self, file):
618         self.file = file
619
620         self.root = None
621
622     def _parse_key(self, key_node, schema):
623         key = GSettingsSchemaKey()
624
625         key.name = key_node.get('name')
626         if not key.name:
627             raise GSettingsSchemaConvertException('A key in schema \'%s\' has no name.' % schema.id)
628         key.type = key_node.get('type')
629         if not key.type:
630             raise GSettingsSchemaConvertException('Key \'%s\' in schema \'%s\' has no type.' % (key.name, schema.id))
631
632         default_node = key_node.find('default')
633         if default_node is None or not default_node.text.strip():
634             raise GSettingsSchemaConvertException('Key \'%s\' in schema \'%s\' has no default value.' % (key.name, schema.id))
635         key.l10n = default_node.get('l10n')
636         key.l10n_context = default_node.get('context')
637         key.default = default_node.text.strip()
638
639         summary_node = key_node.find('summary')
640         if summary_node is not None:
641             key.summary = summary_node.text.strip()
642         description_node = key_node.find('description')
643         if description_node is not None:
644             key.description = description_node.text.strip()
645
646         range_node = key_node.find('range')
647         if range_node is not None:
648             min = None
649             max = None
650             min_node = range_node.find('min')
651             if min_node is not None:
652                 min = min_node.text.strip()
653             max_node = range_node.find('max')
654             if max_node is not None:
655                 max = max_node.text.strip()
656             if min or max:
657                 self.range = (min, max)
658
659         choices_node = key_node.find('choices')
660         if choices_node is not None:
661             self.choices = []
662             for choice_node in choices_node.findall('choice'):
663                 value = choice_node.get('value')
664                 if value:
665                     self.choices.append(value)
666                 else:
667                     raise GSettingsSchemaConvertException('A choice for key \'%s\' in schema \'%s\' has no value.' % (key.name, schema.id))
668
669         return key
670
671     def _parse_schema(self, schema_node):
672         schema = GSettingsSchema()
673
674         schema._children = []
675
676         schema.id = schema_node.get('id')
677         if not schema.id:
678             raise GSettingsSchemaConvertException('A schema has no id.')
679         schema.path = schema_node.get('path')
680         schema.gettext_domain = schema_node.get('gettext-domain')
681
682         for key_node in schema_node.findall('key'):
683             key = self._parse_key(key_node, schema)
684             schema.keys.append(key)
685
686         for child_node in schema_node.findall('child'):
687             child_name = child_node.get('name')
688             if not child_name:
689                 raise GSettingsSchemaConvertException('A child of schema \'%s\' has no name.' % schema.id)
690             child_schema = child_node.get('schema')
691             if not child_schema:
692                 raise GSettingsSchemaConvertException('Child \'%s\' of schema \'%s\' has no schema.' % (child_name, schema.id))
693
694             expected_id = schema.id + '.' + child_name
695             if child_schema != expected_id:
696                 raise GSettingsSchemaConvertException('\'%s\' is too complex for this tool: child \'%s\' of schema \'%s\' has a schema that is not the expected one (\'%s\' vs \'%s\').' % (os.path.basename(self.file), child_name, schema.id, child_schema, expected_id))
697
698             schema._children.append((child_schema, child_name))
699
700         return schema
701
702     def parse(self):
703         self.root = GSettingsSchemaRoot()
704         schemas = []
705         parent = {}
706
707         schemalist_node = ET.parse(self.file).getroot()
708         self.root.gettext_domain = schemalist_node.get('gettext-domain')
709
710         for schema_node in schemalist_node.findall('schema'):
711             schema = self._parse_schema(schema_node)
712
713             for (child_schema, child_name) in schema._children:
714                 if parent.has_key(child_schema):
715                     raise GSettingsSchemaConvertException('Child \'%s\' is declared by two different schemas: \'%s\' and \'%s\'.' % (child_schema, parent[child_schema], schema.id))
716                 parent[child_schema] = schema
717
718             schemas.append(schema)
719
720         # now let's move all schemas where they should leave
721         for schema in schemas:
722             if parent.has_key(schema.id):
723                 parent_schema = parent[schema.id]
724
725                 # check that the paths of parent and child are supported by
726                 # this tool
727                 found = False
728                 for (child_schema, child_name) in parent_schema._children:
729                     if child_schema == schema.id:
730                         found = True
731                         break
732
733                 if not found:
734                     raise GSettingsSchemaConvertException('Internal error: child not found in parent\'s children.')
735
736                 schema.name = child_name
737                 parent_schema.children.append(schema)
738             else:
739                 self.root.schemas.append(schema)
740
741         return self.root
742
743
744 ######################################
745
746
747 def map_gconf_type_to_variant_type(gconftype, gconfsubtype):
748     typemap = { 'string': 's', 'int': 'i', 'float': 'd', 'bool': 'b', 'list': 'a' }
749     result = typemap[gconftype]
750     if gconftype == 'list':
751         result = result + typemap[gconfsubtype]
752     return result
753
754
755 class GConfSchema:
756
757     def __init__(self, node):
758         locale_node = node.find('locale')
759
760         self.key = node.find('key').text
761         self.type = node.find('type').text
762         if self.type == 'list':
763             self.list_type = node.find('list_type').text
764         else:
765             self.list_type = None
766         self.varianttype = map_gconf_type_to_variant_type(self.type, self.list_type)
767
768         applyto_node = node.find('applyto')
769         if applyto_node is not None:
770             self.applyto = node.find('applyto').text
771             self.applyto.strip()
772             self.keyname = self.applyto[self.applyto.rfind('/')+1:]
773             self.prefix = self.applyto[:self.applyto.rfind('/')+1]
774         else:
775             self.applyto = None
776             self.key.strip()
777             self.keyname = self.key[self.key.rfind('/')+1:]
778             self.prefix = self.key[:self.key.rfind('/')+1]
779         self.prefix = os.path.normpath(self.prefix)
780
781         try:
782             self.default = locale_node.find('default').text
783             self.localized = 'messages'
784         except:
785             try:
786                 self.default = node.find('default').text
787                 self.localized = None
788             except:
789                 raise GSettingsSchemaConvertException('No default value for key \'%s\'. A default value is always required in GSettings schemas.' % self.applyto or self.key)
790         self.typed_default = None
791
792         self.short = self._get_value_with_locale(node, locale_node, 'short')
793         self.long = self._get_value_with_locale(node, locale_node, 'long')
794
795         if self.short:
796             self.short = self._oneline(self.short)
797         if self.long:
798             self.long = self._oneline(self.long)
799
800         # Fix the default to be parsable by GVariant
801         if self.type == 'string':
802             if not self.default:
803                 self.default = '\'\''
804             else:
805                 self.default.replace('\'', '\\\'')
806                 self.default = '\'%s\'' % self.default
807         elif self.type == 'bool':
808             self.default = self.default.lower()
809         elif self.type == 'list':
810             l = self.default.strip()
811             if not (l[0] == '[' and l[-1] == ']'):
812                 raise GSettingsSchemaConvertException('Cannot parse default list value \'%s\' for key \'%s\'.' % (self.default, self.applyto or self.key))
813             values = l[1:-1].strip()
814             if not values:
815                 self.typed_default = '@%s []' % self.varianttype
816             elif self.list_type == 'string':
817                 items = [ item.strip() for item in values.split(',') ]
818                 items = [ item.replace('\'', '\\\'') for item in items ]
819                 values = ', '.join([ '\'%s\'' % item for item in items ])
820                 self.default = '[ %s ]' % values
821
822     def _get_value_with_locale(self, node, locale_node, element):
823         element_node = None
824         if locale_node is not None:
825             element_node = locale_node.find(element)
826         if element_node is None:
827             element_node = node.find(element)
828         if element_node is not None:
829             return element_node.text
830         else:
831             return None
832
833     def _oneline(self, s):
834         lines = s.splitlines()
835         result = ''
836         for line in lines:
837             result += ' ' + line.lstrip()
838         return result.strip()
839
840     def get_gsettings_schema_key(self):
841         key = GSettingsSchemaKey()
842         key.fill(self.keyname, self.varianttype, self.default, self.typed_default, self.localized, self.keyname, self.short, self.long, None, None)
843         return key
844
845
846 ######################################
847
848
849 class GConfSchemaParser:
850
851     def __init__(self, file, default_gettext_domain, default_schema_id):
852         self.file = file
853         self.default_gettext_domain = default_gettext_domain
854         self.default_schema_id = default_schema_id
855
856         self.root = None
857         self.default_schema_id_count = 0
858
859     def _insert_schema(self, gconf_schema):
860         schemas_only = (gconf_schema.applyto is None)
861
862         dirpath = gconf_schema.prefix
863         if dirpath[0] != '/':
864             raise GSettingsSchemaConvertException('Key \'%s\' has a relative path. There is no relative path in GSettings schemas.' % gconf_schema.applyto or gconf_schema.key)
865
866         # remove leading 'schemas/' for schemas-only keys
867         if schemas_only and dirpath.startswith('/schemas/'):
868             dirpath = dirpath[len('/schemas'):]
869
870         if len(dirpath) == 1:
871             raise GSettingsSchemaConvertException('Key \'%s\' is a toplevel key. Toplevel keys are not accepted in GSettings schemas.' % gconf_schema.applyto or gconf_schema.key)
872
873         # remove trailing slash because we'll split the string
874         if dirpath[-1] == '/':
875             dirpath = dirpath[:-1]
876         # and also remove leading slash when splitting
877         hierarchy = dirpath[1:].split('/')
878
879         # we don't want to put apps/ and desktop/ keys in the same schema,
880         # so we have a first step where we make sure to create a new schema
881         # to avoid this case if necessary
882         gsettings_schema = None
883         for schema in self.root.schemas:
884             if schemas_only:
885                 schema_path = schema._hacky_path
886             else:
887                 schema_path = schema.path
888             if dirpath.startswith(schema_path):
889                 gsettings_schema = schema
890                 break
891         if not gsettings_schema:
892             gsettings_schema = GSettingsSchema()
893             if schemas_only:
894                 gsettings_schema._hacky_path = '/' + hierarchy[0] + '/'
895             else:
896                 gsettings_schema.path = '/' + hierarchy[0] + '/'
897             self.root.schemas.append(gsettings_schema)
898
899         # we create the schema hierarchy that leads to this key
900         gsettings_dir = gsettings_schema
901         for item in hierarchy[1:]:
902             subdir = None
903             for child in gsettings_dir.children:
904                 if child.name == item:
905                     subdir = child
906                     break
907             if not subdir:
908                 subdir = GSettingsSchema()
909                 # note: the id will be set later on
910                 if gsettings_dir.path:
911                     subdir.path = '%s%s/' % (gsettings_dir.path, item)
912                 subdir.name = item
913                 gsettings_dir.children.append(subdir)
914             gsettings_dir = subdir
915
916         # we have the final directory, so we can put the key there
917         gsettings_dir.keys.append(gconf_schema.get_gsettings_schema_key())
918
919     def _set_children_id(self, schema):
920         for child in schema.children:
921             child.id = '%s.%s' % (schema.id, child.name)
922             self._set_children_id(child)
923
924     def _fix_hierarchy(self):
925         for schema in self.root.schemas:
926             # we created one schema per level, starting at the root level;
927             # however, we don't need to go that far and we can simplify the
928             # hierarchy
929             while len(schema.children) == 1 and not schema.keys:
930                 child = schema.children[0]
931                 schema.children = child.children
932                 schema.keys = child.keys
933                 if schema.path:
934                     schema.path += child.name + '/'
935
936             # now that we have a toplevel schema, set the id
937             if self.default_schema_id:
938                 schema.id = self.default_schema_id
939                 if self.default_schema_id_count > 0:
940                     schema.id += '.FIXME-%s' % self.default_schema_id_count
941                 self.default_schema_id_count += 1
942             else:
943                 schema.id = 'FIXME'
944             self._set_children_id(schema)
945
946     def parse(self):
947         # reset the state of the parser
948         self.root = GSettingsSchemaRoot()
949         self.default_schema_id_count = 0
950
951         gconfschemafile_node = ET.parse(self.file).getroot()
952         for schemalist_node in gconfschemafile_node.findall('schemalist'):
953             for schema_node in schemalist_node.findall('schema'):
954                 gconf_schema = GConfSchema(schema_node)
955                 if gconf_schema.localized:
956                     self.root.gettext_domain = self.default_gettext_domain or 'FIXME'
957                 self._insert_schema(gconf_schema)
958
959         self._fix_hierarchy()
960
961         return self.root
962
963
964 ######################################
965
966
967 def main(args):
968     parser = optparse.OptionParser()
969
970     parser.add_option("-o", "--output", dest="output",
971                       help="output file")
972     parser.add_option("-g", "--gconf", action="store_true", dest="gconf",
973                       default=False, help="convert a gconf schema file")
974     parser.add_option("-d", "--gettext-domain", dest="gettext_domain",
975                       help="default gettext domain to use when converting gconf schema file")
976     parser.add_option("-i", "--schema-id", dest="schema_id",
977                       help="default schema ID to use when converting gconf schema file")
978     parser.add_option("-s", "--simple", action="store_true", dest="simple",
979                       default=False, help="use the simple schema format as output (only for gconf schema conversion)")
980     parser.add_option("-x", "--xml", action="store_true", dest="xml",
981                       default=False, help="use the xml schema format as output")
982     parser.add_option("-f", "--force", action="store_true", dest="force",
983                       default=False, help="overwrite output file if already existing")
984
985     (options, args) = parser.parse_args()
986
987     if len(args) < 1:
988         print >> sys.stderr, 'Need a filename to work on.'
989         return 1
990     elif len(args) > 1:
991         print >> sys.stderr, 'Too many arguments.'
992         return 1
993
994     if options.simple and options.xml:
995         print >> sys.stderr, 'Too many output formats requested.'
996         return 1
997
998     if not options.gconf and options.gettext_domain:
999         print >> sys.stderr, 'Default gettext domain can only be specified when converting a gconf schema.'
1000         return 1
1001
1002     if not options.gconf and options.schema_id:
1003         print >> sys.stderr, 'Default schema ID can only be specified when converting a gconf schema.'
1004         return 1
1005
1006     argfile = os.path.expanduser(args[0])
1007     if not os.path.exists(argfile):
1008         print >> sys.stderr, '\'%s\' does not exist.' % argfile
1009         return 1
1010
1011     if options.output:
1012         options.output = os.path.expanduser(options.output)
1013
1014     try:
1015         if options.output and not options.force and os.path.exists(options.output):
1016             raise GSettingsSchemaConvertException('\'%s\' already exists. Use --force to overwrite it.' % options.output)
1017
1018         if options.gconf:
1019             if not options.simple and not options.xml:
1020                 options.simple = True
1021
1022             try:
1023                 parser = GConfSchemaParser(argfile, options.gettext_domain, options.schema_id)
1024                 schema_root = parser.parse()
1025             except SyntaxError, e:
1026                 raise GSettingsSchemaConvertException('\'%s\' does not look like a valid gconf schema file: %s' % (argfile, e))
1027         else:
1028             # autodetect if file is XML or not
1029             try:
1030                 parser = XMLSchemaParser(argfile)
1031                 schema_root = parser.parse()
1032                 if not options.simple and not options.xml:
1033                     options.simple = True
1034             except SyntaxError, e:
1035                 parser = SimpleSchemaParser(argfile)
1036                 schema_root = parser.parse()
1037                 if not options.simple and not options.xml:
1038                     options.xml = True
1039
1040         if options.xml:
1041             node = schema_root.get_xml_node()
1042             tree = ET.ElementTree(node)
1043             try:
1044                 output = ET.tostring(tree, pretty_print = True)
1045             except TypeError:
1046                 # pretty_print only works with lxml
1047                 output = ET.tostring(tree)
1048         else:
1049             output = schema_root.get_simple_string()
1050
1051         if not options.output:
1052             sys.stdout.write(output)
1053         else:
1054             try:
1055                 fout = open(options.output, 'w')
1056                 fout.write(output)
1057                 fout.close()
1058             except GSettingsSchemaConvertException, e:
1059                 fout.close()
1060                 if os.path.exists(options.output):
1061                     os.unlink(options.output)
1062                 raise e
1063
1064     except GSettingsSchemaConvertException, e:
1065         print >> sys.stderr, '%s' % e
1066         return 1
1067
1068     return 0
1069
1070
1071 if __name__ == '__main__':
1072     try:
1073         res = main(sys.argv)
1074         sys.exit(res)
1075     except KeyboardInterrupt:
1076         pass