Let us consider a task of loading, storing and displaying employee photos:
-
An employee is represented by
Employee
entity. -
Image files are stored in the FileStorage. The
Employee
entity contains a link to the correspondingFileDescriptor
. -
The
Employee
edit screen shows the picture and also supports uploading, downloading and clearing the picture.
Entity class with a link to the image file:
@Table(name = "SAMPLE_EMPLOYEE") @Entity(name = "sample$Employee") public class Employee extends StandardEntity { ... @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "IMAGE_FILE_ID") protected FileDescriptor imageFile; public void setImageFile(FileDescriptor imageFile) { this.imageFile = imageFile; } public FileDescriptor getImageFile() { return imageFile; } }
A fragment of the Employee
edit screen XML descriptor:
<groupBox caption="Photo" spacing="true" height="250px" width="250px" expand="embeddedImage"> <embedded id="embeddedImage" width="100%" align="MIDDLE_CENTER"/> <hbox align="BOTTOM_LEFT" spacing="true"> <upload id="uploadField"/> <button id="downloadImageBtn" caption="Download" invoke="onDownloadImageBtnClick"/> <button id="clearImageBtn" caption="Clear" invoke="onClearImageBtnClick"/> </hbox> </groupBox>
Components used to display, upload and download images are contained within the groupBox container. Its top part shows a picture using the Embedded component, while its bottom part from left to right contains an upload component and a buttons to download and clear the image. As a result, this part of the screen should look like this:
Now, let us have a look at the edit screen controller.
public class EmployeeEdit extends AbstractEditor<Employee> { private Log log = LogFactory.getLog(EmployeeEdit.class); @Inject private DataSupplier dataSupplier; @Inject private FileStorageService fileStorageService; @Inject private FileUploadingAPI fileUploading; @Inject private ExportDisplay exportDisplay; @Inject private Embedded embeddedImage; @Inject private FileUploadField uploadField; @Inject private Button downloadImageBtn; @Inject private Button clearImageBtn; @Inject private Datasource<Employee> employeeDs; private static final int IMG_HEIGHT = 190; private static final int IMG_WIDTH = 220; @Override public void init(Map<String, Object> params) { uploadField.addListener(new FileUploadField.ListenerAdapter() { @Override public void uploadSucceeded(Event event) { FileDescriptor fd = uploadField.getFileDescriptor(); try { fileUploading.putFileIntoStorage(uploadField.getFileId(), fd); } catch (FileStorageException e) { throw new RuntimeException(e); } getItem().setImageFile(dataSupplier.commit(fd, null)); displayImage(); } @Override public void uploadFailed(Event event) { showNotification("Upload failed", NotificationType.HUMANIZED); } }); employeeDs.addListener(new DsListenerAdapter<Employee>() { @Override public void valueChanged(Employee source, String property, @Nullable Object prevValue, @Nullable Object value) { if ("imageFile".equals(property)) { updateImageButtons(value != null); } } }); } @Override protected void postInit() { displayImage(); updateImageButtons(getItem().getImageFile() != null); } public void onDownloadImageBtnClick(Component source) { if (getItem().getImageFile() != null) exportDisplay.show(getItem().getImageFile(), ExportFormat.OCTET_STREAM); } public void onClearImageBtnClick(Component source) { getItem().setImageFile(null); displayImage(); } private void updateImageButtons(boolean enable) { downloadImageBtn.setEnabled(enable); clearImageBtn.setEnabled(enable); } private void displayImage() { byte[] bytes = null; if (getItem().getImageFile() != null) { try { bytes = fileStorageService.loadFile(getItem().getImageFile()); } catch (FileStorageException e) { log.error("Unable to load image file", e); showNotification("Unable to load image file", NotificationType.HUMANIZED); } } if (bytes != null) { embeddedImage.setSource(getItem().getImageFile().getName(), new ByteArrayInputStream(bytes)); embeddedImage.setType(Embedded.Type.IMAGE); BufferedImage image; try { image = ImageIO.read(new ByteArrayInputStream(bytes)); int width = image.getWidth(); int height = image.getHeight(); if (((double) height / (double) width) > ((double) IMG_HEIGHT / (double) IMG_WIDTH)) { embeddedImage.setHeight(String.valueOf(IMG_HEIGHT)); embeddedImage.setWidth(String.valueOf(width * IMG_HEIGHT / height)); } else { embeddedImage.setWidth(String.valueOf(IMG_WIDTH)); embeddedImage.setHeight(String.valueOf(height * IMG_WIDTH / width)); } } catch (IOException e) { log.error("Unable to resize image", e); } // refresh image embeddedImage.setVisible(false); embeddedImage.setVisible(true); } else { embeddedImage.setVisible(false); } } }
-
The
init()
method first initializes theuploadField
field component intended for new images upload. In case of a successful upload, a newFileDescriptor
instance is retrieved from the component and the corresponding files are sent from the temporary storage to the persistent one by invokingFileUploadingAPI.putFileIntoStorage()
. After that, theFileDescriptor
is saved to the DB by invoking the DataSupplier.commit(), and the saved instance is set as the value of theimageFile
attribute of the editedEmployee
entity. Then, the controller'sdisplayImage()
method is invoked to display the uploaded image.After that a listener is added in the
init()
method to the datasource containing anEmployee
instance. The listener enables or locks download and clear buttons, depending on the fact whether the file has been loaded or not. -
postInit()
method performs file display and refreshes the button states, depending on the existence of a loaded file. -
onDownloadImageBtnClick()
is invoked when thedownloadImageBtn
button is clicked; it downloads the file using the ExportDisplay interface. -
onClearImageBtnClick()
is invoked when theclearImageBtn
is clicked; it clears theimageFile
attribute of theEmployee
entity. The file is not deleted from storage. -
displayImage()
loads the file from storage into a byte array, sets the content of theembeddedImage
component and recalculates its size to preserve image height to width ratio.It should be noted that loading files into byte arrays is acceptable for small files only. If the file size is not predictable, you should download it using ExportDisplay, which sends the file through input and output streams and never keeps the entire file in memory.