Job Costing #9 – File Download

This post is part of a series on an ongoing project. And I lied! In my last post I said that I would focus on adding the code from the Java Swing application, and that is the direction I took when working on the project. But I’ve reconsidered, and I think that it makes more sense to push that topic back one post, form an illustrative purpose. So in today’s post I’m going to put a simple stub in the REST service, and focus on building a file download component.

Analyze Stub

First, I’m going to define the REST service that will, in the future, take the three files and the parameters defined by the user and analyze the data to create the export text and export file. In my Kotlin code, I’ll define a new file ‘analyze.kt’.

@RestController
class Analyze {

    @RequestMapping("/jobcost/analyze")
    @CrossOrigin(origins = ["http://localhost:3000"])
    fun handleAnalyze(@RequestBody req: AnalyzeRequest): Response {
        return getTestAnalyzeResponse(req.files, req.params)
    }

    private fun getTestAnalyzeResponse(files: AnalyzeFiles, params: AnalyzeParams?): Response {
        return AnalyzeResponse(
                params ?: AnalyzeParams(
                                "2020-02-01",
                                "2020-02-28",
                                160,
                                120,
                                200,
                                "2020-02-28"
                        ),
                AnalyzeOutput(
                        false,
                        "Test output"
                ),
                files.projectSheet
        ).toResponse()
    }

Pretty straightforward, the controller handles GET requests for /jobcost/analyze and answers all of them with a test response. If the request has no params, then the controller provides default parameters, otherwise it echoes back the params that the user provided. It also generates some simple test output.

For the output file (remember the output of this app is a file that will be imported into the client’s financial system – QuickBooks) it just echoes the name of the project sheet. This means that when the user downloads the output file, it’ll simply download one of the spreadsheets already uploaded. Obviously, that’s not how the finished product will work, but it will let me test the file download functionality.

For reference, the AnalyzeFiles and AnalyzeParams classes are simple Kotlin data classes defined below.

data class AnalyzeFiles(
    @JsonProperty("projectSheet") val projectSheet: String,
    @JsonProperty("employeeCost") val employeeCost: String,
    @JsonProperty("employeeCostPassword") val employeeCostPassword: String,
    @JsonProperty("timeReport") val timeReport: String
)

data class AnalyzeParams(
    @JsonProperty("earliestDate") val earliestDate: String,
    @JsonProperty("latestDate") val latestDate: String,
    @JsonProperty("fullTimeHours") val fullTimeHours: Int,
    @JsonProperty("tooFewHours") val tooFewHours: Int,
    @JsonProperty("tooManyHours") val tooManyHours: Int,
    @JsonProperty("journalEntryDate") val journalEntryDate: String?
)

In Practice

I’m not going to delve into the React code that calls this service, I think that’s a simple exercise. But I will show the API requests exchanged through testing. After the user has uploaded all three files, they click the Analyze button. This sends a AnalyzeRequest without any ‘params’.

{
   "files":{
      "projectSheet":"e337a966abcc0632df09c9e78bf09ed1",
      "employeeCost":"d7eef25b99e8f05c7fae3aab3ad8b737",
      "employeeCostPassword":"fdsa",
      "timeReport":"9729240d7d74cc50b0f486809090b95f"
   }
}

The controller takes this request and returns a success response, providing default ‘params’ as well as the ouput text and file. Note that the ‘exportFile’ is the same as the ‘projectSheet’ file supplied in the request.

{
   "success":{
      "params":{
         "earliestDate":"2020-02-01",
         "latestDate":"2020-02-28",
         "fullTimeHours":160,
         "tooFewHours":120,
         "tooManyHours":200,
         "journalEntryDate":"2020-02-28"
      },
      "output":{
         "isError":false,
         "warnings":"Test output"
      },
      "exportFile":"e337a966abcc0632df09c9e78bf09ed1"
   }
}

The default ‘params’ are set in the page’s input controls and the user can modify them. When the user modifies them, the React page sends in those ‘params’ in subsequent AnalyzeRequest’s.

Default parameters are supplied in the page. The user can modify the parameters and click Analyze again

File Download Frontend

But what I really want to dive into is the file download component, and how that works. In the above example, the ‘exportFile’ holds the name of the output file on the server. This value is sent to the OutputFile component. The OutputFile component is listed below.

export class OutputFile extends React.Component {

