//Dstl (c) Crown Copyright 2017
package uk.gov.dstl.baleen.core.web.servlets;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.uima.fit.descriptor.ConfigurationParameter;
import org.apache.uima.fit.descriptor.ExternalResource;
import org.apache.uima.fit.factory.ExternalResourceFactory;
import org.slf4j.LoggerFactory;
import com.google.common.net.MediaType;
import uk.gov.dstl.baleen.core.utils.ReflectionUtils;
import uk.gov.dstl.baleen.exceptions.InvalidParameterException;
/**
* Abstract class for listing components based on their super type (for example, BaleenAnnotator).
* Returns a YAML formatted list of components, which could in theory be copied into a pipeline
* configuration. In practice though, the order may not be appropriate and some components will
* require configuration.
*
*
*/
public class AbstractComponentApiServlet extends AbstractApiServlet {
private static final long serialVersionUID = 1L;
private transient Optional<String> components = null;
private final Class<?> clazz;
private final String componentClass;
private final String componentPackage;
private final transient List<String> excludePackage;
private final transient List<String> excludeClass;
/**
* Constructor
*
* @param componentClass
* The name of the supertype for which we are listing subclasses
* @param componentPackage
* The default package name, which will be stripped from class names
* @param excludeClass
* A list of classes of which subclasses will be excluded from the listing
* @param excludePackage
* A list of regular expressions against which packages will be tested and, if
* matched, excluded from the listing
* @param clazz
* The subclass of AbstractComponentApiServlet, for creating a logger
*/
public AbstractComponentApiServlet(String componentClass, String componentPackage, List<String> excludeClass,
List<String> excludePackage, Class<?> clazz) {
super(LoggerFactory.getLogger(clazz), clazz);
this.componentClass = componentClass;
this.componentPackage = componentPackage;
this.excludeClass = excludeClass;
this.excludePackage = excludePackage;
this.clazz = clazz;
}
private void calculateComponents() {
Class<?> componentClazz = null;
try {
componentClazz = Class.forName(componentClass);
} catch (ClassNotFoundException e) {
LoggerFactory.getLogger(clazz).warn(
"Unable to find component class - annotator listing will not be available", e);
}
List<Class<?>> excludeClazz = new ArrayList<>();
for (String c : excludeClass) {
try {
Class<?> cl = Class.forName(c);
excludeClazz.add(cl);
} catch (ClassNotFoundException e) {
LoggerFactory.getLogger(clazz).warn("Unable to find component class - component will not be excluded",
e);
}
}
if (componentClazz == null) {
components = Optional.empty();
} else {
List<String> componentsList = classesToFilteredList(ReflectionUtils.getInstance().getSubTypesOf(componentClazz),
componentPackage, excludeClazz, excludePackage);
StringBuilder componentBuilder = new StringBuilder();
for (String s : componentsList) {
componentBuilder.append("- " + s + "\n");
}
components = Optional.of(componentBuilder.toString());
}
}
private List<String> classesToFilteredList(Set<?> components, String componentPackage, List<Class<?>> excludeClass,
List<String> excludePackage) {
List<String> ret = new ArrayList<>();
for (Object o : components) {
try {
Class<?> c = (Class<?>) o;
String s = c.getName();
String p = c.getPackage().getName();
if (excludeByPackage(p, excludePackage) || excludeByClass(c, excludeClass) || isAbstract(c)) {
continue;
}
if (s.startsWith(componentPackage)) {
s = s.substring(componentPackage.length());
s = StringUtils.strip(s, ".");
}
ret.add(s);
} catch (ClassCastException cce) {
LoggerFactory.getLogger(clazz).warn("Unable to cast to class", cce);
}
}
Collections.sort(ret);
return ret;
}
private static boolean isAbstract(Class<?> clazz) {
return Modifier.isAbstract(clazz.getModifiers());
}
/**
* Compare a package with a list of packages to determine whether the package should be excluded
* or not.
*
* @param pkg
* The package to test
* @param excludePkg
* A list of RegEx patterns that describe the set of packages to test against
* @return True if the package should be excluded, false otherwise
*/
public static boolean excludeByPackage(String pkg, List<String> excludePkg) {
for (String ep : excludePkg) {
if (pkg.matches(ep)) {
return true;
}
}
return false;
}
/**
* Compare a package with a list of classes to determine whether the class should be excluded or
* not. Classes in the list, or that inherit from a class in the list, will be excluded.
*
* @param clazz
* The class to test
* @param excludeClazz
* A list of classes to test against
* @return True if the class should be excluded, false otherwise
*/
public static boolean excludeByClass(Class<?> clazz, List<Class<?>> excludeClazz) {
for (Class<?> ec : excludeClazz) {
if (ec.isAssignableFrom(clazz)) {
return true;
}
}
return false;
}
@Override
protected void get(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String path = req.getPathInfo();
if (path == null) {
path = "";
} else if (path.startsWith("/")) {
path = path.substring(1);
}
if (path.isEmpty()) {
if (!getComponents().isPresent()) {
respondWithError(resp, 503, "Unable to load annotator class");
return;
}
respond(resp, MediaType.create("text", "x-yaml"), getComponents().get());
} else {
try {
Class<?> component = getClassFromString(path, componentPackage);
List<Map<String, Object>> parameters = getParameters(component);
respondWithJson(resp, parameters);
} catch (InvalidParameterException ipe) {
LoggerFactory.getLogger(clazz).warn("Could not find requested resource", ipe);
respondWithNotFound(resp);
}
}
}
/**
* Takes a string of the class name and return a Class. First tries looking in the default
* packages, and then if not found it will assume the class is fully qualified and try to use
* the name as it is provided
*
* @param className
* The name of the class
* @param type
* The type that the class should extend
* @param defaultPackage
* The package to look in if the className isn't a fully qualified name
* @return The class specified
*/
@SuppressWarnings("unchecked")
protected <S extends T, T> Class<S> getClassFromString(String className, String... defaultPackage)
throws InvalidParameterException {
for (String pkg : defaultPackage) {
try {
return (Class<S>) Class.forName(pkg + "." + className);
} catch (Exception e) {
LoggerFactory.getLogger(clazz).debug("Couldn't find class {} in package {}", className, pkg, e);
}
}
try {
return (Class<S>) Class.forName(className);
} catch (Exception e) {
throw new InvalidParameterException("Could not find or instantiate analysis engine " + className, e);
}
}
/**
* Get all the parameters (fields annotated with @ConfigurationParameter) and resources (fields
* annotated with @ExternalResource) for the given class
*
* @param clazz
* @return A list containing maps which contains information about each parameter and resource
*/
protected static List<Map<String, Object>> getParameters(Class<?> clazz) {
List<Map<String, Object>> parametersOutput = new ArrayList<>();
Field[] fields = clazz.getDeclaredFields();
if (fields != null) {
for (Field field : fields) {
parametersOutput.addAll(processParameters(field));
parametersOutput.addAll(processResources(field));
}
}
if (clazz.getSuperclass() != null) {
parametersOutput.addAll(getParameters(clazz.getSuperclass()));
}
return parametersOutput;
}
private static List<Map<String, Object>> processParameters(Field field) {
List<Map<String, Object>> parametersOutput = new ArrayList<>();
ConfigurationParameter[] parameters = field.getAnnotationsByType(ConfigurationParameter.class);
for (ConfigurationParameter param : parameters) {
if (ExternalResourceFactory.PARAM_RESOURCE_NAME.equals(param.name())) {
continue;
}
Map<String, Object> parameterOutput = new HashMap<>();
parameterOutput.put("name", param.name());
parameterOutput.put("defaultValue", stringArrayToString(param.defaultValue()));
parameterOutput.put("type", "parameter");
parametersOutput.add(parameterOutput);
}
return parametersOutput;
}
private static List<Map<String, Object>> processResources(Field field) {
List<Map<String, Object>> resourcesOutput = new ArrayList<>();
ExternalResource[] resources = field.getAnnotationsByType(ExternalResource.class);
for (ExternalResource resource : resources) {
Map<String, Object> resourceOutput = new HashMap<>();
resourceOutput.put("key", resource.key());
resourceOutput.put("class", field.getType().getName());
resourceOutput.put("type", "resource");
resourceOutput.put("parameters", getParameters(field.getType()));
resourcesOutput.add(resourceOutput);
}
return resourcesOutput;
}
protected static Object stringArrayToString(String[] arr) {
if (arr.length == 1) {
return arr[0];
} else {
return arr;
}
}
protected Optional<String> getComponents() {
if (components == null) {
calculateComponents();
}
return components;
}
}