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}