Alkahna Alkahna - 4 months ago 15
Javascript Question

Servlet-Response containing text (for display) as well as file download

I'm trying to download a file from my server through a Java Servlet.
The Problem I have is that when I enter the servlet url directly (https://localhost:8443/SSP/settings?type=db_backup) I get the servlet to execute its code and prompt me with a download dialog.

But I would like to call the servlets doGet method via Javascript to wrap it with a progress bar of some kind.

Problem here: Code in servlet is executed but I dont get the download prompt for the file.

My Code so far:

HTML:

<!-- Solution #1 -->
<button class="btn_do_db_backup" type="button">DB-Backup #1</button>

<!-- Solution #2 -->
<form action="/SSP/settings?type=db_backup" method="GET">
<button type="submit">DB-Backup #2</button></br>
</form>


JS:

// Solution #1
$(".btn_do_db_backup").click(function(e){
e.preventDefault();
$.get("settings?type=db_backup", function(data){
if(data != ""){
//further coding
}
});

// Having the code below works but doesnt
// give me the chance to wrap the call with a loading animation

//document.location = "/SSP/settings?type=db_backup";
});


Servlet:

@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
// PART 1
// execute srcipt to generate file to download later on

StringBuffer output = new StringBuffer();
ProcessBuilder builder = new ProcessBuilder("cmd.exe", "/c", "D:\\TEMP\\sql_dump.cmd");
builder.redirectErrorStream(true);
Process p = builder.start();
BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = "";
String filename = "";
int tmp = 0;
while (true) {
line = r.readLine();
if (line == null) { break; }
output.append(line + "\n");
// code for finding filename not optimal but works for now -> redo later on
if(tmp == 1){
filename = line.substring(line.indexOf("db_backup_"), line.indexOf('"', line.indexOf("db_backup_")) );
}
tmp++;
}

// PART 2
// download the file generated above

OutputStream out = response.getOutputStream();
String filepath = "D:\\TEMP\\sql_dump\\";

response.setContentType("APPLICATION/OCTET-STREAM");
response.setHeader("Content-Disposition", "attachment;filename=\"" + filename + "\"");

FileInputStream fileInputStream = new FileInputStream(filepath + filename);

int i;
while ((i = fileInputStream.read()) != -1) {
out.write(i);
}

out.close();
fileInputStream.close();
}


Solution #2 works great, I get a popup to download the file.

Solution #1 calls the servlets doGet-method (via the above JS-Code and the code from my servlet is executed correctly) but I dont get a download popup

I would like to go with solution #1 though as this gives me the opportunity to wrap the
$.post
call with a loading animation.

What am I missing within solution #1 to get that download popup to shop up?

EDIT 1:

I found that
data
in the
$.get()
function is filled with the content of the desired file. I can now display the content of a .txt file in a div for example but I would like to donwload said .txt file instead.

EDIT 2:

Solved it, see my answer below for details & comment/ansewer if you think it can be done in a better way

Answer

after quite some time trying to get it to work I found a solution that works. There may be better ones but thats the one I came up with.

Hope this may be helpfull for others as well.

Basic explanation of what I did here:

  • Have a form do a GET-Request (via JS) to a java servlet
  • The servlet executes a commandline script (in my case a sql-dump of my postgreSQL DB)
  • The servlets gathers the output from the commandline and the contents of the generated file (the sql_dump) and puts them in the response
  • The client gets the response and cuts it into 3 pieces (commandline output, filename & contents of sql_dump-file)
  • Then (via JS) the commandline output is shown in a textarea for a better overview of what the script actually did
  • The contents of the sql_dump-file is processed by JS-Code to generate a file to download (eihter manually via a button or automatically)


So without further ado, here we go with the flow ... code :)

SOLUTION:

HTML:

<form id="form_download_db_backup">
    <input type="submit" value="Create & Download DB-Backup"></br>
    <a download="" id="downloadlink" style="display: none">download</a>
</form>
<div class="db_backup_result" id="db_backup_result" style="display: none;">
    </br>Commandline-Output</br>
    <textarea id ="txta_db_backup_result" rows="4" cols="50"></textarea>
</div>

JS:

