Search code examples
spring-mvcenunciate

Use Enunciate to document an endpoint returning a binary file (image/png)


I am using Enunciate to document a REST service written with spring-webmvc. Some of the endpoints return images. (Please ignore the fact that these images would be better served by another process like nginx or apache webserver.)

I'm trying to configure Enunciate to document the following function, but I don't know how to:

  • annotate this method to document that it returns a binary file (usually a png, but in the future the requester will be able to ask for jpg or png)
  • provide an example showing "/hcm?empl_id=12345". Both the @DocumentationExample on the method and the @DocumentationExample on the emplId are ignored.

Is it possible to document this using Enunciate? Do I need to turn something on? I'm starting to think Enunciate just won't do what I've been tasked with.

/**
 * Returns the HCM photo (image/png) for the staff member with the specified empl_id. If no such
 * image exists, return a 404.
 *
 * @pathExample /hcm?id=12345
 * 
 * @param idType currently only supports "hcm".
 *
 * @param emplId is the HCM EMPLID or the Active Directory EmployeeNumber for a given staff
 *        member
 *
 * @throws IOException when there is a problem accessing the image.
 */

@Override
@GetMapping(value = "/hcm", produces = {MediaType.IMAGE_PNG_VALUE})
@DocumentationExample(value = "/hcm?empl_id=12345")
public void getHcmPhoto(
        @RequestParam(value = "id_type", required = false, defaultValue = "hcm") String idType,
        @DocumentationExample(value = "12345")
        @RequestParam("empl_id") String emplId,
        HttpServletResponse response) throws IOException {
    logger.trace("/hcm call made for emplHcmId: {} {}", idType, emplId);
    final String emplHcmId = getHcmId(idType, emplId);

    if (emplHcmId == null) {
        response.setStatus(HttpStatus.NOT_FOUND.value());
        return;
    }

    File fileToSent = new File(pathToImages, emplHcmId + ".png");
    if (!fileToSend.exists()) {
        logger.debug("Photo {} does not exist", fileToSend.getAbsolutePath());
        response.setStatus(HttpStatus.NOT_FOUND.value());
        return;
    }

    try (InputStream in = new FileInputStream(fileToSend)) {
        logger.trace("Photo {} found", fileToSend.getAbsolutePath());
        response.setContentType(MediaType.IMAGE_PNG_VALUE);
        IOUtils.copy(in, response.getOutputStream());
    }
    catch (IOException ioe) {
        logger.error("Could not send {}: {}", fileToSend.getName(), ioe.getMessage(), ioe);
        throw ioe;
    }
}

I am building with Maven. I am building source jars and referencing them in the .war file

<plugin>
    <groupId>com.webcohesion.enunciate</groupId>
    <artifactId>enunciate-maven-plugin</artifactId>
    <version>${enunciate.version}</version>
    <executions>
        <execution>
            <id>assemble</id>
            <goals>
                <goal>assemble</goal>
            </goals>
            <configuration>
                <sourcepath-includes>
                    <include pattern="com.foo.**">
                        <!-- configure Enunciate to look for the source jars for all dependencies 
                            in the "com.foo.domain" groupId. -->
                        <groupId>com.foo.staff</groupId>
                        <artifactId>com.foo.staff.photo.controller</artifactId>
                    </include>
                </sourcepath-includes>
                <!-- but exclude com.foo.domain:domain-utils <sourcepath-excludes> <exclude>
                        <groupId>com.foo.domain</groupId> <artifactId>domain-utils</artifactId>
                    </exclude> </sourcepath-excludes> -->
                <docsDir>${project.build.directory}/docs</docsDir>
            </configuration>
        </execution>
    </executions>
</plugin>

My enunciate.xml contains

<?xml version="1.0" encoding="UTF-8"?>
<enunciate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://enunciate.webcohesion.com/schemas/enunciate-1.27.xsd">

<!-- 
    <facets>
        <exclude name="internal_api" />
    </facets>
 -->

    <api-classes>
        <include pattern="com.foo.**" />
        <exclude pattern="org.springframework.**" />
    </api-classes>


    <modules>
        <docs docsDir="target/docs" title="Staff Photo REST API"/>
        <spring-web />
    </modules>

</enunciate>

Solution

  • I solved this by returning a ResponseEntity rather than returning void and manipulating the HttpServletResponse. I.e.

    @Override
    @GetMapping(value = "/hcm", produces = {MediaType.IMAGE_PNG_VALUE})
    public ResponseEntity<Resource> getHcmPhoto(
            @RequestParam(value = "id_type", required = false, defaultValue = "hcm") String idType,
            @RequestParam("empl_id") String emplId) throws IOException {
        logger.trace("/hcm call made for emplHcmId: {} {}", idType, emplId);
        final String emplHcmId = getHcmId(idType, emplId);
    
        HttpHeaders headers = new HttpHeaders();
    
        if (emplHcmId == null) {
            return new ResponseEntity<>(null, headers, HttpStatus.NOT_FOUND);
        }
    
        File hcmPhoto = getHcmPhotoFile(emplHcmId);
        if (hcmPhoto == null) {
            return new ResponseEntity<>(null, headers, HttpStatus.NOT_FOUND);
        }
    
        return new ResponseEntity<>(new org.springframework.core.io.FileUrlResource(hcmPhoto.toURI().toURL()),
                headers, HttpStatus.OK);
    }
    

    Overall, this seems better both in terms of how Enunciate documents it and how the code reads. Win-Win!