package org.springframework.cloud;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.logging.Logger;
import javax.activation.DataSource;
import org.springframework.cloud.app.ApplicationInstanceInfo;
import org.springframework.cloud.service.CompositeServiceInfo;
import org.springframework.cloud.service.ServiceConnectorConfig;
import org.springframework.cloud.service.ServiceConnectorCreator;
import org.springframework.cloud.service.ServiceInfo;
import org.springframework.cloud.service.ServiceInfo.ServiceLabel;
import org.springframework.cloud.service.ServiceInfo.ServiceProperty;
/**
* The main user-level API access to application and services for the app in which this instance is embedded in.
*
* The class connects application and the underlying cloud. Besides passing along information about the application
* instance, it allows simple access for using services. It uses {@link ServiceInfo}s obtained using the
* underlying {@link CloudConnector} and allows translating those to service connector such as a {@link DataSource}.
*
* It also passes along information obtained through {@link CloudConnector} to let application take control on how
* to use bound services.
*
* <p>
* NOTE: Users or cloud providers shouldn't need to instantiate an instance of this class (constructor has package-access only for
* unit-testing purpose). Instead, they can obtain an appropriate instance through {@link CloudFactory}
* </p>
*
* @author Ramnivas Laddad
*
*/
public class Cloud {
private CloudConnector cloudConnector;
private ServiceConnectorCreatorRegistry serviceConnectorCreatorRegistry = new ServiceConnectorCreatorRegistry();
/**
* Package-access constructor.
*
* @param cloudConnector the underlying connector
* @param serviceConnectorCreators service connector creators
*/
Cloud(CloudConnector cloudConnector, List<ServiceConnectorCreator<?, ? extends ServiceInfo>> serviceConnectorCreators) {
this.cloudConnector = cloudConnector;
for (ServiceConnectorCreator<?, ? extends ServiceInfo> serviceCreator : serviceConnectorCreators) {
registerServiceConnectorCreator(serviceCreator);
}
}
/**
* @see CloudConnector#getApplicationInstanceInfo()
*
* @return information about the application instance
*/
public ApplicationInstanceInfo getApplicationInstanceInfo() {
return cloudConnector.getApplicationInstanceInfo();
}
/**
* Get {@link ServiceInfo} for the given service id
*
* @param serviceId service id
* @return info for the serviceId
*/
public ServiceInfo getServiceInfo(String serviceId) {
for (ServiceInfo serviceInfo : getServiceInfos()) {
if (serviceInfo.getId().equals(serviceId)) {
return serviceInfo;
}
}
throw new CloudException("No service with id " + serviceId + " found");
}
/**
* @see CloudConnector#getServiceInfos()
* @return information about all services bound to the application
*/
public List<ServiceInfo> getServiceInfos() {
return flatten(cloudConnector.getServiceInfos());
}
/**
* Get {@link ServiceInfo}s for the bound services that could be mapped to the given service connector type.
*
* <p>
* For example, if the connector type is {@link DataSource}, then the method will return all {@link ServiceInfo} objects
* matching bound relational database services.
* <p>
*
* @param <T> The class of the connector to find services for.
* @param serviceConnectorType service connector type.
* Passing null returns all {@link ServiceInfo}s (matching that of {@link Cloud#getServiceInfos()}
* @return information about services bound to the application that could be transformed into the given connector type
*/
public <T> List<ServiceInfo> getServiceInfos(Class<T> serviceConnectorType) {
List<ServiceInfo> allServiceInfos = getServiceInfos();
List<ServiceInfo> matchingServiceInfos = new ArrayList<ServiceInfo>();
for (ServiceInfo serviceInfo : allServiceInfos) {
if (serviceConnectorCreatorRegistry.canCreate(serviceConnectorType, serviceInfo)) {
matchingServiceInfos.add(serviceInfo);
}
}
return matchingServiceInfos;
}
/**
* Get a service connector for the given service id, the connector type, configured with the given config
*
*
* @param <SC> The class of the service connector to return.
* @param serviceId the service id
* @param serviceConnectorType The expected class of service connector such as, DataSource.class.
* @param serviceConnectorConfig service connector configuration (such as pooling parameters).
* @return a service connector of the specified type with the given configuration applied
*
*/
public <SC> SC getServiceConnector(String serviceId, Class<SC> serviceConnectorType,
ServiceConnectorConfig serviceConnectorConfig) {
ServiceInfo serviceInfo = getServiceInfo(serviceId);
return getServiceConnector(serviceInfo, serviceConnectorType, serviceConnectorConfig);
}
/**
* Get the singleton service connector for the given connector type, configured with the given config
*
* @param <SC> The class of the service connector to return.
* @param serviceConnectorType The expected class of service connector such as, DataSource.class.
* @param serviceConnectorConfig service connector configuration (such as pooling parameters).
* @return the single service connector of the specified type with the given configuration applied
*
*/
public <SC> SC getSingletonServiceConnector(Class<SC> serviceConnectorType, ServiceConnectorConfig serviceConnectorConfig) {
List<ServiceInfo> matchingServiceInfos = getServiceInfos(serviceConnectorType);
if (matchingServiceInfos.size() != 1) {
throw new CloudException("No unique service matching " + serviceConnectorType + " found. Expected 1, found "
+ matchingServiceInfos.size());
}
ServiceInfo matchingServiceInfo = matchingServiceInfos.get(0);
return getServiceConnector(matchingServiceInfo, serviceConnectorType, serviceConnectorConfig);
}
/**
* Register a new service connector creator
*
* @param serviceConnectorCreator the service connector to register
*/
public void registerServiceConnectorCreator(ServiceConnectorCreator<?, ? extends ServiceInfo> serviceConnectorCreator) {
serviceConnectorCreatorRegistry.registerCreator(serviceConnectorCreator);
}
/**
* Get properties for app and services.
*
*
*
* <p>
* Application properties always include <code>cloud.application.app-id</code> and <code>cloud.application.instance-id</code>
* with values bound to application id and instance id. The rest of the properties are cloud-provider specific, but take the
* <code>cloud.application.<property-name></code> form. <pre>
* cloud.application.app-id = helloworld
* cloud.application.instance-id = instance-0-0fab098f
* cloud.application.<property-name> = <property-value>
* </pre>
*
* <p>
* Service specific properties are exposed for each bound service, with each key starting in <code>cloud.services</code>. Like
* application properties, these too are cloud and service specific. Each key for a specific service starts with
* <code>cloud.services.<service-id></code> <pre>
* cloud.services.customerDb.type = mysql-5.1
* cloud.services.customerDb.plan = free
* cloud.services.customerDb.connection.hostname = ...
* cloud.services.customerDb.connection.port = ...
* etc...
* </pre>
*
* <p>
* If a there is only a single service of a given type (as defined by the {link ServiceInfo.ServiceLabel}
* annoation's value of the corresponding {@link ServiceInfo} class), that service is aliased
* to the service type. Keys for such properties start in <code>cloud.services.<service-type></code>.
* For example, if there is only a single MySQL service bound to the application, the service properties
* will also be exposed starting with '<code>cloud.services.mysql</code>' key: <pre>
* cloud.services.mysql.type = mysql-5.1
* cloud.services.mysql.plan = free
* cloud.services.mysql.connection.hostname = ...
* cloud.services.mysql.connection.port = ...
* etc...
* </pre>
*
* @return the properties object
*/
public Properties getCloudProperties() {
Map<String, List<ServiceInfo>> mappedServiceInfos = new HashMap<String, List<ServiceInfo>>();
for (ServiceInfo serviceInfo : getServiceInfos()) {
String key = getServiceLabel(serviceInfo);
List<ServiceInfo> serviceInfosForLabel = mappedServiceInfos.get(key);
if (serviceInfosForLabel == null) {
serviceInfosForLabel = new ArrayList<ServiceInfo>();
mappedServiceInfos.put(key, serviceInfosForLabel);
}
serviceInfosForLabel.add(serviceInfo);
}
final String servicePropKeyLead = "cloud.services.";
Properties cloudProperties = new Properties();
for (Entry<String, List<ServiceInfo>> mappedServiceInfo : mappedServiceInfos.entrySet()) {
List<ServiceInfo> serviceInfos = mappedServiceInfo.getValue();
for (ServiceInfo serviceInfo : serviceInfos) {
String idBasedKey = servicePropKeyLead + serviceInfo.getId();
cloudProperties.putAll(getServiceProperties(idBasedKey, serviceInfo));
// If there is only one service for a given label, put props with that label instead of just id
if (serviceInfos.size() == 1) {
String labelBasedKey = servicePropKeyLead + mappedServiceInfo.getKey();
cloudProperties.putAll(getServiceProperties(labelBasedKey, serviceInfo));
}
}
}
cloudProperties.putAll(getAppProperties());
return cloudProperties;
}
private <SC> SC getServiceConnector(ServiceInfo serviceInfo, Class<SC> serviceConnectorType,
ServiceConnectorConfig serviceConnectorConfig) {
ServiceConnectorCreator<SC, ServiceInfo> serviceConnectorCreator = serviceConnectorCreatorRegistry.getServiceCreator(
serviceConnectorType, serviceInfo);
return serviceConnectorCreator.create(serviceInfo, serviceConnectorConfig);
}
private Properties getAppProperties() {
final String appPropLeadKey = "cloud.application.";
Properties appProperties = new Properties();
appProperties.put(appPropLeadKey + "instance-id", getApplicationInstanceInfo().getInstanceId());
appProperties.put(appPropLeadKey + "app-id", getApplicationInstanceInfo().getAppId());
for (Map.Entry<String, Object> entry : getApplicationInstanceInfo().getProperties().entrySet()) {
if (entry.getValue() != null) {
appProperties.put(appPropLeadKey + entry.getKey(), entry.getValue());
}
}
return appProperties;
}
private Properties getServiceProperties(String keyLead, ServiceInfo serviceInfo) {
Properties cloudProperties = new Properties();
try {
BeanInfo beanInfo = Introspector.getBeanInfo(serviceInfo.getClass());
PropertyDescriptor[] propDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propDescriptor : propDescriptors) {
ServiceProperty propAnnotation = propDescriptor.getReadMethod().getAnnotation(ServiceProperty.class);
String key = keyLead;
if (propAnnotation != null) {
if (!propAnnotation.category().isEmpty()) {
key = key + "." + propAnnotation.category();
}
if (!propAnnotation.name().isEmpty()) {
key = key + "." + propAnnotation.name();
} else {
key = key + "." + propDescriptor.getName().toLowerCase();
}
Object value = propDescriptor.getReadMethod().invoke(serviceInfo);
if (value != null) {
cloudProperties.put(key, value);
}
}
}
} catch (Exception e) {
throw new CloudException(e);
}
return cloudProperties;
}
private static String getServiceLabel(ServiceInfo serviceInfo) {
Class<? extends ServiceInfo> serviceInfoClass = serviceInfo.getClass();
ServiceLabel labelAnnotation = serviceInfoClass.getAnnotation(ServiceInfo.ServiceLabel.class);
if (labelAnnotation == null) {
return null;
} else {
return labelAnnotation.value();
}
}
private static List<ServiceInfo> flatten(List<ServiceInfo> serviceInfos) {
List<ServiceInfo> flattened = new ArrayList<ServiceInfo>();
for (ServiceInfo serviceInfo : serviceInfos) {
if (serviceInfo instanceof CompositeServiceInfo) {
// recursively flatten any CompositeServiceInfos
CompositeServiceInfo compositeServiceInfo = (CompositeServiceInfo)serviceInfo;
flattened.addAll(flatten(compositeServiceInfo.getServiceInfos()));
} else {
flattened.add(serviceInfo);
}
}
return flattened;
}
}
class ServiceConnectorCreatorRegistry {
private static Logger logger = Logger.getLogger(Cloud.class.getName());
private List<ServiceConnectorCreator<?, ? extends ServiceInfo>> serviceConnectorCreators = new ArrayList<ServiceConnectorCreator<?, ? extends ServiceInfo>>();
public void registerCreator(ServiceConnectorCreator<?, ? extends ServiceInfo> serviceConnectorCreator) {
serviceConnectorCreators.add(serviceConnectorCreator);
}
public <SC, SI extends ServiceInfo> ServiceConnectorCreator<SC, SI> getServiceCreator(Class<SC> serviceConnectorType,
SI serviceInfo) {
ServiceConnectorCreator<SC, SI> serviceConnectorCreator = getServiceCreatorOrNull(serviceConnectorType, serviceInfo);
if (serviceConnectorCreator != null) {
return serviceConnectorCreator;
} else {
throw new CloudException("No suitable ServiceConnectorCreator found: "
+ "service id=" + serviceInfo.getId() + ", "
+ "service info type=" + serviceInfo.getClass().getName() + ", "
+ "connector type=" + serviceConnectorType);
}
}
public <SC, SI extends ServiceInfo> boolean canCreate(Class<SC> serviceConnectorType, SI serviceInfo) {
return getServiceCreatorOrNull(serviceConnectorType, serviceInfo) != null;
}
public boolean accept(ServiceConnectorCreator<?, ? extends ServiceInfo> creator, Class<?> serviceConnectorType,
ServiceInfo serviceInfo) {
boolean typeBasedAccept = serviceConnectorType == null ||
serviceConnectorType.isAssignableFrom(creator.getServiceConnectorType());
boolean infoBasedAccept = serviceInfo == null ||
creator.getServiceInfoType().isAssignableFrom(serviceInfo.getClass());
return typeBasedAccept && infoBasedAccept;
}
@SuppressWarnings("unchecked")
private <SC, SI extends ServiceInfo> ServiceConnectorCreator<SC, SI> getServiceCreatorOrNull(Class<SC> serviceConnectorType,
SI serviceInfo) {
for (ServiceConnectorCreator<?, ? extends ServiceInfo> serviceConnectorCreator : serviceConnectorCreators) {
logger.fine("Trying connector creator type " + serviceConnectorCreator);
if (accept(serviceConnectorCreator, serviceConnectorType, serviceInfo)) {
return (ServiceConnectorCreator<SC, SI>) serviceConnectorCreator;
}
}
return null;
}
}