1#!/usr/bin/env python3
2#
3# Copyright 2020 - 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"""Configs the jdk.table.xml.
18
19In order to enable the feature "Attach debugger to Android process" in Android
20Studio or IntelliJ, AIDEGen needs the JDK and Android SDK been set-up. The class
21JDKTableXML parses the jdk.table.xml to find the existing JDK and Android SDK
22information. If they do not exist, AIDEGen will create them.
23
24    Usage example:
25    jdk_table_xml = JDKTableXML(jdk_table_xml_file,
26                                jdk_template,
27                                default_jdk_path,
28                                default_android_sdk_path)
29    if jdk_table_xml.config_jdk_table_xml():
30        android_sdk_version = jdk_table_xml.android_sdk_version
31"""
32
33from __future__ import absolute_import
34
35import os
36
37from xml.etree import ElementTree
38
39from aidegen import constant
40from aidegen import templates
41from aidegen.lib import aidegen_metrics
42from aidegen.lib import common_util
43from aidegen.lib import xml_util
44from aidegen.sdk import android_sdk
45
46
47class JDKTableXML():
48    """Configs jdk.table.xml for IntelliJ and Android Studio.
49
50    Attributes:
51        _config_file: The absolute file path of the jdk.table.xml, the file
52                      might not exist.
53        _jdk_content: A string, the content of the JDK configuration.
54        _jdk_path: The path of JDK in android project.
55        _default_android_sdk_path: The default path to the Android SDK.
56        _platform_version: The version name of the platform, e.g. android-29
57        _android_sdk_version: The version name of the Android SDK in the
58                              jdk.table.xml, e.g. Android API 29 Platform
59        _modify_config: A boolean, True to write new content to jdk.table.xml.
60        _xml: An xml.etree.ElementTree object contains the XML parsing result.
61        _sdk: An AndroidSDK object to get the Android SDK path and platform
62              mapping.
63    """
64    _JDK = 'jdk'
65    _NAME = 'name'
66    _TYPE = 'type'
67    _VALUE = 'value'
68    _SDK = 'sdk'
69    _HOMEPATH = 'homePath'
70    _ADDITIONAL = 'additional'
71    _ANDROID_SDK = 'Android SDK'
72    _JAVA_SDK = 'JavaSDK'
73    _JDK_VERSION = 'JDK18'
74    _APPLICATION = 'application'
75    _COMPONENT = 'component'
76    _PROJECTJDKTABLE = 'ProjectJdkTable'
77    _LAST_TAG_TAIL = '\n    '
78    _NEW_TAG_TAIL = '\n  '
79    _ANDROID_SDK_VERSION = 'Android API {CODE_NAME} Platform'
80    _DEFAULT_JDK_TABLE_XML = os.path.join(common_util.get_android_root_dir(),
81                                          constant.AIDEGEN_ROOT_PATH,
82                                          'data',
83                                          'jdk.table.xml')
84    _ILLEGAL_XML = ('The {XML} is not an useful XML file for IntelliJ. Do you '
85                    'agree AIDEGen override it?(y/n)')
86    _IGNORE_XML_WARNING = ('The {XML} is not an useful XML file for IntelliJ. '
87                           'It causes the feature "Attach debugger to Android '
88                           'process" to be disabled.')
89
90    def __init__(self, config_file, jdk_content, jdk_path,
91                 default_android_sdk_path):
92        """JDKTableXML initialize.
93
94        Args:
95            config_file: The absolute file path of the jdk.table.xml, the file
96                         might not exist.
97            jdk_content: A string, the content of the JDK configuration.
98            jdk_path: The path of JDK in android project.
99            default_android_sdk_path: The default absolute path to the Android
100                                      SDK.
101        """
102        self._config_file = config_file
103        self._jdk_content = jdk_content
104        self._jdk_path = jdk_path
105        self._default_android_sdk_path = default_android_sdk_path
106        self._xml = None
107        if os.path.exists(config_file):
108            xml_file = config_file
109        else:
110            xml_file = self._DEFAULT_JDK_TABLE_XML
111            common_util.file_generate(xml_file, templates.JDK_TABLE_XML)
112        self._xml = xml_util.parse_xml(xml_file)
113        self._platform_version = None
114        self._android_sdk_version = None
115        self._modify_config = False
116        self._sdk = android_sdk.AndroidSDK()
117
118    @property
119    def android_sdk_version(self):
120        """Gets the Android SDK version."""
121        return self._android_sdk_version
122
123    def _check_structure(self):
124        """Checks the XML's structure is correct.
125
126        The content of the XML file should have a root tag as <application> and
127        a child tag <component> of it.
128        E.g.
129        <application>
130          <component name="ProjectJdkTable">
131          ...
132          </component>
133        </application>
134
135        Returns:
136            Boolean: True if the structure is correct, otherwise False.
137        """
138        if (not self._xml or self._xml.getroot().tag != self._APPLICATION
139                or self._xml.find(self._COMPONENT) is None
140                or self._xml.find(self._COMPONENT).tag != self._COMPONENT):
141            return False
142        return self._xml.find(self._COMPONENT).get(
143            self._NAME) == self._PROJECTJDKTABLE
144
145    def _override_xml(self):
146        """Overrides the XML file when it's invalid.
147
148        Returns:
149            A boolean, True when developers choose to override the XML file,
150            otherwise False.
151        """
152        input_data = input(self._ILLEGAL_XML.format(XML=self._config_file))
153        while input_data not in ('y', 'n'):
154            input_data = input('Please type y(Yes) or n(No): ')
155        if input_data == 'y':
156            # Record the exception about wrong XML format.
157            if self._xml:
158                aidegen_metrics.send_exception_metrics(
159                    constant.XML_PARSING_FAILURE, '',
160                    ElementTree.tostring(self._xml.getroot()), '')
161            self._xml = xml_util.parse_xml(self._DEFAULT_JDK_TABLE_XML)
162            return True
163        return False
164
165    def _check_jdk18_in_xml(self):
166        """Checks if the JDK18 is already set in jdk.table.xml.
167
168        Returns:
169            Boolean: True if the JDK18 exists else False.
170        """
171        for jdk in self._xml.iter(self._JDK):
172            _name = jdk.find(self._NAME)
173            _type = jdk.find(self._TYPE)
174            if None in (_name, _type):
175                continue
176            if (_type.get(self._VALUE) == self._JAVA_SDK
177                    and _name.get(self._VALUE) == self._JDK_VERSION):
178                return True
179        return False
180
181    def _check_android_sdk_in_xml(self):
182        """Checks if the Android SDK is already set in jdk.table.xml.
183
184        If the Android SDK exists in xml, validate the value of Android SDK path
185        and platform version.
186        1. Check if the Android SDK path is valid.
187        2. Check if the platform version exists in the Android SDK.
188        The Android SDK version can be used to generate enble_debugger module
189        when condition 1 and 2 are true.
190
191        Returns:
192            Boolean: True if the Android SDK configuration exists, otherwise
193                     False.
194        """
195        for tag in self._xml.iter(self._JDK):
196            _name = tag.find(self._NAME)
197            _type = tag.find(self._TYPE)
198            _homepath = tag.find(self._HOMEPATH)
199            _additional = tag.find(self._ADDITIONAL)
200            if None in (_name, _type, _homepath, _additional):
201                continue
202
203            tag_type = _type.get(self._VALUE)
204            home_path = _homepath.get(self._VALUE).replace(
205                constant.USER_HOME, os.path.expanduser('~'))
206            platform = _additional.get(self._SDK)
207            if (tag_type != self._ANDROID_SDK
208                    or not self._sdk.is_android_sdk_path(home_path)
209                    or platform not in self._sdk.platform_mapping):
210                continue
211            self._android_sdk_version = _name.get(self._VALUE)
212            self._platform_version = platform
213            return True
214        return False
215
216    def _append_config(self, new_config):
217        """Adds a <jdk> configuration at the last of <component>.
218
219        Args:
220            new_config: A string of new <jdk> configuration.
221        """
222        node = ElementTree.fromstring(new_config)
223        node.tail = self._NEW_TAG_TAIL
224        component = self._xml.getroot().find(self._COMPONENT)
225        if len(component) > 0:
226            component[-1].tail = self._LAST_TAG_TAIL
227        else:
228            component.text = self._LAST_TAG_TAIL
229        self._xml.getroot().find(self._COMPONENT).append(node)
230
231    def _generate_jdk_config_string(self):
232        """Generates the default JDK configuration."""
233        if self._check_jdk18_in_xml():
234            return
235        self._append_config(self._jdk_content.format(JDKpath=self._jdk_path))
236        self._modify_config = True
237
238    def _generate_sdk_config_string(self):
239        """Generates Android SDK configuration."""
240        if self._check_android_sdk_in_xml():
241            return
242        if self._sdk.path_analysis(self._default_android_sdk_path):
243            # TODO(b/151582629): Revise the API_LEVEL to CODE_NAME when
244            #                    abandoning the sdk_config.py.
245            self._append_config(templates.ANDROID_SDK_XML.format(
246                ANDROID_SDK_PATH=self._sdk.android_sdk_path,
247                CODE_NAME=self._sdk.max_code_name))
248            self._android_sdk_version = self._ANDROID_SDK_VERSION.format(
249                CODE_NAME=self._sdk.max_code_name)
250            self._modify_config = True
251            return
252        # Record the exception about missing Android SDK.
253        aidegen_metrics.send_exception_metrics(
254            constant.LOCATE_SDK_PATH_FAILURE, '',
255            ElementTree.tostring(self._xml.getroot()), '')
256
257    def config_jdk_table_xml(self):
258        """Configures the jdk.table.xml.
259
260        1. Generate the JDK18 configuration if it does not exist.
261        2. Generate the Android SDK configuration if it does not exist and
262           save the Android SDK path.
263        3. Update the jdk.table.xml if AIDEGen needs to append JDK18 or
264           Android SDK configuration.
265
266        Returns:
267            A boolean, True when get the Android SDK version, otherwise False.
268        """
269        if not self._check_structure() and not self._override_xml():
270            print(self._IGNORE_XML_WARNING.format(XML=self._config_file))
271            return False
272        self._generate_jdk_config_string()
273        self._generate_sdk_config_string()
274        if self._modify_config:
275            self._xml.write(self._config_file)
276        return bool(self._android_sdk_version)
277