Home | History | Annotate | Download | only in config
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tradefed.config;
     18 
     19 import org.xml.sax.Attributes;
     20 import org.xml.sax.InputSource;
     21 import org.xml.sax.SAXException;
     22 import org.xml.sax.helpers.DefaultHandler;
     23 
     24 import java.io.IOException;
     25 import java.io.InputStream;
     26 import java.util.ArrayList;
     27 import java.util.Collections;
     28 import java.util.List;
     29 import java.util.Map;
     30 
     31 import javax.xml.parsers.ParserConfigurationException;
     32 import javax.xml.parsers.SAXParser;
     33 import javax.xml.parsers.SAXParserFactory;
     34 
     35 /**
     36  * Parses a configuration.xml file.
     37  * <p/>
     38  * See TODO for expected format
     39  */
     40 class ConfigurationXmlParser {
     41     /**
     42      * SAX callback object. Handles parsing data from the xml tags.
     43      */
     44     static class ConfigHandler extends DefaultHandler {
     45 
     46         private static final String OBJECT_TAG = "object";
     47         private static final String OPTION_TAG = "option";
     48         private static final String INCLUDE_TAG = "include";
     49         private static final String TEMPLATE_INCLUDE_TAG = "template-include";
     50         private static final String CONFIG_TAG = "configuration";
     51         private static final String DEVICE_TAG = "device";
     52 
     53         /** Note that this simply hasn't been implemented; it is not intentionally forbidden. */
     54         static final String INNER_TEMPLATE_INCLUDE_ERROR =
     55                 "Configurations which contain a <template-include> tag, not having a 'default' " +
     56                 "attribute, may not be the target of any <include> or <template-include> tag. " +
     57                 "However, configuration '%s' attempted to include configuration '%s', which " +
     58                 "contains a <template-include> tag without a 'default' attribute.";
     59 
     60         // Settings
     61         private final IConfigDefLoader mConfigDefLoader;
     62         private final ConfigurationDef mConfigDef;
     63         private final Map<String, String> mTemplateMap;
     64         private final String mName;
     65         private final boolean mInsideParentDeviceTag;
     66 
     67         // State-holding members
     68         private String mCurrentConfigObject;
     69         private String mCurrentDeviceObject;
     70         private Boolean isMultiDeviceConfigMode = false;
     71         private List<String> mListDevice = new ArrayList<String>();
     72         private List<String> mOutsideTag = new ArrayList<String>();
     73 
     74         private Boolean isLocalConfig = null;
     75 
     76         ConfigHandler(
     77                 ConfigurationDef def,
     78                 String name,
     79                 IConfigDefLoader loader,
     80                 String parentDeviceObject,
     81                 Map<String, String> templateMap) {
     82             mName = name;
     83             mConfigDef = def;
     84             mConfigDefLoader = loader;
     85             mCurrentDeviceObject = parentDeviceObject;
     86             mInsideParentDeviceTag = (parentDeviceObject != null) ? true : false;
     87 
     88             if (templateMap == null) {
     89                 mTemplateMap = Collections.<String, String>emptyMap();
     90             } else {
     91                 mTemplateMap = templateMap;
     92             }
     93         }
     94 
     95         @Override
     96         public void startElement(String uri, String localName, String name, Attributes attributes)
     97                 throws SAXException {
     98             if (OBJECT_TAG.equals(localName)) {
     99                 final String objectTypeName = attributes.getValue("type");
    100                 if (objectTypeName == null) {
    101                     throw new SAXException(new ConfigurationException(
    102                             "<object> must have a 'type' attribute"));
    103                 }
    104                 if (GlobalConfiguration.isBuiltInObjType(objectTypeName) ||
    105                         Configuration.isBuiltInObjType(objectTypeName)) {
    106                     throw new SAXException(new ConfigurationException(String.format("<object> "
    107                             + "cannot be type '%s' this is a reserved type.", objectTypeName)));
    108                 }
    109                 addObject(objectTypeName, attributes);
    110             } else if (DEVICE_TAG.equals(localName)) {
    111                 if (mCurrentDeviceObject != null) {
    112                     throw new SAXException(new ConfigurationException(
    113                             "<device> tag cannot be included inside another device"));
    114                 }
    115                 // tag is a device tag (new format) for multi device definition.
    116                 String deviceName = attributes.getValue("name");
    117                 if (deviceName == null) {
    118                     throw new SAXException(
    119                             new ConfigurationException("device tag requires a name value"));
    120                 }
    121                 if (deviceName.equals(ConfigurationDef.DEFAULT_DEVICE_NAME)) {
    122                     throw new SAXException(new ConfigurationException(String.format("device name "
    123                             + "cannot be reserved name: '%s'",
    124                             ConfigurationDef.DEFAULT_DEVICE_NAME)));
    125                 }
    126                 if (deviceName.contains(String.valueOf(OptionSetter.NAMESPACE_SEPARATOR))) {
    127                     throw new SAXException(new ConfigurationException(String.format("device name "
    128                             + "cannot contain reserved character: '%s'",
    129                             OptionSetter.NAMESPACE_SEPARATOR)));
    130                 }
    131                 isMultiDeviceConfigMode = true;
    132                 mConfigDef.setMultiDeviceMode(true);
    133                 mCurrentDeviceObject = deviceName;
    134                 addObject(localName, attributes);
    135             } else if (Configuration.isBuiltInObjType(localName)) {
    136                 // tag is a built in local config object
    137                 if (isLocalConfig == null) {
    138                     isLocalConfig = true;
    139                 } else if (!isLocalConfig) {
    140                     throwException(String.format(
    141                             "Attempted to specify local object '%s' for global config!",
    142                             localName));
    143                 }
    144 
    145                 if (mCurrentDeviceObject == null &&
    146                         Configuration.doesBuiltInObjSupportMultiDevice(localName)) {
    147                     // Keep track of all the BuildInObj outside of device tag for final check
    148                     // if it turns out we are in multi mode, we will throw an exception.
    149                     mOutsideTag.add(localName);
    150                 }
    151                 // if we are inside a device object, some tags are not allowed.
    152                 if (mCurrentDeviceObject != null) {
    153                     if (!Configuration.doesBuiltInObjSupportMultiDevice(localName)) {
    154                         // Prevent some tags to be inside of a device in multi device mode.
    155                         throw new SAXException(new ConfigurationException(
    156                                 String.format("Tag %s should not be included in a <device> tag.",
    157                                         localName)));
    158                     }
    159                 }
    160                 addObject(localName, attributes);
    161             } else if (GlobalConfiguration.isBuiltInObjType(localName)) {
    162                 // tag is a built in global config object
    163                 if (isLocalConfig == null) {
    164                     // FIXME: config type should be explicit rather than inferred
    165                     isLocalConfig = false;
    166                 } else if (isLocalConfig) {
    167                     throwException(String.format(
    168                             "Attempted to specify global object '%s' for local config!",
    169                             localName));
    170                 }
    171                 addObject(localName, attributes);
    172             } else if (OPTION_TAG.equals(localName)) {
    173                 String optionName = attributes.getValue("name");
    174                 if (optionName == null) {
    175                     throwException("Missing 'name' attribute for option");
    176                 }
    177 
    178                 String optionKey = attributes.getValue("key");
    179                 // Key is optional at this stage.  If it's actually required, another stage in the
    180                 // configuration validation will throw an exception.
    181 
    182                 String optionValue = attributes.getValue("value");
    183                 if (optionValue == null) {
    184                     throwException("Missing 'value' attribute for option '" + optionName + "'");
    185                 }
    186                 if (mCurrentConfigObject != null) {
    187                     // option is declared within a config object - namespace it with object class
    188                     // name
    189                     optionName = String.format("%s%c%s", mCurrentConfigObject,
    190                             OptionSetter.NAMESPACE_SEPARATOR, optionName);
    191                 }
    192                 if (mCurrentDeviceObject != null) {
    193                     // preprend the device name in extra if inside a device config object.
    194                     optionName = String.format("{%s}%s", mCurrentDeviceObject, optionName);
    195                 }
    196                 mConfigDef.addOptionDef(optionName, optionKey, optionValue, mName);
    197             } else if (CONFIG_TAG.equals(localName)) {
    198                 String description = attributes.getValue("description");
    199                 if (description != null) {
    200                     // Ensure that we only set the description the first time and not when it is
    201                     // loading the <include> configuration.
    202                     if (mConfigDef.getDescription() == null ||
    203                             mConfigDef.getDescription().isEmpty()) {
    204                         mConfigDef.setDescription(description);
    205                     }
    206                 }
    207             } else if (INCLUDE_TAG.equals(localName)) {
    208                 String includeName = attributes.getValue("name");
    209                 if (includeName == null) {
    210                     throwException("Missing 'name' attribute for include");
    211                 }
    212                 try {
    213                     mConfigDefLoader.loadIncludedConfiguration(
    214                             mConfigDef, mName, includeName, mCurrentDeviceObject, mTemplateMap);
    215                 } catch (ConfigurationException e) {
    216                     if (e instanceof TemplateResolutionError) {
    217                         throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
    218                                 mConfigDef.getName(), includeName));
    219                     }
    220                     throw new SAXException(e);
    221                 }
    222             } else if (TEMPLATE_INCLUDE_TAG.equals(localName)) {
    223                 final String templateName = attributes.getValue("name");
    224                 if (templateName == null) {
    225                     throwException("Missing 'name' attribute for template-include");
    226                 }
    227                 if (mCurrentDeviceObject != null) {
    228                     // TODO: Add this use case.
    229                     throwException("<template> inside device object currently not supported.");
    230                 }
    231 
    232                 String includeName = mTemplateMap.get(templateName);
    233                 if (includeName == null) {
    234                     includeName = attributes.getValue("default");
    235                 }
    236                 if (includeName == null) {
    237                     throwTemplateException(mConfigDef.getName(), templateName);
    238                 }
    239                 // Removing the used template from the map to avoid re-using it.
    240                 mTemplateMap.remove(templateName);
    241                 try {
    242                     mConfigDefLoader.loadIncludedConfiguration(
    243                             mConfigDef, mName, includeName, null, mTemplateMap);
    244                 } catch (ConfigurationException e) {
    245                     if (e instanceof TemplateResolutionError) {
    246                         throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
    247                                 mConfigDef.getName(), includeName));
    248                     }
    249                     throw new SAXException(e);
    250                 }
    251             } else {
    252                 throw new SAXException(String.format(
    253                         "Unrecognized tag '%s' in configuration", localName));
    254             }
    255         }
    256 
    257         @Override
    258         public void endElement (String uri, String localName, String qName) throws SAXException {
    259             if (OBJECT_TAG.equals(localName) || Configuration.isBuiltInObjType(localName)
    260                     || GlobalConfiguration.isBuiltInObjType(localName)) {
    261                 mCurrentConfigObject = null;
    262             }
    263             if (DEVICE_TAG.equals(localName) && !mInsideParentDeviceTag) {
    264                 // Only unset if it was not the parent device tag.
    265                 mCurrentDeviceObject = null;
    266             }
    267         }
    268 
    269         void addObject(String objectTypeName, Attributes attributes) throws SAXException {
    270             if (Configuration.DEVICE_NAME.equals(objectTypeName)) {
    271                 // We still want to add a standalone device without any inner object.
    272                 String deviceName = attributes.getValue("name");
    273                 if (!mListDevice.contains(deviceName)) {
    274                     mListDevice.add(deviceName);
    275                     mConfigDef.addConfigObjectDef(objectTypeName,
    276                             DeviceConfigurationHolder.class.getCanonicalName());
    277                     mConfigDef.addExpectedDevice(deviceName);
    278                 }
    279             } else {
    280                 String className = attributes.getValue("class");
    281                 if (className == null) {
    282                     throwException(String.format("Missing class attribute for object %s",
    283                             objectTypeName));
    284                 }
    285                 if (mCurrentDeviceObject != null) {
    286                     // Add the device name as a namespace to the type
    287                     objectTypeName = mCurrentDeviceObject + OptionSetter.NAMESPACE_SEPARATOR
    288                             + objectTypeName;
    289                 }
    290                 int classCount = mConfigDef.addConfigObjectDef(objectTypeName, className);
    291                 mCurrentConfigObject = String.format("%s%c%d", className,
    292                         OptionSetter.NAMESPACE_SEPARATOR, classCount);
    293             }
    294         }
    295 
    296         private void throwException(String reason) throws SAXException {
    297             throw new SAXException(new ConfigurationException(String.format(
    298                     "Failed to parse config xml '%s'. Reason: %s", mConfigDef.getName(), reason)));
    299         }
    300 
    301         private void throwTemplateException(String configName, String templateName)
    302                 throws SAXException {
    303             throw new SAXException(new TemplateResolutionError(configName, templateName));
    304         }
    305     }
    306 
    307     private final IConfigDefLoader mConfigDefLoader;
    308     /**
    309      * If we are loading a config from inside a <device> tag, this will contain the name of the
    310      * current device tag to properly load in context.
    311      */
    312     private final String mParentDeviceObject;
    313 
    314     ConfigurationXmlParser(IConfigDefLoader loader, String parentDeviceObject) {
    315         mConfigDefLoader = loader;
    316         mParentDeviceObject = parentDeviceObject;
    317     }
    318 
    319     /**
    320      * Parses out configuration data contained in given input into the given configdef.
    321      * <p/>
    322      * Currently performs limited error checking.
    323      *
    324      * @param configDef the {@link ConfigurationDef} to load data into
    325      * @param name the name of the configuration currently being loaded. Used for logging only.
    326      * Can be different than configDef.getName in cases of included configs
    327      * @param xmlInput the configuration xml to parse
    328      * @throws ConfigurationException if input could not be parsed or had invalid format
    329      */
    330     void parse(ConfigurationDef configDef, String name, InputStream xmlInput,
    331             Map<String, String> templateMap) throws ConfigurationException {
    332         try {
    333             SAXParserFactory parserFactory = SAXParserFactory.newInstance();
    334             parserFactory.setNamespaceAware(true);
    335             SAXParser parser = parserFactory.newSAXParser();
    336             ConfigHandler configHandler =
    337                     new ConfigHandler(
    338                             configDef, name, mConfigDefLoader, mParentDeviceObject, templateMap);
    339             parser.parse(new InputSource(xmlInput), configHandler);
    340             checkValidMultiConfiguration(configHandler);
    341         } catch (ParserConfigurationException e) {
    342             throwConfigException(name, e);
    343         } catch (SAXException e) {
    344             throwConfigException(name, e);
    345         } catch (IOException e) {
    346             throwConfigException(name, e);
    347         }
    348     }
    349 
    350     /**
    351      * Helper to encapsulate exceptions in a {@link ConfigurationException}
    352      */
    353     private void throwConfigException(String configName, Throwable e)
    354             throws ConfigurationException {
    355         if (e.getCause() instanceof ConfigurationException) {
    356             throw (ConfigurationException)e.getCause();
    357         }
    358         throw new ConfigurationException(String.format("Failed to parse config xml '%s' due to "
    359                 + "'%s'", configName, e), e);
    360     }
    361 
    362     /**
    363      * Validate that the configuration is valid from a multi device configuration standpoint:
    364      * Some tags are not allowed outside the <device> tags.
    365      */
    366     private void checkValidMultiConfiguration(ConfigHandler configHandler) throws SAXException {
    367         if (configHandler.isMultiDeviceConfigMode == true &&
    368                 !configHandler.mOutsideTag.isEmpty()) {
    369             throw new SAXException(new ConfigurationException(String.format("Tags %s "
    370                     + "should be included in a <device> tag.", configHandler.mOutsideTag)));
    371         }
    372     }
    373 }
    374