package com.netflix.discovery.converters; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.node.ArrayNode; import com.netflix.appinfo.AmazonInfo; import com.netflix.appinfo.DataCenterInfo; import com.netflix.appinfo.DataCenterInfo.Name; import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.InstanceInfo.ActionType; import com.netflix.appinfo.InstanceInfo.InstanceStatus; import com.netflix.appinfo.InstanceInfo.PortType; import com.netflix.appinfo.LeaseInfo; import com.netflix.discovery.DiscoveryManager; import com.netflix.discovery.EurekaClientConfig; import com.netflix.discovery.shared.Application; import com.netflix.discovery.shared.Applications; import com.netflix.discovery.util.StringCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Tomasz Bak * @author Spencer Gibb */ public class EurekaJacksonCodec { private static final Logger logger = LoggerFactory.getLogger(EurekaJacksonCodec.class); private static final Version VERSION = new Version(1, 1, 0, null); public static final String NODE_LEASE = "leaseInfo"; public static final String NODE_METADATA = "metadata"; public static final String NODE_DATACENTER = "dataCenterInfo"; public static final String NODE_APP = "application"; protected static final String ELEM_INSTANCE = "instance"; protected static final String ELEM_OVERRIDDEN_STATUS = "overriddenstatus"; protected static final String ELEM_HOST = "hostName"; protected static final String ELEM_INSTANCE_ID = "instanceId"; protected static final String ELEM_APP = "app"; protected static final String ELEM_IP = "ipAddr"; protected static final String ELEM_SID = "sid"; protected static final String ELEM_STATUS = "status"; protected static final String ELEM_PORT = "port"; protected static final String ELEM_SECURE_PORT = "securePort"; protected static final String ELEM_COUNTRY_ID = "countryId"; protected static final String ELEM_IDENTIFYING_ATTR = "identifyingAttribute"; protected static final String ELEM_HEALTHCHECKURL = "healthCheckUrl"; protected static final String ELEM_SECHEALTHCHECKURL = "secureHealthCheckUrl"; protected static final String ELEM_APPGROUPNAME = "appGroupName"; protected static final String ELEM_HOMEPAGEURL = "homePageUrl"; protected static final String ELEM_STATUSPAGEURL = "statusPageUrl"; protected static final String ELEM_VIPADDRESS = "vipAddress"; protected static final String ELEM_SECVIPADDRESS = "secureVipAddress"; protected static final String ELEM_ISCOORDINATINGDISCSOERVER = "isCoordinatingDiscoveryServer"; protected static final String ELEM_LASTUPDATEDTS = "lastUpdatedTimestamp"; protected static final String ELEM_LASTDIRTYTS = "lastDirtyTimestamp"; protected static final String ELEM_ACTIONTYPE = "actionType"; protected static final String ELEM_ASGNAME = "asgName"; protected static final String ELEM_NAME = "name"; protected static final String DATACENTER_METADATA = "metadata"; protected static final String VERSIONS_DELTA_TEMPLATE = "versions_delta"; protected static final String APPS_HASHCODE_TEMPTE = "apps_hashcode"; public static EurekaJacksonCodec INSTANCE = new EurekaJacksonCodec(); /** * XStream codec supports character replacement in field names to generate XML friendly * names. This feature is also configurable, and replacement strings can be provided by a user. * To obey these rules, version and apppsHash key field names must be formatted according to the provided * configuration, which by default replaces '_' with '__' (double underscores). */ private final String versionDeltaKey; private final String appHashCodeKey; private final ObjectMapper mapper; private final Map<Class<?>, ObjectReader> objectReaderByClass; private final Map<Class<?>, ObjectWriter> objectWriterByClass; public EurekaJacksonCodec() { this.versionDeltaKey = formatKey(VERSIONS_DELTA_TEMPLATE); this.appHashCodeKey = formatKey(APPS_HASHCODE_TEMPTE); this.mapper = new ObjectMapper(); this.mapper.setSerializationInclusion(Include.NON_NULL); SimpleModule module = new SimpleModule("eureka1.x", VERSION); module.addSerializer(DataCenterInfo.class, new DataCenterInfoSerializer()); module.addSerializer(InstanceInfo.class, new InstanceInfoSerializer()); module.addSerializer(Application.class, new ApplicationSerializer()); module.addSerializer(Applications.class, new ApplicationsSerializer(this.versionDeltaKey, this.appHashCodeKey)); module.addDeserializer(DataCenterInfo.class, new DataCenterInfoDeserializer()); module.addDeserializer(LeaseInfo.class, new LeaseInfoDeserializer()); module.addDeserializer(InstanceInfo.class, new InstanceInfoDeserializer(this.mapper)); module.addDeserializer(Application.class, new ApplicationDeserializer(this.mapper)); module.addDeserializer(Applications.class, new ApplicationsDeserializer(this.mapper, this.versionDeltaKey, this.appHashCodeKey)); this.mapper.registerModule(module); HashMap<Class<?>, ObjectReader> readers = new HashMap<>(); readers.put(InstanceInfo.class, mapper.reader().withType(InstanceInfo.class).withRootName("instance")); readers.put(Application.class, mapper.reader().withType(Application.class).withRootName("application")); readers.put(Applications.class, mapper.reader().withType(Applications.class).withRootName("applications")); this.objectReaderByClass = readers; HashMap<Class<?>, ObjectWriter> writers = new HashMap<>(); writers.put(InstanceInfo.class, mapper.writer().withType(InstanceInfo.class).withRootName("instance")); writers.put(Application.class, mapper.writer().withType(Application.class).withRootName("application")); writers.put(Applications.class, mapper.writer().withType(Applications.class).withRootName("applications")); this.objectWriterByClass = writers; } protected ObjectMapper getMapper() { return mapper; } protected String getVersionDeltaKey() { return versionDeltaKey; } protected String getAppHashCodeKey() { return appHashCodeKey; } protected static String formatKey(String keyTemplate) { EurekaClientConfig clientConfig = DiscoveryManager.getInstance().getEurekaClientConfig(); String replacement; if (clientConfig == null) { replacement = "__"; } else { replacement = clientConfig.getEscapeCharReplacement(); } StringBuilder sb = new StringBuilder(keyTemplate.length() + 1); for (char c : keyTemplate.toCharArray()) { if (c == '_') { sb.append(replacement); } else { sb.append(c); } } return sb.toString(); } public <T> T readValue(Class<T> type, InputStream entityStream) throws IOException { ObjectReader reader = objectReaderByClass.get(type); if (reader == null) { return mapper.readValue(entityStream, type); } return reader.readValue(entityStream); } public <T> T readValue(Class<T> type, String text) throws IOException { ObjectReader reader = objectReaderByClass.get(type); if (reader == null) { return mapper.readValue(text, type); } return reader.readValue(text); } public <T> void writeTo(T object, OutputStream entityStream) throws IOException { ObjectWriter writer = objectWriterByClass.get(object.getClass()); if (writer == null) { mapper.writeValue(entityStream, object); } else { writer.writeValue(entityStream, object); } } public <T> String writeToString(T object) { try { ObjectWriter writer = objectWriterByClass.get(object.getClass()); if (writer == null) { return mapper.writeValueAsString(object); } return writer.writeValueAsString(object); } catch (IOException e) { throw new IllegalArgumentException("Cannot encode provided object", e); } } public static EurekaJacksonCodec getInstance() { return INSTANCE; } public static void setInstance(EurekaJacksonCodec instance) { INSTANCE = instance; } public static class DataCenterInfoSerializer extends JsonSerializer<DataCenterInfo> { @Override public void serializeWithType(DataCenterInfo dataCenterInfo, JsonGenerator jgen, SerializerProvider provider, TypeSerializer typeSer) throws IOException, JsonProcessingException { jgen.writeStartObject(); // XStream encoded adds this for backwards compatibility issue. Not sure if needed anymore if (dataCenterInfo.getName() == Name.Amazon) { jgen.writeStringField("@class", "com.netflix.appinfo.AmazonInfo"); } else { jgen.writeStringField("@class", "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo"); } jgen.writeStringField(ELEM_NAME, dataCenterInfo.getName().name()); if (dataCenterInfo.getName() == Name.Amazon) { AmazonInfo aInfo = (AmazonInfo) dataCenterInfo; jgen.writeObjectField(DATACENTER_METADATA, aInfo.getMetadata()); } jgen.writeEndObject(); } @Override public void serialize(DataCenterInfo dataCenterInfo, JsonGenerator jgen, SerializerProvider provider) throws IOException { serializeWithType(dataCenterInfo, jgen, provider, null); } } public static class DataCenterInfoDeserializer extends JsonDeserializer<DataCenterInfo> { @Override public DataCenterInfo deserialize(JsonParser jp, DeserializationContext context) throws IOException { JsonNode node = jp.getCodec().readTree(jp); final Name name = Name.valueOf(node.get(ELEM_NAME).asText()); if (name != Name.Amazon) { return new DataCenterInfo() { @Override public Name getName() { return name; } }; } Map<String, String> metaData = new HashMap<String, String>(); JsonNode metaNode = node.get(DATACENTER_METADATA); Iterator<String> metaNamesIt = metaNode.fieldNames(); while (metaNamesIt.hasNext()) { String key = metaNamesIt.next(); String value = metaNode.get(key).asText(); metaData.put(StringCache.intern(key), StringCache.intern(value)); } AmazonInfo amazonInfo = new AmazonInfo(); amazonInfo.setMetadata(metaData); return amazonInfo; } } public static class LeaseInfoDeserializer extends JsonDeserializer<LeaseInfo> { protected static final String ELEM_RENEW_INT = "renewalIntervalInSecs"; protected static final String ELEM_DURATION = "durationInSecs"; protected static final String ELEM_REG_TIMESTAMP = "registrationTimestamp"; protected static final String ELEM_LAST_RENEW_TIMESTAMP = "lastRenewalTimestamp"; protected static final String ELEM_EVICTION_TIMESTAMP = "evictionTimestamp"; protected static final String ELEM_SERVICE_UP_TIMESTAMP = "serviceUpTimestamp"; @Override public LeaseInfo deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { LeaseInfo.Builder builder = LeaseInfo.Builder.newBuilder(); JsonNode node = jp.getCodec().readTree(jp); Iterator<String> fieldNames = node.fieldNames(); while (fieldNames.hasNext()) { String nodeName = fieldNames.next(); if (!node.get(nodeName).isNull()) { long longValue = node.get(nodeName).asLong(); if (ELEM_DURATION.equals(nodeName)) { builder.setDurationInSecs((int) longValue); } else if (ELEM_EVICTION_TIMESTAMP.equals(nodeName)) { builder.setEvictionTimestamp(longValue); } else if (ELEM_LAST_RENEW_TIMESTAMP.equals(nodeName)) { builder.setRenewalTimestamp(longValue); } else if (ELEM_REG_TIMESTAMP.equals(nodeName)) { builder.setRegistrationTimestamp(longValue); } else if (ELEM_RENEW_INT.equals(nodeName)) { builder.setRenewalIntervalInSecs((int) longValue); } else if (ELEM_SERVICE_UP_TIMESTAMP.equals(nodeName)) { builder.setServiceUpTimestamp(longValue); } } } return builder.build(); } } public static class InstanceInfoSerializer extends JsonSerializer<InstanceInfo> { // For backwards compatibility public static final String METADATA_COMPATIBILITY_KEY = "@class"; public static final String METADATA_COMPATIBILITY_VALUE = "java.util.Collections$EmptyMap"; protected static final Object EMPTY_METADATA = Collections.singletonMap(METADATA_COMPATIBILITY_KEY, METADATA_COMPATIBILITY_VALUE); @Override public void serialize(InstanceInfo info, JsonGenerator jgen, SerializerProvider provider) throws IOException { jgen.writeStartObject(); if (info.getInstanceId() != null) { jgen.writeStringField(ELEM_INSTANCE_ID, info.getInstanceId()); } jgen.writeStringField(ELEM_HOST, info.getHostName()); jgen.writeStringField(ELEM_APP, info.getAppName()); jgen.writeStringField(ELEM_IP, info.getIPAddr()); if (!("unknown".equals(info.getSID()) || "na".equals(info.getSID()))) { jgen.writeStringField(ELEM_SID, info.getSID()); } jgen.writeStringField(ELEM_STATUS, info.getStatus().name()); jgen.writeStringField(ELEM_OVERRIDDEN_STATUS, info.getOverriddenStatus().name()); jgen.writeFieldName(ELEM_PORT); jgen.writeStartObject(); jgen.writeNumberField("$", info.getPort()); jgen.writeStringField("@enabled", Boolean.toString(info.isPortEnabled(PortType.UNSECURE))); jgen.writeEndObject(); jgen.writeFieldName(ELEM_SECURE_PORT); jgen.writeStartObject(); jgen.writeNumberField("$", info.getSecurePort()); jgen.writeStringField("@enabled", Boolean.toString(info.isPortEnabled(PortType.SECURE))); jgen.writeEndObject(); jgen.writeNumberField(ELEM_COUNTRY_ID, info.getCountryId()); if (info.getDataCenterInfo() != null) { jgen.writeObjectField(NODE_DATACENTER, info.getDataCenterInfo()); } if (info.getLeaseInfo() != null) { jgen.writeObjectField(NODE_LEASE, info.getLeaseInfo()); } Map<String, String> metadata = info.getMetadata(); if (metadata != null) { if (metadata.isEmpty()) { jgen.writeObjectField(NODE_METADATA, EMPTY_METADATA); } else { jgen.writeObjectField(NODE_METADATA, metadata); } } autoMarshalEligible(info, jgen); jgen.writeEndObject(); } protected void autoMarshalEligible(Object o, JsonGenerator jgen) { try { Class c = o.getClass(); Field[] fields = c.getDeclaredFields(); Annotation annotation; for (Field f : fields) { annotation = f.getAnnotation(Auto.class); if (annotation != null) { f.setAccessible(true); if (f.get(o) != null) { jgen.writeStringField(f.getName(), String.valueOf(f.get(o))); } } } } catch (Throwable th) { logger.error("Error in marshalling the object", th); } } } public static class InstanceInfoDeserializer extends JsonDeserializer<InstanceInfo> { protected ObjectMapper mapper; protected InstanceInfoDeserializer(ObjectMapper mapper) { this.mapper = mapper; } @Override public InstanceInfo deserialize(JsonParser jp, DeserializationContext context) throws IOException { InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(); JsonNode node = jp.getCodec().readTree(jp); /** * These are set via single call to * {@link com.netflix.appinfo.InstanceInfo.Builder#setHealthCheckUrlsForDeser(String, String, String)}. */ String healthChecUrl = null; String healthCheckSecureUrl = null; Iterator<String> fieldNames = node.fieldNames(); while (fieldNames.hasNext()) { String fieldName = fieldNames.next(); JsonNode fieldNode = node.get(fieldName); if (!fieldNode.isNull()) { if (ELEM_HOST.equals(fieldName)) { builder.setHostName(fieldNode.asText()); } else if (ELEM_INSTANCE_ID.equals(fieldName)) { builder.setInstanceId(fieldNode.asText()); } else if (ELEM_APP.equals(fieldName)) { builder.setAppName(fieldNode.asText()); } else if (ELEM_IP.equals(fieldName)) { builder.setIPAddr(fieldNode.asText()); } else if (ELEM_SID.equals(fieldName)) { builder.setSID(fieldNode.asText()); } else if (ELEM_IDENTIFYING_ATTR.equals(fieldName)) { // nothing; } else if (ELEM_STATUS.equals(fieldName)) { builder.setStatus(InstanceStatus.toEnum(fieldNode.asText())); } else if (ELEM_OVERRIDDEN_STATUS.equals(fieldName)) { builder.setOverriddenStatus(InstanceStatus.toEnum(fieldNode.asText())); } else if (ELEM_PORT.equals(fieldName)) { int port = fieldNode.get("$").asInt(); boolean enabled = fieldNode.get("@enabled").asBoolean(); builder.setPort(port); builder.enablePort(PortType.UNSECURE, enabled); } else if (ELEM_SECURE_PORT.equals(fieldName)) { int port = fieldNode.get("$").asInt(); boolean enabled = fieldNode.get("@enabled").asBoolean(); builder.setSecurePort(port); builder.enablePort(PortType.SECURE, enabled); } else if (ELEM_COUNTRY_ID.equals(fieldName)) { builder.setCountryId(Integer.valueOf(fieldNode.asText()).intValue()); } else if (NODE_DATACENTER.equals(fieldName)) { builder.setDataCenterInfo(mapper.treeToValue(fieldNode, DataCenterInfo.class)); } else if (NODE_LEASE.equals(fieldName)) { builder.setLeaseInfo(mapper.treeToValue(fieldNode, LeaseInfo.class)); } else if (NODE_METADATA.equals(fieldName)) { Map<String, String> meta = null; Iterator<String> metaNameIt = fieldNode.fieldNames(); while (metaNameIt.hasNext()) { String key = StringCache.intern(metaNameIt.next()); if (key.equals("@class")) { // For backwards compatibility if (meta == null && !metaNameIt.hasNext()) { // Optimize for empty maps meta = Collections.emptyMap(); } } else { if (meta == null) { meta = new ConcurrentHashMap<String, String>(); } String value = StringCache.intern(fieldNode.get(key).asText()); meta.put(key, value); } } if (meta == null) { meta = Collections.emptyMap(); } builder.setMetadata(meta); } else if (ELEM_HEALTHCHECKURL.equals(fieldName)) { healthChecUrl = fieldNode.asText(); } else if (ELEM_SECHEALTHCHECKURL.equals(fieldName)) { healthCheckSecureUrl = fieldNode.asText(); } else if (ELEM_APPGROUPNAME.equals(fieldName)) { builder.setAppGroupName(fieldNode.asText()); } else if (ELEM_HOMEPAGEURL.equals(fieldName)) { builder.setHomePageUrlForDeser(fieldNode.asText()); } else if (ELEM_STATUSPAGEURL.equals(fieldName)) { builder.setStatusPageUrlForDeser(fieldNode.asText()); } else if (ELEM_VIPADDRESS.equals(fieldName)) { builder.setVIPAddressDeser(fieldNode.asText()); } else if (ELEM_SECVIPADDRESS.equals(fieldName)) { builder.setSecureVIPAddressDeser(fieldNode.asText()); } else if (ELEM_ISCOORDINATINGDISCSOERVER.equals(fieldName)) { builder.setIsCoordinatingDiscoveryServer(fieldNode.asBoolean()); } else if (ELEM_LASTUPDATEDTS.equals(fieldName)) { builder.setLastUpdatedTimestamp(fieldNode.asLong()); } else if (ELEM_LASTDIRTYTS.equals(fieldName)) { builder.setLastDirtyTimestamp(fieldNode.asLong()); } else if (ELEM_ACTIONTYPE.equals(fieldName)) { builder.setActionType(ActionType.valueOf(fieldNode.asText())); } else if (ELEM_ASGNAME.equals(fieldName)) { builder.setASGName(fieldNode.asText()); } else { autoUnmarshalEligible(fieldName, fieldNode.asText(), builder.getRawInstance()); } } } builder.setHealthCheckUrlsForDeser(healthChecUrl, healthCheckSecureUrl); return builder.build(); } protected void autoUnmarshalEligible(String fieldName, String value, Object o) { try { Class c = o.getClass(); Field f = null; try { f = c.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { // TODO XStream version increments metrics counter here } if (f == null) { return; } Annotation annotation = f.getAnnotation(Auto.class); if (annotation == null) { return; } f.setAccessible(true); Class returnClass = f.getType(); if (value != null) { if (!String.class.equals(returnClass)) { Method method = returnClass.getDeclaredMethod("valueOf", java.lang.String.class); Object valueObject = method.invoke(returnClass, value); f.set(o, valueObject); } else { f.set(o, value); } } } catch (Throwable th) { logger.error("Error in unmarshalling the object:", th); } } } public static class ApplicationSerializer extends JsonSerializer<Application> { @Override public void serialize(Application value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { jgen.writeStartObject(); jgen.writeStringField(ELEM_NAME, value.getName()); jgen.writeObjectField(ELEM_INSTANCE, value.getInstances()); jgen.writeEndObject(); } } public static class ApplicationDeserializer extends JsonDeserializer<Application> { protected ObjectMapper mapper; public ApplicationDeserializer(ObjectMapper mapper) { this.mapper = mapper; } @Override public Application deserialize(JsonParser jp, DeserializationContext context) throws IOException { JsonNode node = jp.getCodec().readTree(jp); Application application = new Application(node.get(ELEM_NAME).asText()); JsonNode instanceNode = node.get(ELEM_INSTANCE); if (instanceNode != null) { if (instanceNode instanceof ArrayNode) { ArrayNode instancesNode = (ArrayNode) instanceNode; if (instancesNode != null) { for (JsonNode nextNode : instancesNode) { application.addInstance(mapper.treeToValue(nextNode, InstanceInfo.class)); } } } else { application.addInstance(mapper.treeToValue(instanceNode, InstanceInfo.class)); } } return application; } } public static class ApplicationsSerializer extends JsonSerializer<Applications> { protected String versionDeltaKey; protected String appHashCodeKey; public ApplicationsSerializer(String versionDeltaKey, String appHashCodeKey) { this.versionDeltaKey = versionDeltaKey; this.appHashCodeKey = appHashCodeKey; } @Override public void serialize(Applications applications, JsonGenerator jgen, SerializerProvider provider) throws IOException { jgen.writeStartObject(); jgen.writeStringField(versionDeltaKey, applications.getVersion().toString()); jgen.writeStringField(appHashCodeKey, applications.getAppsHashCode()); jgen.writeObjectField(NODE_APP, applications.getRegisteredApplications()); } } public static class ApplicationsDeserializer extends JsonDeserializer<Applications> { protected ObjectMapper mapper; protected String versionDeltaKey; protected String appHashCodeKey; public ApplicationsDeserializer(ObjectMapper mapper, String versionDeltaKey, String appHashCodeKey) { this.mapper = mapper; this.versionDeltaKey = versionDeltaKey; this.appHashCodeKey = appHashCodeKey; } @Override public Applications deserialize(JsonParser jp, DeserializationContext context) throws IOException { Applications apps = new Applications(); JsonNode node = jp.getCodec().readTree(jp); if (node.get(versionDeltaKey) != null) { apps.setVersion(node.get(versionDeltaKey).asLong()); } if (node.get(appHashCodeKey) != null) { apps.setAppsHashCode(node.get(appHashCodeKey).asText()); } JsonNode appNode = node.get(NODE_APP); if (appNode != null) { if (appNode instanceof ArrayNode) { ArrayNode appsNode = (ArrayNode) appNode; for (JsonNode item : appsNode) { apps.addApplication(mapper.treeToValue(item, Application.class)); } } else { apps.addApplication(mapper.treeToValue(appNode, Application.class)); } } return apps; } } }