    onSubmit = (e) => {
        e.preventDefault();
    };

    render() {
        const isReady = this.props.state.isReady();
        const date = this.props.state.getDate();
        const file = this.props.state.getFile();

        return(
            <div className="OutputFile">
                <form onSubmit={this.onSubmit} className="Form">
                    <div className="FormDiv">
                        <label>Journal Entry Date:</label>
                    </div>
                    <div className="FormDiv">
                        <input type="date" id="qbDate" name="Journal entry import date" size="5" readOnly value={date}/>
                    </div>
                </form>
                <div className="Actions">
                    {isReady ? this.renderWhenReady(file, date) : this.renderWhenNotReady() }
                </div>
            </div>
        )
    }

The function to download the file is driven by the components rendered in the renderWhenReady() function. That function simply creates an <a> tag that lets the user download the file. The download link URL is constructed by a couple functions shown below.

    renderWhenReady = (file, date) => {
        return (
            <a className={"button"} href={this.getDownloadURL(file, date)} download>Download File</a>
        );
    };

    getDownloadURL = (file, date) => {
        return "jobcost/download?file=" + encodeURIComponent(file) + "&date=" + encodeURIComponent(date);
    };

One interesting thing I ran into at this stage is that the React development environment proxy does not proxy normal GET requests – as sent by an anchor tag. This means that, when in development mode, the anchor tag to download “jobcost/download?file=X” does not get proxied to “http://localhost:8080/jobcost/download?file=X”. The link stays as “http://localhost:3000/jobcost/download?file=X” so the GET request doesn’t make it to a Spring controller.

To get around this issue, I chose to create a JavaScript function that tests whether the page is running in a development mode or not. That function is shown below.

    isReactInDevelopmentMode = () => {
        return '_self' in React.createElement('div');
    };

Then I changed the getDownloadURL() function to conditionally build an absolute path or a relative path to download the file depending on the result of isReactInDevelopmentMode().

    getDownloadURL = (file, date) => {
        return this.getURLHostPortion() + "jobcost/download?file=" + encodeURIComponent(file) + "&date=" + encodeURIComponent(date);
    };

    getURLHostPortion = () => {
        if(this.isReactInDevelopmentMode())
            return "http://localhost:8080/";
        else
            return "";
    };

In development, this code generates a download URL of “http://localhost:8080/jobcost/download?file=X&#8221;. In production, this code generates a relative URL of “jobcost/download?file=X”, which will resolve to “http://localhost:8080&#8221; when I run the production JAR locally, but which will also resolve to whatever URL use to serve the app in production.

File Download Backend

In the backend, it’s quite simple to setup a Spring controller that serves up the file for download. Using the Spring Resource package, I just need to get the file as a URL resource and built the ResponseEntity accordingly. This includes setting the content type and content disposition header fields.

The only other bit here is that I append the journal entry date to the downloaded file name if one is supplied in the request.

@RestController
class FileDownload {

    @GetMapping(value = ["/jobcost/download"])
    @CrossOrigin(origins = ["http://localhost:3000"])
    fun handleGetFile(
            @RequestParam("file") fileName: String,
            @RequestParam("date", required = false) date: String?
    ): ResponseEntity<Resource>? {

        return try {
            val resource = getFileAsResource(fileName)
            if(!resource.exists())
                throw Exception("File not found $fileName - file doesn't exist")

            ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"${getImportFileName(date)}\"")
                    .body(resource)
        } catch(ex: MalformedURLException) {
            throw Exception("File not found $fileName", ex)
        }

        return ResponseEntity.ok().build<Resource>()
    }

    private fun getFileAsResource(fileName: String): Resource {
        val file = getFile(fileName)
        return UrlResource(file.normalize().toURI())
    }

    private fun getImportFileName(date: String?) = if(date?.isEmpty() != false) "import.iif" else "import-$date.iif"
}

And there you have it! A file download link being served by a Kotlin/Spring service. Hope you enjoyed this post, and I promise the next post will dive into adding the Java Swing code into the backend app.

Leave a comment