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