Uploading Files to and Download Files from a Wicket Webapp

This page will give an example of how to allow the user to upload and download images (with thumbnail images). Uploading and downloading any kind of file could be done this way. The checks for content type would just be different. This example code was created for the wicket pastebin. Feel free to download the pastebin's entire codebase in order to see how all the pieces work together.

A Service for Persisting and Retrieving Images

ImageService is an interface. Persisting and retrieving images is the responsibility of any implementation of the ImageService interface. By using an interface, if we decide to persist this information in a different way than what we've done in the past, we just create a different service implementation and use that instead.

 public interface ImageService
 {
     public byte[] getImage(ImageEntry imageEntry)
        throws IOException;
     
     public boolean isImageAvailable(ImageEntry imageEntry);
     
     public Date getLastModifyTime(ImageEntry imageEntry);
     
     public byte[] getThumbnail(ImageEntry imageEntry)
        throws IOException;
 
     @Transactional
     public void save(ImageEntry imageEntry, InputStream imageStream)
        throws IOException;
 }

We have a special implementation of the ImageService interface called ImageServiceImpl. This implementation stores the image and a scaled thumbnail as files on the file system in a special folder. The name of the files are generated via random UUIDs. This file name along with other information, such as the file name on the client's computer and the content type is saved into a database record. Retrieving an image or its thumbnail is done by looking up the record in the database via its unique ID, retrieving the full path to the file system file, reading data from the file, and returning the data as a byte array. It could also be done by returning an InputStream instead of the byte array.

