com.example.app.ui.vtcrop.VTCropPictureEditor.java Source code

Java tutorial

Introduction

Here is the source code for com.example.app.ui.vtcrop.VTCropPictureEditor.java

Source

/*
 * Copyright (c) Interactive Information R & D (I2RD) LLC.
 * All Rights Reserved.
 *
 * This software is confidential and proprietary information of
 * I2RD LLC ("Confidential Information"). You shall not disclose
 * such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered
 * into with I2RD.
 */

package com.example.app.ui.vtcrop;

import com.example.app.support.VTCropJSLibrary;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;

import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import net.proteusframework.cms.support.ImageFileUtil;
import net.proteusframework.core.StringFactory;
import net.proteusframework.core.image.Thumbnailer;
import net.proteusframework.core.locale.TextSource;
import net.proteusframework.core.locale.annotation.I18N;
import net.proteusframework.core.locale.annotation.I18NFile;
import net.proteusframework.core.locale.annotation.L10N;
import net.proteusframework.core.metric.Dimension;
import net.proteusframework.core.notification.Notifiable;
import net.proteusframework.internet.http.resource.FactoryResource;
import net.proteusframework.internet.http.resource.html.NDE;
import net.proteusframework.ui.miwt.Image;
import net.proteusframework.ui.miwt.MIWTException;
import net.proteusframework.ui.miwt.component.Container;
import net.proteusframework.ui.miwt.component.FileField;
import net.proteusframework.ui.miwt.component.ImageComponent;
import net.proteusframework.ui.miwt.component.PushButton;
import net.proteusframework.ui.miwt.component.composite.editor.ValueEditor;
import net.proteusframework.ui.miwt.event.ActionEvent;
import net.proteusframework.ui.miwt.event.ActionListener;
import net.proteusframework.ui.miwt.util.CommonActions;

/**
 * UI for user to upload a picture.  The picture will be cropped according to the configuration determined by the
 * {@link VTCropPictureEditorConfig}.
 * <p>
 * You can listen for ActionEvents on {@link  #ACTION_UI_VALUE_UPDATED}  action command to be notified when the user has
 * provided a new picture via this UI.
 * </p>
 *
 * @author Alan Holt (aholt@venturetech.net)
 */
@SuppressWarnings("unused")
@I18NFile(symbolPrefix = "com.lrsuccess.ldp.ui.vtcrop.VTCropPictureEditor", i18n = @I18N(symbol = "Button Text Crop Image", l10n = @L10N("Done Cropping")))
@Configurable
public class VTCropPictureEditor extends Container implements ValueEditor<FileItem> {
    /** html classname for image selected */
    public static final String CLASS_IMAGE_SELECTED = "image-selected";
    /** html classname for image not selected */
    public static final String CLASS_NO_IMAGE_SELECTED = "no-image-selected";
    /** html classname for view mode */
    public static final String CLASS_VIEW_MODE = "mode-view";
    /** html classname for crop mode */
    public static final String CLASS_CROP_MODE = "mode-crop";
    /** UI Value Updated */
    public final static String ACTION_UI_VALUE_UPDATED = "ui-value-updated";
    /** Logger. */
    private final static Logger _logger = LogManager.getLogger(VTCropPictureEditor.class);
    /** File Field. */
    private final FileField _fileField = new FileField();
    /** File Field. */
    private final ImageComponent _picture = new ImageComponent();

    /** Actions container */
    private final Container _actionsCon = new Container();
    /** Additional UI Values.  Only used if the config specified more than one ImageScale */
    private final HashMap<String, FileItem> _additionalUiValues = new HashMap<>();
    /** Picture Editor config */
    private final VTCropPictureEditorConfig _config;
    /** UI Value - UI state isn't held in component. */
    private FileItem _uiValue;
    /** Modified Flag. */
    private boolean _modified;
    /** Editable mode? */
    private boolean _editable = true;

    /** crop button text */
    private TextSource _cropButtonText;

    /** default resource */
    private FactoryResource _defaultResource;

    /** file item factory */
    @Autowired
    private FileItemFactory _fileItemFactory;

