org.springframework.cloud.config.server.environment.EnvironmentController.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.cloud.config.server.environment.EnvironmentController.java

Source

/*
 * Copyright 2013-2015 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.server.environment;

import static org.springframework.cloud.config.server.support.EnvironmentPropertySource.prepareEnvironment;
import static org.springframework.cloud.config.server.support.EnvironmentPropertySource.resolvePlaceholders;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import javax.servlet.http.HttpServletResponse;

import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.nodes.Tag;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * @author Dave Syer
 * @author Spencer Gibb
 * @author Roy Clarkson
 * @author Bartosz Wojtkiewicz
 * @author Rafal Zukowski
 * @author Ivan Corrales Solera
 * @author Daniel Frey
 * @author Ian Bondoc
 *
 */
@RestController
@RequestMapping(method = RequestMethod.GET, path = "${spring.cloud.config.server.prefix:}")
public class EnvironmentController {

    private EnvironmentRepository repository;
    private ObjectMapper objectMapper;

    private boolean stripDocument = true;

    public EnvironmentController(EnvironmentRepository repository) {
        this(repository, new ObjectMapper());
    }

    public EnvironmentController(EnvironmentRepository repository, ObjectMapper objectMapper) {
        this.repository = repository;
        this.objectMapper = objectMapper;
    }

    /**
     * Flag to indicate that YAML documents which are not a map should be stripped of the
     * "document" prefix that is added by Spring (to facilitate conversion to Properties).
     *
     * @param stripDocument the flag to set
     */
    public void setStripDocumentFromYaml(boolean stripDocument) {
        this.stripDocument = stripDocument;
    }

    @RequestMapping("/{name}/{profiles:.*[^-].*}")
    public Environment defaultLabel(@PathVariable String name, @PathVariable String profiles) {
        return labelled(name, profiles, null);
    }

    @RequestMapping("/{name}/{profiles}/{label:.*}")
    public Environment labelled(@PathVariable String name, @PathVariable String profiles,
            @PathVariable String label) {
        if (label != null && label.contains("(_)")) {
            // "(_)" is uncommon in a git branch name, but "/" cannot be matched
            // by Spring MVC
            label = label.replace("(_)", "/");
        }
        Environment environment = this.repository.findOne(name, profiles, label);
        return environment;
    }

    @RequestMapping("/{name}-{profiles}.properties")
    public ResponseEntity<String> properties(@PathVariable String name, @PathVariable String profiles,
            @RequestParam(defaultValue = "true") boolean resolvePlaceholders) throws IOException {
        return labelledProperties(name, profiles, null, resolvePlaceholders);
    }

    @RequestMapping("/{label}/{name}-{profiles}.properties")
    public ResponseEntity<String> labelledProperties(@PathVariable String name, @PathVariable String profiles,
            @PathVariable String label, @RequestParam(defaultValue = "true") boolean resolvePlaceholders)
            throws IOException {
        validateProfiles(profiles);
        Environment environment = labelled(name, profiles, label);
        Map<String, Object> properties = convertToProperties(environment);
        String propertiesString = getPropertiesString(properties);
        if (resolvePlaceholders) {
            propertiesString = resolvePlaceholders(prepareEnvironment(environment), propertiesString);
        }
        return getSuccess(propertiesString);
    }

    @RequestMapping("{name}-{profiles}.json")
    public ResponseEntity<String> jsonProperties(@PathVariable String name, @PathVariable String profiles,
            @RequestParam(defaultValue = "true") boolean resolvePlaceholders) throws Exception {
        return labelledJsonProperties(name, profiles, null, resolvePlaceholders);
    }

    @RequestMapping("/{label}/{name}-{profiles}.json")
    public ResponseEntity<String> labelledJsonProperties(@PathVariable String name, @PathVariable String profiles,
            @PathVariable String label, @RequestParam(defaultValue = "true") boolean resolvePlaceholders)
            throws Exception {
        validateProfiles(profiles);
        Environment environment = labelled(name, profiles, label);
        Map<String, Object> properties = convertToMap(environment);
        String json = this.objectMapper.writeValueAsString(properties);
        if (resolvePlaceholders) {
            json = resolvePlaceholders(prepareEnvironment(environment), json);
        }
        return getSuccess(json, MediaType.APPLICATION_JSON);
    }

    private String getPropertiesString(Map<String, Object> properties) {
        StringBuilder output = new StringBuilder();
        for (Entry<String, Object> entry : properties.entrySet()) {
            if (output.length() > 0) {
                output.append("\n");
            }
            String line = entry.getKey() + ": " + entry.getValue();
            output.append(line);
        }
        return output.toString();
    }