ImageServiceImpl:

 import com.mysticcoders.pastebin.model.ImageEntry;
 
 import com.mysticcoders.pastebin.dao.ImageEntryDAO;
 
 import com.sun.image.codec.jpeg.JPEGCodec;
 import com.sun.image.codec.jpeg.JPEGImageEncoder;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
 
 import java.util.Date;
 import java.util.UUID;
 
 import java.awt.Image;
 import java.awt.Graphics2D;
 import java.awt.RenderingHints;
 
 import java.awt.geom.AffineTransform;
 
 import java.awt.image.BufferedImage;
 
 import javax.swing.ImageIcon;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 /**
  * An implementation of ImageService.
  *
  * @author pchapman
  */
 public class ImageServiceImpl implements ImageService
 {
     // CONSTANTS
     
     private static final Log logger = LogFactory.getLog(ImageServiceImpl.class);
     
     // CONSTRUCTORS
 
     /**
      * Creates a new instance.
      */
     public ImageServiceImpl()
     {
         super();
     }
     
     // MEMBERS
 
     public byte[] getImage(ImageEntry imageEntry)
         throws IOException
     {
         if (isImageAvailable(imageEntry)) {
             // Open the file, then read it in.
             ByteArrayOutputStream outStream = new ByteArrayOutputStream();
             InputStream inStream =
                 new FileInputStream(new File(imageEntry.getFileName()));
             copy(inStream, outStream);
             inStream.close();
             outStream.close();
             return outStream.toByteArray();
         } else {
             return createNotAvailableImage(imageEntry.getContentType());
         }
     }
     
     public Date getLastModifyTime(ImageEntry imageEntry)
     {
         File f = new File(imageEntry.getFileName());
         return new Date(f.lastModified());
     }
  
     public boolean isImageAvailable(ImageEntry imageEntry)
     {
         return (
                 new File(imageEntry.getFileName()).exists() &&
                 new File(imageEntry.getThumbName()).exists()
             );
     }
     
     /* Spring Injected */
     private File imageDir;
     public void setImageDir(String dirName)
     {
         imageDir = new File(dirName);
     }
 
     /* Spring Injected */
     private ImageEntryDAO imageEntryDAO;
     /** The DAO implementation that will be used internally. */
     public void setImageEntryDAO(ImageEntryDAO imageEntryDAO)
     {
         this.imageEntryDAO = imageEntryDAO;
     }
 
     public byte[] getThumbnail(ImageEntry imageEntry)
         throws IOException
     {
         if (isImageAvailable(imageEntry)) {
             // Open the file, then read it in.
             ByteArrayOutputStream outStream = new ByteArrayOutputStream();
             InputStream inStream =
                 new FileInputStream(new File(imageEntry.getThumbName()));
             copy(inStream, outStream);
             inStream.close();
             outStream.close();
             return outStream.toByteArray();
         } else {
             byte[] imageData = createNotAvailableImage(imageEntry.getContentType());
             return scaleImage(imageData, getThumbnailSize());
         }
     }
 
     /* Spring Injected */
     private int thumbnailSize;
     public int getThumbnailSize()
     {
         return this.thumbnailSize;
     }
     public void setThumbnailSize(int size)
     {
         this.thumbnailSize = size;
     }
     
     // METHODS
  
     /**
      * Copies data from src into dst.
      */
     private void copy(InputStream source, OutputStream destination)
         throws IOException
     {
         try {
             // Transfer bytes from source to destination
             byte[] buf = new byte[1024];
             int len;
             while ((len = source.read(buf)) > 0) {
                 destination.write(buf, 0, len);
             }
             source.close();
             destination.close();
             if (logger.isDebugEnabled()) {
                 logger.debug("Copying image...");
             }
         } catch (IOException ioe) {
             logger.error(ioe);
             throw ioe;
         }
     }
     
     private File createImageFile(String suffix)
     {
         UUID uuid = UUID.randomUUID();
         File file = new File(imageDir, uuid.toString() + suffix);
         if (logger.isDebugEnabled()) {
             logger.debug("File " + file.getAbsolutePath() + " created.");
         }
         return file;
     }
     
     private byte[] createNotAvailableImage(String contentType)
         throws IOException
     {
         // Load the "Image Not Available"
         // image from jar, then write it out.
         StringBuffer name = new StringBuffer("com/mysticcoders/resources/ImageNotAvailable.");
         if ("image/jpeg".equalsIgnoreCase(contentType)) {
             name.append("jpg");
         } else if ("image/png".equalsIgnoreCase(contentType)) {
             name.append("png");
         } else {
             name.append("gif");
         }
         URL url = getClass().getClassLoader().getResource(
                 name.toString()
             );
         InputStream in = url.openStream();
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         copy(in, out);
         in.close();
         out.close();
         return out.toByteArray();
     }
  
     /** @see ImageService#save(ImageEntry) */
     public void save(ImageEntry imageEntry, InputStream imageStream)
         throws IOException
     {
         // Read in the image data.
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        copy(imageStream, baos);
        baos.close();
        byte[] imageData = baos.toByteArray();
        baos = null;
 
        // Get the image's suffix
        String suffix = null;
        if ("image/gif".equalsIgnoreCase(imageEntry.getContentType())) {
            suffix = ".gif";
        } else if ("image/jpeg".equalsIgnoreCase(imageEntry.getContentType())) {
            suffix = ".jpeg";
        } else if ("image/png".equalsIgnoreCase(imageEntry.getContentType())) {
            suffix = ".png";
        }
        
        // Create a unique name for the file in the image directory and
        // write the image data into it.
        File newFile = createImageFile(suffix);
        OutputStream outStream = new FileOutputStream(newFile);
        outStream.write(imageData);
        outStream.close();
        imageEntry.setFileName(newFile.getAbsolutePath());
 
        // Create a thumbnail
        newFile = createImageFile(".jpeg");
        byte[] thumbnailData = scaleImage(imageData, getThumbnailSize());
        outStream = new FileOutputStream(newFile);
        outStream.write(thumbnailData);
        outStream.close();        
        imageEntry.setThumbName(newFile.getAbsolutePath());
        
        // Save the image info in the database
        imageEntryDAO.save(imageEntry);
    }
    
    private byte[] scaleImage(byte[] imageData, int maxSize)
        throws IOException
    {
        if (logger.isDebugEnabled()) {
            logger.debug("Scaling image...");
        }
        // Get the image from a file.
        Image inImage = new ImageIcon(imageData).getImage();
 
        // Determine the scale.
        double scale = (double) maxSize / (double) inImage.getHeight(null);
        if (inImage.getWidth(null) > inImage.getHeight(null)) {
            scale = (double) maxSize / (double) inImage.getWidth(null);
        }
 
        // Determine size of new image.
        // One of the dimensions should equal maxSize.
        int scaledW = (int) (scale * inImage.getWidth(null));
        int scaledH = (int) (scale * inImage.getHeight(null));
 
        // Create an image buffer in which to paint on.
        BufferedImage outImage = new BufferedImage(
                scaledW, scaledH, BufferedImage.TYPE_INT_RGB
            );
 
        // Set the scale.
        AffineTransform tx = new AffineTransform();
 
        // If the image is smaller than the desired image size,
        // don't bother scaling.
        if (scale < 1.0d) {
            tx.scale(scale, scale);
        }
 
        // Paint image.
        Graphics2D g2d = outImage.createGraphics();
        g2d.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON
            );
        g2d.drawImage(inImage, tx, null);
        g2d.dispose();
 
        // JPEG-encode the image
        // and write to file.
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(os);
        encoder.encode(outImage);
        os.close();
        if (logger.isDebugEnabled()) {
           logger.debug("Scaling done.");
        }
        return os.toByteArray();
    }
 }