    /**
     * Create an instance.
     *
     * @param cfg the configuration
     */
    public VTCropPictureEditor(VTCropPictureEditorConfig cfg) {
        super();
        addClassName("picture-editor");
        _config = cfg;

        _picture.setImageCaching(false);
        _fileField.setAccept("image/*");
        _fileField.addPropertyChangeListener(FileField.PROP_FILE_ITEMS, evt -> {
            @SuppressWarnings("unchecked")
            final List<FileItem> files = (List<FileItem>) evt.getNewValue();
            if (files != null && files.size() > 0) {
                _uiValue = files.get(0);
                if (files.size() > 1) {
                    _additionalUiValues.clear();
                    for (int i = 1; i < files.size(); i++) {
                        FileItem file = files.get(i);
                        _additionalUiValues.put(file.getName(), file);
                    }
                }
                _picture.setImage(new Image(_uiValue));
                _modified = true;
            }
            _fileField.resetFile();
        });
    }

    /**
     * Add an ActionListener to get an event when the user has uploaded a new image to the UI.  This is essentially a
     * notification that the UIValue has changed.
     *
     * @param listener The ActionListener
     */
    public void addActionListener(ActionListener listener) {
        addListener(ActionListener.class, listener);
    }

    /**
     * Get all additional UI Values that were cropped by the cropper.
     * This should be called if you are expecting additional files, otherwise it will return an empty HashMap.
     *
     * @return a HashMap of additional UI Values that were cropped/scaled by VTCrop.
     */
    public HashMap<String, FileItem> getAdditionalUiValues() {
        HashMap<String, FileItem> additionalUIValues = new HashMap<>();
        _additionalUiValues.entrySet()
                .forEach(entry -> additionalUIValues.put(entry.getKey(), _getUIValue(entry.getValue())));
        return additionalUIValues;
    }

    @Nullable
    private FileItem _getUIValue(@Nullable FileItem uiValue) {
        if (uiValue == null) {
            return null;
        }

        Dimension origDim, newDim;
        try {
            origDim = ImageFileUtil.getDimension(uiValue);
        } catch (IOException e) {
            _logger.error("Skipping scaling because an error occurred.", e);
            return uiValue;
        }

        if (origDim == null) {
            _logger.warn("Skipping scaling because retrieved dimension for ui value was null.");
            return uiValue;
        }

        VTCropPictureEditorConfig.ImageScaleOption option = _config.getImageScaleOptionForName(_uiValue.getName());
        int newDimWidth = option != null && option.scale != null
                ? Double.valueOf(_config.getCropWidth() * option.scale).intValue()
                : _config.getCropWidth();
        int newDimHeight = option != null && option.scale != null
                ? Double.valueOf(_config.getCropHeight() * option.scale).intValue()
                : _config.getCropHeight();

        newDim = ImageFileUtil.getScaledDimension(origDim, newDimWidth, newDimHeight);

        if (!newDim.equals(origDim)) {
            _logger.debug("Scale to " + newDim.getWidthMetric() + " x " + newDim.getHeightMetric());
            FileItem scaledFileItem = _fileItemFactory.createItem(uiValue.getFieldName(), uiValue.getContentType(),
                    uiValue.isFormField(), uiValue.getName());
            try (InputStream is = uiValue.getInputStream(); OutputStream os = scaledFileItem.getOutputStream()) {
                Thumbnailer tn = Thumbnailer.getInstance(is);
                BufferedImage scaledImg = tn.getQualityThumbnail(newDim.getWidthMetric().intValue(),
                        newDim.getHeightMetric().intValue());
                String extension = StringFactory.getExtension(uiValue.getName());
                ImageIO.write(scaledImg, extension, os);
                uiValue.delete();
                uiValue = scaledFileItem;
            } catch (Throwable e) {
                _logger.warn("Skipping scaling due to:", e);
            }
        }

        return uiValue;
    }

    /**
     * Gets crop button text.
     *
     * @return the crop button text
     */
    public TextSource getCropButtonText() {
        return _cropButtonText;
    }

    /**
     * Sets crop button text.
     *
     * @param cropButtonText the crop button text
     */
    public void setCropButtonText(TextSource cropButtonText) {
        _cropButtonText = cropButtonText;
    }

    /**
     * Gets default resource.  This is the default image if no image has been set.
     *
     * @return the default resource
     */
    public FactoryResource getDefaultResource() {
        return _defaultResource;
    }

    /**
     * Sets default resource. This is the default image if no image has been set.
     *
     * @param defaultResource the default resource
     */
    public void setDefaultResource(FactoryResource defaultResource) {
        _defaultResource = defaultResource;
    }

    @Nullable
    @Override
    public FileItem getValue() {
        return _uiValue;
    }

    @Override
    public void setValue(@Nullable FileItem value) {
        _uiValue = value;

        if (value == null) {
            _fileField.resetFile();
            _picture.setImage(getDefaultResource() != null ? new Image(getDefaultResource()) : null);
        } else {
            _picture.setImage(new Image(value));

        }
        _modified = false;
        setBaseClass(null);

        if (isInited())
            _viewDefault();
    }

