1#!/usr/bin/env python3
2#
3# Copyright 2018 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Config class.
18
19History:
20    version 2: Record the user's each preferred ide version by the key name
21               [ide_base.ide_name]_preferred_version. E.g., the key name of the
22               preferred IntelliJ is IntelliJ_preferred_version and the example
23               is as follows.
24               "Android Studio_preferred_version": "/opt/android-studio-3.0/bin/
25               studio.sh"
26               "IntelliJ_preferred_version": "/opt/intellij-ce-stable/bin/
27               idea.sh"
28
29    version 1: Record the user's preferred IntelliJ version by the key name
30               preferred_version and doesn't support any other IDEs. The example
31               is "preferred_version": "/opt/intellij-ce-stable/bin/idea.sh".
32"""
33
34import copy
35import json
36import logging
37import os
38import re
39
40from aidegen import constant
41from aidegen import templates
42from aidegen.lib import common_util
43
44_DIR_LIB = 'lib'
45
46
47class AidegenConfig:
48    """Class manages AIDEGen's configurations.
49
50    Attributes:
51        _config: A dict contains the aidegen config.
52        _config_backup: A dict contains the aidegen config.
53    """
54
55    # Constants of AIDEGen config
56    _DEFAULT_CONFIG_FILE = 'aidegen.config'
57    _CONFIG_DIR = os.path.join(
58        os.path.expanduser('~'), '.config', 'asuite', 'aidegen')
59    _CONFIG_FILE_PATH = os.path.join(_CONFIG_DIR, _DEFAULT_CONFIG_FILE)
60    _KEY_APPEND = 'preferred_version'
61    _KEY_PLUGIN_PREFERENCE = 'Asuite_plugin_preference'
62
63    # Constants of enable debugger
64    _ENABLE_DEBUG_CONFIG_DIR = 'enable_debugger'
65    _ENABLE_DEBUG_CONFIG_FILE = 'enable_debugger.iml'
66    _ENABLE_DEBUG_DIR = os.path.join(_CONFIG_DIR, _ENABLE_DEBUG_CONFIG_DIR)
67    _DIR_SRC = 'src'
68    _DIR_GEN = 'gen'
69    DEBUG_ENABLED_FILE_PATH = os.path.join(_ENABLE_DEBUG_DIR,
70                                           _ENABLE_DEBUG_CONFIG_FILE)
71
72    # Constants of checking deprecated IntelliJ version.
73    # The launch file idea.sh of IntelliJ is in ASCII encoding.
74    ENCODE_TYPE = 'ISO-8859-1'
75    ACTIVE_KEYWORD = '$JAVA_BIN'
76
77    def __init__(self):
78        self._config = {}
79        self._config_backup = {}
80        self._create_config_folder()
81
82    def __enter__(self):
83        self._load_aidegen_config()
84        self._config_backup = copy.deepcopy(self._config)
85        return self
86
87    def __exit__(self, exc_type, exc_val, exc_tb):
88        self._save_aidegen_config()
89
90    def preferred_version(self, ide=None):
91        """AIDEGen configuration getter.
92
93        Args:
94            ide: The string of the relevant IDE name, same as the data of
95                 IdeBase._ide_name or IdeUtil.ide_name(). None represents the
96                 usage of the version 1.
97
98        Returns:
99            The preferred version item of configuration data if exists and is
100            not deprecated, otherwise None.
101        """
102        key = '_'.join([ide, self._KEY_APPEND]) if ide else self._KEY_APPEND
103        preferred_version = self._config.get(key, '')
104        # Backward compatible check.
105        if not preferred_version:
106            preferred_version = self._config.get(self._KEY_APPEND, '')
107
108        if preferred_version:
109            real_version = os.path.realpath(preferred_version)
110            if ide and not self.deprecated_version(ide, real_version):
111                return preferred_version
112            # Backward compatible handling.
113            if not ide and not self.deprecated_intellij_version(real_version):
114                return preferred_version
115        return None
116
117    def set_preferred_version(self, preferred_version, ide=None):
118        """AIDEGen configuration setter.
119
120        Args:
121            preferred_version: A string, user's preferred version to be set.
122            ide: The string of the relevant IDE name, same as the data of
123                 IdeBase._ide_name or IdeUtil.ide_name(). None presents the
124                 usage of the version 1.
125        """
126        key = '_'.join([ide, self._KEY_APPEND]) if ide else self._KEY_APPEND
127        self._config[key] = preferred_version
128
129    @property
130    def plugin_preference(self):
131        """Gets Asuite plugin user's preference
132
133        Returns:
134             A string of the user's preference: yes/no/auto.
135        """
136        return self._config.get(self._KEY_PLUGIN_PREFERENCE, '')
137
138    @plugin_preference.setter
139    def plugin_preference(self, preference):
140        """Sets Asuite plugin user's preference
141
142        Args:
143            preference: A string of the user's preference: yes/no/auto.
144        """
145        self._config[self._KEY_PLUGIN_PREFERENCE] = preference
146
147    def _load_aidegen_config(self):
148        """Load data from configuration file."""
149        if os.path.exists(self._CONFIG_FILE_PATH):
150            try:
151                with open(self._CONFIG_FILE_PATH) as cfg_file:
152                    self._config = json.load(cfg_file)
153            except ValueError as err:
154                info = '{} format is incorrect, error: {}'.format(
155                    self._CONFIG_FILE_PATH, err)
156                logging.info(info)
157            except IOError as err:
158                logging.error(err)
159                raise
160
161    def _save_aidegen_config(self):
162        """Save data to configuration file."""
163        if self._is_config_modified():
164            with open(self._CONFIG_FILE_PATH, 'w') as cfg_file:
165                json.dump(self._config, cfg_file, indent=4)
166
167    def _is_config_modified(self):
168        """Check if configuration data is modified."""
169        return any(key for key in self._config if not key in self._config_backup
170                   or self._config[key] != self._config_backup[key])
171
172    def _create_config_folder(self):
173        """Create the config folder if it doesn't exist."""
174        if not os.path.exists(self._CONFIG_DIR):
175            os.makedirs(self._CONFIG_DIR)
176
177    def _gen_enable_debug_sub_dir(self, dir_name):
178        """Generate a dir under enable debug dir.
179
180        Args:
181            dir_name: A string of the folder name.
182        """
183        _dir = os.path.join(self._ENABLE_DEBUG_DIR, dir_name)
184        if not os.path.exists(_dir):
185            os.makedirs(_dir)
186
187    def _gen_androidmanifest(self):
188        """Generate an AndroidManifest.xml under enable debug dir.
189
190        Once the AndroidManifest.xml does not exist or file size is zero,
191        AIDEGen will generate it with default content to prevent the red
192        underline error in IntelliJ.
193        """
194        _file = os.path.join(self._ENABLE_DEBUG_DIR, constant.ANDROID_MANIFEST)
195        if not os.path.exists(_file) or os.stat(_file).st_size == 0:
196            common_util.file_generate(_file, templates.ANDROID_MANIFEST_CONTENT)
197
198    def _gen_enable_debugger_config(self, android_sdk_version):
199        """Generate the enable_debugger.iml config file.
200
201        Re-generate the enable_debugger.iml everytime for correcting the Android
202        SDK version.
203
204        Args:
205            android_sdk_version: The version name of the Android Sdk in the
206                                 jdk.table.xml.
207        """
208        content = templates.XML_ENABLE_DEBUGGER.format(
209            ANDROID_SDK_VERSION=android_sdk_version)
210        common_util.file_generate(self.DEBUG_ENABLED_FILE_PATH, content)
211
212    def create_enable_debugger_module(self, android_sdk_version):
213        """Create the enable_debugger module.
214
215        1. Create two empty folders named src and gen.
216        2. Create an empty file named AndroidManifest.xml
217        3. Create the enable_denugger.iml.
218
219        Args:
220            android_sdk_version: The version name of the Android Sdk in the
221                                 jdk.table.xml.
222
223        Returns: True if successfully generate the enable debugger module,
224                 otherwise False.
225        """
226        try:
227            self._gen_enable_debug_sub_dir(self._DIR_SRC)
228            self._gen_enable_debug_sub_dir(self._DIR_GEN)
229            self._gen_androidmanifest()
230            self._gen_enable_debugger_config(android_sdk_version)
231            return True
232        except (IOError, OSError) as err:
233            logging.warning(('Can\'t create the enable_debugger module in %s.\n'
234                             '%s'), self._CONFIG_DIR, err)
235            return False
236
237    @staticmethod
238    def deprecated_version(ide, script_path):
239        """Check if the script_path belongs to a deprecated IDE version.
240
241        Args:
242            ide: The string of the relevant IDE name, same as the data of
243                 IdeBase._ide_name or IdeUtil.ide_name().
244            script_path: The path string of the IDE script file.
245
246        Returns: True if the preferred version is deprecated, otherwise False.
247        """
248        if ide == constant.IDE_ANDROID_STUDIO:
249            return AidegenConfig.deprecated_studio_version(script_path)
250        if ide == constant.IDE_INTELLIJ:
251            return AidegenConfig.deprecated_intellij_version(script_path)
252        return False
253
254    @staticmethod
255    def deprecated_intellij_version(idea_path):
256        """Check if the preferred IntelliJ version is deprecated or not.
257
258        The IntelliJ version is deprecated once the string "$JAVA_BIN" doesn't
259        exist in the idea.sh.
260
261        Args:
262            idea_path: the absolute path to idea.sh.
263
264        Returns: True if the preferred version was deprecated, otherwise False.
265        """
266        if os.path.isfile(idea_path):
267            file_content = common_util.read_file_content(
268                idea_path, AidegenConfig.ENCODE_TYPE)
269            return AidegenConfig.ACTIVE_KEYWORD not in file_content
270        return False
271
272    @staticmethod
273    def deprecated_studio_version(script_path):
274        """Check if the preferred Studio version is deprecated or not.
275
276        The Studio version is deprecated once the /android-studio-*/lib folder
277        doesn't exist.
278
279        Args:
280            script_path: the absolute path to the ide script file.
281
282        Returns: True if the preferred version is deprecated, otherwise False.
283        """
284        if not os.path.isfile(script_path):
285            return True
286        script_dir = os.path.dirname(script_path)
287        if not os.path.isdir(script_dir):
288            return True
289        lib_path = os.path.join(os.path.dirname(script_dir), _DIR_LIB)
290        return not os.path.isdir(lib_path)
291
292
293class IdeaProperties:
294    """Class manages IntelliJ's idea.properties attribute.
295
296    Class Attributes:
297        _PROPERTIES_FILE: The property file name of IntelliJ.
298        _KEY_FILESIZE: The key name of the maximun file size.
299        _FILESIZE_LIMIT: The value to be set as the max file size.
300        _RE_SEARCH_FILESIZE: A regular expression to find the current max file
301                             size.
302        _PROPERTIES_CONTENT: The default content of idea.properties to be
303                             generated.
304
305    Attributes:
306        idea_file: The absolute path of the idea.properties.
307                   For example:
308                   In Linux, it is ~/.IdeaIC2019.1/config/idea.properties.
309                   In Mac, it is ~/Library/Preferences/IdeaIC2019.1/
310                   idea.properties.
311    """
312
313    # Constants of idea.properties
314    _PROPERTIES_FILE = 'idea.properties'
315    _KEY_FILESIZE = 'idea.max.intellisense.filesize'
316    _FILESIZE_LIMIT = 100000
317    _RE_SEARCH_FILESIZE = r'%s\s?=\s?(?P<value>\d+)' % _KEY_FILESIZE
318    _PROPERTIES_CONTENT = """# custom IntelliJ IDEA properties
319
320#-------------------------------------------------------------------------------
321# Maximum size of files (in kilobytes) for which IntelliJ IDEA provides coding
322# assistance. Coding assistance for large files can affect editor performance
323# and increase memory consumption.
324# The default value is 2500.
325#-------------------------------------------------------------------------------
326idea.max.intellisense.filesize=100000
327"""
328
329    def __init__(self, config_dir):
330        """IdeaProperties initialize.
331
332        Args:
333            config_dir: The absolute dir of the idea.properties.
334        """
335        self.idea_file = os.path.join(config_dir, self._PROPERTIES_FILE)
336
337    def _set_default_idea_properties(self):
338        """Create the file idea.properties."""
339        common_util.file_generate(self.idea_file, self._PROPERTIES_CONTENT)
340
341    def _reset_max_file_size(self):
342        """Reset the max file size value in the idea.properties."""
343        updated_flag = False
344        properties = common_util.read_file_content(self.idea_file).splitlines()
345        for index, line in enumerate(properties):
346            res = re.search(self._RE_SEARCH_FILESIZE, line)
347            if res and int(res.group('value')) < self._FILESIZE_LIMIT:
348                updated_flag = True
349                properties[index] = '%s=%s' % (self._KEY_FILESIZE,
350                                               str(self._FILESIZE_LIMIT))
351        if updated_flag:
352            common_util.file_generate(self.idea_file, '\n'.join(properties))
353
354    def set_max_file_size(self):
355        """Set the max file size parameter in the idea.properties."""
356        if not os.path.exists(self.idea_file):
357            self._set_default_idea_properties()
358        else:
359            self._reset_max_file_size()
360