views:

29

answers:

1

I am creating a custom component that is an image viewer for a given product number. I access these files using a modified version of BalusC's ImageServlet:

@WebServlet(name="ImageLoader", urlPatterns={"/ImageLoader"})
public class ImageLoader extends HttpServlet {

    private static final int DEFAULT_BUFFER_SIZE = 10240; // 10KB.

    private static String imagePath = "\\\\xxx.xxx.x.x\\root\\path\\to\\images\\";

    /** 
      * This code is a modified version of the ImageServlet found at balusc.blogspot.com.
     * It expects the parameters id and n.
     * <ul>
     * <li><b>id:</b> the product number</li>
     * <li><b>n:</b> the image number to load.</li>
     */
    public void goGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {

        String productNumber = URLDecoder.decode(request.getParameter("id"),"utf-8");
        String img = URLDecoder.decode(request.getParameter("n"),"utf-8");

        if (productNumber == null || img == null) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND); // 404.
            return;
        }

        String path = generatePath(productNumber);

        File image = new File(generatePath(productNumber), generateImageName(img));

        // Check if file actually exists in filesystem.
        if (!image.exists()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND); // 404.
            return;
        }

        String contentType = getServletContext().getMimeType(image.getName());

        if (contentType == null || !contentType.startsWith("image")) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND); // 404.
            return;
        }

        // Init servlet response.
        response.reset();
        response.setBufferSize(DEFAULT_BUFFER_SIZE);
        response.setContentType(contentType);
        response.setHeader("Content-Length", String.valueOf(image.length()));
        response.setHeader("Content-Disposition", "inline; filename=\"" + image.getName() + "\"");

        // Prepare streams.
        BufferedInputStream input = null;
        BufferedOutputStream output = null;

        try {
            // Open streams.
            input = new BufferedInputStream(new FileInputStream(image), DEFAULT_BUFFER_SIZE);
            output = new BufferedOutputStream(response.getOutputStream(), DEFAULT_BUFFER_SIZE);

            // Write file contents to response.
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int length;
            while ((length = input.read(buffer)) > 0) {
                output.write(buffer, 0, length);
            }
        } finally {
            close(output);
            close(input);
        }
     }

    private String generateImageName(String n) {
        int imageNum = Integer.parseInt(n);

        StringBuilder ret = new StringBuilder("img-");
        if (imageNum < 10) {
            ret.append("00");
        }
        else if(imageNum < 100) {
            ret.append("0");
        }
        ret.append(n);
        ret.append(".jpg");
        return ret.toString();
    }


    public static String generatePath(String productNumber) {
        Long productNumberLng = Long.parseLong(productNumber);

        StringBuilder ret = new StringBuilder(imagePath);

        Long thousandPath = productNumberLng - (productNumberLng % 1000);
        ret.append(thousandPath);
        ret.append("s\\");
        ret.append(productNumber);
        ret.append("\\");
        ret.append(productNumber);
        ret.append("\\");

        return ret.toString();
    }

    private static void close(Closeable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Next I created a composite component:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"&gt;

  <!-- INTERFACE -->
  <cc:interface>
      <cc:attribute name="productNumber" shortDescription="The product number whose images should be displayed."

                    type="java.lang.Long" />
      <cc:attribute name="listID" shortDescription="This ID is the html ID of the &lt;ul&gt; element." />
  </cc:interface>

  <!-- IMPLEMENTATION -->
  <cc:implementation>
      <div id="#{cc.clientId}">
          <ul id="#{cc.attrs.listID}">
              <ui:repeat value="#{imageLoaderUtilBean.images}" var="image">
                  <li>

                      <h:graphicImage value="#{image.url}" alt="#{image.name}" />
                  </li>
              </ui:repeat>
          </ul>
      </div>
  </cc:implementation>
</html>

As you can see, I'm just grabbing the list of images from a managed bean. The only reason this is really necessary is because I need to know how many images there are for a given product. This can vary greatly (anywhere from 8 to 100). Here is that code:

@ManagedBean
@RequestScoped
public class ImageLoaderUtilBean {

    @ManagedProperty(value = "#{param.id}")
    private Long productNumber;

    private List<EvfImage> images;

    public List<EvfImage> getImages() {

        if (images == null) {
            setImages(findImages());
        }
        return images;
    }

    public void setImages(List<EvfImage> images) {
        this.images = images;
    }    

    public Long getProductNumber() {
        return productNumber;
    }

    public void setProductNumber(Long productNumber) {
        this.productNumber = productNumber;
    }

    private List<EvfImage> findImages() {


        FilenameFilter jpegFilter = new FilenameFilter() {

            @Override
            public boolean accept(File directory, String filename) {
                return filename.toLowerCase().endsWith(".jpg");
            }
        };

        File directory = new File(ImageLoader.generatePath(productNumber.toString()));
        if (!directory.exists()) {
            return new ArrayList<EvfImage>();
        }
        File[] files = directory.listFiles(jpegFilter);

        List<EvfImage> ret = new ArrayList<EvfImage>();

        for (int i = 1; i <= files.length; i++) {
            EvfImage img = new EvfImage();
            img.setName("file.getName()");
            img.setUrl("/ImageLoader?id=" + productNumber + "&amp;n=" + i);
            ret.add(img);
        }

        return ret;
    }

}

There is a simple object for holding the data I iterate over:

public class EvfImage {

    private String url;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

}

Finally, I test this composite component using a URL of http://localhost:8080/project-name/testImages.xhtml?id=213123. Here is the code for testImages.xhtml:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:sdCom="http://java.sun.com/jsf/composite/components/sd"&gt;
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        <sdCom:imageViewer listID="test" />
    </h:body>
</html>

Here is the problem: the only point of interaction between the application and the composite component should be the tag <sdCom:imageViewer listID="test" />. However, this is a leaky abstraction. The managed bean is given the product number based on the request's id parameter. This is very undesirable. It creates much tighter coupling between the component and the app which is using it. Ideally, I should use the tag as follows: <sdCom:imageViewer listID="test" productNumber="213123"/>. However, I can't figure out a way to do this and still know how many images I need to create.

Thanks in advance, Zack

Edit: It would be perfectly acceptable to call a servlet which takes in the product number and returns the number of images which that product has. However, I have yet to find a way to run a loop n times (for loop) as opposed to running it once for each object in a Collection (foreach loop). I'm pretty much happy with any solution which involves removing that @ManagedProperty("#{param.id}") from the backing bean.

A: 

A replacement of @ManagedProperty(value="#{param.id}") in ImageLoaderUtilBean would be

<sdCom:imageViewer listID="test" productNumber="#{param.id}" />

in combination with the following in cc:implementation before ui:repeat:

<c:set target="#{imageLoaderUtilBean}" property="productNumber" value="#{cc.attrs.productNumber}" />

Where the c: is the (actually discouraged) Facelets' builtin JSTL library which is to be declared as follows:

xmlns:c="http://java.sun.com/jsp/jstl/core"

Facelets has no replacement for c:set (yet?).

BalusC
that would still require me to break the composite component's interface. That would assume that every application which uses this component has a request parameter of `id`. Therefore, a single tag would be dictating the structure of the application which uses it. Also, that would result in issues embedding this tag in a lazy loaded tabbed panel, etc.
Zack
I see what you mean. I made a mistake, I've updated the answer. I'm however not sure if that will work properly since JSTL runs "out sync" of JSF. It's all theory :)
BalusC