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