Add support of YAML
This commit migrates the YAML support available in Spring Boot to the core framework. YAML documents can be loaded either as a properties object or as a map. Issue: SPR-9897
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2002-2014 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.beans.factory.config;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
|
||||
/**
|
||||
* Factory for Map that reads from a YAML source. YAML is a nice human-readable
|
||||
* format for configuration, and it has some useful hierarchical properties. It's
|
||||
* more or less a superset of JSON, so it has a lot of similar features. If
|
||||
* multiple resources are provided the later ones will override entries in the
|
||||
* earlier ones hierarchically - that is all entries with the same nested key of
|
||||
* type Map at any depth are merged. For example:
|
||||
*
|
||||
* <pre class="code">
|
||||
* foo:
|
||||
* bar:
|
||||
* one: two
|
||||
* three: four
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* plus (later in the list)
|
||||
*
|
||||
* <pre class="code">
|
||||
* foo:
|
||||
* bar:
|
||||
* one: 2
|
||||
* five: six
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* results in an effective input of
|
||||
*
|
||||
* <pre class="code">
|
||||
* foo:
|
||||
* bar:
|
||||
* one: 2
|
||||
* three: four
|
||||
* five: six
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* Note that the value of "foo" in the first document is not simply replaced
|
||||
* with the value in the second, but its nested values are merged.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 4.1
|
||||
*/
|
||||
public class YamlMapFactoryBean extends YamlProcessor implements
|
||||
FactoryBean<Map<String, Object>> {
|
||||
|
||||
private boolean singleton = true;
|
||||
|
||||
private Map<String, Object> singletonInstance;
|
||||
|
||||
|
||||
/**
|
||||
* Set whether a shared 'singleton' Map instance should be
|
||||
* created, or rather a new Map instance on each request.
|
||||
* <p>Default is "true" (a shared singleton).
|
||||
*/
|
||||
public final void setSingleton(boolean singleton) {
|
||||
this.singleton = singleton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isSingleton() {
|
||||
return this.singleton;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getObject() {
|
||||
if (!this.singleton || this.singletonInstance == null) {
|
||||
this.singletonInstance = createProperties();
|
||||
}
|
||||
return this.singletonInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return Map.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template method that subclasses may override to construct the object
|
||||
* returned by this factory. The default implementation returns the
|
||||
* merged Map instance.
|
||||
* <p>Invoked lazily the first time {@link #getObject()} is invoked in
|
||||
* case of a shared singleton; else, on each {@link #getObject()} call.
|
||||
* @return the object returned by this factory
|
||||
* @see #process(java.util.Map, MatchCallback)
|
||||
*/
|
||||
protected Map<String, Object> createProperties() {
|
||||
final Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||
process(new MatchCallback() {
|
||||
@Override
|
||||
public void process(Properties properties, Map<String, Object> map) {
|
||||
merge(result, map);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private void merge(Map<String, Object> output, Map<String, Object> map) {
|
||||
for (Entry<String, Object> entry : map.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
Object existing = output.get(key);
|
||||
if (value instanceof Map && existing instanceof Map) {
|
||||
Map<String, Object> result = new LinkedHashMap<String, Object>(
|
||||
(Map) existing);
|
||||
merge(result, (Map) value);
|
||||
output.put(key, result);
|
||||
}
|
||||
else {
|
||||
output.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright 2002-2014 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.beans.factory.config;
|
||||
|
||||
import java.io.IOException;
|
||||
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.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Base class for Yaml factories.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 4.1
|
||||
*/
|
||||
public abstract class YamlProcessor {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private ResolutionMethod resolutionMethod = ResolutionMethod.OVERRIDE;
|
||||
|
||||
private Resource[] resources = new Resource[0];
|
||||
|
||||
private List<DocumentMatcher> documentMatchers = Collections.emptyList();
|
||||
|
||||
private boolean matchDefault = true;
|
||||
|
||||
/**
|
||||
* A map of document matchers allowing callers to selectively use only
|
||||
* some of the documents in a YAML resource. In YAML documents are
|
||||
* separated by <code>---<code> lines, and each document is converted
|
||||
* to properties before the match is made. E.g.
|
||||
*
|
||||
* <pre class="code">
|
||||
* environment: dev
|
||||
* url: http://dev.bar.com
|
||||
* name: Developer Setup
|
||||
* ---
|
||||
* environment: prod
|
||||
* url:http://foo.bar.com
|
||||
* name: My Cool App
|
||||
* </pre>
|
||||
*
|
||||
* when mapped with
|
||||
* <code>documentMatchers = YamlProcessor.mapMatcher({"environment": "prod"})</code>
|
||||
* would end up as
|
||||
*
|
||||
* <pre class="code">
|
||||
* environment=prod
|
||||
* url=http://foo.bar.com
|
||||
* name=My Cool App
|
||||
* url=http://dev.bar.com
|
||||
* </pre>
|
||||
* @param matchers a map of keys to value patterns (regular expressions)
|
||||
*/
|
||||
public void setDocumentMatchers(DocumentMatcher... matchers) {
|
||||
this.documentMatchers = Collections
|
||||
.unmodifiableList(new ArrayList<DocumentMatcher>(Arrays.asList(matchers)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag indicating that a document for which all the
|
||||
* {@link #setDocumentMatchers(DocumentMatcher...) document matchers} abstain will
|
||||
* nevertheless match.
|
||||
* @param matchDefault the flag to set (default true)
|
||||
*/
|
||||
public void setMatchDefault(boolean matchDefault) {
|
||||
this.matchDefault = matchDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to use for resolving resources. Each resource will be converted to a Map, so
|
||||
* this property is used to decide which map entries to keep in the final output from
|
||||
* this factory.
|
||||
* @param resolutionMethod the resolution method to set (defaults to
|
||||
* {@link ResolutionMethod#OVERRIDE}).
|
||||
*/
|
||||
public void setResolutionMethod(ResolutionMethod resolutionMethod) {
|
||||
Assert.notNull(resolutionMethod, "ResolutionMethod must not be null");
|
||||
this.resolutionMethod = resolutionMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set locations of YAML {@link Resource resources} to be loaded.
|
||||
* @see ResolutionMethod
|
||||
*/
|
||||
public void setResources(Resource[] resources) {
|
||||
this.resources = (resources == null ? null : resources.clone());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an opportunity for subclasses to process the Yaml parsed from the supplied
|
||||
* resources. Each resource is parsed in turn and the documents inside checked against
|
||||
* the {@link #setDocumentMatchers(DocumentMatcher...) matchers}. If a document
|
||||
* matches it is passed into the callback, along with its representation as
|
||||
* Properties. Depending on the {@link #setResolutionMethod(ResolutionMethod)} not all
|
||||
* of the documents will be parsed.
|
||||
* @param callback a callback to delegate to once matching documents are found
|
||||
*/
|
||||
protected void process(MatchCallback callback) {
|
||||
Yaml yaml = new Yaml();
|
||||
for (Resource resource : this.resources) {
|
||||
boolean found = process(callback, yaml, resource);
|
||||
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
|
||||
int count = 0;
|
||||
try {
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug("Loading from YAML: " + resource);
|
||||
}
|
||||
for (Object object : yaml.loadAll(resource.getInputStream())) {
|
||||
if (object != null && process(asMap(object), callback)) {
|
||||
count++;
|
||||
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug("Loaded " + count + " document"
|
||||
+ (count > 1 ? "s" : "") + " from YAML resource: " + resource);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
handleProcessError(resource, ex);
|
||||
}
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
private void handleProcessError(Resource resource, IOException ex) {
|
||||
if (this.resolutionMethod != ResolutionMethod.FIRST_FOUND
|
||||
&& this.resolutionMethod != ResolutionMethod.OVERRIDE_AND_IGNORE) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
if (this.logger.isWarnEnabled()) {
|
||||
this.logger.warn("Could not load map from " + resource + ": "
|
||||
+ ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> asMap(Object object) {
|
||||
// YAML can have numbers as keys
|
||||
Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||
if (!(object instanceof Map)) {
|
||||
// A document can be a text literal
|
||||
result.put("document", object);
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<Object, Object> map = (Map<Object, Object>) object;
|
||||
for (Entry<Object, Object> entry : map.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof Map) {
|
||||
value = asMap(value);
|
||||
}
|
||||
Object key = entry.getKey();
|
||||
if (key instanceof CharSequence) {
|
||||
result.put(key.toString(), value);
|
||||
}
|
||||
else {
|
||||
// It has to be a map key in this case
|
||||
result.put("[" + key.toString() + "]", value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean process(Map<String, Object> map, MatchCallback callback) {
|
||||
|
||||
Properties properties = new Properties();
|
||||
assignProperties(properties, map, null);
|
||||
|
||||
if (this.documentMatchers.isEmpty()) {
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug("Merging document (no matchers set)" + map);
|
||||
}
|
||||
callback.process(properties, map);
|
||||
return true;
|
||||
}
|
||||
|
||||
MatchStatus result = MatchStatus.ABSTAIN;
|
||||
for (DocumentMatcher matcher : this.documentMatchers) {
|
||||
MatchStatus match = matcher.matches(properties);
|
||||
result = MatchStatus.getMostSpecific(match, result);
|
||||
if (match == MatchStatus.FOUND) {
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug("Matched document with document matcher: "
|
||||
+ properties);
|
||||
}
|
||||
callback.process(properties, map);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == MatchStatus.ABSTAIN && this.matchDefault) {
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug("Matched document with default matcher: " + map);
|
||||
}
|
||||
callback.process(properties, map);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.logger.debug("Unmatched document");
|
||||
return false;
|
||||
}
|
||||
|
||||
private void assignProperties(Properties properties, Map<String, Object> input,
|
||||
String path) {
|
||||
for (Entry<String, Object> entry : input.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (StringUtils.hasText(path)) {
|
||||
if (key.startsWith("[")) {
|
||||
key = path + key;
|
||||
}
|
||||
else {
|
||||
key = path + "." + key;
|
||||
}
|
||||
}
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof String) {
|
||||
properties.put(key, value);
|
||||
}
|
||||
else if (value instanceof Map) {
|
||||
// Need a compound key
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) value;
|
||||
assignProperties(properties, map, key);
|
||||
}
|
||||
else if (value instanceof Collection) {
|
||||
// Need a compound key
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<Object> collection = (Collection<Object>) value;
|
||||
int count = 0;
|
||||
for (Object object : collection) {
|
||||
assignProperties(properties,
|
||||
Collections.singletonMap("[" + (count++) + "]", object), key);
|
||||
}
|
||||
}
|
||||
else {
|
||||
properties.put(key, value == null ? "" : value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback interface used to process properties in a resulting map.
|
||||
*/
|
||||
public interface MatchCallback {
|
||||
|
||||
/**
|
||||
* Process the properties.
|
||||
* @param properties the properties to process
|
||||
* @param map a mutable result map
|
||||
*/
|
||||
void process(Properties properties, Map<String, Object> map);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy interface used to test if properties match.
|
||||
*/
|
||||
public interface DocumentMatcher {
|
||||
|
||||
/**
|
||||
* Test if the given properties match.
|
||||
* @param properties the properties to test
|
||||
* @return the status of the match.
|
||||
*/
|
||||
MatchStatus matches(Properties properties);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Status returned from {@link DocumentMatcher#matches(java.util.Properties)}
|
||||
*/
|
||||
public static enum MatchStatus {
|
||||
|
||||
/**
|
||||
* A match was found.
|
||||
*/
|
||||
FOUND,
|
||||
|
||||
/**
|
||||
* No match was found.
|
||||
*/
|
||||
NOT_FOUND,
|
||||
|
||||
/**
|
||||
* The matcher should not be considered.
|
||||
*/
|
||||
ABSTAIN;
|
||||
|
||||
/**
|
||||
* Compare two {@link MatchStatus} items, returning the most specific status.
|
||||
*/
|
||||
public static MatchStatus getMostSpecific(MatchStatus a, MatchStatus b) {
|
||||
return a.ordinal() < b.ordinal() ? a : b;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to use for resolving resources.
|
||||
*/
|
||||
public static enum ResolutionMethod {
|
||||
|
||||
/**
|
||||
* Replace values from earlier in the list.
|
||||
*/
|
||||
OVERRIDE,
|
||||
|
||||
/**
|
||||
* Replace values from earlier in the list, ignoring any failures.
|
||||
*/
|
||||
OVERRIDE_AND_IGNORE,
|
||||
|
||||
/**
|
||||
* Take the first resource in the list that exists and use just that.
|
||||
*/
|
||||
FIRST_FOUND
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2002-2014 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.beans.factory.config;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
|
||||
/**
|
||||
* Factory for Java Properties that reads from a YAML source. YAML is a nice
|
||||
* human-readable format for configuration, and it has some useful hierarchical
|
||||
* properties. It's more or less a superset of JSON, so it has a lot of similar
|
||||
* features. The Properties created by this factory have nested paths for
|
||||
* hierarchical objects, so for instance this YAML
|
||||
*
|
||||
* <pre class="code">
|
||||
* environments:
|
||||
* dev:
|
||||
* url: http://dev.bar.com
|
||||
* name: Developer Setup
|
||||
* prod:
|
||||
* url: http://foo.bar.com
|
||||
* name: My Cool App
|
||||
* </pre>
|
||||
*
|
||||
* is transformed into these Properties:
|
||||
*
|
||||
* <pre class="code">
|
||||
* environments.dev.url=http://dev.bar.com
|
||||
* environments.dev.name=Developer Setup
|
||||
* environments.prod.url=http://foo.bar.com
|
||||
* environments.prod.name=My Cool App
|
||||
* </pre>
|
||||
*
|
||||
* Lists are represented as comma-separated values (useful for simple String
|
||||
* values) and also as property keys with <code>[]</code> dereferencers, for
|
||||
* example this YAML:
|
||||
*
|
||||
* <pre class="code">
|
||||
* servers:
|
||||
* - dev.bar.com
|
||||
* - foo.bar.com
|
||||
* </pre>
|
||||
*
|
||||
* becomes java Properties like this:
|
||||
*
|
||||
* <pre class="code">
|
||||
* servers=dev.bar.com,foo.bar.com
|
||||
* servers[0]=dev.bar.com
|
||||
* servers[1]=foo.bar.com
|
||||
* </pre>
|
||||
*
|
||||
* Can create a singleton or a new object on each request. Default is
|
||||
* a singleton.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Stephane Nicoll
|
||||
* @since 4.1
|
||||
*/
|
||||
public class YamlPropertiesFactoryBean extends YamlProcessor implements
|
||||
FactoryBean<Properties> {
|
||||
|
||||
private boolean singleton = true;
|
||||
|
||||
private Properties singletonInstance;
|
||||
|
||||
|
||||
/**
|
||||
* Set whether a shared 'singleton' Properties instance should be
|
||||
* created, or rather a new Properties instance on each request.
|
||||
* <p>Default is "true" (a shared singleton).
|
||||
*/
|
||||
public final void setSingleton(boolean singleton) {
|
||||
this.singleton = singleton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isSingleton() {
|
||||
return this.singleton;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public final Properties getObject() {
|
||||
if (!this.singleton || this.singletonInstance == null) {
|
||||
this.singletonInstance = createProperties();
|
||||
}
|
||||
return this.singletonInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return Properties.class;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Template method that subclasses may override to construct the object
|
||||
* returned by this factory. The default implementation returns a
|
||||
* properties with the content of all resources.
|
||||
* <p>Invoked lazily the first time {@link #getObject()} is invoked in
|
||||
* case of a shared singleton; else, on each {@link #getObject()} call.
|
||||
* @return the object returned by this factory
|
||||
* @see #process(MatchCallback) ()
|
||||
*/
|
||||
protected Properties createProperties() {
|
||||
final Properties result = new Properties();
|
||||
process(new MatchCallback() {
|
||||
@Override
|
||||
public void process(Properties properties, Map<String, Object> map) {
|
||||
result.putAll(properties);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user