5.8.6. Загрузка и вывод изображений
Рассмотрим задачу загрузки, хранения и отображения фотографий сотрудников:
-  
Сотрудник представлен сущностью
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;
    }
} 
  Представление для загрузки Employee вместе с FileDescriptor должно содержать все локальные атрибуты FileDescriptor:
<view class="com.company.sample.entity.Employee"
      name="employee-edit">
    <property name="name"/>
    ...
    <property name="imageFile"
              view="_local">
    </property>
</view> 
  Фрагмент 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 Logger log = LoggerFactory.getLogger(EmployeeEdit.class);
    @Inject
    private DataSupplier dataSupplier;
    @Inject
    private FileStorageService fileStorageService;
    @Inject
    private FileUploadingAPI fileUploadingAPI;
    @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.addFileUploadSucceedListener(event -> {
            FileDescriptor fd = uploadField.getFileDescriptor();
            try {
                fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
            } catch (FileStorageException e) {
                throw new RuntimeException("Error saving file to FileStorage", e);
            }
            getItem().setImageFile(dataSupplier.commit(fd));
            displayImage();
        });
        uploadField.addFileUploadErrorListener(event ->
                showNotification("File upload error", NotificationType.HUMANIZED));
        employeeDs.addItemPropertyChangeListener(event -> {
            if ("imageFile".equals(event.getProperty()))
                updateImageButtons(event.getValue() != null);
        });
    }
    @Override
    protected void postInit() {
        displayImage();
        updateImageButtons(getItem().getImageFile() != null);
    }
    public void onDownloadImageBtnClick() {
        if (getItem().getImageFile() != null)
            exportDisplay.show(getItem().getImageFile(), ExportFormat.OCTET_STREAM);
    }
    public void onClearImageBtnClick() {
        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, при которой файл передается через потоки ввода-вывода и нигде не оказывается в памяти целиком.