    @RequestMapping({ "/{name}-{profiles}.yml", "/{name}-{profiles}.yaml" })
    public ResponseEntity<String> yaml(@PathVariable String name, @PathVariable String profiles,
            @RequestParam(defaultValue = "true") boolean resolvePlaceholders) throws Exception {
        return labelledYaml(name, profiles, null, resolvePlaceholders);
    }

    @RequestMapping({ "/{label}/{name}-{profiles}.yml", "/{label}/{name}-{profiles}.yaml" })
    public ResponseEntity<String> labelledYaml(@PathVariable String name, @PathVariable String profiles,
            @PathVariable String label, @RequestParam(defaultValue = "true") boolean resolvePlaceholders)
            throws Exception {
        validateProfiles(profiles);
        Environment environment = labelled(name, profiles, label);
        Map<String, Object> result = convertToMap(environment);
        if (this.stripDocument && result.size() == 1 && result.keySet().iterator().next().equals("document")) {
            Object value = result.get("document");
            if (value instanceof Collection) {
                return getSuccess(new Yaml().dumpAs(value, Tag.SEQ, FlowStyle.BLOCK));
            } else {
                return getSuccess(new Yaml().dumpAs(value, Tag.STR, FlowStyle.BLOCK));
            }
        }
        String yaml = new Yaml().dumpAsMap(result);

        if (resolvePlaceholders) {
            yaml = resolvePlaceholders(prepareEnvironment(environment), yaml);
        }

        return getSuccess(yaml);
    }

