Handle schemas and api versions in base class.
[leap_pycommon.git] / src / leap / common / config / baseconfig.py
1 # -*- coding: utf-8 -*-
2 # baseconfig.py
3 # Copyright (C) 2013 LEAP
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 """
19 Implements the abstract base class for configuration
20 """
21
22 import copy
23 import logging
24 import functools
25 import os
26
27 from abc import ABCMeta, abstractmethod
28
29 from leap.common.check import leap_assert, leap_check
30 from leap.common.files import mkdir_p
31 from leap.common.config.pluggableconfig import PluggableConfig
32 from leap.common.config.prefixers import get_platform_prefixer
33
34 logger = logging.getLogger(__name__)
35
36
37 class NonExistingSchema(Exception):
38     """
39     Raised if the schema needed to verify the config is None.
40     """
41
42
43 class BaseConfig:
44     """
45     Abstract base class for any JSON based configuration.
46     """
47
48     __metaclass__ = ABCMeta
49
50     """
51     Standalone is a class wide parameter.
52
53     :param standalone: if True it will return the prefix for a
54                        standalone application. Otherwise, it will
55                        return the system
56                        default for configuration storage.
57     :type standalone: bool
58     """
59     standalone = False
60
61     def __init__(self):
62         self._data = {}
63         self._config_checker = None
64         self._api_version = None
65
66     @abstractmethod
67     def _get_schema(self):
68         """
69         Returns the schema corresponding to the version given.
70
71         :rtype: dict or None if the version is not supported.
72         """
73         pass
74
75     def _get_spec(self):
76         """
77         Returns the spec object for the specific configuration.
78         """
79         leap_assert(self._api_version is not None,
80                     "You should set the API version.")
81
82         return self._get_schema()
83
84     def _safe_get_value(self, key):
85         """
86         Tries to return a value only if the config has already been loaded.
87
88         :rtype: depends on the config structure, dict, str, array, int
89         :return: returns the value for the specified key in the config
90         """
91         leap_assert(self._config_checker, "Load the config first")
92         return self._config_checker.config.get(key, None)
93
94     def set_api_version(self, version):
95         """
96         Sets the supported api version.
97
98         :param api_version: the version of the api supported by the provider.
99         :type api_version: str
100         """
101         self._api_version = version
102         leap_assert(self._get_schema(self._api_version) is not None,
103                     "Version %s is not supported." % (version, ))
104
105     def get_path_prefix(self):
106         """
107         Returns the platform dependant path prefixer
108         """
109         return get_platform_prefixer().get_path_prefix(
110             standalone=self.standalone)
111
112     def loaded(self):
113         """
114         Returns True if the configuration has been already
115         loaded. False otherwise
116         """
117         return self._config_checker is not None
118
119     def save(self, path_list):
120         """
121         Saves the current configuration to disk.
122
123         :param path_list: list of components that form the relative
124                           path to configuration. The absolute path
125                           will be calculated depending on the platform.
126         :type path_list: list
127
128         :return: True if saved to disk correctly, False otherwise
129         """
130         config_path = os.path.join(self.get_path_prefix(), *(path_list[:-1]))
131         mkdir_p(config_path)
132
133         try:
134             self._config_checker.serialize(os.path.join(config_path,
135                                                         path_list[-1]))
136         except Exception as e:
137             logger.warning("%s" % (e,))
138             raise
139         return True
140
141     def load(self, path="", data=None, mtime=None, relative=True):
142         """
143         Loads the configuration from disk.
144         It may raise NonExistingSchema exception.
145
146         :param path: if relative=True, this is a relative path
147                      to configuration. The absolute path
148                      will be calculated depending on the platform
149         :type path: str
150
151         :param relative: if True, path is relative. If False, it's absolute.
152         :type relative: bool
153
154         :return: True if loaded from disk correctly, False otherwise
155         :rtype: bool
156         """
157
158         if relative is True:
159             config_path = os.path.join(
160                 self.get_path_prefix(), path)
161         else:
162             config_path = path
163
164         schema = self._get_spec()
165         leap_check(schema is not None,
166                    "There is no schema to use.", NonExistingSchema)
167
168         self._config_checker = PluggableConfig(format="json")
169         self._config_checker.options = copy.deepcopy(schema)
170
171         try:
172             if data is None:
173                 self._config_checker.load(fromfile=config_path, mtime=mtime)
174             else:
175                 self._config_checker.load(data, mtime=mtime)
176         except Exception as e:
177             logger.error("Something went wrong while loading " +
178                          "the config from %s\n%s" % (config_path, e))
179             self._config_checker = None
180             return False
181         return True
182
183
184 class LocalizedKey(object):
185     """
186     Decorator used for keys that are localized in a configuration.
187     """
188
189     def __init__(self, func, **kwargs):
190         self._func = func
191
192     def __call__(self, instance, lang=None):
193         """
194         Tries to return the string for the specified language, otherwise
195         returns the default language string.
196
197         :param lang: language code
198         :type lang: str
199
200         :return: localized value from the possible values returned by
201                  self._func
202                  It returns None in case that the provider does not provides
203                  a matching pair of default_language and string for
204                  that language.
205                  e.g.:
206                      'default_language': 'es',
207                      'description': {'en': 'test description'}
208                 Note that the json schema can't check that.
209         """
210         descriptions = self._func(instance)
211         config_lang = instance.get_default_language()
212         if lang is None:
213             lang = config_lang
214
215         for key in descriptions.keys():
216             if lang.startswith(key):
217                 config_lang = key
218                 break
219
220         description_lang = descriptions.get(config_lang)
221         if description_lang is None:
222             logger.error("There is a misconfiguration in the "
223                          "provider's language strings.")
224
225         return description_lang
226
227     def __get__(self, instance, instancetype):
228         """
229         Implement the descriptor protocol to make decorating instance
230         method possible.
231         """
232         # Return a partial function with the first argument is the instance
233         # of the class decorated.
234         return functools.partial(self.__call__, instance)
235
236 if __name__ == "__main__":
237     try:
238         config = BaseConfig()  # should throw TypeError for _get_spec
239     except Exception as e:
240         assert isinstance(e, TypeError), "Something went wrong"
241         print "Abstract BaseConfig class is working as expected"