• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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, 'r',
152                          encoding='utf-8') as cfg_file:
153                    self._config = json.load(cfg_file)
154            except ValueError as err:
155                info = '{} format is incorrect, error: {}'.format(
156                    self._CONFIG_FILE_PATH, err)
157                logging.info(info)
158            except IOError as err:
159                logging.error(err)
160                raise
161
162    def _save_aidegen_config(self):
163        """Save data to configuration file."""
164        if self._is_config_modified():
165            with open(self._CONFIG_FILE_PATH, 'w',
166                      encoding='utf-8') as cfg_file:
167                json.dump(self._config, cfg_file, indent=4)
168
169    def _is_config_modified(self):
170        """Check if configuration data is modified."""
171        return any(key for key in self._config if key not in self._config_backup
172                   or self._config[key] != self._config_backup[key])
173
174    def _create_config_folder(self):
175        """Create the config folder if it doesn't exist."""
176        if not os.path.exists(self._CONFIG_DIR):
177            os.makedirs(self._CONFIG_DIR)
178
179    def _gen_enable_debug_sub_dir(self, dir_name):
180        """Generate a dir under enable debug dir.
181
182        Args:
183            dir_name: A string of the folder name.
184        """
185        _dir = os.path.join(self._ENABLE_DEBUG_DIR, dir_name)
186        if not os.path.exists(_dir):
187            os.makedirs(_dir)
188
189    def _gen_androidmanifest(self):
190        """Generate an AndroidManifest.xml under enable debug dir.
191
192        Once the AndroidManifest.xml does not exist or file size is zero,
193        AIDEGen will generate it with default content to prevent the red
194        underline error in IntelliJ.
195        """
196        _file = os.path.join(self._ENABLE_DEBUG_DIR, constant.ANDROID_MANIFEST)
197        if not os.path.exists(_file) or os.stat(_file).st_size == 0:
198            common_util.file_generate(_file, templates.ANDROID_MANIFEST_CONTENT)
199
200    def _gen_enable_debugger_config(self, android_sdk_version):
201        """Generate the enable_debugger.iml config file.
202
203        Re-generate the enable_debugger.iml everytime for correcting the Android
204        SDK version.
205
206        Args:
207            android_sdk_version: The version name of the Android Sdk in the
208                                 jdk.table.xml.
209        """
210        content = templates.XML_ENABLE_DEBUGGER.format(
211            ANDROID_SDK_VERSION=android_sdk_version)
212        common_util.file_generate(self.DEBUG_ENABLED_FILE_PATH, content)
213
214    def create_enable_debugger_module(self, android_sdk_version):
215        """Create the enable_debugger module.
216
217        1. Create two empty folders named src and gen.
218        2. Create an empty file named AndroidManifest.xml
219        3. Create the enable_debugger.iml.
220
221        Args:
222            android_sdk_version: The version name of the Android Sdk in the
223                                 jdk.table.xml.
224
225        Returns: True if successfully generate the enable debugger module,
226                 otherwise False.
227        """
228        try:
229            self._gen_enable_debug_sub_dir(self._DIR_SRC)
230            self._gen_enable_debug_sub_dir(self._DIR_GEN)
231            self._gen_androidmanifest()
232            self._gen_enable_debugger_config(android_sdk_version)
233            return True
234        except (IOError, OSError) as err:
235            logging.warning(('Can\'t create the enable_debugger module in %s.\n'
236                             '%s'), self._CONFIG_DIR, err)
237            return False
238
239    @staticmethod
240    def deprecated_version(ide, script_path):
241        """Check if the script_path belongs to a deprecated IDE version.
242
243        Args:
244            ide: The string of the relevant IDE name, same as the data of
245                 IdeBase._ide_name or IdeUtil.ide_name().
246            script_path: The path string of the IDE script file.
247
248        Returns: True if the preferred version is deprecated, otherwise False.
249        """
250        if ide == constant.IDE_ANDROID_STUDIO:
251            return AidegenConfig.deprecated_studio_version(script_path)
252        if ide == constant.IDE_INTELLIJ:
253            return AidegenConfig.deprecated_intellij_version(script_path)
254        return False
255
256    @staticmethod
257    def deprecated_intellij_version(idea_path):
258        """Check if the preferred IntelliJ version is deprecated or not.
259
260        The IntelliJ version is deprecated once the string "$JAVA_BIN" doesn't
261        exist in the idea.sh.
262
263        Args:
264            idea_path: the absolute path to idea.sh.
265
266        Returns: True if the preferred version was deprecated, otherwise False.
267        """
268        if os.path.isfile(idea_path):
269            file_content = common_util.read_file_content(
270                idea_path, AidegenConfig.ENCODE_TYPE)
271            return AidegenConfig.ACTIVE_KEYWORD not in file_content
272        return False
273
274    @staticmethod
275    def deprecated_studio_version(script_path):
276        """Check if the preferred Studio version is deprecated or not.
277
278        The Studio version is deprecated once the /android-studio-*/lib folder
279        doesn't exist.
280
281        Args:
282            script_path: the absolute path to the ide script file.
283
284        Returns: True if the preferred version is deprecated, otherwise False.
285        """
286        if not os.path.isfile(script_path):
287            return True
288        script_dir = os.path.dirname(script_path)
289        if not os.path.isdir(script_dir):
290            return True
291        lib_path = os.path.join(os.path.dirname(script_dir), _DIR_LIB)
292        return not os.path.isdir(lib_path)
293
294
295class IdeaProperties:
296    """Class manages IntelliJ's idea.properties attribute.
297
298    Class Attributes:
299        _PROPERTIES_FILE: The property file name of IntelliJ.
300        _KEY_FILESIZE: The key name of the maximum file size.
301        _FILESIZE_LIMIT: The value to be set as the max file size.
302        _RE_SEARCH_FILESIZE: A regular expression to find the current max file
303                             size.
304        _PROPERTIES_CONTENT: The default content of idea.properties to be
305                             generated.
306
307    Attributes:
308        idea_file: The absolute path of the idea.properties.
309                   For example:
310                   In Linux, it is ~/.IdeaIC2019.1/config/idea.properties.
311                   In Mac, it is ~/Library/Preferences/IdeaIC2019.1/
312                   idea.properties.
313    """
314
315    # Constants of idea.properties
316    _PROPERTIES_FILE = 'idea.properties'
317    _KEY_FILESIZE = 'idea.max.intellisense.filesize'
318    _FILESIZE_LIMIT = 100000
319    _RE_SEARCH_FILESIZE = r'%s\s?=\s?(?P<value>\d+)' % _KEY_FILESIZE
320    _PROPERTIES_CONTENT = """# custom IntelliJ IDEA properties
321
322#-------------------------------------------------------------------------------
323# Maximum size of files (in kilobytes) for which IntelliJ IDEA provides coding
324# assistance. Coding assistance for large files can affect editor performance
325# and increase memory consumption.
326# The default value is 2500.
327#-------------------------------------------------------------------------------
328idea.max.intellisense.filesize=100000
329"""
330
331    def __init__(self, config_dir):
332        """IdeaProperties initialize.
333
334        Args:
335            config_dir: The absolute dir of the idea.properties.
336        """
337        self.idea_file = os.path.join(config_dir, self._PROPERTIES_FILE)
338
339    def _set_default_idea_properties(self):
340        """Create the file idea.properties."""
341        common_util.file_generate(self.idea_file, self._PROPERTIES_CONTENT)
342
343    def _reset_max_file_size(self):
344        """Reset the max file size value in the idea.properties."""
345        updated_flag = False
346        properties = common_util.read_file_content(self.idea_file).splitlines()
347        for index, line in enumerate(properties):
348            res = re.search(self._RE_SEARCH_FILESIZE, line)
349            if res and int(res.group('value')) < self._FILESIZE_LIMIT:
350                updated_flag = True
351                properties[index] = '%s=%s' % (self._KEY_FILESIZE,
352                                               str(self._FILESIZE_LIMIT))
353        if updated_flag:
354            common_util.file_generate(self.idea_file, '\n'.join(properties))
355
356    def set_max_file_size(self):
357        """Set the max file size parameter in the idea.properties."""
358        if not os.path.exists(self.idea_file):
359            self._set_default_idea_properties()
360        else:
361            self._reset_max_file_size()
362