/*
* Copyright 2017 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
*
* 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 org.springframework.cloud.dataflow.registry;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.dataflow.core.ApplicationType;
import org.springframework.cloud.dataflow.registry.support.NoSuchAppRegistrationException;
import org.springframework.cloud.deployer.resource.registry.UriRegistry;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Convenience wrapper for the {@link UriRegistry} that operates on higher level
* {@link AppRegistration} objects and supports on-demand loading of {@link Resource}s.
* <p>
* <p>
* Stores AppRegistration with up to two keys:
* </p>
* <ul>
* <li>{@literal <type>.<name>}: URI for the actual app</li>
* <li>{@literal <type>.<name>.metadata}: Optional URI for the app metadata</li>
* </ul>
*
* @author Mark Fisher
* @author Gunnar Hillert
* @author Thomas Risberg
* @author Eric Bottard
*/
public class AppRegistry {
private static final Logger logger = LoggerFactory.getLogger(AppRegistry.class);
private static final String METADATA_KEY_SUFFIX = "metadata";
private final UriRegistry uriRegistry;
private final ResourceLoader resourceLoader;
public AppRegistry(UriRegistry uriRegistry, ResourceLoader resourceLoader) {
this.uriRegistry = uriRegistry;
this.resourceLoader = resourceLoader;
}
public AppRegistration find(String name, ApplicationType type) {
try {
String key = key(name, type);
URI uri = this.uriRegistry.find(key);
URI metadataUri = metadataUriFromRegistry().apply(key);
return new AppRegistration(name, type, uri, metadataUri, this.resourceLoader);
}
catch (IllegalArgumentException e) {
return null;
}
}
public List<AppRegistration> findAll() {
return this.uriRegistry.findAll().entrySet().stream().flatMap(toValidAppRegistration(metadataUriFromRegistry()))
.collect(Collectors.toList());
}
public AppRegistration save(String name, ApplicationType type, URI uri, URI metadataUri) {
this.uriRegistry.register(key(name, type), uri);
if (metadataUri != null) {
this.uriRegistry.register(metadataKey(name, type), metadataUri);
}
return new AppRegistration(name, type, uri, metadataUri, this.resourceLoader);
}
public List<AppRegistration> importAll(boolean overwrite, Resource... resources) {
Set<String> keysAlreadyThere = overwrite ? Collections.emptySet() : uriRegistry.findAll().keySet();
List<AppRegistration> apps = new ArrayList<>();
for (Resource resource : resources) {
Properties properties = new Properties();
try (InputStream is = resource.getInputStream()) {
properties.load(is);
properties.entrySet().stream().map(toStringAndUri())
.flatMap(toValidAppRegistration(metadataUriFromProperties(properties)))
.filter(ar -> !keysAlreadyThere.contains(key(ar.getName(), ar.getType())))
.collect(Collectors.toList()) // Force eager evaluation to fail
// early
.forEach(ar -> apps.add(save(ar.getName(), ar.getType(), ar.getUri(), ar.getMetadataUri())));
}
catch (IOException e) {
throw new RuntimeException("Error reading from " + resource.getDescription(), e);
}
}
return apps;
}
private Function<Map.Entry<Object, Object>, AbstractMap.SimpleImmutableEntry<String, URI>> toStringAndUri() {
return kv -> {
try {
return new AbstractMap.SimpleImmutableEntry<>((String) kv.getKey(), new URI((String) kv.getValue()));
}
catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
};
}
/**
* Returns a Function that either
* <ul>
* <li>turns a key/value mapping into a valid AppRegistration (1 element Stream),</li>
* <li>silently ignores well formed metadata entries (0 element Stream) or</li>
* <li>fails otherwise.</li>
* </ul>
*
* @param metadataUriExtractor a Function able to compute the (possibly null)
* metadataUri from a given app key
*/
private Function<Map.Entry<String, URI>, Stream<AppRegistration>> toValidAppRegistration(
Function<String, URI> metadataUriExtractor) {
return (Map.Entry<String, URI> kv) -> {
String key = kv.getKey();
String[] tokens = key.split("\\.");
if (tokens.length == 2) {
String name = tokens[1];
ApplicationType type = ApplicationType.valueOf(tokens[0]);
URI appURI = warnOnMalformedURI(key, kv.getValue());
URI metadataURI = metadataUriExtractor.apply(key);
return Stream.of(new AppRegistration(name, type, appURI, metadataURI, resourceLoader));
}
else {
Assert.isTrue(tokens.length == 3 && METADATA_KEY_SUFFIX.equals(tokens[2]),
"Invalid format for app key '" + key + "'in file. Must be <type>.<name> or <type>.<name>"
+ ".metadata");
return Stream.empty();
}
};
}
private Function<String, URI> metadataUriFromProperties(Properties properties) {
return key -> {
String metadataValue = properties.getProperty(metadataKey(key));
try {
return metadataValue != null ? warnOnMalformedURI(key, new URI(metadataValue)) : null;
}
catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
};
}
private Function<String, URI> metadataUriFromRegistry() {
return key -> {
try {
return uriRegistry.find(metadataKey(key));
}
catch (IllegalArgumentException ignored) {
return null;
}
};
}
/**
* Deletes an {@link AppRegistration}. If the {@link AppRegistration} does not exist,
* a {@link NoSuchAppRegistrationException} will be thrown.
*
* @param name Name of the AppRegistration to delete
* @param type Type of the AppRegistration to delete
*/
public void delete(String name, ApplicationType type) {
if (this.find(name, type) != null) {
this.uriRegistry.unregister(key(name, type));
this.uriRegistry.unregister(metadataKey(name, type));
}
else {
throw new NoSuchAppRegistrationException(name, type);
}
}
private String key(String name, ApplicationType type) {
return String.format("%s.%s", type, name);
}
private String metadataKey(String name, ApplicationType type) {
return String.format("%s.%s.%s", type, name, METADATA_KEY_SUFFIX);
}
private String metadataKey(String key) {
return key + "." + METADATA_KEY_SUFFIX;
}
private URI warnOnMalformedURI(String key, URI uri) {
if (StringUtils.isEmpty(uri)) {
logger.warn(String.format("Error when registering '%s': URI is required", key));
}
else if (!StringUtils.hasText(uri.getScheme())) {
logger.warn(
String.format("Error when registering '%s' with URI %s: URI scheme must be specified", key, uri));
}
else if (!StringUtils.hasText(uri.getSchemeSpecificPart())) {
logger.warn(String.format(
"Error when registering '%s' with URI %s: URI scheme-specific part must be " + "specified", key,
uri));
}
return uri;
}
}