io.cucumber.core.plugin.PluginFactory Maven / Gradle / Ivy
package io.cucumber.core.plugin;
import io.cucumber.core.exception.CucumberException;
import io.cucumber.core.logging.Logger;
import io.cucumber.core.logging.LoggerFactory;
import io.cucumber.core.options.CurlOption;
import io.cucumber.plugin.Plugin;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
/**
* This class creates plugin instances from a String.
*
* The String is of the form name[:output] where name is either a fully
* qualified class name or one of the built-in short names. The output is
* optional for some plugins (and mandatory for some).
*
* @see Plugin for specific requirements
*/
public final class PluginFactory {
private static final Logger log = LoggerFactory.getLogger(PluginFactory.class);
private final Class[] CTOR_PARAMETERS = new Class[] {
String.class,
File.class,
URI.class,
URL.class,
OutputStream.class,
// Deprecated
Appendable.class
};
private String pluginUsingDefaultOut = null;
private PrintStream defaultOut = new PrintStream(System.out) {
@Override
public void close() {
// We have no intention to close System.out
}
};
Plugin create(Options.Plugin plugin) {
try {
return instantiate(plugin.pluginString(), plugin.pluginClass(), plugin.argument());
} catch (IOException | URISyntaxException e) {
throw new CucumberException(e);
}
}
private T instantiate(String pluginString, Class pluginClass, String argument)
throws IOException, URISyntaxException {
Map, Constructor> singleArgConstructors = findSingleArgConstructors(pluginClass);
if (argument == null) {// No argument passed
Constructor outputStreamConstructor = singleArgConstructors.get(OutputStream.class);
if (outputStreamConstructor != null) {
return newInstance(outputStreamConstructor, defaultOutOrFailIfAlreadyUsed(pluginString));
}
Constructor emptyConstructor = findEmptyConstructor(pluginClass);
if (emptyConstructor != null) {
return newInstance(emptyConstructor);
}
if (!singleArgConstructors.isEmpty()) {
throw new CucumberException(String.format(
"You must supply an output argument to %s. Like so: %s:DIR|FILE|URL", pluginString, pluginString));
}
throw new CucumberException(String.format(
"%s must have at least one empty constructor or a constructor that declares a single parameter of one of: %s",
pluginClass, asList(CTOR_PARAMETERS)));
}
if (singleArgConstructors.size() != 1) {
throw new CucumberException(
String.format("%s must have exactly one constructor that declares a single parameter of one of: %s",
pluginClass, asList(CTOR_PARAMETERS)));
}
Map.Entry, Constructor> singleArgConstructorEntry = singleArgConstructors.entrySet().iterator()
.next();
Class parameterType = singleArgConstructorEntry.getKey();
Constructor singleArgConstructor = singleArgConstructorEntry.getValue();
return newInstance(singleArgConstructor, convert(argument, parameterType, pluginString, pluginClass));
}
private Map, Constructor> findSingleArgConstructors(Class pluginClass) {
Map, Constructor> result = new HashMap<>();
for (Class ctorArgClass : CTOR_PARAMETERS) {
try {
result.put(ctorArgClass, pluginClass.getConstructor(ctorArgClass));
} catch (NoSuchMethodException ignore) {
}
}
return result;
}
private T newInstance(Constructor constructor, Object... ctorArgs) {
try {
return constructor.newInstance(ctorArgs);
} catch (InstantiationException | IllegalAccessException e) {
throw new CucumberException(e);
} catch (InvocationTargetException e) {
throw new CucumberException(e.getTargetException());
}
}
private PrintStream defaultOutOrFailIfAlreadyUsed(String pluginString) {
try {
if (defaultOut != null) {
pluginUsingDefaultOut = pluginString;
return defaultOut;
} else {
throw new CucumberException("Only one plugin can use STDOUT, now both " +
pluginUsingDefaultOut + " and " + pluginString + " use it. " +
"If you use more than one plugin you must specify output path with " + pluginString
+ ":DIR|FILE|URL");
}
} finally {
defaultOut = null;
}
}
private Constructor findEmptyConstructor(Class pluginClass) {
try {
return pluginClass.getConstructor();
} catch (NoSuchMethodException ignore) {
return null;
}
}
private Object convert(String arg, Class ctorArgClass, String pluginString, Class pluginClass)
throws IOException, URISyntaxException {
if (ctorArgClass.equals(URI.class)) {
return makeURL(arg).toURI();
}
if (ctorArgClass.equals(URL.class)) {
return makeURL(arg);
}
if (ctorArgClass.equals(File.class)) {
return new File(arg);
}
if (ctorArgClass.equals(String.class)) {
return arg;
}
if (ctorArgClass.equals(OutputStream.class)) {
if (arg == null) {
return defaultOutOrFailIfAlreadyUsed(pluginString);
} else {
return openStream(arg);
}
}
if (ctorArgClass.equals(Appendable.class)) {
String recommendedParameters = Arrays.stream(CTOR_PARAMETERS)
.filter(c -> c != Appendable.class)
.map(Class::getName)
.collect(Collectors.joining(", "));
log.error(() -> String.format(
"The %s plugin class takes a java.lang.Appendable in its constructor, which is deprecated and will be removed in the next major release. It should be changed to accept one of %s",
pluginClass.getName(), recommendedParameters));
return new UTF8OutputStreamWriter(openStream(arg));
}
throw new CucumberException(
String.format("Cannot convert %s into a %s to pass to the %s plugin", arg, ctorArgClass, pluginString));
}
private static URL makeURL(String arg) throws MalformedURLException {
if (arg.matches("^(file|http|https):.*")) {
return new URL(arg);
} else {
return new URL("file:" + arg);
}
}
private static OutputStream openStream(String arg) throws IOException, URISyntaxException {
if (arg.matches("^(http|https):.*")) {
CurlOption option = CurlOption.parse(arg);
return new UrlOutputStream(option);
} else if (arg.matches("^file:.*")) {
return createFileOutputStream(new File(new URL(arg).getFile()));
} else {
return createFileOutputStream(new File(arg));
}
}
private static FileOutputStream createFileOutputStream(File file) {
try {
File parentFile = file.getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
}
return new FileOutputStream(file);
} catch (FileNotFoundException e) {
throw new IllegalArgumentException(String.format("" +
"Couldn't create a file output stream for %s.\n" +
"Make sure the the file isn't a directory.\n" +
"The details are in the stack trace below:",
file),
e);
}
}
}