DocumentationResource.java
package com.reallifedeveloper.common.resource.documentation;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.StreamingOutput;
import jakarta.ws.rs.core.UriInfo;
import com.reallifedeveloper.common.domain.ErrorHandling;
import com.reallifedeveloper.common.resource.BaseResource;
import com.reallifedeveloper.common.resource.ResourceUtil;
/**
* A JAX-RS resource that produces HTML documentation from resources on the classpath.
*
* @author RealLifeDeveloper
*/
@SuppressFBWarnings(value = "JAXRS_ENDPOINT", justification = "Please ensure the JAX-RS REST endpoints here are used in a secure way")
public final class DocumentationResource extends BaseResource {
private static final String CONTENT_TYPE_HTML = MediaType.TEXT_HTML + "; charset=UTF-8";
private static final String CONTENT_TYPE_BINARY = MediaType.APPLICATION_OCTET_STREAM;
private static final int BUFFER_SIZE = 4096;
private static final String[] DEFAULT_DOCUMENTS = { "index.md", "readme.md" };
private final String resourceDir;
private final HtmlProducer htmlProducer;
@Context
private @Nullable HttpHeaders httpHeaders;
@Context
private @Nullable UriInfo uriInfo;
/**
* Creates a new {@code DocumentationResource} that reads documents from the specified resource directory, converting them to HTML using
* the given {@link HtmlProducer}.
*
* @param resourceDir the directory from which to read documents
* @param htmlProducer the {@code HtmlProducer} to use to convert documents to HTML
*
* @throws IllegalArgumentException if any argument is {@code null}, of if {@code resourceDir} does not exist
*/
public DocumentationResource(String resourceDir, HtmlProducer htmlProducer) {
ErrorHandling.checkNull("Arguments must not be null: resourceDir=%s, htmlProducer=%s", resourceDir, htmlProducer);
this.resourceDir = resourceDir;
this.htmlProducer = htmlProducer;
if (!resourceExists(resourceDir)) {
throw new IllegalArgumentException("resourceDir does not exist: " + resourceDir);
}
}
/**
* Redirects the client to the proper documentation URL.
*
* @return a response with a status of 301 (Moved Permanently) and the correct location
*/
@GET
@Path("/")
public Response redirect() {
if (uriInfo == null) {
throw new IllegalStateException("uriInfo field has not been set, it is assumed to be injected by Jakarta");
}
URI uri = uriInfo.getAbsolutePathBuilder().path("/doc/").build();
return Response.status(Status.MOVED_PERMANENTLY).location(uri).entity(uri.toString()).build();
}
/**
* Reads a default document (index.md or readme.md) from the resource directory and converts it to HTML.
*
* @return a response containing the HTML produced
*
* @throws jakarta.ws.rs.WebApplicationException with status 404 if no default document was found
*/
@GET
@Path("/doc")
@SuppressWarnings("PMD.AvoidCatchingGenericException")
public Response getDocumentation() {
try {
return handleResource(getDefaultDocument());
} catch (Exception e) {
throw handleError("getDocumentation", e);
}
}
/**
* Reads a document from the resource directory and converts it to HTML.
*
* @param document the document to read
*
* @return a response containing the HTML produced
*
* @throws jakarta.ws.rs.WebApplicationException with status 404 if {@code document} was not found
*/
@GET
@Path("/doc/{document}")
@SuppressWarnings("PMD.AvoidCatchingGenericException")
public Response getDocumentation(@PathParam("document") String document) {
try {
return handleResource(resourceDir + "/" + document);
} catch (Exception e) {
throw handleError("getDocumentation", e);
}
}
@SuppressWarnings("PMD.UseTryWithResources")
private Response handleResource(String resourceName) throws IOException {
Response response;
if (htmlProducer.canHandle(resourceName)) {
String html = htmlProducer.produce(resourceName);
response = Response.ok(html).header(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_HTML)
.cacheControl(ResourceUtil.cacheControl(CACHE_1_HOUR)).build();
} else {
InputStream input = getClass().getResourceAsStream(resourceName);
if (input == null) {
throw new FileNotFoundException("Resource not found: " + resourceName);
}
StreamingOutput stream = new StreamingOutput() {
@Override
public void write(OutputStream output) throws IOException {
try {
byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
output.flush();
} finally {
input.close();
}
}
};
response = Response.ok(stream).header(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_BINARY).build();
}
return response;
}
private String getDefaultDocument() throws IOException {
for (String defaultDocument : DEFAULT_DOCUMENTS) {
String resourceName = resourceDir + "/" + defaultDocument;
if (resourceExists(resourceName)) {
return resourceName;
}
}
throw new FileNotFoundException("Resource not found: " + resourceDir + "/" + Arrays.asList(DEFAULT_DOCUMENTS));
}
private boolean resourceExists(String resourceName) {
return getClass().getResource(resourceName) != null;
}
}