Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-add wildcard types and modify checks for acceptable types. #1977

Merged
merged 3 commits into from Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 65 additions & 7 deletions fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraLdp.java
Expand Up @@ -11,6 +11,7 @@
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.HttpHeaders.LINK;
import static javax.ws.rs.core.HttpHeaders.LOCATION;
import static javax.ws.rs.core.MediaType.TEXT_HTML_TYPE;
import static javax.ws.rs.core.MediaType.WILDCARD;
import static javax.ws.rs.core.Response.noContent;
import static javax.ws.rs.core.Response.notAcceptable;
Expand Down Expand Up @@ -56,7 +57,9 @@
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;
import javax.ws.rs.BadRequestException;
Expand All @@ -74,7 +77,6 @@
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
Expand Down Expand Up @@ -137,6 +139,42 @@ public class FedoraLdp extends ContentExposingResource {
private static final MediaType DEFAULT_RDF_CONTENT_TYPE = TURTLE_TYPE;
private static final MediaType DEFAULT_NON_RDF_CONTENT_TYPE = APPLICATION_OCTET_STREAM_TYPE;

/**
* List of RDF_TYPES for comparison, text/plain isn't really an RDF type but it is still accepted.
*/
private static final List<MediaType> RDF_TYPES = Stream.of(TURTLE_WITH_CHARSET, JSON_LD,
N3_WITH_CHARSET, N3_ALT2_WITH_CHARSET, RDF_XML, NTRIPLES, TEXT_PLAIN_WITH_CHARSET
).map(MediaType::valueOf).collect(Collectors.toList());

/**
* This predicate allows comparing a list of accept headers to a list of RDF types.
* It is needed to account for charset variations.
*/
private static final Predicate<List<MediaType>> IS_RDF_TYPE = t -> {
assert t != null;
return t.stream()
.anyMatch(c -> RDF_TYPES.stream().anyMatch(c::isCompatible));
};

/**
* This predicate checks if the list does not have a mediatype that is wildcard
*/
private static final Predicate<List<MediaType>> NOT_WILDCARD = t -> {
assert t != null;
return t.stream().noneMatch(MediaType::isWildcardType);
};

/**
* This predicate checks if the list does not have a mediatype that is compatible with text html
*/
private static final Predicate<List<MediaType>> NOT_HTML =
t -> t.stream().noneMatch(TEXT_HTML_TYPE::isCompatible);

private static final VariantListBuilder RDF_VARIANT_BUILDER = VariantListBuilder.newInstance();
static {
RDF_TYPES.forEach(t -> RDF_VARIANT_BUILDER.mediaTypes(t).add());
}

@PathParam("path") protected String externalPath;

@Inject
Expand Down Expand Up @@ -172,9 +210,9 @@ public FedoraLdp(final String externalPath) {
* @throws UnsupportedAlgorithmException if unsupported digest algorithm occurred
*/
@HEAD
@Produces({ TURTLE_WITH_CHARSET + ";qs=1.0", JSON_LD + ";qs=0.8",
N3_WITH_CHARSET, N3_ALT2_WITH_CHARSET, RDF_XML, NTRIPLES, TEXT_PLAIN_WITH_CHARSET,
TEXT_HTML_WITH_CHARSET })
@Produces({TURTLE_WITH_CHARSET + ";qs=1.0", JSON_LD + ";qs=0.8",
N3_WITH_CHARSET, N3_ALT2_WITH_CHARSET, RDF_XML, NTRIPLES, TEXT_PLAIN_WITH_CHARSET,
TEXT_HTML_WITH_CHARSET, "*/*"})
public Response head(@DefaultValue("false") @QueryParam("inline") final boolean inlineDisposition)
throws UnsupportedAlgorithmException {
LOGGER.info("HEAD for: {}", externalPath);
Expand All @@ -184,6 +222,9 @@ public Response head(@DefaultValue("false") @QueryParam("inline") final boolean
return getMemento(datetimeHeader, resource(), inlineDisposition);
}

final ImmutableList<MediaType> acceptableMediaTypes = ImmutableList.copyOf(headers
.getAcceptableMediaTypes());

checkCacheControlHeaders(request, servletResponse, resource(), transaction());

addResourceHttpHeaders(resource(), inlineDisposition);
Expand All @@ -194,6 +235,12 @@ public Response head(@DefaultValue("false") @QueryParam("inline") final boolean
final Binary binary = (Binary) resource();
final MediaType mediaType = getBinaryResourceMediaType(binary);

if (!acceptableMediaTypes.isEmpty()) {
if (acceptableMediaTypes.stream().noneMatch(t -> t.isCompatible(mediaType))) {
return notAcceptable(VariantListBuilder.newInstance().mediaTypes(mediaType).build()).build();
}
}

if (binary.isRedirect()) {
builder = temporaryRedirect(binary.getExternalURI());
}
Expand All @@ -207,8 +254,13 @@ public Response head(@DefaultValue("false") @QueryParam("inline") final boolean
builder.header(DIGEST, handleWantDigestHeader(binary, wantDigest));
}
} else {
final String accept = headers.getHeaderString(HttpHeaders.ACCEPT);
if (accept == null || "*/*".equals(accept)) {
if (!acceptableMediaTypes.isEmpty() && NOT_WILDCARD.test(acceptableMediaTypes)) {
// Accept header is not empty and is not */*
if (!IS_RDF_TYPE.test(acceptableMediaTypes)) {
return notAcceptable(VariantListBuilder.newInstance().mediaTypes().build()).build();
}
} else if (acceptableMediaTypes.isEmpty() || !NOT_WILDCARD.test(acceptableMediaTypes)) {
// If there is no Accept header or it is */*, so default to text/turtle
builder.type(TURTLE_WITH_CHARSET);
}
setVaryAndPreferenceAppliedHeaders(servletResponse, prefer, resource());
Expand Down Expand Up @@ -243,7 +295,7 @@ public Response options() {
@GET
@Produces({TURTLE_WITH_CHARSET + ";qs=1.0", JSON_LD + ";qs=0.8",
N3_WITH_CHARSET, N3_ALT2_WITH_CHARSET, RDF_XML, NTRIPLES, TEXT_PLAIN_WITH_CHARSET,
TEXT_HTML_WITH_CHARSET})
TEXT_HTML_WITH_CHARSET, "*/*"})
public Response getResource(
@HeaderParam("Range") final String rangeValue,
@DefaultValue("false") @QueryParam("inline") final boolean inlineDisposition)
Expand Down Expand Up @@ -284,6 +336,12 @@ public Response getResource(
return getBinaryContent(rangeValue, binary);
}
} else {
if (!acceptableMediaTypes.isEmpty() && NOT_WILDCARD.test(acceptableMediaTypes) &&
NOT_HTML.test(acceptableMediaTypes) &&
!IS_RDF_TYPE.test(acceptableMediaTypes)) {
// Accept header is not empty and is not */* and is not text/html and is not a valid RDF type.
return notAcceptable(RDF_VARIANT_BUILDER.build()).build();
}
return getContent(getChildrenLimit(), resource());
}
}
Expand Down
Expand Up @@ -160,6 +160,7 @@
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
Expand Down Expand Up @@ -386,40 +387,79 @@ public void testCreateBinaryAsArchivalGroupWithPutFails() throws Exception {

@Test
public void testHeadTurtleContentType() throws IOException {
testHeadDefaultContentType(RDFMediaType.TURTLE_WITH_CHARSET);
testDefaultContentType(RDFMediaType.TURTLE_WITH_CHARSET, false);
}

@Test
public void testHeadRDFContentType() throws IOException {
testHeadDefaultContentType(RDFMediaType.RDF_XML);
testDefaultContentType(RDFMediaType.RDF_XML, false);
}

@Test
public void testHeadJSONLDContentType() throws IOException {
testHeadDefaultContentType(RDFMediaType.JSON_LD);
testDefaultContentType(RDFMediaType.JSON_LD, false);
}

@Test
public void testHeadDefaultContentType() throws IOException {
testHeadDefaultContentType(null);
testDefaultContentType(null, false);
}

private void testHeadDefaultContentType(final String mimeType) throws IOException {
@Test
public void testHeadAnyContentType() throws IOException {
testDefaultContentType(RDFMediaType.WILDCARD, false);
}

@Test
public void testGetTurtleContentType() throws IOException {
testDefaultContentType(RDFMediaType.TURTLE_WITH_CHARSET, true);
}

@Test
public void testGetRDFContentType() throws IOException {
testDefaultContentType(RDFMediaType.RDF_XML, true);
}

@Test
public void testGetJSONLDContentType() throws IOException {
testDefaultContentType(RDFMediaType.JSON_LD, true);
}

@Test
public void testGetDefaultContentType() throws IOException {
testDefaultContentType(null, true);
}

@Test
public void testGetAnyContentType() throws IOException {
testDefaultContentType(RDFMediaType.WILDCARD, true);
}

private void testDefaultContentType(final String mimeType, final boolean isGet) throws IOException {
final String id = getRandomUniqueId();
createObjectAndClose(id);

final HttpHead headObjMethod = headObjMethod(id);
final HttpRequestBase req;
if (isGet) {
req = getObjMethod(id);
} else {
req = headObjMethod(id);
}
String mt = mimeType;
final String returnType;
if (mt != null) {
headObjMethod.addHeader("Accept", mt);
req.addHeader("Accept", mt);
if (mt.equals(RDFMediaType.WILDCARD)) {
returnType = RDFMediaType.TURTLE_WITH_CHARSET;
} else {
returnType = mt;
}
} else {
mt = RDFMediaType.TURTLE_WITH_CHARSET;
returnType = mt;
}
try (final CloseableHttpResponse response = execute(headObjMethod)) {
final Collection<String> contentTypes = getHeader(response, CONTENT_TYPE);
final String contentType = contentTypes.iterator().next();
assertTrue("Didn't find LDP valid content-type header: " + contentType +
"; expected result: " + mt, contentType.contains(mt));
try (final CloseableHttpResponse response = execute(req)) {
checkContentTypeMatches(response, returnType);
testHeadVaryAndPreferHeaders(response);
}
}
Expand Down Expand Up @@ -3291,6 +3331,69 @@ public void testResponseContentTypes() throws Exception {
}
}

/**
* Without the asterisks on the GET/HEAD methods Jersey will fail to serve all other content types for binaries.
*/
@Test
public void testBinaryAcceptHeaders() throws IOException {
final HttpPost post = postObjMethod();
post.setHeader(CONTENT_TYPE, "application/pdf");
post.setEntity(new StringEntity("Some fake content", UTF_8));
final String objectUrl;
try (final CloseableHttpResponse response = execute(post)) {
assertEquals(CREATED.getStatusCode(), getStatus(response));
objectUrl = getLocation(response);
}
final HttpGet httpGet = new HttpGet(objectUrl);
httpGet.setHeader(ACCEPT, "image/tiff");
assertEquals(NOT_ACCEPTABLE.getStatusCode(), getStatus(httpGet));

final HttpGet httpGet2 = new HttpGet(objectUrl);
httpGet2.setHeader(ACCEPT, "application/pdf");
assertEquals(OK.getStatusCode(), getStatus(httpGet2));

final HttpGet httpGet3 = new HttpGet(objectUrl);
httpGet3.addHeader(ACCEPT, "*/*");
try (final var response = execute(httpGet3)) {
assertEquals(OK.getStatusCode(), getStatus(response));
checkContentTypeMatches(response, "application/pdf");
}

final HttpGet httpGet4 = new HttpGet(objectUrl);
try (final var response = execute(httpGet4)) {
assertEquals(OK.getStatusCode(), getStatus(response));
checkContentTypeMatches(response, "application/pdf");
}

final HttpHead httpHead = new HttpHead(objectUrl);
httpHead.setHeader(ACCEPT, "image/tiff");
assertEquals(NOT_ACCEPTABLE.getStatusCode(), getStatus(httpHead));

final HttpHead httpHead2 = new HttpHead(objectUrl);
httpHead2.setHeader(ACCEPT, "application/pdf");
assertEquals(OK.getStatusCode(), getStatus(httpHead2));

final HttpHead httpHead3 = new HttpHead(objectUrl);
httpHead3.setHeader(ACCEPT, "*/*");
try (final var response = execute(httpHead3)) {
assertEquals(OK.getStatusCode(), getStatus(response));
checkContentTypeMatches(response, "application/pdf");
}

final HttpHead httpHead4 = new HttpHead(objectUrl);
try (final var response = execute(httpHead4)) {
assertEquals(OK.getStatusCode(), getStatus(response));
checkContentTypeMatches(response, "application/pdf");
}
}

private static void checkContentTypeMatches(final CloseableHttpResponse response, final String contentType) {
final Collection<String> contentTypes = getHeader(response, CONTENT_TYPE);
final String type = contentTypes.iterator().next();
assertTrue("Didn't find LDP valid content-type header: " + type +
"; expected result: " + contentType, type.contains(contentType));
}
bbpennel marked this conversation as resolved.
Show resolved Hide resolved

@Test
public void testDescribeRdfCached() throws IOException {
try (final CloseableHttpClient cachClient = CachingHttpClientBuilder.create().setCacheConfig(DEFAULT).build()) {
Expand Down