1 /* 2 * Copyright (C) 2012 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.command.CommandScheduler; 20 import com.android.tradefed.command.ICommandScheduler; 21 import com.android.tradefed.device.DeviceManager; 22 import com.android.tradefed.device.DeviceSelectionOptions; 23 import com.android.tradefed.device.IDeviceManager; 24 import com.android.tradefed.device.IDeviceMonitor; 25 import com.android.tradefed.device.IDeviceSelection; 26 import com.android.tradefed.device.IMultiDeviceRecovery; 27 import com.android.tradefed.host.HostOptions; 28 import com.android.tradefed.host.IHostOptions; 29 import com.android.tradefed.invoker.shard.IShardHelper; 30 import com.android.tradefed.invoker.shard.StrictShardHelper; 31 import com.android.tradefed.log.ITerribleFailureHandler; 32 import com.android.tradefed.util.ArrayUtil; 33 import com.android.tradefed.util.MultiMap; 34 import com.android.tradefed.util.hostmetric.IHostMonitor; 35 import com.android.tradefed.util.keystore.IKeyStoreFactory; 36 import com.android.tradefed.util.keystore.StubKeyStoreFactory; 37 38 import org.kxml2.io.KXmlSerializer; 39 40 import java.io.File; 41 import java.io.IOException; 42 import java.io.PrintStream; 43 import java.util.ArrayList; 44 import java.util.Collection; 45 import java.util.HashMap; 46 import java.util.LinkedHashMap; 47 import java.util.List; 48 import java.util.Map; 49 50 51 /** 52 * An {@link IGlobalConfiguration} implementation that stores the loaded config objects in a map 53 */ 54 public class GlobalConfiguration implements IGlobalConfiguration { 55 // type names for built in configuration objects 56 public static final String DEVICE_MONITOR_TYPE_NAME = "device_monitor"; 57 public static final String HOST_MONITOR_TYPE_NAME = "host_monitor"; 58 public static final String DEVICE_MANAGER_TYPE_NAME = "device_manager"; 59 public static final String WTF_HANDLER_TYPE_NAME = "wtf_handler"; 60 public static final String HOST_OPTIONS_TYPE_NAME = "host_options"; 61 public static final String DEVICE_REQUIREMENTS_TYPE_NAME = "device_requirements"; 62 public static final String SCHEDULER_TYPE_NAME = "command_scheduler"; 63 public static final String MULTI_DEVICE_RECOVERY_TYPE_NAME = "multi_device_recovery"; 64 public static final String KEY_STORE_TYPE_NAME = "key_store"; 65 public static final String SHARDING_STRATEGY_TYPE_NAME = "sharding_strategy"; 66 67 public static final String GLOBAL_CONFIG_VARIABLE = "TF_GLOBAL_CONFIG"; 68 private static final String GLOBAL_CONFIG_FILENAME = "tf_global_config.xml"; 69 70 private static Map<String, ObjTypeInfo> sObjTypeMap = null; 71 private static IGlobalConfiguration sInstance = null; 72 private static final Object sInstanceLock = new Object(); 73 74 // Empty embedded configuration available by default 75 private static final String DEFAULT_EMPTY_CONFIG_NAME = "empty"; 76 77 // Configurations to be passed to subprocess 78 private static final String[] CONFIGS_FOR_SUBPROCESS_WHITE_LIST = 79 new String[] {KEY_STORE_TYPE_NAME}; 80 81 /** Mapping of config object type name to config objects. */ 82 private Map<String, List<Object>> mConfigMap; 83 private MultiMap<String, String> mOptionMap; 84 private final String mName; 85 private final String mDescription; 86 87 /** 88 * Returns a reference to the singleton {@link GlobalConfiguration} instance for this TF 89 * instance. 90 * 91 * @throws IllegalStateException if {@link #createGlobalConfiguration(String[])} has not 92 * already been called. 93 */ 94 public static IGlobalConfiguration getInstance() { 95 if (sInstance == null) { 96 throw new IllegalStateException("GlobalConfiguration has not yet been initialized!"); 97 } 98 return sInstance; 99 } 100 101 /** 102 * Returns a reference to the singleton {@link DeviceManager} instance for this TF 103 * instance. 104 * 105 * @throws IllegalStateException if {@link #createGlobalConfiguration(String[])} has not 106 * already been called. 107 */ 108 public static IDeviceManager getDeviceManagerInstance() { 109 if (sInstance == null) { 110 throw new IllegalStateException("GlobalConfiguration has not yet been initialized!"); 111 } 112 return sInstance.getDeviceManager(); 113 } 114 115 public static List<IHostMonitor> getHostMonitorInstances() { 116 if (sInstance == null) { 117 throw new IllegalStateException("GlobalConfiguration has not yet been initialized!"); 118 } 119 return sInstance.getHostMonitors(); 120 } 121 122 /** 123 * Sets up the {@link GlobalConfiguration} singleton for this TF instance. Must be called 124 * once and only once, before anything attempts to call {@link #getInstance()} 125 * 126 * @throws IllegalStateException if called more than once 127 */ 128 public static List<String> createGlobalConfiguration(String[] args) 129 throws ConfigurationException { 130 synchronized (sInstanceLock) { 131 if (sInstance != null) { 132 throw new IllegalStateException("GlobalConfiguration is already initialized!"); 133 } 134 135 List<String> nonGlobalArgs = new ArrayList<String>(args.length); 136 IConfigurationFactory configFactory = ConfigurationFactory.getInstance(); 137 String globalConfigPath = getGlobalConfigPath(); 138 sInstance = configFactory.createGlobalConfigurationFromArgs( 139 ArrayUtil.buildArray(new String[] {globalConfigPath}, args), nonGlobalArgs); 140 if (!DEFAULT_EMPTY_CONFIG_NAME.equals(globalConfigPath)) { 141 // Only print when using different from default 142 System.out.format("Success! Using global config \"%s\"\n", globalConfigPath); 143 } 144 145 // Validate that madatory options have been set 146 sInstance.validateOptions(); 147 return nonGlobalArgs; 148 } 149 } 150 151 /** 152 * Returns the path to a global config, if one exists, or <code>null</code> if none could be 153 * found. 154 * <p /> 155 * Search locations, in decreasing order of precedence 156 * <ol> 157 * <li><code>$TF_GLOBAL_CONFIG</code> environment variable</li> 158 * <li><code>tf_global_config.xml</code> file in $PWD</li> 159 * <li>(FIXME) <code>tf_global_config.xml</code> file in dir where <code>tradefed.sh</code> 160 * lives</li> 161 * </ol> 162 */ 163 private static String getGlobalConfigPath() { 164 String path = System.getenv(GLOBAL_CONFIG_VARIABLE); 165 if (path != null) { 166 // don't actually check for accessibility here, since the variable might be specifying 167 // a java resource rather than a filename. Even so, this can help the user figure out 168 // which global config (if any) was picked up by TF. 169 System.out.format( 170 "Attempting to use global config \"%s\" from variable $%s.\n", 171 path, GLOBAL_CONFIG_VARIABLE); 172 return path; 173 } 174 175 File file = new File(GLOBAL_CONFIG_FILENAME); 176 if (file.exists()) { 177 path = file.getPath(); 178 System.out.format("Attempting to use autodetected global config \"%s\".\n", path); 179 return path; 180 } 181 182 // FIXME: search in tradefed.sh launch dir (or classpath?) 183 184 // Use default empty known global config 185 return DEFAULT_EMPTY_CONFIG_NAME; 186 } 187 188 /** 189 * Container struct for built-in config object type 190 */ 191 private static class ObjTypeInfo { 192 final Class<?> mExpectedType; 193 /** true if a list (ie many objects in a single config) are supported for this type */ 194 final boolean mIsListSupported; 195 196 ObjTypeInfo(Class<?> expectedType, boolean isList) { 197 mExpectedType = expectedType; 198 mIsListSupported = isList; 199 } 200 } 201 202 /** 203 * Determine if given config object type name is a built in object 204 * 205 * @param typeName the config object type name 206 * @return <code>true</code> if name is a built in object type 207 */ 208 static boolean isBuiltInObjType(String typeName) { 209 return getObjTypeMap().containsKey(typeName); 210 } 211 212 private static synchronized Map<String, ObjTypeInfo> getObjTypeMap() { 213 if (sObjTypeMap == null) { 214 sObjTypeMap = new HashMap<String, ObjTypeInfo>(); 215 sObjTypeMap.put(HOST_OPTIONS_TYPE_NAME, new ObjTypeInfo(IHostOptions.class, false)); 216 sObjTypeMap.put(DEVICE_MONITOR_TYPE_NAME, new ObjTypeInfo(IDeviceMonitor.class, true)); 217 sObjTypeMap.put(HOST_MONITOR_TYPE_NAME, new ObjTypeInfo(IHostMonitor.class, true)); 218 sObjTypeMap.put(DEVICE_MANAGER_TYPE_NAME, new ObjTypeInfo(IDeviceManager.class, false)); 219 sObjTypeMap.put(DEVICE_REQUIREMENTS_TYPE_NAME, new ObjTypeInfo(IDeviceSelection.class, 220 false)); 221 sObjTypeMap.put(WTF_HANDLER_TYPE_NAME, 222 new ObjTypeInfo(ITerribleFailureHandler.class, false)); 223 sObjTypeMap.put(SCHEDULER_TYPE_NAME, 224 new ObjTypeInfo(ICommandScheduler.class, false)); 225 sObjTypeMap.put(MULTI_DEVICE_RECOVERY_TYPE_NAME, 226 new ObjTypeInfo(IMultiDeviceRecovery.class, true)); 227 sObjTypeMap.put(KEY_STORE_TYPE_NAME, 228 new ObjTypeInfo(IKeyStoreFactory.class, false)); 229 sObjTypeMap.put( 230 SHARDING_STRATEGY_TYPE_NAME, new ObjTypeInfo(IShardHelper.class, false)); 231 232 } 233 return sObjTypeMap; 234 } 235 236 /** 237 * Creates a {@link GlobalConfiguration} with default config objects 238 */ 239 GlobalConfiguration(String name, String description) { 240 mName = name; 241 mDescription = description; 242 mConfigMap = new LinkedHashMap<String, List<Object>>(); 243 mOptionMap = new MultiMap<String, String>(); 244 setHostOptions(new HostOptions()); 245 setDeviceRequirements(new DeviceSelectionOptions()); 246 setDeviceManager(new DeviceManager()); 247 setCommandScheduler(new CommandScheduler()); 248 setKeyStoreFactory(new StubKeyStoreFactory()); 249 setShardingStrategy(new StrictShardHelper()); 250 } 251 252 /** 253 * @return the name of this {@link Configuration} 254 */ 255 public String getName() { 256 return mName; 257 } 258 259 /** 260 * @return a short user readable description this {@link Configuration} 261 */ 262 public String getDescription() { 263 return mDescription; 264 } 265 266 /** 267 * {@inheritDoc} 268 */ 269 @Override 270 public IHostOptions getHostOptions() { 271 return (IHostOptions) getConfigurationObject(HOST_OPTIONS_TYPE_NAME); 272 } 273 274 /** 275 * {@inheritDoc} 276 */ 277 @Override 278 @SuppressWarnings("unchecked") 279 public List<IDeviceMonitor> getDeviceMonitors() { 280 return (List<IDeviceMonitor>) getConfigurationObjectList(DEVICE_MONITOR_TYPE_NAME); 281 } 282 283 /** 284 * {@inheritDoc} 285 */ 286 @Override 287 @SuppressWarnings("unchecked") 288 public List<IHostMonitor> getHostMonitors() { 289 return (List<IHostMonitor>) getConfigurationObjectList(HOST_MONITOR_TYPE_NAME); 290 } 291 292 /** 293 * {@inheritDoc} 294 */ 295 @Override 296 public ITerribleFailureHandler getWtfHandler() { 297 return (ITerribleFailureHandler) getConfigurationObject(WTF_HANDLER_TYPE_NAME); 298 } 299 300 /** 301 * {@inheritDoc} 302 */ 303 @Override 304 public IKeyStoreFactory getKeyStoreFactory() { 305 return (IKeyStoreFactory) getConfigurationObject(KEY_STORE_TYPE_NAME); 306 } 307 308 /** {@inheritDoc} */ 309 @Override 310 public IShardHelper getShardingStrategy() { 311 return (IShardHelper) getConfigurationObject(SHARDING_STRATEGY_TYPE_NAME); 312 } 313 314 /** {@inheritDoc} */ 315 @Override 316 public IDeviceManager getDeviceManager() { 317 return (IDeviceManager)getConfigurationObject(DEVICE_MANAGER_TYPE_NAME); 318 } 319 320 /** 321 * {@inheritDoc} 322 */ 323 @Override 324 public IDeviceSelection getDeviceRequirements() { 325 return (IDeviceSelection)getConfigurationObject(DEVICE_REQUIREMENTS_TYPE_NAME); 326 } 327 328 /** 329 * {@inheritDoc} 330 */ 331 @Override 332 public ICommandScheduler getCommandScheduler() { 333 return (ICommandScheduler)getConfigurationObject(SCHEDULER_TYPE_NAME); 334 } 335 336 /** 337 * {@inheritDoc} 338 */ 339 @Override 340 @SuppressWarnings("unchecked") 341 public List<IMultiDeviceRecovery> getMultiDeviceRecoveryHandlers() { 342 return (List<IMultiDeviceRecovery>)getConfigurationObjectList( 343 MULTI_DEVICE_RECOVERY_TYPE_NAME); 344 } 345 346 /** 347 * Internal helper to get the list of config object 348 */ 349 private List<?> getConfigurationObjectList(String typeName) { 350 return mConfigMap.get(typeName); 351 } 352 353 /** 354 * {@inheritDoc} 355 */ 356 @Override 357 public Object getConfigurationObject(String typeName) { 358 List<?> configObjects = getConfigurationObjectList(typeName); 359 if (configObjects == null) { 360 return null; 361 } 362 ObjTypeInfo typeInfo = getObjTypeMap().get(typeName); 363 if (typeInfo != null && typeInfo.mIsListSupported) { 364 throw new IllegalStateException( 365 String.format( 366 "Wrong method call for type %s. Used getConfigurationObject() for a " 367 + "config object that is stored as a list", 368 typeName)); 369 } 370 if (configObjects.size() != 1) { 371 throw new IllegalStateException(String.format( 372 "Attempted to retrieve single object for %s, but %d are present", 373 typeName, configObjects.size())); 374 } 375 return configObjects.get(0); 376 } 377 378 /** 379 * Return a copy of all config objects 380 */ 381 private Collection<Object> getAllConfigurationObjects() { 382 Collection<Object> objectsCopy = new ArrayList<Object>(); 383 for (List<Object> objectList : mConfigMap.values()) { 384 objectsCopy.addAll(objectList); 385 } 386 return objectsCopy; 387 } 388 389 /** 390 * {@inheritDoc} 391 */ 392 @Override 393 public void injectOptionValue(String optionName, String optionValue) 394 throws ConfigurationException { 395 injectOptionValue(optionName, null, optionValue); 396 } 397 398 /** 399 * {@inheritDoc} 400 */ 401 @Override 402 public void injectOptionValue(String optionName, String optionKey, String optionValue) 403 throws ConfigurationException { 404 OptionSetter optionSetter = new OptionSetter(getAllConfigurationObjects()); 405 optionSetter.setOptionValue(optionName, optionKey, optionValue); 406 407 if (optionKey != null) { 408 mOptionMap.put(optionName, optionKey + "=" + optionValue); 409 } else { 410 mOptionMap.put(optionName, optionValue); 411 } 412 } 413 414 /** 415 * {@inheritDoc} 416 */ 417 @Override 418 public List<String> getOptionValues(String optionName) { 419 return mOptionMap.get(optionName); 420 } 421 422 /** 423 * {@inheritDoc} 424 */ 425 @Override 426 public void setHostOptions(IHostOptions hostOptions) { 427 setConfigurationObjectNoThrow(HOST_OPTIONS_TYPE_NAME, hostOptions); 428 } 429 430 /** 431 * {@inheritDoc} 432 */ 433 @Override 434 public void setDeviceMonitor(IDeviceMonitor monitor) { 435 setConfigurationObjectNoThrow(DEVICE_MONITOR_TYPE_NAME, monitor); 436 } 437 438 /** {@inheritDoc} */ 439 @Override 440 public void setHostMonitors(List<IHostMonitor> hostMonitors) { 441 setConfigurationObjectListNoThrow(HOST_MONITOR_TYPE_NAME, hostMonitors); 442 } 443 444 /** 445 * {@inheritDoc} 446 */ 447 @Override 448 public void setWtfHandler(ITerribleFailureHandler wtfHandler) { 449 setConfigurationObjectNoThrow(WTF_HANDLER_TYPE_NAME, wtfHandler); 450 } 451 452 /** 453 * {@inheritDoc} 454 */ 455 @Override 456 public void setKeyStoreFactory(IKeyStoreFactory factory) { 457 setConfigurationObjectNoThrow(KEY_STORE_TYPE_NAME, factory); 458 } 459 460 /** {@inheritDoc} */ 461 @Override 462 public void setShardingStrategy(IShardHelper sharding) { 463 setConfigurationObjectNoThrow(SHARDING_STRATEGY_TYPE_NAME, sharding); 464 } 465 466 /** {@inheritDoc} */ 467 @Override 468 public void setDeviceManager(IDeviceManager manager) { 469 setConfigurationObjectNoThrow(DEVICE_MANAGER_TYPE_NAME, manager); 470 } 471 472 /** 473 * {@inheritDoc} 474 */ 475 @Override 476 public void setDeviceRequirements(IDeviceSelection devRequirements) { 477 setConfigurationObjectNoThrow(DEVICE_REQUIREMENTS_TYPE_NAME, devRequirements); 478 } 479 480 /** 481 * {@inheritDoc} 482 */ 483 @Override 484 public void setCommandScheduler(ICommandScheduler scheduler) { 485 setConfigurationObjectNoThrow(SCHEDULER_TYPE_NAME, scheduler); 486 } 487 488 /** 489 * {@inheritDoc} 490 */ 491 @Override 492 public void setConfigurationObject(String typeName, Object configObject) 493 throws ConfigurationException { 494 if (configObject == null) { 495 throw new IllegalArgumentException("configObject cannot be null"); 496 } 497 mConfigMap.remove(typeName); 498 addObject(typeName, configObject); 499 } 500 501 /** 502 * {@inheritDoc} 503 */ 504 @Override 505 public void setConfigurationObjectList(String typeName, List<?> configList) 506 throws ConfigurationException { 507 if (configList == null) { 508 throw new IllegalArgumentException("configList cannot be null"); 509 } 510 mConfigMap.remove(typeName); 511 for (Object configObject : configList) { 512 addObject(typeName, configObject); 513 } 514 } 515 516 /** 517 * A wrapper around {@link #setConfigurationObjectList(String, List)} that will not throw {@link 518 * ConfigurationException}. 519 * 520 * <p>Intended to be used in cases where its guaranteed that <var>configObject</var> is the 521 * correct type 522 */ 523 private void setConfigurationObjectListNoThrow(String typeName, List<?> configList) { 524 try { 525 setConfigurationObjectList(typeName, configList); 526 } catch (ConfigurationException e) { 527 // should never happen 528 throw new IllegalArgumentException(e); 529 } 530 } 531 532 /** 533 * Adds a loaded object to this configuration. 534 * 535 * @param typeName the unique object type name of the configuration object 536 * @param configObject the configuration object 537 * @throws ConfigurationException if object was not the correct type 538 */ 539 private void addObject(String typeName, Object configObject) throws ConfigurationException { 540 List<Object> objList = mConfigMap.get(typeName); 541 if (objList == null) { 542 objList = new ArrayList<Object>(1); 543 mConfigMap.put(typeName, objList); 544 } 545 ObjTypeInfo typeInfo = getObjTypeMap().get(typeName); 546 if (typeInfo != null && !typeInfo.mExpectedType.isInstance(configObject)) { 547 throw new ConfigurationException(String.format( 548 "The config object %s is not the correct type. Expected %s, received %s", 549 typeName, typeInfo.mExpectedType.getCanonicalName(), 550 configObject.getClass().getCanonicalName())); 551 } 552 if (typeInfo != null && !typeInfo.mIsListSupported && objList.size() > 0) { 553 throw new ConfigurationException(String.format( 554 "Only one config object allowed for %s, but multiple were specified.", 555 typeName)); 556 } 557 objList.add(configObject); 558 } 559 560 /** 561 * A wrapper around {@link #setConfigurationObject(String, Object)} that will not throw 562 * {@link ConfigurationException}. 563 * <p/> 564 * Intended to be used in cases where its guaranteed that <var>configObject</var> is the 565 * correct type. 566 * 567 * @param typeName 568 * @param configObject 569 */ 570 private void setConfigurationObjectNoThrow(String typeName, Object configObject) { 571 try { 572 setConfigurationObject(typeName, configObject); 573 } catch (ConfigurationException e) { 574 // should never happen 575 throw new IllegalArgumentException(e); 576 } 577 } 578 579 /** 580 * {@inheritDoc} 581 */ 582 @Override 583 public List<String> setOptionsFromCommandLineArgs(List<String> listArgs) 584 throws ConfigurationException { 585 ArgsOptionParser parser = new ArgsOptionParser(getAllConfigurationObjects()); 586 return parser.parse(listArgs); 587 } 588 589 /** 590 * Outputs a command line usage help text for this configuration to given printStream. 591 * 592 * @param out the {@link PrintStream} to use. 593 * @throws ConfigurationException 594 */ 595 public void printCommandUsage(boolean importantOnly, PrintStream out) 596 throws ConfigurationException { 597 out.println(String.format("'%s' configuration: %s", getName(), getDescription())); 598 out.println(); 599 if (importantOnly) { 600 out.println("Printing help for only the important options. " + 601 "To see help for all options, use the --help-all flag"); 602 out.println(); 603 } 604 for (Map.Entry<String, List<Object>> configObjectsEntry : mConfigMap.entrySet()) { 605 for (Object configObject : configObjectsEntry.getValue()) { 606 String optionHelp = printOptionsForObject(importantOnly, 607 configObjectsEntry.getKey(), configObject); 608 // only print help for object if optionHelp is non zero length 609 if (optionHelp.length() > 0) { 610 String classAlias = ""; 611 if (configObject.getClass().isAnnotationPresent(OptionClass.class)) { 612 final OptionClass classAnnotation = configObject.getClass().getAnnotation( 613 OptionClass.class); 614 classAlias = String.format("'%s' ", classAnnotation.alias()); 615 } 616 out.printf(" %s%s options:", classAlias, configObjectsEntry.getKey()); 617 out.println(); 618 out.print(optionHelp); 619 out.println(); 620 } 621 } 622 } 623 } 624 625 /** 626 * Prints out the available config options for given configuration object. 627 * 628 * @param importantOnly print only the important options 629 * @param objectTypeName the config object type name. Used to generate more descriptive error 630 * messages 631 * @param configObject the config object 632 * @return a {@link String} of option help text 633 * @throws ConfigurationException 634 */ 635 private String printOptionsForObject(boolean importantOnly, String objectTypeName, 636 Object configObject) throws ConfigurationException { 637 return ArgsOptionParser.getOptionHelp(importantOnly, configObject); 638 } 639 640 /** 641 * {@inheritDoc} 642 */ 643 @Override 644 public void validateOptions() throws ConfigurationException { 645 new ArgsOptionParser(getAllConfigurationObjects()).validateMandatoryOptions(); 646 } 647 648 /** {@inheritDoc} */ 649 @Override 650 public void cloneConfigWithFilter(File outputXml, String[] whitelistConfigs) 651 throws IOException { 652 KXmlSerializer serializer = ConfigurationUtil.createSerializer(outputXml); 653 serializer.startTag(null, ConfigurationUtil.CONFIGURATION_NAME); 654 if (whitelistConfigs == null) { 655 whitelistConfigs = CONFIGS_FOR_SUBPROCESS_WHITE_LIST; 656 } 657 for (String config : whitelistConfigs) { 658 ConfigurationUtil.dumpClassToXml( 659 serializer, config, getConfigurationObject(config), new ArrayList<>()); 660 } 661 serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME); 662 serializer.endDocument(); 663 } 664 } 665