aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tradefed/config/ConfigurationXmlParser.java
blob: 32e06fdc19f1f9d6318c53cabdb145da56257ba4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tradefed.config;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

/**
 * Parses a configuration.xml file.
 * <p/>
 * See TODO for expected format
 */
class ConfigurationXmlParser {
    /**
     * SAX callback object. Handles parsing data from the xml tags.
     */
    static class ConfigHandler extends DefaultHandler {

        private static final String OBJECT_TAG = "object";
        private static final String OPTION_TAG = "option";
        private static final String INCLUDE_TAG = "include";
        private static final String TEMPLATE_INCLUDE_TAG = "template-include";
        private static final String CONFIG_TAG = "configuration";
        private static final String DEVICE_TAG = "device";

        /** Note that this simply hasn't been implemented; it is not intentionally forbidden. */
        static final String INNER_TEMPLATE_INCLUDE_ERROR =
                "Configurations which contain a <template-include> tag, not having a 'default' " +
                "attribute, may not be the target of any <include> or <template-include> tag. " +
                "However, configuration '%s' attempted to include configuration '%s', which " +
                "contains a <template-include> tag without a 'default' attribute.";

        // Settings
        private final IConfigDefLoader mConfigDefLoader;
        private final ConfigurationDef mConfigDef;
        private final Map<String, String> mTemplateMap;
        private final String mName;
        private final boolean mInsideParentDeviceTag;

        // State-holding members
        private String mCurrentConfigObject;
        private String mCurrentDeviceObject;
        private Boolean isMultiDeviceConfigMode = false;
        private List<String> mListDevice = new ArrayList<String>();
        private List<String> mOutsideTag = new ArrayList<String>();

        private Boolean isLocalConfig = null;

