blob: 12532c89ea55839f04bb504a9518298028345503 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.config;
import com.android.ddmlib.Log;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.ClassPathScanner;
import com.android.tradefed.util.ClassPathScanner.IClassPathFilter;
import com.android.tradefed.util.StreamUtil;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* Factory for creating {@link IConfiguration}.
*/
public class ConfigurationFactory implements IConfigurationFactory {
private static final String LOG_TAG = "ConfigurationFactory";
private static IConfigurationFactory sInstance = null;
private static final String CONFIG_SUFFIX = ".xml";
private static final String CONFIG_PREFIX = "config/";
private Map<String, ConfigurationDef> mConfigDefMap;
/**
* A {@link IClassPathFilter} for configuration XML files.
*/
private class ConfigClasspathFilter implements IClassPathFilter {
/**
* {@inheritDoc}
*/
@Override
public boolean accept(String pathName) {
// only accept entries that match the pattern, and that we don't already know about
return pathName.startsWith(CONFIG_PREFIX) && pathName.endsWith(CONFIG_SUFFIX) &&
!mConfigDefMap.containsKey(pathName);
}
/**
* {@inheritDoc}
*/
@Override
public String transform(String pathName) {
// strip off CONFIG_PREFIX and CONFIG_SUFFIX
int pathStartIndex = CONFIG_PREFIX.length();
int pathEndIndex = pathName.length() - CONFIG_SUFFIX.length();
return pathName.substring(pathStartIndex, pathEndIndex);
}
}
/**
* A {@link Comparator} for {@link ConfigurationDef} that sorts by
* {@link ConfigurationDef#getName()}.
*/
private static class ConfigDefComparator implements Comparator<ConfigurationDef> {
/**
* {@inheritDoc}
*/
@Override
public int compare(ConfigurationDef d1, ConfigurationDef d2) {
return d1.getName().compareTo(d2.getName());
}
}
/**
* Implementation of {@link IConfigDefLoader} that tracks the included configurations from one
* root config, and throws an exception on circular includes.
*/
class ConfigLoader implements IConfigDefLoader {
private final boolean mIsGlobalConfig;
private Set<String> mIncludedConfigs = new HashSet<String>();
public ConfigLoader(boolean isGlobalConfig) {
mIsGlobalConfig = isGlobalConfig;
}
/**
* {@inheritDoc}
*/
@Override
public ConfigurationDef getConfigurationDef(String name) throws ConfigurationException {
// first attempt to load cached config def
ConfigurationDef def = mConfigDefMap.get(name);
if (def == null) {
// not found - load from file
def = new ConfigurationDef(name);
loadConfiguration(name, def);
mConfigDefMap.put(name, def);
}
return def;
}
@Override
public void loadIncludedConfiguration(ConfigurationDef parent, String name)
throws ConfigurationException {
if (mIncludedConfigs.contains(name)) {
throw new ConfigurationException(String.format(
"Circular configuration include: config '%s' is already included", name));
}
mIncludedConfigs.add(name);
loadConfiguration(name, parent);
}
/**
* Loads a configuration.
*
* @param name the name of a built-in configuration to load or a file
* path to configuration xml to load
* @return the loaded {@link ConfigurationDef}
* @throws ConfigurationException if a configuration with given
* name/file path cannot be loaded or parsed
*/
void loadConfiguration(String name, ConfigurationDef def) throws ConfigurationException {
Log.i(LOG_TAG, String.format("Loading configuration '%s'", name));
BufferedInputStream bufStream = getConfigStream(name);
ConfigurationXmlParser parser = new ConfigurationXmlParser(this);
parser.parse(def, name, bufStream);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isGlobalConfig() {
return mIsGlobalConfig;
}
}
ConfigurationFactory() {
mConfigDefMap = new Hashtable<String, ConfigurationDef>();
}
/**
* Get the singleton {@link IConfigurationFactory} instance.
*/
public static IConfigurationFactory getInstance() {
if (sInstance == null) {
sInstance = new ConfigurationFactory();
}
return sInstance;
}
/**
* Retrieve the {@link ConfigurationDef} for the given name
*
* @param name the name of a built-in configuration to load or a file path to configuration xml
* to load
* @return {@link ConfigurationDef}
* @throws ConfigurationException if an error occurred loading the config
*/
private ConfigurationDef getConfigurationDef(String name, boolean isGlobal)
throws ConfigurationException {
return new ConfigLoader(isGlobal).getConfigurationDef(name);
}
/**
* {@inheritDoc}
*/
@Override
public IConfiguration createConfigurationFromArgs(String[] arrayArgs)
throws ConfigurationException {
List<String> listArgs = new ArrayList<String>(arrayArgs.length);
IConfiguration config = internalCreateConfigurationFromArgs(arrayArgs, listArgs);
config.setOptionsFromCommandLineArgs(listArgs);
return config;
}
/**
* Creates a {@link Configuration} from the name given in arguments.
* <p/>
* Note will not populate configuration with values from options
*
* @param arrayArgs the full list of command line arguments, including the config name
* @param optionArgsRef an empty list, that will be populated with the remaining option
* arguments
* @return
* @throws ConfigurationException
*/
private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs,
List<String> optionArgsRef) throws ConfigurationException {
if (arrayArgs.length == 0) {
throw new ConfigurationException("Configuration to run was not specified");
}
optionArgsRef.addAll(Arrays.asList(arrayArgs));
// first arg is config name
final String configName = optionArgsRef.remove(0);
ConfigurationDef configDef = getConfigurationDef(configName, false);
return configDef.createConfiguration();
}
/**
* {@inheritDoc}
*/
@Override
public IGlobalConfiguration createGlobalConfigurationFromArgs(String[] arrayArgs,
List<String> remainingArgs) throws ConfigurationException {
List<String> listArgs = new ArrayList<String>(arrayArgs.length);
IGlobalConfiguration config =
internalCreateGlobalConfigurationFromArgs(arrayArgs, listArgs);
remainingArgs.addAll(config.setOptionsFromCommandLineArgs(listArgs));
return config;
}
/**
* Creates a {@link Configuration} from the name given in arguments.
* <p/>
* Note will not populate configuration with values from options
*
* @param arrayArgs the full list of command line arguments, including the config name
* @param optionArgsRef an empty list, that will be populated with the remaining option
* arguments
* @return
* @throws ConfigurationException
*/
private IGlobalConfiguration internalCreateGlobalConfigurationFromArgs(String[] arrayArgs,
List<String> optionArgsRef) throws ConfigurationException {
if (arrayArgs.length == 0) {
throw new ConfigurationException("Configuration to run was not specified");
}
optionArgsRef.addAll(Arrays.asList(arrayArgs));
// first arg is config name
final String configName = optionArgsRef.remove(0);
ConfigurationDef configDef = getConfigurationDef(configName, false);
return configDef.createGlobalConfiguration();
}
/**
* {@inheritDoc}
*/
@Override
public void printHelp(PrintStream out) {
// print general help
// TODO: move this statement to Console
out.println("Use 'run command <configuration_name> --help' to get list of options for a " +
"configuration");
out.println("Use 'dump config <configuration_name>' to display the configuration's XML " +
"content.");
out.println();
out.println("Available configurations include:");
try {
loadAllConfigs(true);
} catch (ConfigurationException e) {
// ignore, should never happen
}
// sort the configs by name before displaying
SortedSet<ConfigurationDef> configDefs = new TreeSet<ConfigurationDef>(
new ConfigDefComparator());
configDefs.addAll(mConfigDefMap.values());
for (ConfigurationDef def: configDefs) {
out.printf(" %s: %s", def.getName(), def.getDescription());
out.println();
}
}
/**
* Loads all configurations found in classpath.
*
* @param discardExceptions true if any ConfigurationException should be ignored. Exposed for
* unit testing
* @throws ConfigurationException
*/
void loadAllConfigs(boolean discardExceptions) throws ConfigurationException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);
boolean failed = false;
ClassPathScanner cpScanner = new ClassPathScanner();
Set<String> configNames = cpScanner.getClassPathEntries(new ConfigClasspathFilter());
for (String configName : configNames) {
try {
ConfigurationDef configDef = getConfigurationDef(configName, false);
mConfigDefMap.put(configName, configDef);
} catch (ConfigurationException e) {
ps.printf("Failed to load %s: %s", configName, e.getMessage());
ps.println();
failed = true;
}
}
if (failed) {
if (discardExceptions) {
CLog.e("Failure loading configs");
CLog.e(baos.toString());
} else {
throw new ConfigurationException(baos.toString());
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void printHelpForConfig(String[] args, boolean importantOnly, PrintStream out) {
try {
IConfiguration config = internalCreateConfigurationFromArgs(args,
new ArrayList<String>(args.length));
config.printCommandUsage(importantOnly, out);
} catch (ConfigurationException e) {
// config must not be specified. Print generic help
printHelp(out);
}
}
/**
* {@inheritDoc}
*/
@Override
public void dumpConfig(String configName, PrintStream out) {
try {
InputStream configStream = getConfigStream(configName);
StreamUtil.copyStreams(configStream, out);
} catch (ConfigurationException e) {
Log.e(LOG_TAG, e);
} catch (IOException e) {
Log.e(LOG_TAG, e);
}
}
/**
* Return the path prefix of config xml files on classpath
* <p/>
* Exposed so unit tests can mock.
* @return {@link String} path with trailing /
*/
String getConfigPrefix() {
return CONFIG_PREFIX;
}
/**
* Loads an InputStream for given config name
*
* @param name the configuration name to load
* @return a {@link BufferedInputStream} for reading config contents
* @throws ConfigurationException if config could not be found
*/
private BufferedInputStream getConfigStream(String name) throws ConfigurationException {
InputStream configStream = getClass().getResourceAsStream(
String.format("/%s%s%s", getConfigPrefix(), name, CONFIG_SUFFIX));
if (configStream == null) {
// now try to load from file
try {
configStream = new FileInputStream(name);
} catch (FileNotFoundException e) {
throw new ConfigurationException(String.format("Could not find configuration '%s'",
name));
}
}
// buffer input for performance - just in case config file is large
return new BufferedInputStream(configStream);
}
/**
* Utility method that checks that all configs can be loaded, parsed, and all option values
* set.
*
* @throws ConfigurationException if one or more configs failed to load
*/
void loadAndPrintAllConfigs() throws ConfigurationException {
loadAllConfigs(false);
boolean failed = false;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);
for (ConfigurationDef def : mConfigDefMap.values()) {
try {
def.createConfiguration().printCommandUsage(false,
new PrintStream(StreamUtil.nullOutputStream()));
} catch (ConfigurationException e) {
ps.printf("Failed to print %s: %s", def.getName(), e.getMessage());
ps.println();
failed = true;
}
}
if (failed) {
throw new ConfigurationException(baos.toString());
}
}
}