8

Idea: I have a Spring web MVC action that should accomplish one or both of these taks:

  • Download a file from a remote server and write the input stream to the response output stream
  • Or catch the exception of the download, set one of multiple error messages and redirect to the /addresses page. The address page will display the error

Problem: Spring is unable to download a file and redirect in case of a problem - somehow flash attributes don't work because the get lost in the redirect:

@ResponseBody
@RequestMapping(value = "/download/{fileaddress}", method = RequestMethod.GET)
public void download(HttpServletRequest request, HttpServletResponse response, @PathVariable(value = "fileaddress") String fileaddress) throws Exception
{
    if(fileaddress != null && fileaddress.length() > 0)
    {
        try
        {
            // Get the remove file based on the fileaddress
            RemoteFile remotefile = new RemoteFile(fileaddress);

            // Set the input stream
            InputStream inputstream = remotefile.getInputStream();

            // Write the input stream to the output stream or throw an exception
            Utils.writeTo(inputstream, response.getOutputStream());
        }
        catch(MyExceptionA)
        {
            // TODO: Define error message a and pass it to /addresses
            // PROBLEM: Flash attributes that contain all critical error information don't work
            response.sendRedirect(request.getContextPath() + "/addresses");
        }
        catch(MyExceptionB)
        {
            // TODO: Add another error message and redirect
            response.sendRedirect(request.getContextPath() + "/addresses");
        }
        catch(MyExceptionC)
        {
            // TODO: Add another error message and redirect
            response.sendRedirect(request.getContextPath() + "/addresses");
        }
        catch(MyExceptionN)
        {
            // TODO: Add another error message and redirect
            response.sendRedirect(request.getContextPath() + "/addresses");
        }
    }
    else
    {
        // TODO: Add error message
        response.sendRedirect(request.getContextPath() + "/addresses");
    }
}

JSP page of /addresses:

<%@ page pageEncoding="UTF-8" %>
<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>

<tags:index>
    <jsp:attribute name="content">
        <core:if test="${not empty error}">
            <div class="alert alert-danger">
                <p>${error}</p>
            </div>
        </core:if>
        <p>Page under construction!</p>
    </jsp:attribute>
</tags:index>

Question: How I am able to display the error message (Simple string for example) in the /addresses site? Working with different URL parameter (error=errora, error=errorb ...) is a huge pain, if there are multiple error types and passing the error message as GET parameter looks unprofessional and is the root of encoding problems.

swaechter
  • 1,173
  • 3
  • 20
  • 41

4 Answers4

8

What you need is the RedirectAttributes a specialization of the Model which controllers can use to select attributes for a redirect scenario. So for a working example see below code:

@ResponseBody
@RequestMapping(value = "/download/{fileaddress}", method = RequestMethod.GET)
public Object download(@PathVariable(value = "fileaddress") String fileaddress, RedirectAttributes redirectAttrs) throws Exception {
    if(StringUtils.hasText(fileaddress)){
        try{
            // Get the remove file based on the fileaddress
            RemoteFile remotefile = new RemoteFile(fileaddress);

            // Set the input stream
            InputStream inputstream = remotefile.getInputStream();
            // asume that it was a PDF file
            HttpHeaders responseHeaders = new HttpHeaders();
            InputStreamResource inputStreamResource = new InputStreamResource(inputStream);
            responseHeaders.setContentLength(contentLengthOfStream);
            responseHeaders.setContentType(MediaType.valueOf("application/pdf"));
            return new ResponseEntity<InputStreamResource> (inputStreamResource,
                                       responseHeaders,
                                       HttpStatus.OK);
         } catch (MyExceptionA | MyExceptionB | MyExceptionC | MyExceptionD ex) {
           redirectAttrs.addFlashAttribute("error", ex.getMessage());
         }        
    } else {
        redirectAttrs.addFlashAttribute("error", "File name is required");
    }
    return "redirect:/addresses";
}
Babl
  • 6,198
  • 24
  • 31
  • Here might be a problem since IOUtils.toByteArray(inputstream) reads everything from file into memory. If a file is too big, that approach doesn't work. – Kuvaldis Nov 18 '15 at 15:29
  • I do agree, but the question is about the ``RedirectAttributes`` not about how to stream files :D – Babl Nov 18 '15 at 18:31
  • I have updated the code example to fix your in memory issue :) – Babl Nov 18 '15 at 18:37
  • Thank you very much, you saved my morning ;) The HttpHeader/ResponseEntity in combination with the flash attributes made the trick! PS: You have a missing bracket where you return the ResponseEntity – swaechter Nov 19 '15 at 07:22
