One of the challenges in writing abstraction software like Sunshower is that we have to map a ton of vendor-specific properties into our data-model. I had considered writing a mapping language to transform, say, an EC2 Instance or Azure VM into one of our generic Sunshower compute instances, but decided against it because we just dump all the generic vendor properties like spotInstanceRequestId
into properties on our internal model analog, and writing a compiler to do that is for big companies.
But how to avoid individually mapping each darn property by hand? Writing code like:
instance.addProperty( new Property( Property.Type.String, "ami-launch-index", "aws.i18n.ami-launch-index", String.valueOf(ec2instance.getAmiLaunchIndex())));
for each of exactly 100 hojillion properties across AWS’s and Azure’s and GCE’s and etc. data model is a breeding ground for ennui and bugs.
Attempt 1
Java 7 introduced the Beans API which makes introspecting Java beans super easy. There’s also a little-known feature of Java Matcher.replaceAll/First
that allows you to reference a regular-expression capture group in the replacement string, so I whipped up:
PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(com.amazonaws.services.ec2.model.Instance.class, Object.class) .getPropertyDescriptors(); instance.setProperties( Stream.of(propertyDescriptors) .flatMap( t -> { Property.Type type = resolveType(t.getPropertyType()); if (type == null) { return Stream.empty(); } else { Object value = ReflectionUtils.invokeMethod(t.getReadMethod(), ec2instance); return Stream.of( new Property( type, t.getDisplayName().replaceAll("(.)(\\p{Upper})", "$1-$2").toLowerCase(), "aws.i18n." + t.getDisplayName() .replaceAll("(.)(\\p{Upper})", "$1-$2") .toLowerCase(), value == null ? null : String.valueOf(value))); } }) .collect(Collectors.toList()));
Which got me what I wanted:
instance .getProperties() .forEach( t -> { System.out.println( String.format( "Name: %s, key: %s, value: %s", t.getName(), t.getKey(), t.getValue())); }); }
Name: role, key: role, value: io.sunshower.stratosphere.core.topology.model.Instance Name: aws.i18n.architecture, key: architecture, value: x86_64 Name: aws.i18n.client-token, key: client-token, value: sunsh-WebSe-1KKFOLRIA853V Name: aws.i18n.ebs-optimized, key: ebs-optimized, value: false Name: aws.i18n.ena-support, key: ena-support, value: true Name: aws.i18n.hypervisor, key: hypervisor, value: xen Name: aws.i18n.image-id, key: image-id, value: ami-39595240 Name: aws.i18n.instance-id, key: instance-id, value: i-05b1e0984260d51dd Name: aws.i18n.instance-lifecycle, key: instance-lifecycle, value: null Name: aws.i18n.instance-type, key: instance-type, value: t2.micro Name: aws.i18n.kernel-id, key: kernel-id, value: null Name: aws.i18n.key-name, key: key-name, value: sunshower-io Name: aws.i18n.platform, key: platform, value: null Name: aws.i18n.private-dns-name, key: private-dns-name, value: ip-172-31-12-114.us-west-2.compute.internal Name: aws.i18n.private-ip-address, key: private-ip-address, value: 172.31.12.114 ...etc.
But it wasn’t very pretty or maintainable. So, refactoring:
Attempt 2
public class Properties { public static <T> void map( Class<T> type, T instance, Class<?> bound, PropertyAwareObject<?> target, PropertyMappingConfiguration cfg) throws IntrospectionException { PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(type, bound).getPropertyDescriptors(); target.setProperties( Stream.of(propertyDescriptors) .filter(cfg::accept) .flatMap(t -> cfg.map(t, instance)) .collect(Collectors.toList())); } } public class ProviderPropertyMappingConfiguration implements PropertyMappingConfiguration { private final String prefix; public ProviderPropertyMappingConfiguration(String prefix) { this.prefix = prefix; } @Override public Property.Type resolveType(PropertyDescriptor descriptor) { Class<?> propertyType = descriptor.getPropertyType(); if (Boolean.class.equals(propertyType)) { return Property.Type.Boolean; } if (isIntegral(propertyType)) { return Property.Type.Integer; } if (String.class.equals(propertyType)) { return Property.Type.String; } return null; } @Override public boolean accept(PropertyDescriptor propertyDescriptor) { Class<?> propertyType = propertyDescriptor.getPropertyType(); return Boolean.class.equals(propertyType) || String.class.equals(propertyType) || isIntegral(propertyType); } @Override public String mapKeyName(PropertyDescriptor descriptor) { return descriptor.getDisplayName().replaceAll("(.)(\\p{Upper})", "$1-$2").toLowerCase(); } @Override public String mapName(PropertyDescriptor descriptor) { return prefix + ".i18n." + descriptor.getDisplayName().replaceAll("(.)(\\p{Upper})", "$1-$2").toLowerCase(); } @Override public String mapValue(PropertyDescriptor propertyDescriptor, Object instance) { Object result = ReflectionUtils.invokeMethod(propertyDescriptor.getReadMethod(), instance); return result == null ? null : String.valueOf(result); } private boolean isIntegral(Class<?> propertyType) { return Integer.class.equals(propertyType) || int.class.equals(propertyType) || long.class.equals(propertyType) || Long.class.equals(propertyType); } @Override public <T> Stream<Property> map(PropertyDescriptor propertyDescriptor, T instance) { Property.Type type = resolveType(propertyDescriptor); return type == null ? Stream.empty() : Stream.of( new Property( type, mapKeyName(propertyDescriptor), mapName(propertyDescriptor), mapValue(propertyDescriptor, instance))); } }
Allowing us to easily map any properties:
Name: role, key: role, value: io.sunshower.stratosphere.core.topology.model.Instance Name: aws.i18n.ami-launch-index, key: ami-launch-index, value: 0 Name: aws.i18n.architecture, key: architecture, value: x86_64 Name: aws.i18n.client-token, key: client-token, value: sunsh-WebSe-1KKFOLRIA853V Name: aws.i18n.ebs-optimized, key: ebs-optimized, value: false Name: aws.i18n.ena-support, key: ena-support, value: true Name: aws.i18n.hypervisor, key: hypervisor, value: xen ...etc.