package org.odata4j.examples.jersey.consumer;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response.StatusType;
import org.core4j.Enumerable;
import org.core4j.xml.XDocument;
import org.core4j.xml.XmlFormat;
import org.odata4j.consumer.AbstractODataClient;
import org.odata4j.consumer.ConsumerBatchRequestHelper;
import org.odata4j.consumer.ODataClientBatchResponse;
import org.odata4j.consumer.ODataClientRequest;
import org.odata4j.consumer.ODataClientResponse;
import org.odata4j.consumer.ODataConsumer;
import org.odata4j.consumer.behaviors.OClientBehavior;
import org.odata4j.consumer.behaviors.OClientBehaviors;
import org.odata4j.core.OBatchSupport;
import org.odata4j.core.OChangeSetRequest;
import org.odata4j.core.ODataConstants;
import org.odata4j.core.ODataConstants.Charsets;
import org.odata4j.core.ODataVersion;
import org.odata4j.core.OError;
import org.odata4j.core.OErrors;
import org.odata4j.core.Throwables;
import org.odata4j.exceptions.BadRequestException;
import org.odata4j.exceptions.ODataProducerException;
import org.odata4j.exceptions.ODataProducerExceptions;
import org.odata4j.exceptions.ServerErrorException;
import org.odata4j.format.Entry;
import org.odata4j.format.FormatParserFactory;
import org.odata4j.format.FormatType;
import org.odata4j.format.FormatWriter;
import org.odata4j.format.FormatWriterFactory;
import org.odata4j.format.Parameters;
import org.odata4j.format.SingleLink;
import org.odata4j.internal.BOMWorkaroundReader;
import org.odata4j.internal.InternalUtil;
import org.odata4j.stax2.XMLEventReader2;
import org.odata4j.stax2.util.StaxUtil;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.api.client.PartialRequestBuilder;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.core.header.reader.HttpHeaderReader;
import com.sun.jersey.multipart.BodyPart;
import com.sun.jersey.multipart.MultiPart;
/**
* OData client based on Jersey.
*/
class ODataJerseyClient extends AbstractODataClient {
public static final String MULTIPART_BASE = "multipart";
public static final MediaType MULTIPART_BASE_TYPE = new MediaType(MULTIPART_BASE, null);
private final OClientBehavior[] requiredBehaviors = new OClientBehavior[] { OClientBehaviors.methodTunneling("MERGE") }; // jersey hates MERGE, tunnel through POST
private final OClientBehavior[] behaviors;
private final Client client;
public ODataJerseyClient(FormatType type, JerseyClientFactory clientFactory, OClientBehavior... behaviors) {
super(type);
this.behaviors = Enumerable.create(requiredBehaviors).concat(Enumerable.create(behaviors)).toArray(OClientBehavior.class);
this.client = JerseyClientUtil.newClient(clientFactory, behaviors);
}
/**
* Sets the ChunkedEncodingSize for jersey client.
*/
private int setClientChunKSize() {
//default to 32 MB
int clientChunKSize = 32 * 1024 * 1024;;
String chunkedEncodingSizeVarValue = InternalUtil.getSystemPropertyValue(ODataConstants.JERSEY_CLIENT_CHUNKED_ENCODING_SIZE);
if (chunkedEncodingSizeVarValue != null && !chunkedEncodingSizeVarValue.isEmpty()) {
try {
//The value passed on the system variable is in MB and we need to convert it to bytes
clientChunKSize= Integer.parseInt(chunkedEncodingSizeVarValue) * 1024 * 1024;;
} catch (NumberFormatException numFormatException) {
// We ignore the exception and use default;
}
}
return clientChunKSize;
}
public Reader getFeedReader(ODataClientResponse response) {
ClientResponse clientResponse = ((JerseyClientResponse) response).getClientResponse();
if (ODataConsumer.dump.responseBody()) {
String textEntity = clientResponse.getEntity(String.class);
dumpResponseBody(textEntity, clientResponse.getType());
return new BOMWorkaroundReader(new StringReader(textEntity));
}
InputStream textEntity = clientResponse.getEntityInputStream();
try {
return new BOMWorkaroundReader(new InputStreamReader(textEntity, Charsets.Upper.UTF_8));
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
public String requestBody(FormatType formatType, ODataClientRequest request) throws ODataProducerException {
ODataClientResponse response = doRequest(formatType, request, Status.OK);
String entity = ((JerseyClientResponse) response).getClientResponse().getEntity(String.class);
response.close();
return entity;
}
/**
* This is consumer side to create a batch request, and then handle the response.
* @param batchRequest
* the batchRequest is a POST with end point $batch, the content-type should be multipart/mixed.
* @param childRequests
* this is a list of the operation that will be part of the batch request. it can also contain
* change set.
*
*/
@Override
public List<ODataClientBatchResponse> batchRequest(FormatType reqType1, ODataClientRequest batchRequest, List<?> childRequests) {
List<ODataClientBatchResponse> result = null;
if (behaviors != null) {
for (OClientBehavior behavior : behaviors)
batchRequest = behavior.transform(batchRequest);
}
WebResource webResource = JerseyClientUtil.resource(client, batchRequest.getUrl(), behaviors);
WebResource.Builder b = webResource.getRequestBuilder();
String boundary = null;
String cType = null;
FormatType formatType = getFormatType();
// set headers
b = b.accept(formatType.getAcceptableMediaTypes());
for (String header : batchRequest.getHeaders().keySet()) {
// parse content type to get the boundary string
if (header.equals(ODataConstants.Headers.CONTENT_TYPE)) {
cType = batchRequest.getHeaders().get(header);
boundary = cType.substring(cType.indexOf('=') + 1);
}
b.header(header, batchRequest.getHeaders().get(header));
}
if (ODataConsumer.dump.requestHeaders())
dumpHeaders(batchRequest, webResource, b);
if (boundary == null) {
throw new BadRequestException("batchRequest's content type should contain boundary");
}
// now create the pay load for the batch request
StringBuilder sb = new StringBuilder();
for (Object req : childRequests) {
// new way to add the request
sb.append("\n--").append(boundary).append("\n");
sb.append(((OBatchSupport) req).formatRequest(this.getFormatType()));
}
// ending the batch multi part
if (childRequests.size() > 0) {
sb.append("\n--").append(boundary).append("--\n");
}
String entity = sb.toString();
b.entity(entity, cType);
if (ODataConsumer.dump.requestBody()) {
dump(entity);
}
// execute request
ClientResponse response = null;
try {
response = b.method(batchRequest.getMethod(), ClientResponse.class);
Integer status = response.getStatus();
String responseContentType = response.getHeaders().getFirst(ODataConstants.Headers.CONTENT_TYPE);
MediaType mType = getMediaType(responseContentType);
// check the response if it is multi part, if not, an error occured, throw exception
if (!mType.isCompatible(MULTIPART_BASE_TYPE)) {
String errMsg = response.getEntity(String.class);
OError error = OErrors.error(status.toString(), errMsg, null);
throw new ServerErrorException.Factory().createException(error);
}
result = parseResponse(response, childRequests);
} catch (ClientHandlerException e) {
Throwables.propagate(e);
}
return result;
}
private List<ODataClientBatchResponse> parseResponse(ClientResponse response, List<?> childRequests) {
ODataVersion version = InternalUtil.getDataServiceVersion(response.getHeaders()
.getFirst(ODataConstants.Headers.DATA_SERVICE_VERSION));
MultiPart mp = response.getEntity(MultiPart.class); // input stream can only be consumed once
// this is the list will hold individual request result.
List<ODataClientBatchResponse> batchResultList = new ArrayList<ODataClientBatchResponse>(childRequests.size());
if (ODataConsumer.dump.responseHeaders()) {
dumpHeaders(response);
}
int i = 0;
for (BodyPart bp : mp.getBodyParts()) {
ODataClientBatchResponse ocbr = null;
MediaType cType = bp.getMediaType();
if (cType.isCompatible(new MediaType("multipart", "mixed"))) {
MultiPart cmp = bp.getEntityAs(MultiPart.class);
OChangeSetRequest csr = (OChangeSetRequest) childRequests.get(i);
List<String> payloadList = new ArrayList<String>();
for (BodyPart cbp : cmp.getBodyParts()) {
String content = cbp.getEntityAs(String.class);
payloadList.add(content);
}
ocbr = ConsumerBatchRequestHelper.parseChangeSetOperationResponse(version, payloadList, csr, getFormatType());
} else {
String content = bp.getEntityAs(String.class);
OBatchSupport so = (OBatchSupport) childRequests.get(i);
ocbr = ConsumerBatchRequestHelper.parseSingleOperationResponse(version, content, so, getFormatType());
}
batchResultList.add(ocbr);
i++;
}
return batchResultList;
}
@SuppressWarnings("unchecked")
protected ODataClientResponse doRequest(FormatType reqType, ODataClientRequest request, StatusType... expectedResponseStatus) throws ODataProducerException {
if (behaviors != null) {
for (OClientBehavior behavior : behaviors)
request = behavior.transform(request);
}
if(request.getPayload() != null && request.getPayload() instanceof InputStream) {
this.client.setChunkedEncodingSize(setClientChunKSize());
}
WebResource webResource = JerseyClientUtil.resource(client, request.getUrl(), behaviors);
// set query params
for (String qpn : request.getQueryParams().keySet())
webResource = webResource.queryParam(qpn, request.getQueryParams().get(qpn));
WebResource.Builder b = webResource.getRequestBuilder();
// set headers
b = b.accept(reqType.getAcceptableMediaTypes());
for (String header : request.getHeaders().keySet())
b.header(header, request.getHeaders().get(header));
if (!request.getHeaders().containsKey(ODataConstants.Headers.USER_AGENT))
b.header(ODataConstants.Headers.USER_AGENT, "odata4j.org");
if (ODataConsumer.dump.requestHeaders())
dumpHeaders(request, webResource, b);
// request body
if (request.getPayload() != null) {
Class<?> payloadClass;
if (request.getPayload() instanceof Entry)
payloadClass = Entry.class;
else if (request.getPayload() instanceof SingleLink)
payloadClass = SingleLink.class;
else if (request.getPayload() instanceof Parameters)
payloadClass = Parameters.class;
else if (request.getPayload() instanceof InputStream)
payloadClass = InputStream.class;
else
throw new IllegalArgumentException("Unsupported payload: " + request.getPayload());
if (request.getPayload() instanceof InputStream) {
// send media stream as payload
String contentType = request.getHeaders().containsKey(ODataConstants.Headers.CONTENT_TYPE)
? request.getHeaders().get(ODataConstants.Headers.CONTENT_TYPE)
: ODataConstants.APPLICATION_OCTET_STREAM;
b.entity(request.getPayload(), contentType);
} else {
StringWriter sw = new StringWriter();
FormatWriter<Object> fw = (FormatWriter<Object>)
FormatWriterFactory.getFormatWriter(payloadClass, null, this.getFormatType().toString(), null);
fw.write(null, sw, request.getPayload());
String entity = sw.toString();
if (ODataConsumer.dump.requestBody())
dump(entity);
// allow the client to override the default format writer content-type
String contentType = request.getHeaders().containsKey(ODataConstants.Headers.CONTENT_TYPE)
? request.getHeaders().get(ODataConstants.Headers.CONTENT_TYPE)
: fw.getContentType();
b.entity(entity, contentType);
}
}
// execute request
ClientResponse response = null;
try {
response = b.method(request.getMethod(), ClientResponse.class);
} catch (ClientHandlerException e) {
Throwables.propagate(e);
}
if (ODataConsumer.dump.responseHeaders())
dumpHeaders(response);
StatusType status = response.getClientResponseStatus();
for (StatusType expStatus : expectedResponseStatus)
if (expStatus.getStatusCode() == status.getStatusCode())
return new JerseyClientResponse(response);
// the server responded with an unexpected status
RuntimeException exception;
String textEntity = response.getEntity(String.class); // input stream can only be consumed once
try {
// report error as ODataProducerException in case we get a well-formed OData error...
MediaType contentType = response.getType();
OError error = FormatParserFactory.getParser(OError.class, contentType, null).parse(new StringReader(textEntity));
exception = ODataProducerExceptions.create(status, error);
} catch (RuntimeException e) {
// ... otherwise throw a RuntimeError
exception = new RuntimeException(String.format("Expected status %s, found %s. Server response:",
Enumerable.create(expectedResponseStatus).join(" or "), status) + "\n" + textEntity, e);
}
throw exception;
}
protected XMLEventReader2 toXml(ODataClientResponse response) {
ClientResponse clientResponse = ((JerseyClientResponse) response).getClientResponse();
if (ODataConsumer.dump.responseBody()) {
String textEntity = clientResponse.getEntity(String.class);
dumpResponseBody(textEntity, clientResponse.getType());
return StaxUtil.newXMLEventReader(new BOMWorkaroundReader(new StringReader(textEntity)));
}
InputStream textEntity = clientResponse.getEntityInputStream();
try {
return StaxUtil.newXMLEventReader(new BOMWorkaroundReader(new InputStreamReader(textEntity, Charsets.Upper.UTF_8)));
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
private void dumpResponseBody(String textEntity, MediaType type) {
String logXml = textEntity;
if (type.toString().contains("xml") || logXml != null && logXml.startsWith("<feed")) {
try {
logXml = XDocument.parse(logXml).toString(XmlFormat.INDENTED);
} catch (Exception ignore) {}
}
dump(logXml);
}
private void dumpHeaders(ClientResponse response) {
dump("Status: " + response.getStatus());
dump(response.getHeaders());
}
private static boolean dontTryRequestHeaders;
@SuppressWarnings("unchecked")
private MultivaluedMap<String, Object> getRequestHeaders(WebResource.Builder b) {
if (dontTryRequestHeaders)
return null;
// protected MultivaluedMap<String, Object> metadata;
try {
Field f = PartialRequestBuilder.class.getDeclaredField("metadata");
f.setAccessible(true);
return (MultivaluedMap<String, Object>) f.get(b);
} catch (Exception e) {
dontTryRequestHeaders = true;
return null;
}
}
private void dumpHeaders(ODataClientRequest request, WebResource webResource, WebResource.Builder b) {
dump(request.getMethod() + " " + webResource);
dump(getRequestHeaders(b));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void dump(MultivaluedMap headers) {
if (headers == null)
return;
for (Object header : headers.keySet())
dump(header + ": " + headers.getFirst(header));
}
private static void dump(String message) {
System.out.println(message);
}
/**
* create the MediaType instance based on content type
* @param contentType
* @return
*/
public static MediaType getMediaType(String contentType) {
try {
List<MediaType> list = new ArrayList<MediaType>();
list = HttpHeaderReader.readMediaTypes(list, contentType);
return list.get(0);
} catch (ParseException e) {
throw new IllegalArgumentException("cannot parse the content type " + contentType);
}
}
@Override
public Reader getFeedReader(String textEntity) {
if (ODataConsumer.dump.responseBody()) {
dump(textEntity);
}
return new BOMWorkaroundReader(new StringReader(textEntity));
}
}