2

Update: I've thought the question is about situations where RedirectAttributes are not available because otherwise, the solution is pretty obvious (use RedirectAttributes).

Orignal answer:

I'm using the following code to bind messages to the flash map in situations where Spring doesn't support RedirectAttributes (e.g. in ExceptionHandler methods):

public static Feedback getInstance(HttpServletRequest request, HttpServletResponse response) throws IllegalArgumentException {
    FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
    Object o = flashMap.get(KEY);
    if (o != null) {
        if (o instanceof Feedback) {
            return Feedback.class.cast(o);
        } else {
            throw new IllegalArgumentException(...);
        }
    } else {
        FeedbackContainer feedbackContainer = new FeedbackContainer();
        flashMap.put(KEY, feedbackContainer);
        FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
        flashMapManager.saveOutputFlashMap(flashMap, request, response);
        return feedbackContainer;
    }

where Feedback / FeedbackContainer is a container for messages which is then accessed in JPSs via JSON serialization. In your case you may use a mere String with key "error" and access it directly in the JSP:

void storeErrorMsg(HttpServletRequest request, HttpServletResponse response, String message) {
   FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
   flashMap.put("error", message);
   FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
   flashMapManager.saveOutputFlashMap(flashMap, request, response);
}

The main reason for using my own message container is the possibility to have multiple messages with different levels and additional getInstance methods for RedirectAttributes, Model or ModelMap, so I don't have to care about duplicate feedback bindings and/or the different binding code.

Robin
  • 2,747
  • 2
  • 17
  • 23
  • Thank you for the idea, but can you edit the question in a way it provides a full working example? For example, how is KEY defined? – swaechter Nov 17 '15 at 14:23
  • KEY is a constant containing the string to use for storing my feedback instance, e.g. "feedback" so in the JSP I can access all message via `${feedback.all}` (if `Feedback` defines a method `getAll()`). In your case it's simply "error" but you may choose any value you want. – Robin Nov 17 '15 at 14:50
1

i can't explain about why it is not downloading a file from server but coming to your second query , you can redirect in two ways either take your method return type as ModelAndView and do like this

ModelAndView mav = new ModelAndView("write here your jsp page name even is will work for remote server");
        try {
            mav.addObject("some key","here you can write any message that will you can show in your jsp page..");

then return mav. like this you can redirect how many pages you want with if condition.

and second approach you can take your method return type String and you can directly redirect to your required jsp.

suri
  • 275
  • 1
  • 6
  • 19
  • Just see the download as an input stream where a problem can occur (Disconnect during the write process etc.) The problem is: You cannot write to the output stream and later on redirect in case of a problem - so I think your answer is not really possible – swaechter Nov 10 '15 at 08:13
0

I would suggest the following approach: Since you have some remote file as an input stream you cannot read it from the point where an exception occurred. Hence you can decrease error chances with first downloading the file to a temp folder on local host.

Here if there is an error, you can either redirect as you suggested, or just make another attempt and redirect after several failed attempts. This way you are not writing anything to the stream until you are sure the file downloaded successfully.

Once the file is downloaded to the server you can pass it to the client. For instance you can read bytes and save current position. Suppose there is an exception in the middle of writing to the stream. You can restore writing from failed point reading from the middle.

A small example

...
    final String localFileName = saveFileLocally(fileaddress)
    if (localFileName == null) {
        response.sendRedirect(request.getContextPath() + "/addresses");
    } else {
        safeWrite(new BufferedInputStream(new FileInputStream(new File(localFileName))), response.getOutputStream());
    }
...

safeWrite should support local file read exception handling with reading from the file middle. There should be standard java api for such operations.

Kuvaldis
  • 348
  • 1
  • 9