Implement VaultEnvironmentRepository.

This is an alternative to VCS or file based repository implementations.

All properties are stored ecrypted in vault, regardless of whether or
not that actaully need to be encrypted.

Supports properties style (this.is.a.property) or vault nested
properties.

Adds a watch to client and server, if enabled, the client will long poll
the server and wait for changes rather than relying on a message broker
to send change events.

Adds a server side watch for consul keys, this is useful when vault is
backed by consul (and currently the only use case, since there is not a
consul repository implementation).

Closes gh-397
This commit is contained in:
Spencer Gibb
2016-05-04 17:00:31 -06:00
parent 0850de9737
commit 753db0ed22
32 changed files with 742 additions and 692 deletions

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>

View File

@@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -80,4 +81,16 @@ public class ConfigClientAutoConfiguration {
}
}
@Configuration
@ConditionalOnClass(ContextRefresher.class)
@ConditionalOnBean(ContextRefresher.class)
@ConditionalOnProperty(value = "spring.cloud.config.watch.enabled")
protected static class ConfigClientWatchConfiguration {
@Bean
public ConfigClientWatch configClientWatch(ContextRefresher contextRefresher) {
return new ConfigClientWatch(contextRefresher);
}
}
}

View File

@@ -34,6 +34,8 @@ import org.springframework.web.util.UriComponentsBuilder;
public class ConfigClientProperties {
public static final String PREFIX = "spring.cloud.config";
public static final String TOKEN_HEADER = "X-Config-Token";
public static final String STATE_HEADER = "X-Config-State";
/**
* Flag to say that remote configuration is enabled. Default true;
@@ -83,6 +85,11 @@ public class ConfigClientProperties {
*/
private boolean failFast = false;
/**
* Security Token passed thru to underlying environment repository.
*/
private String token;
private ConfigClientProperties() {
}
@@ -170,6 +177,14 @@ public class ConfigClientProperties {
this.failFast = failFast;
}
public String getToken() {
return this.token;
}
public void setToken(String token) {
this.token = token;
}
private String[] extractCredentials() {
String[] result = new String[3];
String uri = this.uri;
@@ -272,7 +287,7 @@ public class ConfigClientProperties {
+ (this.label == null ? "" : this.label) + ", username=" + this.username
+ ", password=" + this.password + ", uri=" + this.uri
+ ", discovery.enabled=" + this.discovery.enabled + ", failFast="
+ this.failFast + "]";
+ this.failFast + ", token=" + this.token + "]";
}
}

View File

@@ -0,0 +1,24 @@
package org.springframework.cloud.config.client;
/**
* @author Spencer Gibb
*/
public class ConfigClientStateHolder {
private static ThreadLocal<String> state = new ThreadLocal<>();
public static void resetState() {
state.remove();
}
public static void setState(String newState) {
if (newState == null) {
resetState();
return;
}
state.set(newState);
}
public static String getState() {
return state.get();
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2013-2016 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.config.client;
import java.io.Closeable;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.Scheduled;
import static org.springframework.util.StringUtils.hasText;
/**
* @author Spencer Gibb
*/
public class ConfigClientWatch implements Closeable, EnvironmentAware {
private static Log log = LogFactory
.getLog(ConfigServicePropertySourceLocator.class);
private final AtomicBoolean running = new AtomicBoolean(false);
private final ContextRefresher refresher;
private Environment environment;
public ConfigClientWatch(ContextRefresher refresher) {
this.refresher = refresher;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@PostConstruct
public void start() {
this.running.compareAndSet(false, true);
}
@Scheduled(initialDelayString = "${spring.cloud.config.watch.initialDelay:180000}", fixedDelayString = "${spring.cloud.config.watch.delay:500}")
public void watchConfigServer() {
if (this.running.get()) {
String newState = this.environment.getProperty("config.client.state");
String oldState = ConfigClientStateHolder.getState();
// only refresh if state has changed
if (stateChanged(oldState, newState)) {
ConfigClientStateHolder.setState(newState);
this.refresher.refresh();
}
}
}
/* for testing */ boolean stateChanged(String oldState, String newState) {
return (!hasText(oldState) && hasText(newState))
|| (hasText(oldState) && !oldState.equals(newState));
}
@Override
public void close() {
this.running.compareAndSet(true, false);
}
}

View File

@@ -51,9 +51,9 @@ public class ConfigServiceBootstrapConfiguration {
@Bean
@ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
public ConfigServicePropertySourceLocator configServicePropertySource() {
public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {
ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
configClientProperties());
properties);
return locator;
}

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.config.client;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
@@ -29,6 +30,7 @@ import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
@@ -37,12 +39,17 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.retry.annotation.Retryable;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER;
import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER;
/**
* @author Dave Syer
*
@@ -54,36 +61,40 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
.getLog(ConfigServicePropertySourceLocator.class);
private RestTemplate restTemplate;
private ConfigClientProperties defaults;
private ConfigClientProperties defaultProperties;
public ConfigServicePropertySourceLocator(ConfigClientProperties defaults) {
this.defaults = defaults;
public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) {
this.defaultProperties = defaultProperties;
}
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(
org.springframework.core.env.Environment environment) {
ConfigClientProperties client = this.defaults.override(environment);
ConfigClientProperties properties = this.defaultProperties.override(environment);
CompositePropertySource composite = new CompositePropertySource("configService");
RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(client)
RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(properties)
: this.restTemplate;
Exception error = null;
String errorBody = null;
logger.info("Fetching config from server at: " + client.getRawUri());
logger.info("Fetching config from server at: " + properties.getRawUri());
try {
String[] labels = new String[]{""};
if (StringUtils.hasText(client.getLabel())) {
labels = StringUtils.commaDelimitedListToStringArray(client.getLabel());
String[] labels = new String[] { "" };
if (StringUtils.hasText(properties.getLabel())) {
labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel());
}
String state = ConfigClientStateHolder.getState();
// Try all the labels until one works
for (String label : labels) {
Environment result = getRemoteEnvironment(restTemplate, client.getRawUri(), client.getName(), client.getProfile(), label.trim());
Environment result = getRemoteEnvironment(restTemplate,
properties, label.trim(), state);
if (result != null) {
logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s",
logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s",
result.getName(),
result.getProfiles() == null ? "" : Arrays.asList(result.getProfiles()),
result.getLabel(), result.getVersion()));
result.getLabel(), result.getVersion(), result.getState()));
for (PropertySource source : result.getPropertySources()) {
@SuppressWarnings("unchecked")
@@ -92,6 +103,13 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
composite.addPropertySource(new MapPropertySource(source
.getName(), map));
}
if (StringUtils.hasText(result.getState()) || StringUtils.hasText(result.getVersion())) {
HashMap<String, Object> map = new HashMap<>();
putValue(map, "config.client.state", result.getState());
putValue(map, "config.client.version", result.getVersion());
composite.addFirstPropertySource(new MapPropertySource("configClient", map));
}
return composite;
}
}
@@ -106,7 +124,7 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
catch (Exception e) {
error = e;
}
if (client != null && client.isFailFast()) {
if (properties.isFailFast()) {
throw new IllegalStateException(
"Could not locate PropertySource and the fail fast property is set, failing",
error);
@@ -117,8 +135,20 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
}
private Environment getRemoteEnvironment(RestTemplate restTemplate, String uri, String name, String profile, String label) {
private void putValue(HashMap<String, Object> map, String key, String value) {
if (StringUtils.hasText(value)) {
map.put(key, value);
}
}
private Environment getRemoteEnvironment(RestTemplate restTemplate, ConfigClientProperties properties,
String label, String state) {
String path = "/{name}/{profile}";
String name = properties.getName();
String profile = properties.getProfile();
String token = properties.getToken();
String uri = properties.getRawUri();
Object[] args = new String[] { name, profile };
if (StringUtils.hasText(label)) {
args = new String[] { name, profile, label };
@@ -127,16 +157,24 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
ResponseEntity<Environment> response = null;
try {
response = restTemplate.exchange(uri + path,
HttpMethod.GET, new HttpEntity<Void>((Void) null),
Environment.class, args);
} catch (HttpClientErrorException e) {
if(e.getStatusCode() != HttpStatus.NOT_FOUND ) {
HttpHeaders headers = new HttpHeaders();
if (StringUtils.hasText(token)) {
headers.add(TOKEN_HEADER, token);
}
if (StringUtils.hasText(state)) { //TODO: opt in to sending state?
headers.add(STATE_HEADER, state);
}
final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
response = restTemplate.exchange(uri + path, HttpMethod.GET,
entity, Environment.class, args);
}
catch (HttpClientErrorException e) {
if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
throw e;
}
}
if (response==null || response.getStatusCode()!=HttpStatus.OK) {
if (response == null || response.getStatusCode() != HttpStatus.OK) {
return null;
}
Environment result = response.getBody();
@@ -148,7 +186,9 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
}
private RestTemplate getSecureRestTemplate(ConfigClientProperties client) {
RestTemplate template = new RestTemplate();
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setReadTimeout((60 * 1000 * 3) + 5000); //TODO 3m5s, make configurable?
RestTemplate template = new RestTemplate(requestFactory);
String password = client.getPassword();
if (password != null) {
template.setInterceptors(Arrays
@@ -173,8 +213,7 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
byte[] token = Base64
.encode((this.username + ":" + this.password).getBytes());
byte[] token = Base64Utils.encode((this.username + ":" + this.password).getBytes());
request.getHeaders().add("Authorization", "Basic " + new String(token));
return execution.execute(request, body);
}
@@ -182,634 +221,3 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
}
}
/**
* Base64 encoder which is a reduced version of Robert Harder's public domain
* implementation (version 2.3.7). See <a
* href="http://iharder.net/base64">http://iharder.net/base64</a> for more information.
* <p>
* For internal use only.
*
* @author Luke Taylor
* @since 3.0
*/
final class Base64 {
/** No options specified. Value is zero. */
public final static int NO_OPTIONS = 0;
/** Specify encoding in first bit. Value is one. */
public final static int ENCODE = 1;
/** Specify decoding in first bit. Value is zero. */
public final static int DECODE = 0;
/** Do break lines when encoding. Value is 8. */
public final static int DO_BREAK_LINES = 8;
/**
* Encode using Base64-like encoding that is URL- and Filename-safe as described in
* Section 4 of RFC3548: <a
* href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs
* .org/rfcs/rfc3548.html</a>. It is important to note that data encoded this way is
* <em>not</em> officially valid Base64, or at the very least should not be called
* Base64 without also specifying that is was encoded using the URL- and Filename-safe
* dialect.
*/
public final static int URL_SAFE = 16;
/**
* Encode using the special "ordered" dialect of Base64 described here: <a
* href="http://www.faqs.org/qa/rfcc-1940.html"
* >http://www.faqs.org/qa/rfcc-1940.html</a>.
*/
public final static int ORDERED = 32;
/** Maximum line length (76) of Base64 output. */
private final static int MAX_LINE_LENGTH = 76;
/** The equals sign (=) as a byte. */
private final static byte EQUALS_SIGN = (byte) '=';
/** The new line character (\n) as a byte. */
private final static byte NEW_LINE = (byte) '\n';
private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
/** The 64 valid Base64 values. */
/* Host platform me be something funny like EBCDIC, so we hardcode these values. */
private final static byte[] _STANDARD_ALPHABET = { (byte) 'A', (byte) 'B',
(byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H',
(byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
(byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T',
(byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f',
(byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l',
(byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r',
(byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x',
(byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9',
(byte) '+', (byte) '/' };
/**
* Translates a Base64 value to either its 6-bit reconstruction value or a negative
* number indicating some other meaning.
**/
private final static byte[] _STANDARD_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9,
-9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
62, // Plus sign at decimal 43
-9, -9, -9, // Decimal 44 - 46
63, // Slash at decimal 47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255
};
/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */
/**
* Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: <a
* href
* ="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>.
* Notice that the last two bytes become "hyphen" and "underscore" instead of "plus"
* and "slash."
*/
private final static byte[] _URL_SAFE_ALPHABET = { (byte) 'A', (byte) 'B',
(byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H',
(byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
(byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T',
(byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f',
(byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l',
(byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r',
(byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x',
(byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9',
(byte) '-', (byte) '_' };
/**
* Used in decoding URL- and Filename-safe dialects of Base64.
*/
private final static byte[] _URL_SAFE_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9,
-9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
-9, // Plus sign at decimal 43
-9, // Decimal 44
62, // Minus sign at decimal 45
-9, // Decimal 46
-9, // Slash at decimal 47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-9, -9, -9, -9, // Decimal 91 - 94
63, // Underscore at decimal 95
-9, // Decimal 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255
};
/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */
/**
* I don't get the point of this technique, but someone requested it, and it is
* described here: <a
* href="http://www.faqs.org/qa/rfcc-1940.html">http://www.faqs.org/
* qa/rfcc-1940.html</a>.
*/
private final static byte[] _ORDERED_ALPHABET = { (byte) '-', (byte) '0', (byte) '1',
(byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7',
(byte) '8', (byte) '9', (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D',
(byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J',
(byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V',
(byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) '_', (byte) 'a',
(byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g',
(byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm',
(byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's',
(byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
(byte) 'z' };
/**
* Used in decoding the "ordered" dialect of Base64.
*/
private final static byte[] _ORDERED_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9,
-9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
-9, // Plus sign at decimal 43
-9, // Decimal 44
0, // Minus sign at decimal 45
-9, // Decimal 46
-9, // Slash at decimal 47
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M'
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z'
-9, -9, -9, -9, // Decimal 91 - 94
37, // Underscore at decimal 95
-9, // Decimal 96
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm'
51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255
};
public static byte[] decode(byte[] bytes) {
return decode(bytes, 0, bytes.length, NO_OPTIONS);
}
public static byte[] encode(byte[] bytes) {
return encodeBytesToBytes(bytes, 0, bytes.length, NO_OPTIONS);
}
public static boolean isBase64(byte[] bytes) {
try {
decode(bytes);
}
catch (InvalidBase64CharacterException e) {
return false;
}
return true;
}
/**
* Returns one of the _SOMETHING_ALPHABET byte arrays depending on the options
* specified. It's possible, though silly, to specify ORDERED <b>and</b> URLSAFE in
* which case one of them will be picked, though there is no guarantee as to which one
* will be picked.
*/
private static byte[] getAlphabet(int options) {
if ((options & URL_SAFE) == URL_SAFE) {
return _URL_SAFE_ALPHABET;
}
else if ((options & ORDERED) == ORDERED) {
return _ORDERED_ALPHABET;
}
else {
return _STANDARD_ALPHABET;
}
}
/**
* Returns one of the _SOMETHING_DECODABET byte arrays depending on the options
* specified. It's possible, though silly, to specify ORDERED and URL_SAFE in which
* case one of them will be picked, though there is no guarantee as to which one will
* be picked.
*/
private static byte[] getDecodabet(int options) {
if ((options & URL_SAFE) == URL_SAFE) {
return _URL_SAFE_DECODABET;
}
else if ((options & ORDERED) == ORDERED) {
return _ORDERED_DECODABET;
}
else {
return _STANDARD_DECODABET;
}
}
/* ******** E N C O D I N G M E T H O D S ******** */
/**
* <p>
* Encodes up to three bytes of the array <var>source</var> and writes the resulting
* four Base64 bytes to <var>destination</var>. The source and destination arrays can
* be manipulated anywhere along their length by specifying <var>srcOffset</var> and
* <var>destOffset</var>. This method does not check to make sure your arrays are
* large enough to accomodate <var>srcOffset</var> + 3 for the <var>source</var> array
* or <var>destOffset</var> + 4 for the <var>destination</var> array. The actual
* number of significant bytes in your array is given by <var>numSigBytes</var>.
* </p>
* <p>
* This is the lowest level of the encoding methods with all possible parameters.
* </p>
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
* @param numSigBytes the number of significant bytes in your array
* @param destination the array to hold the conversion
* @param destOffset the index where output will be put
* @return the <var>destination</var> array
* @since 1.3
*/
private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes,
byte[] destination, int destOffset, int options) {
byte[] ALPHABET = getAlphabet(options);
// 1 2 3
// 01234567890123456789012345678901 Bit position
// --------000000001111111122222222 Array position from threeBytes
// --------| || || || | Six bit groups to index ALPHABET
// >>18 >>12 >> 6 >> 0 Right shift necessary
// 0x3f 0x3f 0x3f Additional AND
// Create buffer with zero-padding if there are only one or two
// significant bytes passed in the array.
// We have to shift left 24 in order to flush out the 1's that appear
// when Java treats a value as negative that is cast from a byte to an int.
int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
switch (numSigBytes) {
case 3:
destination[destOffset] = ALPHABET[(inBuff >>> 18)];
destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f];
return destination;
case 2:
destination[destOffset] = ALPHABET[(inBuff >>> 18)];
destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
case 1:
destination[destOffset] = ALPHABET[(inBuff >>> 18)];
destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = EQUALS_SIGN;
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
default:
return destination;
}
}
/**
*
* @param source The data to convert
* @param off Offset in array where conversion should begin
* @param len Length of data to convert
* @param options Specified options
* @return The Base64-encoded data as a String
* @see Base64#DO_BREAK_LINES
* @throws java.io.IOException if there is an error
* @throws NullPointerException if source array is null
* @throws IllegalArgumentException if source array, offset, or length are invalid
* @since 2.3.1
*/
private static byte[] encodeBytesToBytes(byte[] source, int off, int len, int options) {
if (source == null) {
throw new NullPointerException("Cannot serialize a null array.");
} // end if: null
if (off < 0) {
throw new IllegalArgumentException("Cannot have negative offset: " + off);
} // end if: off < 0
if (len < 0) {
throw new IllegalArgumentException("Cannot have length offset: " + len);
} // end if: len < 0
if (off + len > source.length) {
throw new IllegalArgumentException(String.format(
"Cannot have offset of %d and length of %d with array of length %d",
off, len, source.length));
} // end if: off < 0
boolean breakLines = (options & DO_BREAK_LINES) > 0;
// int len43 = len * 4 / 3;
// byte[] outBuff = new byte[ ( len43 ) // Main 4:3
// + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding
// + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines
// Try to determine more precisely how big the array needs to be.
// If we get it right, we don't have to do an array copy, and
// we save a bunch of memory.
int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual
// encoding
if (breakLines) {
encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters
}
byte[] outBuff = new byte[encLen];
int d = 0;
int e = 0;
int len2 = len - 2;
int lineLength = 0;
for (; d < len2; d += 3, e += 4) {
encode3to4(source, d + off, 3, outBuff, e, options);
lineLength += 4;
if (breakLines && lineLength >= MAX_LINE_LENGTH) {
outBuff[e + 4] = NEW_LINE;
e++;
lineLength = 0;
} // end if: end of line
} // en dfor: each piece of array
if (d < len) {
encode3to4(source, d + off, len - d, outBuff, e, options);
e += 4;
} // end if: some padding needed
// Only resize array if we didn't guess it right.
if (e <= outBuff.length - 1) {
byte[] finalOut = new byte[e];
System.arraycopy(outBuff, 0, finalOut, 0, e);
// System.err.println("Having to resize array from " + outBuff.length + " to "
// + e );
return finalOut;
}
else {
// System.err.println("No need to resize array.");
return outBuff;
}
}
/* ******** D E C O D I N G M E T H O D S ******** */
/**
* Decodes four bytes from array <var>source</var> and writes the resulting bytes (up
* to three of them) to <var>destination</var>. The source and destination arrays can
* be manipulated anywhere along their length by specifying <var>srcOffset</var> and
* <var>destOffset</var>. This method does not check to make sure your arrays are
* large enough to accomodate <var>srcOffset</var> + 4 for the <var>source</var> array
* or <var>destOffset</var> + 3 for the <var>destination</var> array. This method
* returns the actual number of bytes that were converted from the Base64 encoding.
* <p>
* This is the lowest level of the decoding methods with all possible parameters.
* </p>
*
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
* @param destination the array to hold the conversion
* @param destOffset the index where output will be put
* @param options alphabet type is pulled from this (standard, url-safe, ordered)
* @return the number of decoded bytes converted
* @throws NullPointerException if source or destination arrays are null
* @throws IllegalArgumentException if srcOffset or destOffset are invalid or there is
* not enough room in the array.
* @since 1.3
*/
private static int decode4to3(final byte[] source, final int srcOffset,
final byte[] destination, final int destOffset, final int options) {
// Lots of error checking and exception throwing
if (source == null) {
throw new NullPointerException("Source array was null.");
} // end if
if (destination == null) {
throw new NullPointerException("Destination array was null.");
} // end if
if (srcOffset < 0 || srcOffset + 3 >= source.length) {
throw new IllegalArgumentException(
String.format(
"Source array with length %d cannot have offset of %d and still process four bytes.",
source.length, srcOffset));
} // end if
if (destOffset < 0 || destOffset + 2 >= destination.length) {
throw new IllegalArgumentException(
String.format(
"Destination array with length %d cannot have offset of %d and still store three bytes.",
destination.length, destOffset));
} // end if
byte[] DECODABET = getDecodabet(options);
// Example: Dk==
if (source[srcOffset + 2] == EQUALS_SIGN) {
// Two ways to do the same thing. Don't know which way I like best.
// int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
// | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
| ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12);
destination[destOffset] = (byte) (outBuff >>> 16);
return 1;
}
// Example: DkL=
else if (source[srcOffset + 3] == EQUALS_SIGN) {
// Two ways to do the same thing. Don't know which way I like best.
// int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
// | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
// | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
| ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
| ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6);
destination[destOffset] = (byte) (outBuff >>> 16);
destination[destOffset + 1] = (byte) (outBuff >>> 8);
return 2;
}
// Example: DkLE
else {
// Two ways to do the same thing. Don't know which way I like best.
// int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
// | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
// | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
// | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
| ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
| ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6)
| ((DECODABET[source[srcOffset + 3]] & 0xFF));
destination[destOffset] = (byte) (outBuff >> 16);
destination[destOffset + 1] = (byte) (outBuff >> 8);
destination[destOffset + 2] = (byte) (outBuff);
return 3;
}
}
/**
* Low-level access to decoding ASCII characters in the form of a byte array.
* <strong>Ignores GUNZIP option, if it's set.</strong> This is not generally a
* recommended method, although it is used internally as part of the decoding process.
* Special case: if len = 0, an empty array is returned. Still, if you need more speed
* and reduced memory footprint (and aren't gzipping), consider this method.
*
* @param source The Base64 encoded data
* @param off The offset of where to begin decoding
* @param len The length of characters to decode
* @param options Can specify options such as alphabet type to use
* @return decoded data
* @throws IllegalArgumentException If bogus characters exist in source data
*/
private static byte[] decode(final byte[] source, final int off, final int len,
final int options) {
// Lots of error checking and exception throwing
if (source == null) {
throw new NullPointerException("Cannot decode null source array.");
} // end if
if (off < 0 || off + len > source.length) {
throw new IllegalArgumentException(
String.format(
"Source array with length %d cannot have offset of %d and process %d bytes.",
source.length, off, len));
} // end if
if (len == 0) {
return new byte[0];
}
else if (len < 4) {
throw new IllegalArgumentException(
"Base64-encoded string must have at least four characters, but length specified was "
+ len);
} // end if
byte[] DECODABET = getDecodabet(options);
int len34 = len * 3 / 4; // Estimate on array size
byte[] outBuff = new byte[len34]; // Upper limit on size of output
int outBuffPosn = 0; // Keep track of where we're writing
byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space
int b4Posn = 0; // Keep track of four byte input buffer
int i = 0; // Source array counter
byte sbiDecode = 0; // Special value from DECODABET
for (i = off; i < off + len; i++) { // Loop through source
sbiDecode = DECODABET[source[i] & 0xFF];
// White space, Equals sign, or legit Base64 character
// Note the values such as -5 and -9 in the
// DECODABETs at the top of the file.
if (sbiDecode >= WHITE_SPACE_ENC) {
if (sbiDecode >= EQUALS_SIGN_ENC) {
b4[b4Posn++] = source[i]; // Save non-whitespace
if (b4Posn > 3) { // Time to decode?
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options);
b4Posn = 0;
// If that was the equals sign, break out of 'for' loop
if (source[i] == EQUALS_SIGN) {
break;
}
}
}
}
else {
// There's a bad input character in the Base64 stream.
throw new InvalidBase64CharacterException(String.format(
"Bad Base64 input character decimal %d in array position %d",
(source[i]) & 0xFF, i));
}
}
byte[] out = new byte[outBuffPosn];
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
return out;
}
}
@SuppressWarnings("serial")
class InvalidBase64CharacterException extends IllegalArgumentException {
InvalidBase64CharacterException(String message) {
super(message);
}
}

View File

@@ -40,24 +40,36 @@ public class Environment {
private String label;
private List<PropertySource> propertySources = new ArrayList<PropertySource>();
private List<PropertySource> propertySources = new ArrayList<>();
private String version;
private String state;
public Environment(String name, String... profiles) {
this(name, profiles, "master", null);
this(name, profiles, "master", null, null);
}
/**
* Copies all fields except propertySources
* @param env
*/
public Environment(Environment env) {
this(env.getName(), env.getProfiles(), env.getLabel(), env.getVersion(), env.getState());
}
@JsonCreator
public Environment(@JsonProperty("name") String name,
@JsonProperty("profiles") String[] profiles,
@JsonProperty("label") String label,
@JsonProperty("version") String version) {
@JsonProperty("version") String version,
@JsonProperty("state") String state) {
super();
this.name = name;
this.profiles = profiles;
this.label = label;
this.version = version;
this.state = state;
}
public void add(PropertySource propertySource) {
@@ -104,11 +116,20 @@ public class Environment {
this.version = version;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
@Override
public String toString() {
return "Environment [name=" + name + ", profiles=" + Arrays.asList(profiles)
+ ", label=" + label + ", propertySources=" + propertySources
+ ", version=" + version+ "]";
+ ", version=" + version
+ ", state=" + state + "]";
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2013-2016 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.config.client;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import org.junit.Test;
/**
* @author Spencer Gibb
*/
public class ConfigClientWatchTests {
@Test
public void stateChangedWorks() {
ConfigClientWatch watch = new ConfigClientWatch(null);
assertThat(watch.stateChanged(null, "1"), is(true));
assertThat(watch.stateChanged("1", "2"), is(true));
assertThat(watch.stateChanged("1", null), is(true));
assertThat(watch.stateChanged("1", "1"), is(false));
}
}

View File

@@ -44,7 +44,7 @@ public class ConfigServicePropertySourceLocatorTests {
@Test
public void sunnyDay() {
Environment body = new Environment("app", "master");
mockRequestResponseWithoutLabel(new ResponseEntity<Environment>(body,
mockRequestResponseWithoutLabel(new ResponseEntity<>(body,
HttpStatus.OK));
this.locator.setRestTemplate(this.restTemplate);
assertNotNull(this.locator.locate(this.environment));
@@ -54,7 +54,7 @@ public class ConfigServicePropertySourceLocatorTests {
public void sunnyDayWithLabel() {
Environment body = new Environment("app", "master");
mockRequestResponseWithLabel(
new ResponseEntity<Environment>(body, HttpStatus.OK), "v1.0.0");
new ResponseEntity<>(body, HttpStatus.OK), "v1.0.0");
this.locator.setRestTemplate(this.restTemplate);
EnvironmentTestUtils.addEnvironment(this.environment,
"spring.cloud.config.label:v1.0.0");
@@ -71,7 +71,7 @@ public class ConfigServicePropertySourceLocatorTests {
@Test
public void failsQuietly() {
mockRequestResponseWithoutLabel(new ResponseEntity<String>("Wah!",
mockRequestResponseWithoutLabel(new ResponseEntity<>("Wah!",
HttpStatus.INTERNAL_SERVER_ERROR));
this.locator.setRestTemplate(this.restTemplate);
assertNull(this.locator.locate(this.environment));