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 UriInfo uriInfo;
48
49
    /**
50
     * Creates a new {@code DocumentationResource} that reads documents from the specified resource directory, converting them to HTML using
51
     * the given {@link HtmlProducer}.
52
     *
53
     * @param resourceDir  the directory from which to read documents
54
     * @param htmlProducer the {@code HtmlProducer} to use to convert documents to HTML
55
     *
56
     * @throws IllegalArgumentException if any argument is {@code null}, of if {@code resourceDir} does not exist
57
     */
58
    public DocumentationResource(String resourceDir, HtmlProducer htmlProducer) {
59 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);
60
        this.resourceDir = resourceDir;
61
        this.htmlProducer = htmlProducer;
62 1 1. <init> : negated conditional → KILLED
        if (!resourceExists(resourceDir)) {
63
            throw new IllegalArgumentException("resourceDir does not exist: " + resourceDir);
64
        }
65
    }
66
67
    /**
68
     * Redirects the client to the proper documentation URL.
69
     *
70
     * @return a response with a status of 301 (Moved Permanently) and the correct location
71
     */
72
    @GET
73
    @Path("/")
74
    public Response redirect() {
75 1 1. redirect : negated conditional → KILLED
        if (uriInfo == null) {
76
            throw new IllegalStateException("uriInfo field has not been set, it is assumed to be injected by Jakarta");
77
        }
78
        URI uri = uriInfo.getAbsolutePathBuilder().path("/doc/").build();
79 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();
80
    }
81
82
    /**
83
     * Reads a default document (index.md or readme.md) from the resource directory and converts it to HTML.
84
     *
85
     * @return a response containing the HTML produced
86
     *
87
     * @throws jakarta.ws.rs.WebApplicationException with status 404 if no default document was found
88
     */
89
    @GET
90
    @Path("/doc")
91
    @SuppressWarnings("PMD.AvoidCatchingGenericException")
92
    public Response getDocumentation() {
93
        try {
94 1 1. getDocumentation : replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::getDocumentation → KILLED
            return handleResource(getDefaultDocument());
95
        } catch (Exception e) {
96
            throw handleError("getDocumentation", e);
97
        }
98
    }
99
100
    /**
101
     * Reads a document from the resource directory and converts it to HTML.
102
     *
103
     * @param document the document to read
104
     *
105
     * @return a response containing the HTML produced
106
     *
107
     * @throws jakarta.ws.rs.WebApplicationException with status 404 if {@code document} was not found
108
     */
109
    @GET
110
    @Path("/doc/{document}")
111
    @SuppressWarnings("PMD.AvoidCatchingGenericException")
112
    public Response getDocumentation(@PathParam("document") String document) {
113
        try {
114 1 1. getDocumentation : replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::getDocumentation → KILLED
            return handleResource(resourceDir + "/" + document);
115
        } catch (Exception e) {
116
            throw handleError("getDocumentation", e);
117
        }
118
    }
119
120
    @SuppressWarnings("PMD.UseTryWithResources")
121
    private Response handleResource(String resourceName) throws IOException {
122
        Response response;
123 1 1. handleResource : negated conditional → KILLED
        if (htmlProducer.canHandle(resourceName)) {
124
            String html = htmlProducer.produce(resourceName);
125
            response = Response.ok(html).header(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_HTML)
126
                    .cacheControl(ResourceUtil.cacheControl(CACHE_1_HOUR)).build();
127
        } else {
128
            InputStream input = getClass().getResourceAsStream(resourceName);
129 1 1. handleResource : negated conditional → KILLED
            if (input == null) {
130
                throw new FileNotFoundException("Resource not found: " + resourceName);
131
            }
132
            StreamingOutput stream = new StreamingOutput() {
133
                @Override
134
                public void write(OutputStream output) throws IOException {
135
                    try {
136
                        byte[] buffer = new byte[BUFFER_SIZE];
137
                        int len;
138 1 1. write : negated conditional → KILLED
                        while ((len = input.read(buffer)) != -1) {
139 1 1. write : removed call to java/io/OutputStream::write → KILLED
                            output.write(buffer, 0, len);
140
                        }
141 1 1. write : removed call to java/io/OutputStream::flush → SURVIVED
                        output.flush();
142
                    } finally {
143 1 1. write : removed call to java/io/InputStream::close → SURVIVED
                        input.close();
144
                    }
145
                }
146
            };
147
            response = Response.ok(stream).header(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_BINARY).build();
148
        }
149 1 1. handleResource : replaced return value with null for com/reallifedeveloper/common/resource/documentation/DocumentationResource::handleResource → KILLED
        return response;
150
    }
151
152
    private String getDefaultDocument() throws IOException {
153
        for (String defaultDocument : DEFAULT_DOCUMENTS) {
154
            String resourceName = resourceDir + "/" + defaultDocument;
155 1 1. getDefaultDocument : negated conditional → KILLED
            if (resourceExists(resourceName)) {
156 1 1. getDefaultDocument : replaced return value with "" for com/reallifedeveloper/common/resource/documentation/DocumentationResource::getDefaultDocument → KILLED
                return resourceName;
157
            }
158
        }
159
        throw new FileNotFoundException("Resource not found: " + resourceDir + "/" + Arrays.asList(DEFAULT_DOCUMENTS));
160
    }
161
162
    private boolean resourceExists(String resourceName) {
163 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;
164
    }
165
}

Mutations

59

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

62

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

75

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

79

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

94

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

114

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

123

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

129

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

138

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

139

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

141

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

143

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

149

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

155

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

156

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

163

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.2