1 | package com.reallifedeveloper.common.resource.documentation; | |
2 | ||
3 | import java.io.FileNotFoundException; | |
4 | import java.io.IOException; | |
5 | import java.io.InputStream; | |
6 | import java.io.OutputStream; | |
7 | import java.net.URI; | |
8 | import java.util.Arrays; | |
9 | ||
10 | import org.checkerframework.checker.nullness.qual.Nullable; | |
11 | ||
12 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | |
13 | import jakarta.ws.rs.GET; | |
14 | import jakarta.ws.rs.Path; | |
15 | import jakarta.ws.rs.PathParam; | |
16 | import jakarta.ws.rs.core.Context; | |
17 | import jakarta.ws.rs.core.HttpHeaders; | |
18 | import jakarta.ws.rs.core.MediaType; | |
19 | import jakarta.ws.rs.core.Response; | |
20 | import jakarta.ws.rs.core.Response.Status; | |
21 | import jakarta.ws.rs.core.StreamingOutput; | |
22 | import jakarta.ws.rs.core.UriInfo; | |
23 | ||
24 | import com.reallifedeveloper.common.domain.ErrorHandling; | |
25 | import com.reallifedeveloper.common.resource.BaseResource; | |
26 | import com.reallifedeveloper.common.resource.ResourceUtil; | |
27 | ||
28 | /** | |
29 | * A JAX-RS resource that produces HTML documentation from resources on the classpath. | |
30 | * | |
31 | * @author RealLifeDeveloper | |
32 | */ | |
33 | @SuppressFBWarnings(value = "JAXRS_ENDPOINT", justification = "Please ensure the JAX-RS REST endpoints here are used in a secure way") | |
34 | public final class DocumentationResource extends BaseResource { | |
35 | ||
36 | private static final String CONTENT_TYPE_HTML = MediaType.TEXT_HTML + "; charset=UTF-8"; | |
37 | private static final String CONTENT_TYPE_BINARY = MediaType.APPLICATION_OCTET_STREAM; | |
38 | ||
39 | private static final int BUFFER_SIZE = 4096; | |
40 | ||
41 | private static final String[] DEFAULT_DOCUMENTS = { "index.md", "readme.md" }; | |
42 | ||
43 | private final String resourceDir; | |
44 | private final HtmlProducer htmlProducer; | |
45 | ||
46 | @Context | |
47 | private @Nullable HttpHeaders httpHeaders; | |
48 | ||
49 | @Context | |
50 | private @Nullable UriInfo uriInfo; | |
51 | ||
52 | /** | |
53 | * Creates a new {@code DocumentationResource} that reads documents from the specified resource directory, converting them to HTML using | |
54 | * the given {@link HtmlProducer}. | |
55 | * | |
56 | * @param resourceDir the directory from which to read documents | |
57 | * @param htmlProducer the {@code HtmlProducer} to use to convert documents to HTML | |
58 | * | |
59 | * @throws IllegalArgumentException if any argument is {@code null}, of if {@code resourceDir} does not exist | |
60 | */ | |
61 | public DocumentationResource(String resourceDir, HtmlProducer htmlProducer) { | |
62 |
1
1. <init> : removed call to com/reallifedeveloper/common/domain/ErrorHandling::checkNull → KILLED |
ErrorHandling.checkNull("Arguments must not be null: resourceDir=%s, htmlProducer=%s", resourceDir, htmlProducer); |
63 | this.resourceDir = resourceDir; | |
64 | this.htmlProducer = htmlProducer; | |
65 |
1
1. <init> : negated conditional → KILLED |
if (!resourceExists(resourceDir)) { |
66 | throw new IllegalArgumentException("resourceDir does not exist: " + resourceDir); | |
67 | } | |
68 | } | |
69 | ||
70 | /** | |
71 | * Redirects the client to the proper documentation URL. | |
72 | * | |
73 | * @return a response with a status of 301 (Moved Permanently) and the correct location | |
74 | */ | |
75 | @GET | |
76 | @Path("/") | |
77 | public Response redirect() { | |
78 |
1
1. redirect : negated conditional → KILLED |
if (uriInfo == null) { |
79 | throw new IllegalStateException("uriInfo field has not been set, it is assumed to be injected by Jakarta"); | |
80 | } | |
81 | URI uri = uriInfo.getAbsolutePathBuilder().path("/doc/").build(); | |
82 |
1
1. redirect : replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::redirect → KILLED |
return Response.status(Status.MOVED_PERMANENTLY).location(uri).entity(uri.toString()).build(); |
83 | } | |
84 | ||
85 | /** | |
86 | * Reads a default document (index.md or readme.md) from the resource directory and converts it to HTML. | |
87 | * | |
88 | * @return a response containing the HTML produced | |
89 | * | |
90 | * @throws jakarta.ws.rs.WebApplicationException with status 404 if no default document was found | |
91 | */ | |
92 | @GET | |
93 | @Path("/doc") | |
94 | @SuppressWarnings("PMD.AvoidCatchingGenericException") | |
95 | public Response getDocumentation() { | |
96 | try { | |
97 |
1
1. getDocumentation : replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::getDocumentation → KILLED |
return handleResource(getDefaultDocument()); |
98 | } catch (Exception e) { | |
99 | throw handleError("getDocumentation", e); | |
100 | } | |
101 | } | |
102 | ||
103 | /** | |
104 | * Reads a document from the resource directory and converts it to HTML. | |
105 | * | |
106 | * @param document the document to read | |
107 | * | |
108 | * @return a response containing the HTML produced | |
109 | * | |
110 | * @throws jakarta.ws.rs.WebApplicationException with status 404 if {@code document} was not found | |
111 | */ | |
112 | @GET | |
113 | @Path("/doc/{document}") | |
114 | @SuppressWarnings("PMD.AvoidCatchingGenericException") | |
115 | public Response getDocumentation(@PathParam("document") String document) { | |
116 | try { | |
117 |
1
1. getDocumentation : replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::getDocumentation → KILLED |
return handleResource(resourceDir + "/" + document); |
118 | } catch (Exception e) { | |
119 | throw handleError("getDocumentation", e); | |
120 | } | |
121 | } | |
122 | ||
123 | @SuppressWarnings("PMD.UseTryWithResources") | |
124 | private Response handleResource(String resourceName) throws IOException { | |
125 | Response response; | |
126 |
1
1. handleResource : negated conditional → KILLED |
if (htmlProducer.canHandle(resourceName)) { |
127 | String html = htmlProducer.produce(resourceName); | |
128 | response = Response.ok(html).header(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_HTML) | |
129 | .cacheControl(ResourceUtil.cacheControl(CACHE_1_HOUR)).build(); | |
130 | } else { | |
131 | InputStream input = getClass().getResourceAsStream(resourceName); | |
132 |
1
1. handleResource : negated conditional → KILLED |
if (input == null) { |
133 | throw new FileNotFoundException("Resource not found: " + resourceName); | |
134 | } | |
135 | StreamingOutput stream = new StreamingOutput() { | |
136 | @Override | |
137 | public void write(OutputStream output) throws IOException { | |
138 | try { | |
139 | byte[] buffer = new byte[BUFFER_SIZE]; | |
140 | int len; | |
141 |
1
1. write : negated conditional → KILLED |
while ((len = input.read(buffer)) != -1) { |
142 |
1
1. write : removed call to java/io/OutputStream::write → KILLED |
output.write(buffer, 0, len); |
143 | } | |
144 |
1
1. write : removed call to java/io/OutputStream::flush → SURVIVED |
output.flush(); |
145 | } finally { | |
146 |
1
1. write : removed call to java/io/InputStream::close → SURVIVED |
input.close(); |
147 | } | |
148 | } | |
149 | }; | |
150 | response = Response.ok(stream).header(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_BINARY).build(); | |
151 | } | |
152 |
1
1. handleResource : replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::handleResource → KILLED |
return response; |
153 | } | |
154 | ||
155 | private String getDefaultDocument() throws IOException { | |
156 | for (String defaultDocument : DEFAULT_DOCUMENTS) { | |
157 | String resourceName = resourceDir + "/" + defaultDocument; | |
158 |
1
1. getDefaultDocument : negated conditional → KILLED |
if (resourceExists(resourceName)) { |
159 |
1
1. getDefaultDocument : replaced return value with "" for com/reallifedeveloper/common/resource/documentation/DocumentationResource::getDefaultDocument → KILLED |
return resourceName; |
160 | } | |
161 | } | |
162 | throw new FileNotFoundException("Resource not found: " + resourceDir + "/" + Arrays.asList(DEFAULT_DOCUMENTS)); | |
163 | } | |
164 | ||
165 | private boolean resourceExists(String resourceName) { | |
166 |
2
1. resourceExists : replaced boolean return with true for com/reallifedeveloper/common/resource/documentation/DocumentationResource::resourceExists → KILLED 2. resourceExists : negated conditional → KILLED |
return getClass().getResource(resourceName) != null; |
167 | } | |
168 | } | |
Mutations | ||
62 |
1.1 |
|
65 |
1.1 |
|
78 |
1.1 |
|
82 |
1.1 |
|
97 |
1.1 |
|
117 |
1.1 |
|
126 |
1.1 |
|
132 |
1.1 |
|
141 |
1.1 |
|
142 |
1.1 |
|
144 |
1.1 |
|
146 |
1.1 |
|
152 |
1.1 |
|
158 |
1.1 |
|
159 |
1.1 |
|
166 |
1.1 2.2 |