001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.cloud;
018
019import java.util.Collection;
020import java.util.Collections;
021import java.util.List;
022import java.util.Map;
023import java.util.Properties;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027
028import org.springframework.boot.SpringApplication;
029import org.springframework.boot.context.config.ConfigFileApplicationListener;
030import org.springframework.boot.env.EnvironmentPostProcessor;
031import org.springframework.boot.json.JsonParser;
032import org.springframework.boot.json.JsonParserFactory;
033import org.springframework.core.Ordered;
034import org.springframework.core.env.CommandLinePropertySource;
035import org.springframework.core.env.ConfigurableEnvironment;
036import org.springframework.core.env.Environment;
037import org.springframework.core.env.MutablePropertySources;
038import org.springframework.core.env.PropertiesPropertySource;
039import org.springframework.util.StringUtils;
040
041/**
042 * An {@link EnvironmentPostProcessor} that knows where to find VCAP (a.k.a. Cloud
043 * Foundry) meta data in the existing environment. It parses out the VCAP_APPLICATION and
044 * VCAP_SERVICES meta data and dumps it in a form that is easily consumed by
045 * {@link Environment} users. If the app is running in Cloud Foundry then both meta data
046 * items are JSON objects encoded in OS environment variables. VCAP_APPLICATION is a
047 * shallow hash with basic information about the application (name, instance id, instance
048 * index, etc.), and VCAP_SERVICES is a hash of lists where the keys are service labels
049 * and the values are lists of hashes of service instance meta data. Examples are:
050 *
051 * <pre class="code">
052 * VCAP_APPLICATION: {"instance_id":"2ce0ac627a6c8e47e936d829a3a47b5b","instance_index":0,
053 *   "version":"0138c4a6-2a73-416b-aca0-572c09f7ca53","name":"foo",
054 *   "uris":["foo.cfapps.io"], ...}
055 * VCAP_SERVICES: {"rds-mysql-1.0":[{"name":"mysql","label":"rds-mysql-1.0","plan":"10mb",
056 *   "credentials":{"name":"d04fb13d27d964c62b267bbba1cffb9da","hostname":"mysql-service-public.clqg2e2w3ecf.us-east-1.rds.amazonaws.com",
057 *   "host":"mysql-service-public.clqg2e2w3ecf.us-east-1.rds.amazonaws.com","port":3306,"user":"urpRuqTf8Cpe6",
058 *   "username":"urpRuqTf8Cpe6","password":"pxLsGVpsC9A5S"}
059 * }]}
060 * </pre>
061 *
062 * These objects are flattened into properties. The VCAP_APPLICATION object goes straight
063 * to {@code vcap.application.*} in a fairly obvious way, and the VCAP_SERVICES object is
064 * unwrapped so that it is a hash of objects with key equal to the service instance name
065 * (e.g. "mysql" in the example above), and value equal to that instances properties, and
066 * then flattened in the same way. E.g.
067 *
068 * <pre class="code">
069 * vcap.application.instance_id: 2ce0ac627a6c8e47e936d829a3a47b5b
070 * vcap.application.version: 0138c4a6-2a73-416b-aca0-572c09f7ca53
071 * vcap.application.name: foo
072 * vcap.application.uris[0]: foo.cfapps.io
073 *
074 * vcap.services.mysql.name: mysql
075 * vcap.services.mysql.label: rds-mysql-1.0
076 * vcap.services.mysql.credentials.name: d04fb13d27d964c62b267bbba1cffb9da
077 * vcap.services.mysql.credentials.port: 3306
078 * vcap.services.mysql.credentials.host: mysql-service-public.clqg2e2w3ecf.us-east-1.rds.amazonaws.com
079 * vcap.services.mysql.credentials.username: urpRuqTf8Cpe6
080 * vcap.services.mysql.credentials.password: pxLsGVpsC9A5S
081 * ...
082 * </pre>
083 *
084 * N.B. this initializer is mainly intended for informational use (the application and
085 * instance ids are particularly useful). For service binding you might find that Spring
086 * Cloud is more convenient and more robust against potential changes in Cloud Foundry.
087 *
088 * @author Dave Syer
089 * @author Andy Wilkinson
090 */
091public class CloudFoundryVcapEnvironmentPostProcessor
092                implements EnvironmentPostProcessor, Ordered {
093
094        private static final Log logger = LogFactory
095                        .getLog(CloudFoundryVcapEnvironmentPostProcessor.class);
096
097        private static final String VCAP_APPLICATION = "VCAP_APPLICATION";
098
099        private static final String VCAP_SERVICES = "VCAP_SERVICES";
100
101        // Before ConfigFileApplicationListener so values there can use these ones
102        private int order = ConfigFileApplicationListener.DEFAULT_ORDER - 1;
103
104        public void setOrder(int order) {
105                this.order = order;
106        }
107
108        @Override
109        public int getOrder() {
110                return this.order;
111        }
112
113        @Override
114        public void postProcessEnvironment(ConfigurableEnvironment environment,
115                        SpringApplication application) {
116                if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) {
117                        Properties properties = new Properties();
118                        JsonParser jsonParser = JsonParserFactory.getJsonParser();
119                        addWithPrefix(properties,
120                                        getPropertiesFromApplication(environment, jsonParser),
121                                        "vcap.application.");
122                        addWithPrefix(properties, getPropertiesFromServices(environment, jsonParser),
123                                        "vcap.services.");
124                        MutablePropertySources propertySources = environment.getPropertySources();
125                        if (propertySources.contains(
126                                        CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) {
127                                propertySources.addAfter(
128                                                CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
129                                                new PropertiesPropertySource("vcap", properties));
130                        }
131                        else {
132                                propertySources
133                                                .addFirst(new PropertiesPropertySource("vcap", properties));
134                        }
135                }
136        }
137
138        private void addWithPrefix(Properties properties, Properties other, String prefix) {
139                for (String key : other.stringPropertyNames()) {
140                        String prefixed = prefix + key;
141                        properties.setProperty(prefixed, other.getProperty(key));
142                }
143        }
144
145        private Properties getPropertiesFromApplication(Environment environment,
146                        JsonParser parser) {
147                Properties properties = new Properties();
148                try {
149                        String property = environment.getProperty(VCAP_APPLICATION, "{}");
150                        Map<String, Object> map = parser.parseMap(property);
151                        extractPropertiesFromApplication(properties, map);
152                }
153                catch (Exception ex) {
154                        logger.error("Could not parse VCAP_APPLICATION", ex);
155                }
156                return properties;
157        }
158
159        private Properties getPropertiesFromServices(Environment environment,
160                        JsonParser parser) {
161                Properties properties = new Properties();
162                try {
163                        String property = environment.getProperty(VCAP_SERVICES, "{}");
164                        Map<String, Object> map = parser.parseMap(property);
165                        extractPropertiesFromServices(properties, map);
166                }
167                catch (Exception ex) {
168                        logger.error("Could not parse VCAP_SERVICES", ex);
169                }
170                return properties;
171        }
172
173        private void extractPropertiesFromApplication(Properties properties,
174                        Map<String, Object> map) {
175                if (map != null) {
176                        flatten(properties, map, "");
177                }
178        }
179
180        private void extractPropertiesFromServices(Properties properties,
181                        Map<String, Object> map) {
182                if (map != null) {
183                        for (Object services : map.values()) {
184                                @SuppressWarnings("unchecked")
185                                List<Object> list = (List<Object>) services;
186                                for (Object object : list) {
187                                        @SuppressWarnings("unchecked")
188                                        Map<String, Object> service = (Map<String, Object>) object;
189                                        String key = (String) service.get("name");
190                                        if (key == null) {
191                                                key = (String) service.get("label");
192                                        }
193                                        flatten(properties, service, key);
194                                }
195                        }
196                }
197        }
198
199        @SuppressWarnings("unchecked")
200        private void flatten(Properties properties, Map<String, Object> input, String path) {
201                input.forEach((key, value) -> {
202                        String name = getPropertyName(path, key);
203                        if (value instanceof Map) {
204                                // Need a compound key
205                                flatten(properties, (Map<String, Object>) value, name);
206                        }
207                        else if (value instanceof Collection) {
208                                // Need a compound key
209                                Collection<Object> collection = (Collection<Object>) value;
210                                properties.put(name,
211                                                StringUtils.collectionToCommaDelimitedString(collection));
212                                int count = 0;
213                                for (Object item : collection) {
214                                        String itemKey = "[" + (count++) + "]";
215                                        flatten(properties, Collections.singletonMap(itemKey, item), name);
216                                }
217                        }
218                        else if (value instanceof String) {
219                                properties.put(name, value);
220                        }
221                        else if (value instanceof Number) {
222                                properties.put(name, value.toString());
223                        }
224                        else if (value instanceof Boolean) {
225                                properties.put(name, value.toString());
226                        }
227                        else {
228                                properties.put(name, (value != null) ? value : "");
229                        }
230                });
231        }
232
233        private String getPropertyName(String path, String key) {
234                if (!StringUtils.hasText(path)) {
235                        return key;
236                }
237                if (key.startsWith("[")) {
238                        return path + key;
239                }
240                return path + "." + key;
241        }
242
243}