Display dynamic image from database or remote source with p:graphicImage and StreamedContent
Asked Answered
L

4

57

I'm trying to display image bytes which is saved in database as a StreamedContent in the <p:graphicImage> as follows:

<p:graphicImage  value="#{item.imageF}" width="50"  id="grpImage" height="80"/>
private StreamedContent content; // getter and setter

public StreamedContent getImageF() {

    if (student.getImage() != null) {
        InputStream is = new ByteArrayInputStream(student.getImage());
        System.out.println("Byte :"+student.getImage());
        content = new DefaultStreamedContent(is, "", student.getStuID());
        System.out.println("ddd ------------------------------- " + content);
        return content;
    }

    return content;
}

This returns a blank image. How is this caused and how can I solve it?

The stdout prints the following:

INFO: Byte :[B@a2fb48
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@b0887b
INFO: Byte :[B@a2fb48
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1d06a92
INFO: Byte :[B@d52f0b
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@39a60
INFO: Byte :[B@d52f0b
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@8c3daa
INFO: Byte :[B@124728a
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1dbe05b
INFO: Byte :[B@124728a
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@66a266
INFO: Byte :[B@a2fb48
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1293976
INFO: Byte :[B@a2fb48
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@17b7399
INFO: Byte :[B@d52f0b
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1e245a5
INFO: Byte :[B@d52f0b
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@4a7153
INFO: Byte :[B@124728a
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1561bfd
INFO: Byte :[B@124728a
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@47a8c2
Learning answered 21/11, 2011 at 4:36 Comment(0)
N
112

The <p:graphicImage> requires a special getter method. It will namely be invoked twice per generated image, each in a completely different HTTP request.

The first HTTP request, which has requested the HTML result of a JSF page, will invoke the getter for the first time in order to generate the HTML <img> element with the right unique and auto-generated URL in the src attribute which contains information about which bean and getter exactly should be invoked whenever the webbrowser is about to request the image. Note that the getter does at this moment not need to return the image's contents. It would not be used in any way as that's not how HTML works (images are not "inlined" in HTML output, but they are instead requested separately).

Once the webbrowser retrieves the HTML result as HTTP response, it will parse the HTML source in order to present the result visually to the enduser. Once the webbrowser encounters an <img> element during parsing the HTML source, then it will send a brand new HTTP request on the URL as specified in its src attribute in order to download the content of that image and embed it in the visual presentation. This will invoke the getter method for the second time which in turn should return the actual image content.

In your particular case PrimeFaces was apparently either unable to identify and invoke the getter in order to retrieve the actual image content, or the getter didn't return the expected image content. The usage of #{item} variable name and the lot of calls in the log suggests that you were using it in an <ui:repeat> or a <h:dataTable>. Most likely the backing bean is request scoped and the datamodel isn't properly preserved during the request for the image and JSF won't be able to invoke the getter during the right iteration round. A view scoped bean would also not work as the JSF view state is nowhere available when the browser actually requests the image.


To solve this problem, your best bet is to rewrite the getter method as such so that it can be invoked on a per-request basis wherein you pass the unique image identifier as a <f:param> instead of relying on some backing bean properties which may go "out of sync" during subsequent HTTP requests. It would make completely sense to use a separate application scoped managed bean for this which doesn't have any state. Moreover, an InputStream can be read only once, not multiple times.

In other words: never declare StreamedContent nor any InputStream or even UploadedFile as a bean property; only create it brand-new in the getter of a stateless @ApplicationScoped bean when the webbrowser actually requests the image content.

E.g.

<p:dataTable value="#{bean.students}" var="student">
    <p:column>
        <p:graphicImage value="#{studentImages.image}">
            <f:param name="studentId" value="#{student.id}" />
        </p:graphicImage>
    </p:column>
</p:dataTable>

Where the StudentImages backing bean can look like this:

@Named // Or @ManagedBean
@ApplicationScoped
public class StudentImages {

    @EJB
    private StudentService service;

    public StreamedContent getImage() throws IOException {
        FacesContext context = FacesContext.getCurrentInstance();

        if (context.getCurrentPhaseId() == PhaseId.RENDER_RESPONSE) {
            // So, we're rendering the HTML. Return a stub StreamedContent so that it will generate right URL.
            return new DefaultStreamedContent();
        }
        else {
            // So, browser is requesting the image. Return a real StreamedContent with the image bytes.
            String studentId = context.getExternalContext().getRequestParameterMap().get("studentId");
            Student student = studentService.find(Long.valueOf(studentId));
            return new DefaultStreamedContent(new ByteArrayInputStream(student.getImage()));
        }
    }

}

Please note that this is a very special case wherein performing business logic in a getter method is completely legit, considering how the <p:graphicImage> works under the covers. Invoking business logic in getters is namely usually frowned upon, see also Why JSF calls getters multiple times. Don't use this special case as excuse for other standard (non-special) cases. Please also note that you can't make use of EL 2.2 feature of passing method arguments like so #{studentImages.image(student.id)} because this argument won't end up in the image URL. Thus you really need to pass them as <f:param>.


If you happen to use OmniFaces 2.0 or newer, then consider using its <o:graphicImage> instead which can be used more intuitively, with an application scoped getter method directly delegating to the service method and supporting EL 2.2 method arguments.

Thus so:

<p:dataTable value="#{bean.students}" var="student">
    <p:column>
        <o:graphicImage value="#{studentImages.getImage(student.id)}" />
    </p:column>
</p:dataTable>

With

@Named // Or @ManagedBean
@ApplicationScoped
public class StudentImages {

    @EJB
    private StudentService service;

    public byte[] getImage(Long studentId) {
        return studentService.find(studentId).getImage();
    }

}

See also the blog on the subject.

Neolamarckism answered 17/9, 2012 at 1:26 Comment(17)
BalusC, this is exactly what I was looking for, however I just couldn't figure out how the response should be sent back to the client. You're truly a legend. Thank you very much. :)Ideally
@Neolamarckism Is it possible to use ManageBean with RequestScope. How to render dynamic image with RequestScope bean?Madera
@Cyc: just use exactly the same code. It's only unnecessarily expensive to recreate the very same bean instance again and again while it can perfectly be reused across multiple images.Neolamarckism
@BalusC, What I mean is I will keep studenList as Map in RequestScope bean when bean initialization state. I will retrieve Student from the Map at getImage() method. Is it ok?Madera
Fantastic! A minor typo in the code? The f:param name is "id" but .get("studentId") does not match that name.Pothole
Isn't injecting a stateless EJB into an application scoped bean as shown in this example, harmful across requests? (Isn't it shared across requests?) Apparently that should not, since stateless EJBs like Servlets are not created upon requests but I cannot convince myself. Am I guessing wrong?Isoelectronic
@Tiny: EJBs and CDI managed beans are injected as proxies. The method call on the proxy instance basically locates the current instance in their scope and then invokes the desired method on it. You see this easily back in stacktraces from exceptions coming from those methods. Their scope is therefore not tied to the scope of the client and can easily be smaller.Neolamarckism
What a pitty for me Omnifaces 2.0 being CDI dependent :-(Eldrida
@Xtreme: consider moving on before it's too late.Neolamarckism
Hi, I applied the same solution. The problem I have is once the PDF is updated in database and I try to reload it, it still gets the same PDF (as if from some cache) and not the new one from database. What should be done for thatManipulator
@BalusC, hi, isnt ApplicationScoped shared across all the application ? and wouldnt this make all the data to be shared among all the users that make request's? so for example user A will see the same document as user B ? , ... im kind of begginer in JSF so sorry if the answer is obviousLiberty
@Julios_Rodmax: the data is not saved as instance variable of bean for future reuse. It's created brand new in getter method on every invocation. In fact, you should never save any InputStream as instance variable for future reuse. It can be read only once! This is not JSF specific, this is just basic Java.Neolamarckism
thanks @BalusC!!, one more question, so i have my jsf page(xhtml) , i have to use values from a viewscope bean(because i need to work with the data of the user) , but for the InputStream i have to use ApplicationScoped, can i mix 2 beans(the view scope and the ApplicationScoped) in the same jsf?Liberty
@BalusC, hi , i have a question , what if for example we have user A and user B, since we are using ApplicationScoped bean and is shared accross the the application, if they both do a request for a file almost at the same time , does user B have to wait until the request of user A ends? for example if user A request a large file that takes 30 secons, user B will have to wait that?Liberty
@Julios_Rodmax: If the method is not synchronized then there's absolutely no delay. Java supports multithreading, you know.Neolamarckism
@Neolamarckism , can i ask you? , i have p:fileUpload to upload a pdf, but i want to make a preview of it , i can get the InputStream with the metod of the FileUploadEvent listener, but with the ApplicationScoped bean i need a service to consume it with an id, but do you think theres a way to make a preview of the uploaded file without saving it in a database? thanks for all your help , i appreciate a lot !Liberty
Hi @BalusC. Is it possible to listen to the second time invoking the getter and do some actions when the response is sent?Irisation
A
6

Try including a mime type. In your posted example, you have it as "". The blank image may be because it doesn't recognize the stream as a image file since you made that field an empty string. So add a mime type of image/png or image/jpg and see if that works:

String mimeType = "image/jpg";
StreamedContent file = new DefaultStreamedContent(bytes, mimeType, filename);  
Annular answered 26/9, 2012 at 22:24 Comment(0)
C
5

There's a couple possibilities here (and please post the entire class if this isn't it).

1) You're not initializing the image properly 2) Your stream is empty so you're getting nothing

I'm assuming student.getImage() has a signature of byte[] so first make sure that that data is actually intact and represents an image. Secondly--you're not specifying a content-type which should be "image/jpg" or whatever you're using.

Here's some boilerplate code to check it with, I'm using Primefaces 2 for this.

/** 'test' package with 'test/test.png' on the path */
@RequestScoped
@ManagedBean(name="imageBean")
public class ImageBean
{
    private DefaultStreamedContent content;

    public StreamedContent getContent()
    {
        if(content == null)
        {
            /* use your database call here */
            BufferedInputStream in = new BufferedInputStream(ImageBean.class.getClassLoader().getResourceAsStream("test/test.png"));
            ByteArrayOutputStream out = new ByteArrayOutputStream();

            int val = -1;
            /* this is a simple test method to double check values from the stream */
            try
            {
                while((val = in.read()) != -1)
                    out.write(val);
            }
            catch(IOException e)
            {
                e.printStackTrace();
            }

            byte[] bytes = out.toByteArray();
            System.out.println("Bytes -> " + bytes.length);
            content = new DefaultStreamedContent(new ByteArrayInputStream(bytes), "image/png", "test.png");
        }

        return content;
    }
}

and some markup...

<html 
    xmlns="http://www.w3.org/1999/xhtml" 
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:p="http://primefaces.prime.com.tr/ui"
>

    <h:head>

    </h:head>

    <h:body>
        <p:graphicImage value="#{imageBean.content}" />
    </h:body>
</html>

If that code works then you're set up properly. Despite the fact it is garbage code for the streams (don't use it in production) it should give you a point to troubleshoot from. My guess is that you might have something happening in your JPA or other Database framework where you're byte[] is empty or it is formatted wrong. Alternatively you could just have a content-type problem.

Lastly, I would clone the data from the bean so that student.getImage() would only be copied into a new array and then used. This way if you have something unknown going on (something else moving the object or changing the byte[] you're not messing with your streams.

Do something like:

byte[] data = new byte[student.getImage().length]
for(int i = 0; i < data.length; i++)
  data[i] = student.getImage()[i];

so that your bean has a copy (or Arrays.copy()--whatever floats your boat). I can't stress enough how something simple like this/content type is usually what's wrong. Good luck with it.

Configuration answered 27/11, 2011 at 17:48 Comment(0)
A
4

The answer from BalusC is (as usual) the correct one.

But keep one thing (as already stated by him) in mind. The final request is done from the browser to get the URL from the constructed <img> tag. This is not done in a 'jsf context'.

So if you try to e.g. access the phaseId (logging or whatever reason)

context.getCurrentPhaseId().getName()

This will result in a NullPointerException and the somehow misleading error message you will get is:

org.primefaces.application.resource.StreamedContentHandler () - Error in streaming dynamic resource. Error reading 'image' on type a.b.SomeBean

It took me quite some time to figure out what was the problem.

Active answered 1/9, 2016 at 16:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.