DocumentationResource.java

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
Location : <init>
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:constructorNullHtmlProducer()]
removed call to com/reallifedeveloper/common/domain/ErrorHandling::checkNull → KILLED

65

1.1
Location : <init>
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:constructorNonExistingResourceDir()]
negated conditional → KILLED

78

1.1
Location : redirect
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:redirectWihoutUriInfoThrowsException()]
negated conditional → KILLED

82

1.1
Location : redirect
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:redirect()]
replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::redirect → KILLED

97

1.1
Location : getDocumentation
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:getDocumentationDefaultDocument()]
replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::getDocumentation → KILLED

117

1.1
Location : getDocumentation
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:getDocumentationNamedDocument()]
replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::getDocumentation → KILLED

126

1.1
Location : handleResource
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:getDocumentationNamedDocument()]
negated conditional → KILLED

132

1.1
Location : handleResource
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:getDocumentationNamedDocumentDoesNotExist()]
negated conditional → KILLED

141

1.1
Location : write
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:getDocumentationNamedBinaryDocument()]
negated conditional → KILLED

142

1.1
Location : write
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:getDocumentationNamedBinaryDocument()]
removed call to java/io/OutputStream::write → KILLED

144

1.1
Location : write
Killed by : none
removed call to java/io/OutputStream::flush → SURVIVED
Covering tests

146

1.1
Location : write
Killed by : none
removed call to java/io/InputStream::close → SURVIVED
Covering tests

152

1.1
Location : handleResource
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:getDocumentationNamedDocument()]
replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::handleResource → KILLED

158

1.1
Location : getDefaultDocument
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:getDocumentationDefaultDocument()]
negated conditional → KILLED

159

1.1
Location : getDefaultDocument
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:getDocumentationDefaultDocument()]
replaced return value with "" for com/reallifedeveloper/common/resource/documentation/DocumentationResource::getDefaultDocument → KILLED

166

1.1
Location : resourceExists
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:constructorNonExistingResourceDir()]
replaced boolean return with true for com/reallifedeveloper/common/resource/documentation/DocumentationResource::resourceExists → KILLED

2.2
Location : resourceExists
Killed by : com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.common.resource.documentation.DocumentationResourceTest]/[method:constructorNonExistingResourceDir()]
negated conditional → KILLED

Active mutators

Tests examined


Report generated by PIT 1.20.0