All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.nosan.embedded.cassandra.DefaultCassandraDatabaseFactory Maven / Gradle / Ivy

There is a newer version: 5.0.0
Show newest version
/*
 * Copyright 2020-2021 the original author or authors.
 *
 * 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
 *
 *      https://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.github.nosan.embedded.cassandra;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;

import com.github.nosan.embedded.cassandra.commons.FileSystemResource;
import com.github.nosan.embedded.cassandra.commons.Resource;
import com.github.nosan.embedded.cassandra.commons.StringUtils;
import com.github.nosan.embedded.cassandra.commons.UrlResource;

class DefaultCassandraDatabaseFactory implements CassandraDatabaseFactory {

	private final String name;

	private final Version version;

	private final Map environmentVariables;

	private final Map configProperties;

	private final Map systemProperties;

	private final Set jvmOptions;

	DefaultCassandraDatabaseFactory(String name, Version version, Map environmentVariables,
			Map configProperties, Map systemProperties, Set jvmOptions) {
		this.name = name;
		this.version = version;
		this.environmentVariables = Collections.unmodifiableMap(environmentVariables);
		this.configProperties = Collections.unmodifiableMap(configProperties);
		this.systemProperties = Collections.unmodifiableMap(systemProperties);
		this.jvmOptions = Collections.unmodifiableSet(jvmOptions);
	}

	@Override
	public CassandraDatabase create(Path workingDirectory) throws Exception {
		Version version = this.version;
		Resource configFile = getConfigFile(workingDirectory, this.systemProperties.get("cassandra.config"));
		Map systemProperties = new LinkedHashMap<>();
		for (Map.Entry entry : this.systemProperties.entrySet()) {
			systemProperties.put(entry.getKey(), Objects.toString(getValue(entry.getValue()), ""));
		}
		configureSystemProperties(systemProperties);
		Map configProperties = loadProperties(configFile);
		setProperties(null, this.configProperties, configProperties);
		configureConfigProperties(configProperties);
		//Since Cassandra 4.X.X has a format IP:PORT, 0 port must be replaced with the storage_port.
		if (version.getMajor() >= 4) {
			configureSeeds(configProperties, systemProperties);
		}
		Path newConfigFile = Files.createTempFile(workingDirectory.resolve("conf"), "",
				"-" + configFile.getFileName().orElse("cassandra.yaml"));
		writeProperties(configProperties, newConfigFile);
		systemProperties.put("cassandra.config", newConfigFile.toUri().toString());
		List jvmExtraOpts = new ArrayList<>(this.jvmOptions);
		systemProperties.forEach((name, value) -> {
			if (value.equals("")) {
				jvmExtraOpts.add("-D" + name);
			}
			else {
				jvmExtraOpts.add("-D" + name + "=" + value);
			}
		});
		Map environmentVariables = new LinkedHashMap<>();
		for (Map.Entry entry : this.environmentVariables.entrySet()) {
			environmentVariables.put(entry.getKey(), Objects.toString(entry.getValue(), ""));
		}
		environmentVariables.merge("JVM_EXTRA_OPTS", String.join(" ", jvmExtraOpts), (s1, s2) -> s1 + " " + s2);
		if (!environmentVariables.containsKey("JAVA_HOME")) {
			Optional.ofNullable(System.getProperty("java.home"))
					.filter(StringUtils::hasText).ifPresent(path -> environmentVariables.put("JAVA_HOME", path));
		}
		Files.createDirectories(workingDirectory.resolve("logs"));
		Files.createDirectories(workingDirectory.resolve("data"));
		if (isWindows()) {
			Path pidFile = Files.createTempFile(workingDirectory.resolve("bin"), "", "-cassandra.pid");
			return new WindowsCassandraDatabase(this.name, version, newConfigFile, workingDirectory,
					environmentVariables, configProperties, systemProperties, this.jvmOptions, pidFile);
		}
		return new UnixCassandraDatabase(this.name, version, newConfigFile, workingDirectory, environmentVariables,
				configProperties, systemProperties, this.jvmOptions);
	}

	private static boolean isWindows() {
		String os = System.getProperty("os.name");
		return os.toLowerCase(Locale.ENGLISH).startsWith("windows");
	}

	private static void writeProperties(Map properties, Path configFile) throws IOException {
		try (Writer writer = Files.newBufferedWriter(configFile)) {
			DumperOptions dumperOptions = new DumperOptions();
			dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
			new Yaml(dumperOptions).dump(properties, writer);
		}
	}

	@SuppressWarnings("unchecked")
	private static Map loadProperties(Resource file) throws IOException {
		try (InputStream is = file.getInputStream()) {
			Map properties = new Yaml().loadAs(is, Map.class);
			if (properties == null) {
				return new LinkedHashMap<>(0);
			}
			return properties;
		}
	}

	private static Resource getConfigFile(Path workingDirectory, Object url) throws MalformedURLException {
		if (url instanceof Resource) {
			return ((Resource) url);
		}
		if (url instanceof Path) {
			return new FileSystemResource(((Path) url).toAbsolutePath());
		}
		if (url instanceof File) {
			return new FileSystemResource(((File) url));
		}
		if (url instanceof URL) {
			return new UrlResource(((URL) url));
		}
		if (url instanceof URI) {
			return new UrlResource(((URI) url).toURL());
		}
		if (url != null) {
			return new UrlResource(new URL(url.toString()));
		}
		return new FileSystemResource(workingDirectory.resolve("conf/cassandra.yaml"));
	}

	private static void configureSystemProperties(Map systemProperties) throws IOException {
		setPort("cassandra.native_transport_port", systemProperties);
		setPort("cassandra.storage_port", systemProperties);
		setPort("cassandra.ssl_storage_port", systemProperties);
		setPort("cassandra.rpc_port", systemProperties);
		setPort("cassandra.jmx.remote.port", systemProperties);
		setPort("cassandra.jmx.local.port", systemProperties);
		setPort("com.sun.management.jmxremote.rmi.port", systemProperties);
	}

	private static void configureConfigProperties(Map configProperties) throws IOException {
		setPort("native_transport_port", configProperties);
		setPort("storage_port", configProperties);
		setPort("ssl_storage_port", configProperties);
		setPort("rpc_port", configProperties);
		setPort("native_transport_port_ssl", configProperties);
	}

	private static void setPort(String name, Map target) throws IOException {
		if (Objects.toString(target.get(name), "").equals("0")) {
			try (ServerSocket ss = new ServerSocket(0)) {
				target.put(name, Integer.toString(ss.getLocalPort()));
			}
		}
	}

	private static void setProperties(String parentName, Map source, Map target)
			throws IOException {
		for (Map.Entry entry : source.entrySet()) {
			String fullName = (parentName != null) ? parentName + "." + entry.getKey() : entry.getKey();
			setProperty(fullName, parentName, entry.getKey(), getValue(entry.getValue()), target);
		}
	}

	@SuppressWarnings("unchecked")
	private static void setProperty(String fullName, String parentName, String name, Object value,
			Map target) throws IOException {
		int index = -1;
		while ((index = name.indexOf('.', index + 1)) != -1) {
			int backslashes = 0;
			for (int i = index - 1; i >= 0; i--) {
				if (name.charAt(i) == '\\') {
					backslashes++;
				}
				else {
					break;
				}
			}
			String n = name.substring(index - backslashes, index).replace("\\\\", "\\");
			name = name.substring(0, index - backslashes) + n + name.substring(index);
			index -= (backslashes / 2);
			if (backslashes % 2 != 0) {
				name = name.substring(0, index - 1) + name.substring(index);
				index--;
				continue;
			}
			break;
		}
		if (index != -1) {
			String rootName = name.substring(0, index);
			if (rootName.isEmpty()) {
				throw new IllegalArgumentException(String.format("Config property: '%s' is invalid", name));
			}
			if (target.get(rootName) instanceof Map) {
				Map rootValue = (Map) target.get(rootName);
				target.put(rootName, rootValue);
				setProperty(fullName, rootName, name.substring(index + 1), value, rootValue);
			}
			else if (target.get(rootName) == null) {
				Map rootValueMap = new LinkedHashMap<>();
				target.put(rootName, rootValueMap);
				setProperty(fullName, rootName, name.substring(index + 1), value, rootValueMap);
			}
			else {
				throw new IllegalArgumentException(String.format("Config property: '%s: %s'"
								+ " cannot be set. Property: '%s.%s' has a type: '%s'"
								+ " and it cannot have nested properties.", fullName, value, parentName, rootName,
						target.get(rootName).getClass().getCanonicalName()));
			}
			return;
		}
		if (name.isEmpty()) {
			throw new IllegalArgumentException("Config property must not be empty");
		}
		if (target.get(name) instanceof Map && value instanceof Map) {
			Map rootValue = (Map) target.get(name);
			target.put(name, rootValue);
			setProperties(name, ((Map) value), rootValue);
		}
		else {
			target.put(name, value);
		}
	}

	private static void configureSeeds(Map configProperties, Map systemProperties) {
		String storagePort = Optional.ofNullable(systemProperties.get("cassandra.storage_port"))
				.orElseGet(() -> Objects.toString(configProperties.get("storage_port"), "7000"));
		for (Map seedProvider : getSeedProvider(configProperties)) {
			for (Map parameter : getParameters(seedProvider)) {
				List seeds = getSeeds(parameter);
				if (!seeds.isEmpty()) {
					seeds.replaceAll(seed -> {
						int index = seed.indexOf(':');
						//IPV6, [IP]:port
						if (index != -1 && seed.indexOf(':', index + 1) != -1) {
							return seed.replaceAll("]:0\\b(\\s*)$", String.format("]:%s$1", storagePort));
						}
						//IPV4, IP:port
						if (index != -1) {
							return seed.replaceAll(":0\\b(\\s*)$", String.format(":%s$1", storagePort));
						}
						return seed;
					});
					parameter.put("seeds", String.join(",", seeds));
				}
			}
		}
	}

	@SuppressWarnings("unchecked")
	private static List> getSeedProvider(Map configProperties) {
		List> seedProvider = (List>) configProperties.get("seed_provider");
		if (seedProvider == null) {
			return Collections.emptyList();
		}
		return seedProvider;
	}

	@SuppressWarnings("unchecked")
	private static List> getParameters(Map seedProvider) {
		List> parameters = (List>) seedProvider.get("parameters");
		if (parameters == null) {
			return Collections.emptyList();
		}
		return parameters;
	}

	private static List getSeeds(Map parameter) {
		return Optional.ofNullable(parameter)
				.map(p -> p.get("seeds"))
				.map(String::valueOf)
				.map(seeds -> Arrays.stream(seeds.split(",")).collect(Collectors.toList()))
				.orElse(Collections.emptyList());
	}

	private static Object getValue(Object object) throws IOException {
		if (object instanceof Path) {
			return ((Path) object).normalize().toAbsolutePath().toString();
		}
		if (object instanceof File) {
			return ((File) object).toPath().normalize().toAbsolutePath().toString();
		}
		if (object instanceof URI) {
			return object.toString();
		}
		if (object instanceof URL) {
			return object.toString();
		}
		if (object instanceof Resource) {
			try {
				return getValue(Paths.get(((Resource) object).toURI()));
			}
			catch (Exception ex) {
				//ignore
			}
			try {
				return getValue(((Resource) object).toURI());
			}
			catch (IOException ex) {
				//ignore
			}
			return getValue(((Resource) object).toURL());
		}
		if (object instanceof InetAddress) {
			return ((InetAddress) object).getHostName();
		}
		if (object instanceof Map) {
			Map result = new LinkedHashMap<>();
			for (Map.Entry entry : ((Map) object).entrySet()) {
				result.put(entry.getKey(), getValue(entry.getValue()));
			}
			return result;
		}
		if (object instanceof Collection) {
			List result = new ArrayList<>();
			for (Object o : ((Collection) object)) {
				result.add(getValue(o));
			}
			return result;
		}
		if (object instanceof Object[]) {
			List result = new ArrayList<>();
			for (Object o : ((Object[]) object)) {
				result.add(getValue(o));
			}
			return result;

		}
		return object;
	}

}