5.8.5. Загрузка и вывод изображений

Рассмотрим задачу загрузки, хранения и отображения фотографий сотрудников:

  • Сотрудник представлен сущностью Employee.

  • Файлы изображений хранятся в FileStorage. Сущность Employee содержит ссылку на соответствующий FileDescriptor.

  • Экран редактирования Employee отображает фотографию, а также дает возможность загрузить, выгрузить и очистить изображение.

Класс сущности со ссылкой на файл изображения:

@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;
    }
}

Фрагмент XML-дескриптора экрана редактирования Employee:

<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>

Компоненты отображения и загрузки/выгрузки фотографии заключены внутрь контейнера groupBox. В верхней его части с помощью компонента embedded выводится изображение, а в нижней слева направо расположены компонент upload для загрузки файла и кнопки выгрузки и очистки изображения. В результате эта часть экрана должна выглядеть следующим образом:

Теперь рассмотрим контроллер экрана редактирования.

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);
        }
    }
}
  • В методе init() сначала инициализируется компонент uploadField, предназначенный для загрузки новой фотографии. В случае успешной загрузки из компонента получается экземпляр нового FileDescriptor, и соответствующий файл отправляется из временного хранилища в постоянное вызовом FileUploadingAPI.putFileIntoStorage(). После этого FileDescriptor сохраняется в БД вызовом DataSupplier.commit(), и сохраненный экземпляр устанавливается в атрибуте imageFile редактируемой сущности Employee. Затем вызывается метод displayImage() контроллера для отображения загруженной фотографии.

    Далее в методе init() источнику данных, содержащему редактируемый экземпляр Employee, добавляется слушатель для запрещения или разрешения кнопок выгрузки и очистки файла в зависимости от того, загружен файл или нет.

  • Метод postInit() вызывает отображение файла и обновляет состояние кнопок в зависимости от наличия загруженного файла.

  • Метод onDownloadImageBtnClick() вызывается при нажатии кнопки downloadImageBtn и выполняет выгрузку файла с помощью интерфейса ExportDisplay.

  • Метод onClearImageBtnClick() вызывается при нажатии кнопки clearImageBtn и очищает атрибут imageFile сущности Employee. Удаления файла из хранилища не производится.

  • Метод displayImage() выгружает файл из хранилища в байтовый массив, устанавливает содержимое компонента embeddedImage, и перерасчитывает его размеры для сохранения пропорций изображения.

    Следует иметь в виду, что выгрузка файлов из хранилища в байтовый массив приемлема только для небольших файлов. Если размер файла непредсказуем, следует использовать только выгрузку через ExportDisplay, при которой файл передается через потоки ввода-вывода и нигде не оказывается в памяти целиком.