//Dstl (c) Crown Copyright 2017
package uk.gov.dstl.baleen.core.utils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import com.google.common.base.Splitter;
import com.google.common.io.Files;
/**
* A helper to deal with YAML files and their basic map representation.
*
* Configuration can be accessed by either getting the whole YAML tree or by a
* path representation e.g.:
*
* "logging.enabled"
*
* This class also contains helper methods to get a type safe value from the YAML configuration.
*
*
*
*/
public class YamlConfiguration {
/**
* The separator between path elements.
*
*/
public static final char SEP = '.';
private static final Splitter PERIOD_SPLITTER = Splitter.on(SEP);
private static final Splitter NEWLINE_SPLITTER = Splitter.on(Pattern
.compile("\r?\n"));
private static final Logger LOGGER = LoggerFactory
.getLogger(YamlConfiguration.class);
private static final String TAB_AS_SPACES = " ";
private Map<String, Object> root;
/**
* New instance.
*
*/
public YamlConfiguration() {
//Empty constructor, do nothing
}
/**
* Read configuration from a string.
*
*
* @param file
* @return this instance for chaining
* @throws IOException
*/
@SuppressWarnings("unchecked")
public YamlConfiguration read(String string) throws IOException {
Yaml yaml = new Yaml();
root = (Map<String, Object>) yaml.load(cleanTabs(string));
return this;
}
/**
* Read configuration from a file.
*
*
* @param file
* @return this instance for chaining
* @throws IOException
*/
public YamlConfiguration read(File file) throws IOException {
// Read to string, so that we can check for tabs etc
String yamlString = Files.toString(file, StandardCharsets.UTF_8);
return read(yamlString);
}
/**
* Get the entire data tree.
*
* @return
*/
public Map<String, Object> getRoot() {
return root;
}
/**
* Get the path as a list of items.
*
* @param path
* @return list or empty list if doesn't exist
*/
@SuppressWarnings("unchecked")
public <T> List<T> getAsList(String path) {
Optional<Object> o = internalGet(path);
List<T> ret = Collections.emptyList();
if (o.isPresent() && o.get() instanceof List) {
try {
ret = (List<T>) o.get();
} catch (ClassCastException cce) {
LOGGER.warn("Requested path is not a list of the correct type",
cce);
}
} else {
LOGGER.debug("Path not found, or isn't an instanceof List - an empty list will be returned");
}
return ret;
}
/**
* Get the list of objects as a list of maps.
*
* @param path
* @return list of maps, empty if doesn't exist.
*/
public List<Map<String, Object>> getAsListOfMaps(String path) {
return getAsList(path);
}
/**
* Get a value as a path.
*
* Note type unsafe conversion (so may through a runtime exception)
*
* @param path
* @return
*/
public <T> Optional<T> get(String path) {
return Optional.ofNullable(get(path, null));
}
/**
* Get a value from a path, returning returning default value if missing.
*
* @param path
* @param defaultValue
* @return
*/
@SuppressWarnings("unchecked")
public <T> T get(String path, T defaultValue) {
Optional<Object> o = internalGet(path);
T ret = defaultValue;
if (o.isPresent()) {
try {
ret = (T) o.get();
} catch (ClassCastException cce) {
LOGGER.warn("Requested path cannot be cast to requested type",
cce);
}
}
return ret;
}
private Optional<Object> internalGet(String path) {
List<String> split = PERIOD_SPLITTER.splitToList(path);
Object current = root;
for (String p : split) {
if (current instanceof Map<?, ?>) {
current = ((Map<?, ?>) current).get(p);
} else {
return Optional.empty();
}
}
return Optional.ofNullable(current);
}
/**
* Read YAML from a file.
*
* @param file
* @return
* @throws IOException
*/
public static YamlConfiguration readFromFile(File file) throws IOException {
YamlConfiguration yc = new YamlConfiguration();
yc.read(file);
return yc;
}
/**
* Read from a resource on the classpath.
*
* @param clazz
* @param resourcePath
* @return
* @throws IOException
*/
public static YamlConfiguration readFromResource(Class<?> clazz,
String resourcePath) throws IOException {
URL url = clazz.getResource(resourcePath);
return readFromFile(new File(url.getFile()));
}
/**
* Strips any tabs which are at the beginning on
*
* @param string
* @return
*/
public static String cleanTabs(String yaml) {
if (yaml.contains("\t")) {
LOGGER.warn(
"Yaml contains a tab characters, automatically converting to {} spaces "
+ "(if they occur at the beginning of a sentence). This may cause parsing"
+ " errors, please reformat the Yaml to use spaces only.",
TAB_AS_SPACES.length());
List<String> lines = NEWLINE_SPLITTER.splitToList(yaml);
StringBuilder sb = new StringBuilder();
for (String line : lines) {
String cleanLine = replaceStartingTabsWithSpaces(line);
sb.append(cleanLine);
sb.append("\n");
}
return sb.toString();
} else {
return yaml;
}
}
private static String replaceStartingTabsWithSpaces(String line) {
int index = StringUtils.indexOfAnyBut(line, "\t ");
if (index == -1) {
return line;
} else {
String start = line.substring(0, index).replaceAll("\t", TAB_AS_SPACES);
String end = line.substring(index);
return start + end;
}
}
}