Uploading Images

To create a page on which users can upload files (such as images) do the following:

Create a form (wicket.markup.html.form.Form). Set the form to accept multi-part posts by passing true to the setMultiPart(boolean) method of Form. Add a file upload field (wicket.markup.html.form.upload.FileUploadField). In this example, we'll assume the file upload field is called imageFile.

In the form's onSubmit() method, you can process the uploaded file:

 // A dao object that will hold information about the uploaded image.
 ImageEntry imageEntry = null;
 // The FileUpload object that will be provided by wicket that holds info about
 // the file uploaded to the webapp
 FileUpload fupload = imageFile.getFileUpload();
 if (fupload h1.  null) {
     // No image was provided
     getForm().error("Please upload an image.");
     return;
 } else if (fupload.getSize()  0) {
     getForm().error("The image you attempted to upload is empty.");
     return;
 } else if (! checkContentType(fupload.getContentType())) {
     // checkContentType(String) said that they uploaded a file of the wrong type.
     getForm().error("Only images of types png, jpg, and gif are allowed.");
     return;
 } else {
     // Create a new dao object and populate it with info about the image
     imageEntry = new ImageEntry();
     imageEntry.setContentType(fupload.getContentType());
     imageEntry.setName(fupload.getClientFileName());
 }
 
 // Save the data
 if (imageEntry != null) {
     // Get the PasteService that will allow me to save the dao and the file data.
     ImageService imageService = (ImageService) PastebinApplication.getInstance().getBean("imageService");
     try {
         imageService.save(imageEntry, fupload.getInputStream());
     } catch (IOException ioe) {
         // It'd be nice to have a logger so that we could log the error.
         getForm().error("Sorry, there was an error uploading the image.");
     }
 }

All the above code really does is get the content type, file name, and an InputStream of the uploaded file. The content type and file name are used to populate a dao object. The dao object and the InputStream are sent to the service to be persisted in whatever method it chooses.

Now for the service:

Downloading Images

In order for users to be able to download stored images, you must make a subclass of wicket.markup.html.DynamicWebResource that can provide a wicket.markup.html.DynamicWebResource.ResourceState. The ResourceState instance provides wicket with the means to determine the resource's content type, it's size, the last time it was modified, and to access the content itself. Being able to access the last time it was modified helps to ensure that browser caching works as desired. If your web resource is changing dynamically, you will wnat to make sure that the ResourceState is returning updated modified times when queried. For our purposes, the modified time never changes. We are most concerned with parsing the URL parameters and thereby determining which image to return and whether to return the full image or a thumbnail. There are two URL parameters we use. The url parameter imageEntryId provides the ID of the image record in the database. The presence of the parameter "thumbnail" indicates that the image's thumbnail should be returned. The value of the thumbnail parameter is not required or checked.

