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