/**
* Copyright 2016 StreamSets Inc.
*
* Licensed under the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.streamsets.datacollector.util;
import com.fasterxml.jackson.annotation.JsonValue;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.streamsets.datacollector.vault.Vault;
import com.streamsets.datacollector.vault.VaultRuntimeException;
import com.streamsets.pipeline.api.ext.DataCollectorServices;
import com.streamsets.pipeline.api.impl.Utils;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
public class Configuration {
private static final String VAULT_SERVICE_KEY = "com.streamsets.datacollector.vault";
private static File fileRefsBaseDir;
// Only RuntimeModules should be calling this
public static void setFileRefsBaseDir(File dir) {
fileRefsBaseDir = dir;
}
private abstract static class Ref {
private String unresolvedValue;
protected Ref(String unresolvedValue) {
this.unresolvedValue = unresolvedValue;
}
public abstract String getPrefix();
public abstract String getSuffix();
@Deprecated
public abstract String getDelimiter();
protected static boolean isValueMyRef(String prefix, String suffix, String value) {
String trimmed = value.trim();
return trimmed.startsWith(prefix) && trimmed.endsWith(suffix);
}
@Deprecated
protected static boolean isValueMyRef(String tokenDelimiter, String value) {
String trimmed = value.trim();
return trimmed.startsWith(tokenDelimiter) && trimmed.endsWith(tokenDelimiter);
}
public String getUnresolvedValue() {
return unresolvedValue;
}
public static String getUnresolvedValueWithoutDelimiter(
String unresolvedValue, String prefix, String suffix, String delimiter
) {
String unquoted = unresolvedValue.replace("\"", "").replace("'", "");
if (isValueMyRef(prefix, suffix, unresolvedValue)) {
return unquoted.substring(prefix.length(), unquoted.length() - suffix.length());
}
return unquoted.substring(delimiter.length(), unquoted.length() - delimiter.length());
}
protected String getUnresolvedValueWithoutDelimiter() {
return getUnresolvedValueWithoutDelimiter(unresolvedValue, getPrefix(), getSuffix(), getDelimiter());
}
public abstract String getValue();
@Override
public String toString() {
return Utils.format("{}='{}'", getClass().getSimpleName(), unresolvedValue);
}
}
private static class StringRef extends Ref {
protected StringRef(String unresolvedValue) {
super(unresolvedValue);
}
@Override
public String getPrefix() {
return "";
}
@Override
public String getSuffix() {
return "";
}
@Override
public String getDelimiter() {
return "";
}
@Override
public String getValue() {
return getUnresolvedValue();
}
}
public static class FileRef extends Ref {
@Deprecated
public static final String DELIMITER = "@";
public static final String PREFIX = "${file(";
public static final String SUFFIX = ")}";
public FileRef(String unresolvedValue) {
super(unresolvedValue);
Preconditions.checkState(fileRefsBaseDir != null, "fileRefsBaseDir has not been set");
}
public static boolean isValueMyRef(String value) {
return isValueMyRef(PREFIX, SUFFIX, value) || isValueMyRef(DELIMITER, value);
}
@Override
public String getPrefix() {
return PREFIX;
}
@Override
public String getSuffix() {
return SUFFIX;
}
@Override
public String getDelimiter() {
return DELIMITER;
}
@Override
public String getValue() {
StringBuilder sb = new StringBuilder();
File configFile;
String configFileName = getUnresolvedValueWithoutDelimiter();
if (Paths.get(configFileName).isAbsolute()) {
configFile = new File(configFileName);
} else {
configFile = new File(fileRefsBaseDir, configFileName);
}
try (Reader reader = new FileReader(configFile)) {
int c = reader.read();
while (c > -1) {
sb.append((char) c);
c = reader.read();
}
return sb.toString();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
public static class VaultRef extends Ref {
private static final String PREFIX = "${vault(";
private static final String SUFFIX = ")}";
protected VaultRef(String unresolvedValue) {
super(unresolvedValue);
}
@Override
public String getPrefix() {
return PREFIX;
}
@Override
public String getSuffix() {
return SUFFIX;
}
@Override
public String getDelimiter() {
throw new UnsupportedOperationException();
}
public static boolean isValueMyRef(String value) {
String trimmed = value.trim();
return trimmed.startsWith(PREFIX) && trimmed.endsWith(SUFFIX);
}
@Override
protected String getUnresolvedValueWithoutDelimiter() {
return getUnresolvedValue().substring(PREFIX.length(), getUnresolvedValue().length() - SUFFIX.length());
}
@Override
public String getValue() {
String[] params = getUnresolvedValueWithoutDelimiter().replace("\"", "").replace("'", "").split(",");
if (params.length != 2) {
throw new VaultRuntimeException(getUnresolvedValue() + " does not comply with format vault(path, key)");
}
Vault vault = DataCollectorServices.instance().get(VAULT_SERVICE_KEY);
final String path = params[0].trim();
final String key = params[1].trim();
return vault.read(path, key);
}
}
private static class EnvRef extends Ref {
@Deprecated
private static final String DELIMITER = "$";
private static final String PREFIX = "${env(";
private static final String SUFFIX = ")}";
protected EnvRef(String unresolvedValue) {
super(unresolvedValue);
}
public static boolean isValueMyRef(String value) {
return isValueMyRef(PREFIX, SUFFIX, value) || isValueMyRef(DELIMITER, value);
}
@Override
public String getPrefix() {
return PREFIX;
}
@Override
public String getSuffix() { return SUFFIX; }
@Override
public String getDelimiter() {
return DELIMITER;
}
@Override
public String getValue() {
return System.getenv(getUnresolvedValueWithoutDelimiter());
}
}
private static Ref createRef(String value) {
Ref ref;
if (FileRef.isValueMyRef(value)) {
ref = new FileRef(value);
} else if (EnvRef.isValueMyRef(value)) {
ref = new EnvRef(value);
} else if (VaultRef.isValueMyRef(value)) {
ref = new VaultRef(value);
} else {
ref = new StringRef(value);
}
return ref;
}
private Map<String, Ref> map;
public Configuration() {
this(new LinkedHashMap<String, Ref>());
}
private Configuration(Map<String, Ref> map) {
this.map = map;
}
public Configuration getUnresolvedConfiguration() {
Map<String, Ref> subSetMap = new LinkedHashMap<>();
for (Map.Entry<String, Ref> entry : map.entrySet()) {
subSetMap.put(entry.getKey(), new StringRef(entry.getValue().getUnresolvedValue()));
}
return new Configuration(subSetMap);
}
public Configuration getSubSetConfiguration(String namePrefix) {
Preconditions.checkNotNull(namePrefix, "namePrefix cannot be null");
Map<String, Ref> subSetMap = new LinkedHashMap<>();
for (Map.Entry<String, Ref> entry : map.entrySet()) {
if (entry.getKey().startsWith(namePrefix)) {
subSetMap.put(entry.getKey(), entry.getValue());
}
}
return new Configuration(subSetMap);
}
@JsonValue
public Map<String, String> getValues() {
Map<String, String> values = new LinkedHashMap<>();
for (Map.Entry<String, Ref> entry : map.entrySet()) {
values.put(entry.getKey(), entry.getValue().getValue());
}
return values;
}
public Set<String> getNames() {
return new HashSet<>(map.keySet());
}
public boolean hasName(String name) {
Preconditions.checkNotNull(name, "name cannot be null");
return map.containsKey(name);
}
public void set(String name, String value) {
Preconditions.checkNotNull(name, "name cannot be null");
Preconditions.checkNotNull(value, "value cannot be null, use unset");
map.put(name, createRef(value));
}
public void unset(String name) {
Preconditions.checkNotNull(name, "name cannot be null");
map.remove(name);
}
public void set(String name, int value) {
set(name, Integer.toString(value));
}
public void set(String name, long value) {
set(name, Long.toString(value));
}
public void set(String name, boolean value) {
set(name, Boolean.toString(value));
}
private String get(String name) {
Preconditions.checkNotNull(name, "name cannot be null");
return map.containsKey(name) ? map.get(name).getValue() : null;
}
public String get(String name, String defaultValue) {
String value = get(name);
return (value != null) ? value : defaultValue;
}
public long get(String name, long defaultValue) {
String value = get(name);
return (value != null) ? Long.parseLong(value) : defaultValue;
}
public int get(String name, int defaultValue) {
String value = get(name);
return (value != null) ? Integer.parseInt(value) : defaultValue;
}
public boolean get(String name, boolean defaultValue) {
String value = get(name);
return (value != null) ? Boolean.parseBoolean(value) : defaultValue;
}
public void load(Reader reader) throws IOException {
Preconditions.checkNotNull(reader, "reader cannot be null");
Properties props = new Properties();
props.load(reader);
for (Map.Entry entry : props.entrySet()) {
map.put((String) entry.getKey(), createRef((String) entry.getValue()));
}
loadConfigIncludes();
reader.close();
}
public static final String CONFIG_INCLUDES = "config.includes";
void loadConfigIncludes() {
String includes = get(CONFIG_INCLUDES, null);
if (includes != null) {
map.remove(CONFIG_INCLUDES);
for (String include : Splitter.on(",").trimResults().omitEmptyStrings().split(includes)) {
File file = new File(fileRefsBaseDir, include);
try (Reader reader = new FileReader(file)) {
Configuration conf = new Configuration();
conf.load(reader);
conf.map.remove(CONFIG_INCLUDES);
for (Map.Entry<String, Ref> entry : conf.map.entrySet()) {
map.put(entry.getKey(), entry.getValue());
}
} catch (IOException ex) {
throw new IllegalArgumentException(Utils.format("Include config file '{}' could not be read: {}",
file.getAbsolutePath(),
ex.toString()
));
}
}
}
}
public void save(Writer writer) throws IOException {
Preconditions.checkNotNull(writer, "writer cannot be null");
Properties props = new Properties();
for (Map.Entry<String, Ref> entry : map.entrySet()) {
props.setProperty(entry.getKey(), entry.getValue().getUnresolvedValue());
}
props.store(writer, "");
writer.close();
}
@Override
public String toString() {
return Utils.format("Configuration['{}']", map);
}
}