    /**
     * Method {@code convertToMap} converts an {@code Environment} to a nested Map which represents a yml/json structure.
     *
     * @param input the environment to be converted
     * @return the nested map containing the environment's properties
     */
    private Map<String, Object> convertToMap(Environment input) {
        // First use the current convertToProperties to get a flat Map from the environment
        Map<String, Object> properties = convertToProperties(input);

        // The root map which holds all the first level properties
        Map<String, Object> rootMap = new LinkedHashMap<>();
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            PropertyNavigator nav = new PropertyNavigator(key);
            nav.setMapValue(rootMap, value);
        }
        return rootMap;
    }

    @ExceptionHandler(NoSuchLabelException.class)
    public void noSuchLabel(HttpServletResponse response) throws IOException {
        response.sendError(HttpStatus.NOT_FOUND.value());
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public void illegalArgument(HttpServletResponse response) throws IOException {
        response.sendError(HttpStatus.BAD_REQUEST.value());
    }

    private void validateProfiles(String profiles) {
        if (profiles.contains("-")) {
            throw new IllegalArgumentException(
                    "Properties output not supported for name or profiles containing hyphens");
        }
    }

    private HttpHeaders getHttpHeaders(MediaType mediaType) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(mediaType);
        return httpHeaders;
    }

    private ResponseEntity<String> getSuccess(String body) {
        return new ResponseEntity<>(body, getHttpHeaders(MediaType.TEXT_PLAIN), HttpStatus.OK);
    }

    private ResponseEntity<String> getSuccess(String body, MediaType mediaType) {
        return new ResponseEntity<>(body, getHttpHeaders(mediaType), HttpStatus.OK);
    }

    private Map<String, Object> convertToProperties(Environment profiles) {

        // Map of unique keys containing full map of properties for each unique
        // key
        Map<String, Map<String, Object>> map = new LinkedHashMap<>();
        List<PropertySource> sources = new ArrayList<>(profiles.getPropertySources());
        Collections.reverse(sources);
        Map<String, Object> combinedMap = new TreeMap<>();
        for (PropertySource source : sources) {

            @SuppressWarnings("unchecked")
            Map<String, Object> value = (Map<String, Object>) source.getSource();
            for (String key : value.keySet()) {

                if (!key.contains("[")) {

                    // Not an array, add unique key to the map
                    combinedMap.put(key, value.get(key));

                } else {

                    // An existing array might have already been added to the property map
                    // of an unequal size to the current array. Replace the array key in
                    // the current map.
                    key = key.substring(0, key.indexOf("["));
                    Map<String, Object> filtered = new TreeMap<>();
                    for (String index : value.keySet()) {
                        if (index.startsWith(key + "[")) {
                            filtered.put(index, value.get(index));
                        }
                    }
                    map.put(key, filtered);
                }
            }

        }

        // Combine all unique keys for array values into the combined map
        for (Entry<String, Map<String, Object>> entry : map.entrySet()) {
            combinedMap.putAll(entry.getValue());
        }

        postProcessProperties(combinedMap);
        return combinedMap;
    }

    private void postProcessProperties(Map<String, Object> propertiesMap) {
        for (Iterator<String> iter = propertiesMap.keySet().iterator(); iter.hasNext();) {
            String key = iter.next();
            if (key.equals("spring.profiles")) {
                iter.remove();
            }
        }
    }

    /**
     * Class {@code PropertyNavigator} is used to navigate through the property key and create necessary Maps and Lists
     * making up the nested structure to finally set the property value at the leaf node.
     * <p>
     * The following rules in yml/json are implemented:
     * <pre>
     * 1. an array element can be:
     *    - a value (leaf)
     *    - a map
     *    - a nested array
     * 2. a map value can be:
     *    - a value (leaf)
     *    - a nested map
     *    - an array
     * </pre>
     */
    private static class PropertyNavigator {

        private enum NodeType {
            LEAF, MAP, ARRAY
        }

        private final String propertyKey;
        private int currentPos;
        private NodeType valueType;

        private PropertyNavigator(String propertyKey) {
            this.propertyKey = propertyKey;
            currentPos = -1;
            valueType = NodeType.MAP;
        }

        private void setMapValue(Map<String, Object> map, Object value) {
            String key = getKey();
            if (NodeType.MAP.equals(valueType)) {
                Map<String, Object> nestedMap = (Map<String, Object>) map.get(key);
                if (nestedMap == null) {
                    nestedMap = new LinkedHashMap<>();
                    map.put(key, nestedMap);
                }
                setMapValue(nestedMap, value);
            } else if (NodeType.ARRAY.equals(valueType)) {
                List<Object> list = (List<Object>) map.get(key);
                if (list == null) {
                    list = new ArrayList<>();
                    map.put(key, list);
                }
                setListValue(list, value);
            } else {
                map.put(key, value);
            }
        }

        private void setListValue(List<Object> list, Object value) {
            int index = getIndex();
            // Fill missing elements if needed
            while (list.size() <= index) {
                list.add(null);
            }
            if (NodeType.MAP.equals(valueType)) {
                Map<String, Object> map = (Map<String, Object>) list.get(index);
                if (map == null) {
                    map = new LinkedHashMap<>();
                    list.set(index, map);
                }
                setMapValue(map, value);
            } else if (NodeType.ARRAY.equals(valueType)) {
                List<Object> nestedList = (List<Object>) list.get(index);
                if (nestedList == null) {
                    nestedList = new ArrayList<>();
                    list.set(index, nestedList);
                }
                setListValue(nestedList, value);
            } else {
                list.set(index, value);
            }
        }

        private int getIndex() {
            // Consider [
            int start = currentPos + 1;

            for (int i = start; i < propertyKey.length(); i++) {
                char c = propertyKey.charAt(i);
                if (c == ']') {
                    currentPos = i;
                    break;
                } else if (!Character.isDigit(c)) {
                    throw new IllegalArgumentException("Invalid key: " + propertyKey);
                }
            }
            // If no closing ] or if '[]'
            if (currentPos < start || currentPos == start) {
                throw new IllegalArgumentException("Invalid key: " + propertyKey);
            } else {
                int index = Integer.parseInt(propertyKey.substring(start, currentPos));
                // Skip the closing ]
                currentPos++;
                if (currentPos == propertyKey.length()) {
                    valueType = NodeType.LEAF;
                } else {
                    switch (propertyKey.charAt(currentPos)) {
                    case '.':
                        valueType = NodeType.MAP;
                        break;
                    case '[':
                        valueType = NodeType.ARRAY;
                        break;
                    default:
                        throw new IllegalArgumentException("Invalid key: " + propertyKey);
                    }
                }
                return index;
            }
        }

        private String getKey() {
            // Consider initial value or previous char '.' or '['
            int start = currentPos + 1;
            for (int i = start; i < propertyKey.length(); i++) {
                char currentChar = propertyKey.charAt(i);
                if (currentChar == '.') {
                    valueType = NodeType.MAP;
                    currentPos = i;
                    break;
                } else if (currentChar == '[') {
                    valueType = NodeType.ARRAY;
                    currentPos = i;
                    break;
                }
            }
            // If there's no delimiter then it's a key of a leaf
            if (currentPos < start) {
                currentPos = propertyKey.length();
                valueType = NodeType.LEAF;
                // Else if we encounter '..' or '.[' or start of the property is . or [ then it's invalid
            } else if (currentPos == start) {
                throw new IllegalArgumentException("Invalid key: " + propertyKey);
            }
            return propertyKey.substring(start, currentPos);
        }
    }
}