ImageResource:

 import java.util.Locale;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 import com.mysticcoders.pastebin.core.ImageService;
 import com.mysticcoders.pastebin.dao.ImageEntryDAO;
 
 import com.mysticcoders.pastebin.model.ImageEntry;
 import com.mysticcoders.pastebin.web.PastebinApplication;
 
 import wicket.markup.html.DynamicWebResource;
 
 import wicket.util.time.Time;
 import wicket.util.value.ValueMap;
 
 /**
  * A resource that provides images from the holding dir.  An image can either
  * be a full-size image that has been uploaded, or a thumbnail.
  *
  * @author pchapman
  */
 public class ImageResource extends DynamicWebResource
 {
    // CONSTANTS
 
    public static final Log logger = LogFactory.getLog(ImageResource.class);
 
    private static ImageService imageService = (ImageService)PastebinApplication.getInstance().getBean("imageService");
     
    private static final long serialVersionUID = 1L;
 
    // CONSTRUCTORS
 
    public ImageResource()
    {
        super();
    }
 
    public ImageResource(Locale local)
    {
        super(local);
    }
 
    // METHODS
 
    // MEMBERS
 
    /**
     * Loads the image entry by the Id stored in the parameters, or null if
     * no Id was provided.
     * @return The image entry.
     */
    private ImageEntry getImageEntry(ValueMap params)
    {
        ImageEntry imageEntry = null;
        try {
            if (params.containsKey("imageEntryId")) {
                ImageEntryDAO imageEntryDAO =
                    (ImageEntryDAO) PastebinApplication.getInstance()
                        .getBean("imageEntryDAO");
                imageEntry =
                    (ImageEntry) imageEntryDAO.lookupImageEntry(
                            new Long(params.getLong("imageEntryId"))
                        );
                if (logger.isDebugEnabled()) {
                    logger.debug("imageEntry.name:" + imageEntry.getName());
                }
            }
            return imageEntry;
        } catch (Exception e) {
            logger.error(e);
            return null;
        }
    }
 
    @Override
    protected ResourceState getResourceState()
    {
        ValueMap params = getParameters();
 
        ImageEntry imageEntry = getImageEntry(params);
        if (imageEntry == null) {
            return new ResourceState();
        }
        ImageResourceState state =
            new ImageResourceState(
                    Time.valueOf(
                            imageService.getLastModifyTime(imageEntry)
                        )
            );
        if (imageEntry != null) {
            try {
                if (isThumbnail(params)) {
                    state.setContentType("image/jpeg");
                    state.setData(imageService.getThumbnail(imageEntry));
                } else {
                    state.setContentType(imageEntry.getContentType());
                    state.setData(imageService.getImage(imageEntry));
                }
            } catch (Exception e) {
                logger.error(e);
            }
        }
 
        return state;
    }
 
    public boolean isThumbnail(ValueMap params)
    {
        return params.containsKey("thumbnail");
    }
     
    class ImageResourceState extends ResourceState
    {
        // CONSTRUCTORS
         
        ImageResourceState(Time lastModified)
        {
            super();
            this.lastModified = lastModified;
        }
         
        // MEMBERS
         
        private String contentType;
        @Override
        public String getContentType()
        {
            return contentType;
        }
        void setContentType(String contentType)
        {
            this.contentType = contentType;
        }
  
        private byte[] data;
        @Override
        public byte[] getData()
        {
            return data;
        }
        void setData(byte[] data)
        {
            this.data = data;
        }
  
        @Override
        public int getLength()
        {
            return data.length;
        }
  
        private Time lastModified;
        @Override
        public Time lastModifiedTime()
        {
            return lastModified;
        }
          
        // METHODS
    }
 }

Registering and Using the Image Resource

To register the ImageResource with wicket, make the following call from your Application subclass' init() method:

 getSharedResources().add("imageResource", new ImageResource());

To get the url for an ImageResource:

 ResourceReference imageResource = new ResourceReference("imageResource");
 String url=RequestCycle.get().urlFor(imageResource)+"?id="+id;

Enjoy!

  • No labels