/*
* Copyright 2012 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.discovery.provider;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Map;
import com.netflix.discovery.converters.wrappers.CodecWrappers;
import com.netflix.discovery.converters.wrappers.CodecWrappers.LegacyJacksonJson;
import com.netflix.discovery.converters.wrappers.DecoderWrapper;
import com.netflix.discovery.converters.wrappers.EncoderWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A custom provider implementation for Jersey that dispatches to the
* implementation that serializes/deserializes objects sent to and from eureka
* server.
*
* @author Karthik Ranganathan
*/
@Provider
@Produces({"application/json", "application/xml"})
@Consumes("*/*")
public class DiscoveryJerseyProvider implements MessageBodyWriter<Object>, MessageBodyReader<Object> {
private static final Logger LOGGER = LoggerFactory.getLogger(DiscoveryJerseyProvider.class);
private final EncoderWrapper jsonEncoder;
private final DecoderWrapper jsonDecoder;
// XML support is maintained for legacy/custom clients. These codecs are used only on the server side only, while
// Eureka client is using JSON only.
private final EncoderWrapper xmlEncoder;
private final DecoderWrapper xmlDecoder;
public DiscoveryJerseyProvider() {
this(null, null);
}
public DiscoveryJerseyProvider(EncoderWrapper jsonEncoder, DecoderWrapper jsonDecoder) {
this.jsonEncoder = jsonEncoder == null ? CodecWrappers.getEncoder(LegacyJacksonJson.class) : jsonEncoder;
this.jsonDecoder = jsonDecoder == null ? CodecWrappers.getDecoder(LegacyJacksonJson.class) : jsonDecoder;
LOGGER.info("Using JSON encoding codec {}", this.jsonEncoder.codecName());
LOGGER.info("Using JSON decoding codec {}", this.jsonDecoder.codecName());
if (jsonEncoder instanceof CodecWrappers.JacksonJsonMini) {
throw new UnsupportedOperationException("Encoder: " + jsonEncoder.codecName() + "is not supported for the client");
}
this.xmlEncoder = CodecWrappers.getEncoder(CodecWrappers.XStreamXml.class);
this.xmlDecoder = CodecWrappers.getDecoder(CodecWrappers.XStreamXml.class);
LOGGER.info("Using XML encoding codec {}", this.xmlEncoder.codecName());
LOGGER.info("Using XML decoding codec {}", this.xmlDecoder.codecName());
}
@Override
public boolean isReadable(Class serializableClass, Type type, Annotation[] annotations, MediaType mediaType) {
return isSupportedMediaType(mediaType) && isSupportedCharset(mediaType) && isSupportedEntity(serializableClass);
}
@Override
public Object readFrom(Class serializableClass, Type type,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap headers, InputStream inputStream) throws IOException {
DecoderWrapper decoder;
if (MediaType.MEDIA_TYPE_WILDCARD.equals(mediaType.getSubtype())) {
decoder = xmlDecoder;
} else if ("json".equalsIgnoreCase(mediaType.getSubtype())) {
decoder = jsonDecoder;
} else {
decoder = xmlDecoder; // default
}
try {
return decoder.decode(inputStream, serializableClass);
} catch (Throwable e) {
if (e instanceof Error) { // See issue: https://github.com/Netflix/eureka/issues/72 on why we catch Error here.
closeInputOnError(inputStream);
throw new WebApplicationException(createErrorReply(500, e, mediaType));
}
LOGGER.debug("Cannot parse request body", e);
throw new WebApplicationException(createErrorReply(400, "cannot parse request body", mediaType));
}
}
@Override
public long getSize(Object serializableObject, Class serializableClass, Type type, Annotation[] annotations, MediaType mediaType) {
return -1;
}
@Override
public boolean isWriteable(Class serializableClass, Type type, Annotation[] annotations, MediaType mediaType) {
return isSupportedMediaType(mediaType) && isSupportedEntity(serializableClass);
}
@Override
public void writeTo(Object serializableObject, Class serializableClass,
Type type, Annotation[] annotations, MediaType mediaType,
MultivaluedMap headers, OutputStream outputStream) throws IOException, WebApplicationException {
EncoderWrapper encoder = "json".equalsIgnoreCase(mediaType.getSubtype()) ? jsonEncoder : xmlEncoder;
// XML codec may not be available
if (encoder == null) {
throw new WebApplicationException(createErrorReply(400, "No codec available to serialize content type " + mediaType, mediaType));
}
encoder.encode(serializableObject, outputStream);
}
private boolean isSupportedMediaType(MediaType mediaType) {
if (MediaType.APPLICATION_JSON_TYPE.isCompatible(mediaType)) {
return true;
}
if (MediaType.APPLICATION_XML_TYPE.isCompatible(mediaType)) {
return xmlDecoder != null;
}
return false;
}
/**
* As content is cached, we expect both ends use UTF-8 always. If no content charset encoding is explicitly
* defined, UTF-8 is assumed as a default.
* As legacy clients may use ISO 8859-1 we accept it as well, although result may be unspecified if
* characters out of ASCII 0-127 range are used.
*/
private static boolean isSupportedCharset(MediaType mediaType) {
Map<String, String> parameters = mediaType.getParameters();
if (parameters == null || parameters.isEmpty()) {
return true;
}
String charset = parameters.get("charset");
return charset == null
|| "UTF-8".equalsIgnoreCase(charset)
|| "ISO-8859-1".equalsIgnoreCase(charset);
}
/**
* Checks for the {@link Serializer} annotation for the given class.
*
* @param entityType The class to be serialized/deserialized.
* @return true if the annotation is present, false otherwise.
*/
private static boolean isSupportedEntity(Class<?> entityType) {
try {
Annotation annotation = entityType.getAnnotation(Serializer.class);
if (annotation != null) {
return true;
}
} catch (Throwable th) {
LOGGER.warn("Exception in checking for annotations", th);
}
return false;
}
private static Response createErrorReply(int status, Throwable cause, MediaType mediaType) {
StringBuilder sb = new StringBuilder(cause.getClass().getName());
if (cause.getMessage() != null) {
sb.append(": ").append(cause.getMessage());
}
return createErrorReply(status, sb.toString(), mediaType);
}
private static Response createErrorReply(int status, String errorMessage, MediaType mediaType) {
String message;
if (MediaType.APPLICATION_JSON_TYPE.equals(mediaType)) {
message = "{\"error\": \"" + errorMessage + "\"}";
} else {
message = "<error><message>" + errorMessage + "</message></error>";
}
return Response.status(status).entity(message).type(mediaType).build();
}
private static void closeInputOnError(InputStream inputStream) {
if (inputStream != null) {
LOGGER.error("Unexpected error occurred during de-serialization of discovery data, done connection cleanup");
try {
inputStream.close();
} catch (IOException e) {
LOGGER.debug("Cannot close input", e);
}
}
}
}