$("#form_download_db_backup").submit(function(e){
    e.preventDefault();
    var spinner = new Spinner().spin();
    var target = document.getElementById('content');
    target.appendChild(spinner.el);
    $.ajax({
        url:'settings?type=db_backup',
        type:'get',
        success:function(data){
            spinner.stop();
            if(data != ""){
                var str_data = "" + data;

                // Cut commanline output from data
                var commandline_output = str_data.substring( 0, str_data.indexOf("--End") );

                //show commanline output in textarea
                $("#txta_db_backup_result").html(commandline_output);

                // Cut content of db_backup file from data
                var sql_dump_content = str_data.substring( str_data.indexOf("--sql_d_s--") + 13,str_data.indexOf("--sql_d_e--") );//|

                // Cut filename from data
                var filename = str_data.substring( str_data.indexOf("--sql_d_fns--") + 15,str_data.indexOf("--sql_d_fne--") - 2 );

                //-------------------------------------------------------------
                // Prepare download of backupfile
                var link = document.getElementById('downloadlink');
                var textFile = null;
                var blob_data = new Blob([sql_dump_content], {type: 'text/plain'});

                // FOR IE10+ Compatibility
                if(window.navigator.msSaveOrOpenBlob) {
                    window.navigator.msSaveBlob(blob_data, filename);
                }

                // If we are replacing a previously generated file we need to
                // manually revoke the object URL to avoid memory leaks.
                if (textFile !== null) {
                  window.URL.revokeObjectURL(textFile);
                }
                textFile = window.URL.createObjectURL(blob_data);
                link.href = textFile;
                link.download = filename;
                //link.style.display = 'block'; // Use this to make download link visible for manual download
                link.click(); // Use this to start download automalically
                //-------------------------------------------------------------

                // show div containing commandline output & (optional) downloadlink
                document.getElementById("db_backup_result").style.display = 'block';
            }
        }
    });
});

Java-Servlet:

@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
    String type = request.getParameter("type");

    if(null != type)switch (type) {
        case "db_backup":

            ServletOutputStream out = response.getOutputStream();

            // Prepare multipart response
            response.setContentType("multipart/x-mixed-replace;boundary=End");

            // Start: First part of response ////////////////////////////////////////////////////////////////////////

            // execute commandline script to backup the database
            ProcessBuilder builder = new ProcessBuilder("cmd.exe", "/c", "D:\\TEMP\\sql_dump.cmd");
            builder.redirectErrorStream(true);
            Process p = builder.start();
            BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
            String line = "";
            String filename = "";
            int tmp = 0;

            while (true) {
                line = r.readLine();
                if (line == null) { break; }
                // code for finding filename not optimal but works for now -> redo later on
                if(tmp == 1){
                    filename = line.substring(line.indexOf("db_backup_"), line.indexOf('"', line.indexOf("db_backup_")) );
                }
                else{
                    line = line.replace("\u201E", "\'"); // replaces the lowercase " (DOUBLE LOW-9 QUOTATION MARK)
                    line = line.replace("\u201C", "\'"); // replaces the uppercase " (LEFT DOUBLE QUOTATION MARK)
                }
                out.println(line);
                tmp++;
            }

            // End: First part of response ////////////////////////////////////////////////////////////////////////

            // Separator of firt & second part
            out.println("--End");
            out.flush();

            // Add filename in response (name of download file)
            out.println("--sql_d_fns--"); // separator for filename (used to extract filename from response data)
            out.println(filename);
            out.println("--sql_d_fne--"); // separator for filename (used to extract filename from response data)

            // Start: Second part of response ////////////////////////////////////////////////////////////////////////
            out.println("--sql_d_s--"); // separator for content of db-dump (this is the text thats going to be downloaded later on)
            String filepath = "D:\\TEMP\\sql_dump\\";

            FileInputStream fileInputStream = new FileInputStream(filepath + filename);

            int i;
            while ((i = fileInputStream.read()) != -1) {
                out.write(i);
            }
            out.println("--sql_d_e--"); // separator for content of db-dump (this is the text thats going to be downloaded later on)
            // End: Second part of response ////////////////////////////////////////////////////////////////////////

            // End the multipart response
            out.println("--End--");
            out.flush();
            break;
        default:
            break;
    }
}

postgreSQL dump contain "lowercase" & "uppercase" quotation marks which I had to replace. I put a link to each here in case someone struggles with them as well. They have multiple encodings for those characters listed there.

Unicode Character 'DOUBLE LOW-9 QUOTATION MARK' (U+201E)

Unicode Character 'LEFT DOUBLE QUOTATION MARK' (U+201C)