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
62    # Constants of enable debugger
63    _ENABLE_DEBUG_CONFIG_DIR = 'enable_debugger'
64    _ENABLE_DEBUG_CONFIG_FILE = 'enable_debugger.iml'
65    _ENABLE_DEBUG_DIR = os.path.join(_CONFIG_DIR, _ENABLE_DEBUG_CONFIG_DIR)
66    _DIR_SRC = 'src'
67    _DIR_GEN = 'gen'
68    DEBUG_ENABLED_FILE_PATH = os.path.join(_ENABLE_DEBUG_DIR,
69                                           _ENABLE_DEBUG_CONFIG_FILE)
70
71    # Constants of checking deprecated IntelliJ version.
72    # The launch file idea.sh of IntelliJ is in ASCII encoding.
73    ENCODE_TYPE = 'ISO-8859-1'
74    ACTIVE_KEYWORD = '$JAVA_BIN'
75
76    def __init__(self):
77        self._config = {}
78        self._config_backup = {}
79        self._create_config_folder()
80
81    def __enter__(self):
82        self._load_aidegen_config()
83        self._config_backup = copy.deepcopy(self._config)
84        return self
85
86    def __exit__(self, exc_type, exc_val, exc_tb):
87        self._save_aidegen_config()
88
89    def preferred_version(self, ide=None):
90        """AIDEGen configuration getter.
91
92        Args:
93            ide: The string of the relevant IDE name, same as the data of
94                 IdeBase._ide_name or IdeUtil.ide_name(). None represents the
95                 usage of the version 1.
96
97        Returns:
98            The preferred version item of configuration data if exists and is
99            not deprecated, otherwise None.
100        """
101        key = '_'.join([ide, self._KEY_APPEND]) if ide else self._KEY_APPEND
102        preferred_version = self._config.get(key, '')
103        # Backward compatible check.
104        if not preferred_version:
105            preferred_version = self._config.get(self._KEY_APPEND, '')
106
107        if preferred_version:
108            real_version = os.path.realpath(preferred_version)
109            if ide and not self.deprecated_version(ide, real_version):
110                return preferred_version
111            # Backward compatible handling.
112            if not ide and not self.deprecated_intellij_version(real_version):
113                return preferred_version
114        return None
115
116    def set_preferred_version(self, preferred_version, ide=None):
117        """AIDEGen configuration setter.
118
119        Args:
120            preferred_version: A string, user's preferred version to be set.
121            ide: The string of the relevant IDE name, same as the data of
122                 IdeBase._ide_name or IdeUtil.ide_name(). None presents the
123                 usage of the version 1.
124        """
125        key = '_'.join([ide, self._KEY_APPEND]) if ide else self._KEY_APPEND
126        self._config[key] = preferred_version
127
128    def _load_aidegen_config(self):
129        """Load data from configuration file."""
130        if os.path.exists(self._CONFIG_FILE_PATH):
131            try:
132                with open(self._CONFIG_FILE_PATH) as cfg_file:
133                    self._config = json.load(cfg_file)
134            except ValueError as err:
135                info = '{} format is incorrect, error: {}'.format(
136                    self._CONFIG_FILE_PATH, err)
137                logging.info(info)
138            except IOError as err:
139                logging.error(err)
140                raise
141
142    def _save_aidegen_config(self):
143        """Save data to configuration file."""
144        if self._is_config_modified():
145            with open(self._CONFIG_FILE_PATH, 'w') as cfg_file:
146                json.dump(self._config, cfg_file, indent=4)
147
148    def _is_config_modified(self):
149        """Check if configuration data is modified."""
150        return any(key for key in self._config if not key in self._config_backup
151                   or self._config[key] != self._config_backup[key])
152
153    def _create_config_folder(self):
154        """Create the config folder if it doesn't exist."""
155        if not os.path.exists(self._CONFIG_DIR):
156            os.makedirs(self._CONFIG_DIR)
157
158    def _gen_enable_debug_sub_dir(self, dir_name):
159        """Generate a dir under enable debug dir.
160
161        Args:
162            dir_name: A string of the folder name.
163        """
164        _dir = os.path.join(self._ENABLE_DEBUG_DIR, dir_name)
165        if not os.path.exists(_dir):
166            os.makedirs(_dir)
167
168    def _gen_androidmanifest(self):
169        """Generate an AndroidManifest.xml under enable debug dir.
170
171        Once the AndroidManifest.xml does not exist or file size is zero,
172        AIDEGen will generate it with default content to prevent the red
173        underline error in IntelliJ.
174        """
175        _file = os.path.join(self._ENABLE_DEBUG_DIR, constant.ANDROID_MANIFEST)
176        if not os.path.exists(_file) or os.stat(_file).st_size == 0:
177            common_util.file_generate(_file, templates.ANDROID_MANIFEST_CONTENT)
178
179    def _gen_enable_debugger_config(self, android_sdk_version):
180        """Generate the enable_debugger.iml config file.
181
182        Re-generate the enable_debugger.iml everytime for correcting the Android
183        SDK version.
184
185        Args:
186            android_sdk_version: The version name of the Android Sdk in the
187                                 jdk.table.xml.
188        """
189        content = templates.XML_ENABLE_DEBUGGER.format(
190            ANDROID_SDK_VERSION=android_sdk_version)
191        common_util.file_generate(self.DEBUG_ENABLED_FILE_PATH, content)
192
193    def create_enable_debugger_module(self, android_sdk_version):
194        """Create the enable_debugger module.
195
196        1. Create two empty folders named src and gen.
197        2. Create an empty file named AndroidManifest.xml
198        3. Create the enable_denugger.iml.
199
200        Args:
201            android_sdk_version: The version name of the Android Sdk in the
202                                 jdk.table.xml.
203
204        Returns: True if successfully generate the enable debugger module,
205                 otherwise False.
206        """
207        try:
208            self._gen_enable_debug_sub_dir(self._DIR_SRC)
209            self._gen_enable_debug_sub_dir(self._DIR_GEN)
210            self._gen_androidmanifest()
211            self._gen_enable_debugger_config(android_sdk_version)
212            return True
213        except (IOError, OSError) as err:
214            logging.warning(('Can\'t create the enable_debugger module in %s.\n'
215                             '%s'), self._CONFIG_DIR, err)
216            return False
217
218    @staticmethod
219    def deprecated_version(ide, script_path):
220        """Check if the script_path belongs to a deprecated IDE version.
221
222        Args:
223            ide: The string of the relevant IDE name, same as the data of
224                 IdeBase._ide_name or IdeUtil.ide_name().
225            script_path: The path string of the IDE script file.
226
227        Returns: True if the preferred version is deprecated, otherwise False.
228        """
229        if ide == constant.IDE_ANDROID_STUDIO:
230            return AidegenConfig.deprecated_studio_version(script_path)
231        if ide == constant.IDE_INTELLIJ:
232            return AidegenConfig.deprecated_intellij_version(script_path)
233        return False
234
235    @staticmethod
236    def deprecated_intellij_version(idea_path):
237        """Check if the preferred IntelliJ version is deprecated or not.
238
239        The IntelliJ version is deprecated once the string "$JAVA_BIN" doesn't
240        exist in the idea.sh.
241
242        Args:
243            idea_path: the absolute path to idea.sh.
244
245        Returns: True if the preferred version was deprecated, otherwise False.
246        """
247        if os.path.isfile(idea_path):
248            file_content = common_util.read_file_content(
249                idea_path, AidegenConfig.ENCODE_TYPE)
250            return AidegenConfig.ACTIVE_KEYWORD not in file_content
251        return False
252
253    @staticmethod
254    def deprecated_studio_version(script_path):
255        """Check if the preferred Studio version is deprecated or not.
256
257        The Studio version is deprecated once the /android-studio-*/lib folder
258        doesn't exist.
259
260        Args:
261            script_path: the absolute path to the ide script file.
262
263        Returns: True if the preferred version is deprecated, otherwise False.
264        """
265        if not os.path.isfile(script_path):
266            return True
267        script_dir = os.path.dirname(script_path)
268        if not os.path.isdir(script_dir):
269            return True
270        lib_path = os.path.join(os.path.dirname(script_dir), _DIR_LIB)
271        return not os.path.isdir(lib_path)
272
273
274class IdeaProperties:
275    """Class manages IntelliJ's idea.properties attribute.
276
277    Class Attributes:
278        _PROPERTIES_FILE: The property file name of IntelliJ.
279        _KEY_FILESIZE: The key name of the maximun file size.
280        _FILESIZE_LIMIT: The value to be set as the max file size.
281        _RE_SEARCH_FILESIZE: A regular expression to find the current max file
282                             size.
283        _PROPERTIES_CONTENT: The default content of idea.properties to be
284                             generated.
285
286    Attributes:
287        idea_file: The absolute path of the idea.properties.
288                   For example:
289                   In Linux, it is ~/.IdeaIC2019.1/config/idea.properties.
290                   In Mac, it is ~/Library/Preferences/IdeaIC2019.1/
291                   idea.properties.
292    """
293
294    # Constants of idea.properties
295    _PROPERTIES_FILE = 'idea.properties'
296    _KEY_FILESIZE = 'idea.max.intellisense.filesize'
297    _FILESIZE_LIMIT = 100000
298    _RE_SEARCH_FILESIZE = r'%s\s?=\s?(?P<value>\d+)' % _KEY_FILESIZE
299    _PROPERTIES_CONTENT = """# custom IntelliJ IDEA properties
300
301#-------------------------------------------------------------------------------
302# Maximum size of files (in kilobytes) for which IntelliJ IDEA provides coding
303# assistance. Coding assistance for large files can affect editor performance
304# and increase memory consumption.
305# The default value is 2500.
306#-------------------------------------------------------------------------------
307idea.max.intellisense.filesize=100000
308"""
309
310    def __init__(self, config_dir):
311        """IdeaProperties initialize.
312
313        Args:
314            config_dir: The absolute dir of the idea.properties.
315        """
316        self.idea_file = os.path.join(config_dir, self._PROPERTIES_FILE)
317
318    def _set_default_idea_properties(self):
319        """Create the file idea.properties."""
320        common_util.file_generate(self.idea_file, self._PROPERTIES_CONTENT)
321
322    def _reset_max_file_size(self):
323        """Reset the max file size value in the idea.properties."""
324        updated_flag = False
325        properties = common_util.read_file_content(self.idea_file).splitlines()
326        for index, line in enumerate(properties):
327            res = re.search(self._RE_SEARCH_FILESIZE, line)
328            if res and int(res.group('value')) < self._FILESIZE_LIMIT:
329                updated_flag = True
330                properties[index] = '%s=%s' % (self._KEY_FILESIZE,
331                                               str(self._FILESIZE_LIMIT))
332        if updated_flag:
333            common_util.file_generate(self.idea_file, '\n'.join(properties))
334
335    def set_max_file_size(self):
336        """Set the max file size parameter in the idea.properties."""
337        if not os.path.exists(self.idea_file):
338            self._set_default_idea_properties()
339        else:
340            self._reset_max_file_size()
341