Skip to content
This repository was archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
Merge pull request #35 from tmuehl/Issue-34-fix-content-type-detection
Browse files Browse the repository at this point in the history
Issue 34 fix content type detection
  • Loading branch information
jhorstmann authored Jul 7, 2016
2 parents 1b94703 + 390c699 commit b95fa77
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 22 deletions.
5 changes: 0 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,6 @@
</dependency>

<!-- problem handling and dependencies -->
<dependency>
<groupId>org.zalando</groupId>
<artifactId>jackson-datatype-problem</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
Expand Down
7 changes: 3 additions & 4 deletions src/main/java/org/zalando/fahrschein/IOProblem.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
package org.zalando.fahrschein;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.zalando.problem.Problem;

import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;

@JsonDeserialize(using = IOProblemDeserializer.class)
public class IOProblem extends IOException implements Problem {
private final URI type;
private final String title;
private final Response.StatusType status;
private final Optional<String> detail;
private final Optional<URI> instance;

@JsonCreator
public IOProblem(@JsonProperty("type") URI type, @JsonProperty("title") String title, @JsonProperty("status") int status, @JsonProperty("detail") Optional<String> detail, @JsonProperty("instance") Optional<URI> instance) {
public IOProblem(URI type, String title, int status, Optional<String> detail, Optional<URI> instance) {
super(formatMessage(type, title, status));
this.type = type;
this.title = title;
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/org/zalando/fahrschein/IOProblemDeserializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.zalando.fahrschein;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;

import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;

public class IOProblemDeserializer extends JsonDeserializer<IOProblem>{

@Override
public IOProblem deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final JsonNode rootNode = jp.getCodec().readTree(jp);
final String type = rootNode.get("type").asText();
final String title = rootNode.get("title").asText();
final int status = rootNode.get("status").asInt();

final JsonNode detailNode = rootNode.get("detail");
final Optional<String> detail = ofNullable(detailNode != null ? detailNode.asText(null) : null);

final JsonNode instanceNode = rootNode.get("instance");
final String instance = instanceNode != null ? instanceNode.asText(null) : null;

try {
Optional<URI> maybeInstance = instance != null ? Optional.of(new URI(instance)) : empty();

return new IOProblem(new URI(type), title, status, detail, maybeInstance);
} catch (URISyntaxException e) {
throw new IOException("Cannot deserialize IOProblem json", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@
import java.util.Set;

import static java.util.Arrays.asList;
import static org.springframework.util.ObjectUtils.nullSafeEquals;

public class ProblemHandlingClientHttpRequest implements ClientHttpRequest {

private static final Set<String> PROBLEM_CONTENT_TYPES = new HashSet<>(asList("application/json", "application/problem+json"));
public static final MediaType APPLICATION_PROBLEM_JSON = new MediaType("application", "problem+json");
private static final Set<MediaType> PROBLEM_CONTENT_TYPES = new HashSet<>(asList(APPLICATION_PROBLEM_JSON));
private static final URI DEFAULT_PROBLEM_TYPE = URI.create("about:blank");

private final ClientHttpRequest clientHttpRequest;
private final ObjectMapper objectMapper;
private final ObjectMapper objectMapper = new ObjectMapper();

public ProblemHandlingClientHttpRequest(ClientHttpRequest clientHttpRequest, ObjectMapper objectMapper) {
public ProblemHandlingClientHttpRequest(ClientHttpRequest clientHttpRequest) {
this.clientHttpRequest = clientHttpRequest;
this.objectMapper = objectMapper;
}

@Override
Expand All @@ -38,7 +39,7 @@ public ClientHttpResponse execute() throws IOException {
if (!statusCode.is2xxSuccessful()) {

final MediaType contentType = response.getHeaders().getContentType();
if (contentType != null && PROBLEM_CONTENT_TYPES.contains(contentType.getType())) {
if (isProblemContentType(contentType)) {
try (InputStream is = response.getBody()) {
final IOProblem problem = objectMapper.readValue(is, IOProblem.class);
if (problem != null) {
Expand All @@ -55,6 +56,21 @@ public ClientHttpResponse execute() throws IOException {
return response;
}

private boolean isProblemContentType(final MediaType contentType) {
if (contentType == null) {
return false;
}

for (MediaType problemContentType : PROBLEM_CONTENT_TYPES) {
if (nullSafeEquals(problemContentType.getType(), contentType.getType())
&& nullSafeEquals(problemContentType.getSubtype(), contentType.getSubtype())) {
return true;
}
}

return false;
}

@Override
public HttpMethod getMethod() {
return clientHttpRequest.getMethod();
Expand All @@ -74,4 +90,5 @@ public HttpHeaders getHeaders() {
public OutputStream getBody() throws IOException {
return clientHttpRequest.getBody();
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.zalando.fahrschein;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
Expand All @@ -10,16 +9,14 @@

public class ProblemHandlingClientHttpRequestFactory implements ClientHttpRequestFactory {
private final ClientHttpRequestFactory delegate;
private final ObjectMapper objectMapper;

public ProblemHandlingClientHttpRequestFactory(ClientHttpRequestFactory delegate, ObjectMapper objectMapper) {
public ProblemHandlingClientHttpRequestFactory(ClientHttpRequestFactory delegate) {
this.delegate = delegate;
this.objectMapper = objectMapper;
}

@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
final ClientHttpRequest request = delegate.createRequest(uri, httpMethod);
return new ProblemHandlingClientHttpRequest(request, objectMapper);
return new ProblemHandlingClientHttpRequest(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.zalando.fahrschein;

import org.junit.Assert;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class ProblemHandlingClientHttpRequestTest {

public static final Charset UTF8 = Charset.forName("UTF-8");

@Test
public void recognisesJsonAndProblemBodies() throws IOException {
final ClientHttpRequest clientHttpRequest = mock(ClientHttpRequest.class);

when(clientHttpRequest.execute()).thenReturn(createResponse());

final ProblemHandlingClientHttpRequest problemHandlingClientHttpRequest =
new ProblemHandlingClientHttpRequest(clientHttpRequest);

Exception actualException = null;
try {
problemHandlingClientHttpRequest.execute();
Assert.fail("No exception was thrown.");
} catch (Exception e) {
actualException = e;
}

assertThat("Expected different exception type.", actualException, instanceOf(IOProblem.class));

final IOProblem ioProblem = (IOProblem) actualException;

assertThat(ioProblem.getTitle(), equalTo("Not Found"));
assertThat(ioProblem.getDetail().get(), equalTo("EventType does not exist."));
}

private ClientHttpResponse createResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST;
}

@Override
public int getRawStatusCode() throws IOException {
return 400;
}

@Override
public String getStatusText() throws IOException {
return "Bad Request";
}

@Override
public void close() {
// do nothing
}

@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("{\"type\":\"http://httpstatus.es/404\",\"title\":\"Not Found\",\"status\":404,\"detail\":\"EventType does not exist.\"}".getBytes(UTF8));
}

@Override
public HttpHeaders getHeaders() {
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Content-type", "application/problem+json;charset=UTF-8");
return httpHeaders;
}
};
}


}
4 changes: 1 addition & 3 deletions src/test/java/org/zalando/fahrschein/salesorder/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import org.zalando.fahrschein.ZignAccessTokenProvider;
import org.zalando.fahrschein.salesorder.domain.SalesOrderPlaced;
import org.zalando.jackson.datatype.money.MoneyModule;
import org.zalando.problem.ProblemModule;

import java.io.IOException;
import java.net.URI;
Expand All @@ -53,7 +52,6 @@ public static void main(String[] args) throws IOException, URISyntaxException, I
objectMapper.registerModule(new JavaTimeModule());
objectMapper.registerModule(new Jdk8Module());
objectMapper.registerModule(new MoneyModule());
objectMapper.registerModule(new ProblemModule());
objectMapper.registerModule(new GuavaModule());
objectMapper.registerModule(new ParameterNamesModule());

Expand Down Expand Up @@ -86,7 +84,7 @@ public static void main(String[] args) throws IOException, URISyntaxException, I
final SimpleClientHttpRequestFactory requestFactoryDelegate = new SimpleClientHttpRequestFactory();
requestFactoryDelegate.setConnectTimeout(400);
requestFactoryDelegate.setReadTimeout(60*1000);
final ClientHttpRequestFactory requestFactory = new AuthorizedClientHttpRequestFactory(new ProblemHandlingClientHttpRequestFactory(requestFactoryDelegate, objectMapper), tokenProvider);
final ClientHttpRequestFactory requestFactory = new AuthorizedClientHttpRequestFactory(new ProblemHandlingClientHttpRequestFactory(requestFactoryDelegate), tokenProvider);

final CursorManager cursorManager = new InMemoryCursorManager();
//final ManagedCursorManager cursorManager = new ManagedCursorManager(baseUri, requestFactory, objectMapper);
Expand Down

0 comments on commit b95fa77

Please sign in to comment.