        ConfigHandler(
                ConfigurationDef def,
                String name,
                IConfigDefLoader loader,
                String parentDeviceObject,
                Map<String, String> templateMap) {
            mName = name;
            mConfigDef = def;
            mConfigDefLoader = loader;
            mCurrentDeviceObject = parentDeviceObject;
            mInsideParentDeviceTag = (parentDeviceObject != null) ? true : false;

            if (templateMap == null) {
                mTemplateMap = Collections.<String, String>emptyMap();
            } else {
                mTemplateMap = templateMap;
            }
        }

        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes)
                throws SAXException {
            if (OBJECT_TAG.equals(localName)) {
                final String objectTypeName = attributes.getValue("type");
                if (objectTypeName == null) {
                    throw new SAXException(new ConfigurationException(
                            "<object> must have a 'type' attribute"));
                }
                if (GlobalConfiguration.isBuiltInObjType(objectTypeName) ||
                        Configuration.isBuiltInObjType(objectTypeName)) {
                    throw new SAXException(new ConfigurationException(String.format("<object> "
                            + "cannot be type '%s' this is a reserved type.", objectTypeName)));
                }
                addObject(objectTypeName, attributes);
            } else if (DEVICE_TAG.equals(localName)) {
                if (mCurrentDeviceObject != null) {
                    throw new SAXException(new ConfigurationException(
                            "<device> tag cannot be included inside another device"));
                }
                // tag is a device tag (new format) for multi device definition.
                String deviceName = attributes.getValue("name");
                if (deviceName == null) {
                    throw new SAXException(
                            new ConfigurationException("device tag requires a name value"));
                }
                if (deviceName.equals(ConfigurationDef.DEFAULT_DEVICE_NAME)) {
                    throw new SAXException(new ConfigurationException(String.format("device name "
                            + "cannot be reserved name: '%s'",
                            ConfigurationDef.DEFAULT_DEVICE_NAME)));
                }
                if (deviceName.contains(String.valueOf(OptionSetter.NAMESPACE_SEPARATOR))) {
                    throw new SAXException(new ConfigurationException(String.format("device name "
                            + "cannot contain reserved character: '%s'",
                            OptionSetter.NAMESPACE_SEPARATOR)));
                }
                isMultiDeviceConfigMode = true;
                mConfigDef.setMultiDeviceMode(true);
                mCurrentDeviceObject = deviceName;
                addObject(localName, attributes);
            } else if (Configuration.isBuiltInObjType(localName)) {
                // tag is a built in local config object
                if (isLocalConfig == null) {
                    isLocalConfig = true;
                } else if (!isLocalConfig) {
                    throwException(String.format(
                            "Attempted to specify local object '%s' for global config!",
                            localName));
                }

                if (mCurrentDeviceObject == null &&
                        Configuration.doesBuiltInObjSupportMultiDevice(localName)) {
                    // Keep track of all the BuildInObj outside of device tag for final check
                    // if it turns out we are in multi mode, we will throw an exception.
                    mOutsideTag.add(localName);
                }
                // if we are inside a device object, some tags are not allowed.
                if (mCurrentDeviceObject != null) {
                    if (!Configuration.doesBuiltInObjSupportMultiDevice(localName)) {
                        // Prevent some tags to be inside of a device in multi device mode.
                        throw new SAXException(new ConfigurationException(
                                String.format("Tag %s should not be included in a <device> tag.",
                                        localName)));
                    }
                }
                addObject(localName, attributes);
            } else if (GlobalConfiguration.isBuiltInObjType(localName)) {
                // tag is a built in global config object
                if (isLocalConfig == null) {
                    // FIXME: config type should be explicit rather than inferred
                    isLocalConfig = false;
                } else if (isLocalConfig) {
                    throwException(String.format(
                            "Attempted to specify global object '%s' for local config!",
                            localName));
                }
                addObject(localName, attributes);
            } else if (OPTION_TAG.equals(localName)) {
                String optionName = attributes.getValue("name");
                if (optionName == null) {
                    throwException("Missing 'name' attribute for option");
                }

                String optionKey = attributes.getValue("key");
                // Key is optional at this stage.  If it's actually required, another stage in the
                // configuration validation will throw an exception.

                String optionValue = attributes.getValue("value");
                if (optionValue == null) {
                    throwException("Missing 'value' attribute for option '" + optionName + "'");
                }
                if (mCurrentConfigObject != null) {
                    // option is declared within a config object - namespace it with object class
                    // name
                    optionName = String.format("%s%c%s", mCurrentConfigObject,
                            OptionSetter.NAMESPACE_SEPARATOR, optionName);
                }
                if (mCurrentDeviceObject != null) {
                    // preprend the device name in extra if inside a device config object.
                    optionName = String.format("{%s}%s", mCurrentDeviceObject, optionName);
                }
                mConfigDef.addOptionDef(optionName, optionKey, optionValue, mName);
            } else if (CONFIG_TAG.equals(localName)) {
                String description = attributes.getValue("description");
                if (description != null) {
                    // Ensure that we only set the description the first time and not when it is
                    // loading the <include> configuration.
                    if (mConfigDef.getDescription() == null ||
                            mConfigDef.getDescription().isEmpty()) {
                        mConfigDef.setDescription(description);
                    }
                }
            } else if (INCLUDE_TAG.equals(localName)) {
                String includeName = attributes.getValue("name");
                if (includeName == null) {
                    throwException("Missing 'name' attribute for include");
                }
                try {
                    mConfigDefLoader.loadIncludedConfiguration(
                            mConfigDef, mName, includeName, mCurrentDeviceObject, mTemplateMap);
                } catch (ConfigurationException e) {
                    if (e instanceof TemplateResolutionError) {
                        throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
                                mConfigDef.getName(), includeName));
                    }
                    throw new SAXException(e);
                }
            } else if (TEMPLATE_INCLUDE_TAG.equals(localName)) {
                final String templateName = attributes.getValue("name");
                if (templateName == null) {
                    throwException("Missing 'name' attribute for template-include");
                }
                if (mCurrentDeviceObject != null) {
                    // TODO: Add this use case.
                    throwException("<template> inside device object currently not supported.");
                }

                String includeName = mTemplateMap.get(templateName);
                if (includeName == null) {
                    includeName = attributes.getValue("default");
                }
                if (includeName == null) {
                    throwTemplateException(mConfigDef.getName(), templateName);
                }
                // Removing the used template from the map to avoid re-using it.
                mTemplateMap.remove(templateName);
                try {
                    mConfigDefLoader.loadIncludedConfiguration(
                            mConfigDef, mName, includeName, null, mTemplateMap);
                } catch (ConfigurationException e) {
                    if (e instanceof TemplateResolutionError) {
                        throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
                                mConfigDef.getName(), includeName));
                    }
                    throw new SAXException(e);
                }
            } else {
                throw new SAXException(String.format(
                        "Unrecognized tag '%s' in configuration", localName));
            }
        }

        @Override
        public void endElement (String uri, String localName, String qName) throws SAXException {
            if (OBJECT_TAG.equals(localName) || Configuration.isBuiltInObjType(localName)
                    || GlobalConfiguration.isBuiltInObjType(localName)) {
                mCurrentConfigObject = null;
            }
            if (DEVICE_TAG.equals(localName) && !mInsideParentDeviceTag) {
                // Only unset if it was not the parent device tag.
                mCurrentDeviceObject = null;
            }
        }

        void addObject(String objectTypeName, Attributes attributes) throws SAXException {
            if (Configuration.DEVICE_NAME.equals(objectTypeName)) {
                // We still want to add a standalone device without any inner object.
                String deviceName = attributes.getValue("name");
                if (!mListDevice.contains(deviceName)) {
                    mListDevice.add(deviceName);
                    mConfigDef.addConfigObjectDef(objectTypeName,
                            DeviceConfigurationHolder.class.getCanonicalName());
                    mConfigDef.addExpectedDevice(deviceName);
                }
            } else {
                String className = attributes.getValue("class");
                if (className == null) {
                    throwException(String.format("Missing class attribute for object %s",
                            objectTypeName));
                }
                if (mCurrentDeviceObject != null) {
                    // Add the device name as a namespace to the type
                    objectTypeName = mCurrentDeviceObject + OptionSetter.NAMESPACE_SEPARATOR
                            + objectTypeName;
                }
                int classCount = mConfigDef.addConfigObjectDef(objectTypeName, className);
                mCurrentConfigObject = String.format("%s%c%d", className,
                        OptionSetter.NAMESPACE_SEPARATOR, classCount);
            }
        }

        private void throwException(String reason) throws SAXException {
            throw new SAXException(new ConfigurationException(String.format(
                    "Failed to parse config xml '%s'. Reason: %s", mConfigDef.getName(), reason)));
        }

        private void throwTemplateException(String configName, String templateName)
                throws SAXException {
            throw new SAXException(new TemplateResolutionError(configName, templateName));
        }
    }

    private final IConfigDefLoader mConfigDefLoader;
    /**
     * If we are loading a config from inside a <device> tag, this will contain the name of the
     * current device tag to properly load in context.
     */
    private final String mParentDeviceObject;

    ConfigurationXmlParser(IConfigDefLoader loader, String parentDeviceObject) {
        mConfigDefLoader = loader;
        mParentDeviceObject = parentDeviceObject;
    }

    /**
     * Parses out configuration data contained in given input into the given configdef.
     * <p/>
     * Currently performs limited error checking.
     *
     * @param configDef the {@link ConfigurationDef} to load data into
     * @param name the name of the configuration currently being loaded. Used for logging only.
     * Can be different than configDef.getName in cases of included configs
     * @param xmlInput the configuration xml to parse
     * @throws ConfigurationException if input could not be parsed or had invalid format
     */
    void parse(ConfigurationDef configDef, String name, InputStream xmlInput,
            Map<String, String> templateMap) throws ConfigurationException {
        try {
            SAXParserFactory parserFactory = SAXParserFactory.newInstance();
            parserFactory.setNamespaceAware(true);
            SAXParser parser = parserFactory.newSAXParser();
            ConfigHandler configHandler =
                    new ConfigHandler(
                            configDef, name, mConfigDefLoader, mParentDeviceObject, templateMap);
            parser.parse(new InputSource(xmlInput), configHandler);
            checkValidMultiConfiguration(configHandler);
        } catch (ParserConfigurationException e) {
            throwConfigException(name, e);
        } catch (SAXException e) {
            throwConfigException(name, e);
        } catch (IOException e) {
            throwConfigException(name, e);
        }
    }

    /**
     * Helper to encapsulate exceptions in a {@link ConfigurationException}
     */
    private void throwConfigException(String configName, Throwable e)
            throws ConfigurationException {
        if (e.getCause() instanceof ConfigurationException) {
            throw (ConfigurationException)e.getCause();
        }
        throw new ConfigurationException(String.format("Failed to parse config xml '%s' due to "
                + "'%s'", configName, e), e);
    }

    /**
     * Validate that the configuration is valid from a multi device configuration standpoint:
     * Some tags are not allowed outside the <device> tags.
     */
    private void checkValidMultiConfiguration(ConfigHandler configHandler) throws SAXException {
        if (configHandler.isMultiDeviceConfigMode == true &&
                !configHandler.mOutsideTag.isEmpty()) {
            throw new SAXException(new ConfigurationException(String.format("Tags %s "
                    + "should be included in a <device> tag.", configHandler.mOutsideTag)));
        }
    }
}