    @Override
    public ValueEditor.ModificationState getModificationState() {
        return (_modified) ? ValueEditor.ModificationState.CHANGED : ValueEditor.ModificationState.UNCHANGED;
    }

    @Nullable
    @Override
    public FileItem getUIValue(Level logErrorLevel) {
        return _getUIValue(_uiValue);
    }

    @Override
    public boolean validateUIValue(@SuppressWarnings("rawtypes") Notifiable notifiable) {
        // Nothing to validate.
        return true;
    }

    @Override
    public FileItem commitValue() throws MIWTException {
        setValue(getUIValue(Level.DEBUG));
        return getValue();
    }

    @Override
    public void init() {
        super.init();

        setAttribute("data-crop_opts", _config.toJson());

        // Create drop-zone container in case someone wants to use DnD and VTCrop with this.
        add(of("drop-zone", _picture));

        add(of("crop-file", _fileField));

        _actionsCon.addClassName("actions bottom");
        add(_actionsCon);

        if (getValue() == null && getDefaultResource() != null)
            _picture.setImage(new Image(getDefaultResource()));

        _viewDefault();
    }

    @Override
    public List<NDE> getNDEs() {
        return Collections.singletonList(VTCropJSLibrary.VTCropPictureEditor.getNDE());
    }

    @Override
    public boolean isEditable() {
        return _editable;
    }

    @Override
    public void setEditable(boolean b) {
        if (b != _editable) {
            _editable = b;
            _viewDefault();
        } else {
            _editable = b;
        }
    }

    /**
     * Remove an action listener.
     *
     * @param listener The listener.
     */
    public void removeActionListener(ActionListener listener) {
        removeListener(ActionListener.class, listener);
    }

    /**
     * Switch the UI to the default view.  The default view for editable mode: the file field is hidden and a button is shown to
     * change the picture. In non-editable mode, it just shows the current picture.
     */
    private void _viewDefault() {
        setBaseClass(true);

        _actionsCon.removeAllComponents();
        _fileField.setVisible(false);

        if (!_editable)
            return;

        final PushButton change = CommonActions.CHANGE.push();
        final PushButton saveAndCrop = CommonActions.SAVE.push();
        saveAndCrop.addClassName("done-cropping");
        saveAndCrop.setLabel(getCropButtonText() != null ? getCropButtonText()
                : VTCropPictureEditorLOK.BUTTON_TEXT_CROP_IMAGE());
        saveAndCrop.setVisible(false);
        final PushButton cancel = CommonActions.CANCEL.push();
        cancel.setVisible(false);
        final PushButton remove = CommonActions.REMOVE.push();
        remove.setVisible(false);

        _actionsCon.add(remove);
        _actionsCon.add(saveAndCrop);
        _actionsCon.add(cancel);
        _actionsCon.add(change);

        saveAndCrop.addActionListener(ev1 -> {
            fire(new ActionEvent(this, this, ACTION_UI_VALUE_UPDATED));
            _viewDefault();
        });

        cancel.addActionListener(ev1 -> _viewDefault());

        remove.addActionListener(ev -> {
            setValue(null);
            _modified = true;
            fire(new ActionEvent(this, this, ACTION_UI_VALUE_UPDATED));
            _viewDefault();
        });

        if (_uiValue != null) {
            remove.setVisible(true);
        }

        change.addActionListener(ev -> {
            setBaseClass(false);

            _fileField.setVisible(true);

            change.setVisible(false);
            remove.setVisible(false);
            saveAndCrop.setVisible(true);
            cancel.setVisible(true);
        });
    }

    /**
     * Set the base part of the classname based on the data's current state.
     *
     * @param viewMode boolean flag -- if true, adds the view mode classname to component, otherwise adds the crop mode classname
     * if null, does nothing to the mode classnames
     */
    private void setBaseClass(@Nullable Boolean viewMode) {
        if (_uiValue == null) {
            removeClassName(CLASS_IMAGE_SELECTED);
            addClassName(CLASS_NO_IMAGE_SELECTED);
        } else {
            removeClassName(CLASS_NO_IMAGE_SELECTED);
            addClassName(CLASS_IMAGE_SELECTED);
        }
        if (viewMode != null) {
            if (viewMode) {
                removeClassName(CLASS_CROP_MODE);
                addClassName(CLASS_VIEW_MODE);
            } else {
                removeClassName(CLASS_VIEW_MODE);
                addClassName(CLASS_CROP_MODE);
            }
        }
    }
}