/*******************************************************************************
* Copyright (c) 2015 Development Gateway, Inc and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the MIT License (MIT)
* which accompanies this distribution, and is available at
* https://opensource.org/licenses/MIT
*
* Contributors:
* Development Gateway - initial API and implementation
*******************************************************************************/
package org.devgateway.toolkit.forms.wicket.components.form;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import org.apache.log4j.Logger;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
import org.apache.wicket.event.IEvent;
import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxLink;
import org.apache.wicket.feedback.FeedbackMessage;
import org.apache.wicket.feedback.IFeedbackMessageFilter;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.FormComponentPanel;
import org.apache.wicket.markup.html.form.upload.FileUpload;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.StringResourceModel;
import org.apache.wicket.model.util.ListModel;
import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
import org.apache.wicket.request.resource.ContentDisposition;
import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.resource.AbstractResourceStreamWriter;
import org.devgateway.toolkit.forms.security.SecurityConstants;
import org.devgateway.toolkit.forms.wicket.components.ComponentUtil;
import org.devgateway.toolkit.forms.wicket.components.util.CustomDownloadLink;
import org.devgateway.toolkit.forms.wicket.events.EditingDisabledEvent;
import org.devgateway.toolkit.forms.wicket.events.EditingEnabledEvent;
import org.devgateway.toolkit.persistence.dao.FileContent;
import org.devgateway.toolkit.persistence.dao.FileMetadata;
import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
import de.agilecoders.wicket.core.markup.html.bootstrap.components.TooltipBehavior;
import de.agilecoders.wicket.core.markup.html.bootstrap.components.TooltipConfig;
import de.agilecoders.wicket.core.markup.html.bootstrap.image.GlyphIconType;
import de.agilecoders.wicket.core.markup.html.bootstrap.image.IconBehavior;
import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.fileinput.BootstrapFileInput;
import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.fileinput.FileInputConfig;
import de.agilecoders.wicket.jquery.Key;
/**
* @author idobre
* @since 11/13/14
*
* Multi upload file component that acts as a form component
*/
public class FileInputBootstrapFormComponentWrapper<T> extends FormComponentPanel<T> {
private static final long serialVersionUID = 1L;
protected static Logger logger = Logger.getLogger(FileInputBootstrapFormComponentWrapper.class);
protected Collection<FileMetadata> filesModel;
private int maxFiles = 0;
private final NotificationPanel fileUploadFeedback = new NotificationPanel("fileUploadFeedback");
private static final TooltipConfig TOOLTIP_CONFIG =
new TooltipConfig().withPlacement(TooltipConfig.Placement.bottom);
private WebMarkupContainer alreadyUploadedFiles;
private WebMarkupContainer pendingFiles;
protected BootstrapFileInput bootstrapFileInput;
protected Boolean visibleOnlyToAdmin = false;
private Boolean disableDeleteButton = false;
public FileInputBootstrapFormComponentWrapper(final String id, final IModel<T> model) {
super(id, model);
setOutputMarkupId(true);
setRenderBodyOnly(true); // we need this because bootstrap is adding
// unnecessary classes to the component
}
public FileInputBootstrapFormComponentWrapper<T> maxFiles(final int maxFiles) {
this.maxFiles = maxFiles;
return this;
}
@SuppressWarnings("unchecked")
@Override
protected void onInitialize() {
super.onInitialize();
if (getModel().getObject() == null) {
getModel().setObject((T) new HashSet<FileMetadata>());
}
filesModel = (Collection<FileMetadata>) getModel().getObject();
addAlreadyUploadedFilesComponent();
addPendingFilesComponent();
addFileUploadFeedbackComponent();
addBootstrapFileInputComponent();
bootstrapFileInput.withShowUpload(true).withShowRemove(false).withShowPreview(true).withShowCaption(true);
}
public boolean isVisibleAlreadyUploadedFiles() {
return filesModel != null && filesModel.size() > 0;
}
/**
* already uploaded files section
*/
private void addAlreadyUploadedFilesComponent() {
alreadyUploadedFiles = new WebMarkupContainer("alreadyUploadedFiles") {
private static final long serialVersionUID = 1L;
@Override
protected void onInitialize() {
super.onInitialize();
setVisibilityAllowed(isVisibleAlreadyUploadedFiles());
}
};
alreadyUploadedFiles.setOutputMarkupPlaceholderTag(true);
alreadyUploadedFiles.setOutputMarkupId(true);
add(alreadyUploadedFiles);
alreadyUploadedFiles
.add(new Label("uploadedFilesTitle", new StringResourceModel("uploadedFilesTitle", this, null)));
AbstractReadOnlyModel<List<FileMetadata>> alreadyUploadedFilesModel =
new AbstractReadOnlyModel<List<FileMetadata>>() {
private static final long serialVersionUID = 1L;
@Override
public List<FileMetadata> getObject() {
List<FileMetadata> fileObject = new ArrayList<>();
// get only the already uploaded files
for (FileMetadata file : filesModel) {
if (!file.isNew()) {
fileObject.add(file);
}
}
return fileObject;
}
};
ListView<FileMetadata> list = new ListView<FileMetadata>("list", alreadyUploadedFilesModel) {
private static final long serialVersionUID = 1L;
private List<IndicatingAjaxLink<Void>> deleteButtons = new ArrayList<>();
@Override
protected void populateItem(final ListItem<FileMetadata> item) {
// make file name clickable
Link<FileMetadata> downloadLink = new Link<FileMetadata>("downloadLink", item.getModel()) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
final FileMetadata modelObject = getModelObject();
AbstractResourceStreamWriter rstream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(final OutputStream output) throws IOException {
output.write(modelObject.getContent().getBytes());
}
@Override
public String getContentType() {
return modelObject.getContentType();
}
};
ResourceStreamRequestHandler handler =
new ResourceStreamRequestHandler(rstream, modelObject.getName());
handler.setContentDisposition(ContentDisposition.ATTACHMENT);
getRequestCycle().scheduleRequestHandlerAfterCurrent(handler);
}
};
downloadLink.add(new Label("downloadText", item.getModelObject().getName()));
downloadLink.add(new TooltipBehavior(new StringResourceModel("downloadUploadedFileTooltip",
FileInputBootstrapFormComponentWrapper.this, null), TOOLTIP_CONFIG));
item.add(downloadLink);
Link<FileMetadata> download = new CustomDownloadLink("download", item.getModel());
item.add(download);
IndicatingAjaxLink<Void> delete = new IndicatingAjaxLink<Void>("delete") {
private static final long serialVersionUID = 1L;
@SuppressWarnings("unchecked")
@Override
public void onClick(final AjaxRequestTarget target) {
filesModel.remove(item.getModelObject());
FileInputBootstrapFormComponentWrapper.this.getModel().setObject((T) filesModel);
target.add(alreadyUploadedFiles);
}
};
delete.add(new IconBehavior(GlyphIconType.remove));
delete.add(new TooltipBehavior(new StringResourceModel("removeUploadedFileTooltip",
FileInputBootstrapFormComponentWrapper.this, null), TOOLTIP_CONFIG));
delete.setVisible(true);
item.add(delete);
deleteButtons.add(delete);
// there are situation when we want to display the delete button
// only to admins
if (visibleOnlyToAdmin) {
MetaDataRoleAuthorizationStrategy.authorize(delete, Component.RENDER,
SecurityConstants.Roles.ROLE_ADMIN);
}
if (disableDeleteButton) {
delete.setVisibilityAllowed(false);
}
}
@Override
public void onEvent(final IEvent<?> event) {
/*
* disable 'delete' buttons based on the form state
*/
if (event.getPayload() instanceof EditingDisabledEvent) {
for (IndicatingAjaxLink<?> del : deleteButtons) {
del.setVisibilityAllowed(false);
}
}
if (event.getPayload() instanceof EditingEnabledEvent) {
for (IndicatingAjaxLink<?> del : deleteButtons) {
del.setVisibilityAllowed(true);
}
}
}
};
alreadyUploadedFiles.add(list);
}
/**
* pending files section
*/
private void addPendingFilesComponent() {
pendingFiles = new WebMarkupContainer("pendingFiles") {
private static final long serialVersionUID = 1L;
@Override
protected void onConfigure() {
if (filesModel != null && filesModel.size() > 0) {
for (FileMetadata file : filesModel) {
if (file.isNew()) {
setVisibilityAllowed(true);
return;
}
}
}
setVisibilityAllowed(false);
}
};
pendingFiles.setOutputMarkupPlaceholderTag(true);
pendingFiles.setOutputMarkupId(true);
add(pendingFiles);
pendingFiles.add(new Label("pendingFilesTitle", new StringResourceModel("pendingFilesTitle", this, null)));
AbstractReadOnlyModel<List<FileMetadata>> pendingFilesModel = new AbstractReadOnlyModel<List<FileMetadata>>() {
private static final long serialVersionUID = 1L;
@Override
public List<FileMetadata> getObject() {
List<FileMetadata> fileObject = new ArrayList<>();
// get only the files without an ID (this files are pending for
// upload)
for (FileMetadata file : filesModel) {
if (file.isNew()) {
fileObject.add(file);
}
}
return fileObject;
}
};
ListView<FileMetadata> list = new ListView<FileMetadata>("list", pendingFilesModel) {
private static final long serialVersionUID = 1L;
@Override
protected void populateItem(final ListItem<FileMetadata> item) {
item.add(new Label("fileTitle", item.getModelObject().getName()));
IndicatingAjaxLink<Void> delete = new IndicatingAjaxLink<Void>("delete") {
private static final long serialVersionUID = 1L;
@SuppressWarnings("unchecked")
@Override
public void onClick(final AjaxRequestTarget target) {
filesModel.remove(item.getModelObject());
FileInputBootstrapFormComponentWrapper.this.getModel().setObject((T) filesModel);
target.add(pendingFiles);
}
};
delete.add(new IconBehavior(GlyphIconType.remove));
delete.add(new TooltipBehavior(new StringResourceModel("removeUploadedFileTooltip",
FileInputBootstrapFormComponentWrapper.this, null), TOOLTIP_CONFIG));
delete.setVisible(true);
item.add(delete);
}
};
pendingFiles.add(list);
}
private void addFileUploadFeedbackComponent() {
fileUploadFeedback.setOutputMarkupId(true);
// show only the messages (fatal, success) generated by this component
fileUploadFeedback.setFilter(new IFeedbackMessageFilter() {
private static final long serialVersionUID = 1L;
@Override
public boolean accept(final FeedbackMessage message) {
final Component reporter = message.getReporter();
// try to avoid displaying the error messages that comes from
// parent (GenericBootstrapFormComponent)
// for example errors like 'FIELD is required.'
if (message.getLevel() == FeedbackMessage.ERROR) {
return false;
}
return reporter != null && (FileInputBootstrapFormComponentWrapper.this.contains(reporter, true)
|| Objects.equal(FileInputBootstrapFormComponentWrapper.this, reporter));
}
});
add(fileUploadFeedback);
}
private void addBootstrapFileInputComponent() {
// this is where the newly uploaded files are saved
final IModel<List<FileUpload>> internalUploadModel = new ListModel<>();
/*
* some customization of the BootstrapFileInput Component
*/
FileInputConfig fileInputConfig = new FileInputConfig();
fileInputConfig.put(new Key<String>("browseLabel"),
new StringResourceModel("browseLabel", FileInputBootstrapFormComponentWrapper.this, null).getString());
fileInputConfig.put(new Key<String>("uploadClass"), "btn btn-blue");
fileInputConfig.put(new Key<String>("browseClass"), "btn btn-blue");
bootstrapFileInput = new BootstrapFileInput("bootstrapFileInput", internalUploadModel, fileInputConfig) {
private static final long serialVersionUID = 1L;
@SuppressWarnings("unchecked")
@Override
protected void onSubmit(final AjaxRequestTarget target) {
super.onSubmit(target);
List<FileUpload> fileUploads = internalUploadModel.getObject();
if (fileUploads != null) {
// check if we uploaded too many files
if (maxFiles > 0 && filesModel.size() + fileUploads.size() > maxFiles) {
if (maxFiles == 1) {
FileInputBootstrapFormComponentWrapper.this.fatal(new StringResourceModel("OneUpload",
FileInputBootstrapFormComponentWrapper.this, null).getString());
} else {
FileInputBootstrapFormComponentWrapper.this.fatal(new StringResourceModel("tooManyFiles",
FileInputBootstrapFormComponentWrapper.this, Model.of(maxFiles)).getString());
}
FileInputBootstrapFormComponentWrapper.this.invalid();
} else {
// convert the uploaded files to the internal structure
// and update the model
for (FileUpload upload : fileUploads) {
FileMetadata fileMetadata = new FileMetadata();
fileMetadata.setName(upload.getClientFileName());
fileMetadata.setContentType(upload.getContentType());
fileMetadata.setSize(upload.getSize());
FileContent fileContent = new FileContent();
fileContent.setBytes(upload.getBytes());
fileMetadata.setContent(fileContent);
filesModel.add(fileMetadata);
// don't display the success notification
// FileInputBootstrapFormComponentWrapper.this.success(new
// StringResourceModel("successUpload",
// FileInputBootstrapFormComponentWrapper.this,
// null, new
// Model(upload.getClientFileName())).getString());
}
}
}
FileInputBootstrapFormComponentWrapper.this.getModel().setObject((T) filesModel);
target.add(fileUploadFeedback);
target.add(pendingFiles);
}
};
add(bootstrapFileInput);
/**
* due to an upgrade of FormGroup in wicket7/wicket-bootrap-0.10, the
* visitor that finds inner FormComponentS, will now find two instead of
* one: the FileInputBootstrapFormComponentWrapper and also the
* BootstrapFileInputField. This is the RIGHT result, previously in
* wicket 6.x it only got the first level of children, hence only one
* FormComponent (the FileInputBootstrapFormComponentWrapper). It would
* then read the label from FileInputBootstrapFormComponentWrapper and
* use it for displaying the label of the FormGroup. In
* wicket7/wicket-bootstrap-0.10 this will result in reading the label
* of BootstrapFileInputField which is null. So you will notice no
* labels for FormGroupS. We fix this by forcing the label of the
* underlying fileInput element to the same model as the label used by
* FileInputBootstrapFormComponentWrapper
*/
FormComponent<?> fileInput = (FormComponent<?>) bootstrapFileInput.get("fileInputForm").get("fileInput");
fileInput.setLabel(this.getLabel());
// there are situation when we want to display the upload file component
// only to admins
if (visibleOnlyToAdmin) {
MetaDataRoleAuthorizationStrategy.authorize(bootstrapFileInput, Component.RENDER,
SecurityConstants.Roles.ROLE_ADMIN);
}
// for download the documents when you're already signed in as admin and
// want to read only
if (disableDeleteButton) {
MetaDataRoleAuthorizationStrategy.authorize(bootstrapFileInput, Component.RENDER,
MetaDataRoleAuthorizationStrategy.NO_ROLE);
}
}
@Override
public void onEvent(final IEvent<?> event) {
ComponentUtil.enableDisableEvent(this, event);
}
@SuppressWarnings("unchecked")
@Override
public void convertInput() {
final Collection<FileMetadata> modelObject = filesModel;
setConvertedInput((T) modelObject);
/*
* if we still have issues like CCR-310 then we will need to update the
* files setters like: if (this.upload == null) { this.upload = upload;
* } else { this.upload.clear(); if(upload != null) {
* this.upload.addAll(upload); } }
*/
}
public void setVisibleOnlyToAdmin(final Boolean visibleOnlyToAdmin) {
this.visibleOnlyToAdmin = visibleOnlyToAdmin;
}
public Boolean getDisableDeleteButton() {
return disableDeleteButton;
}
public void setDisableDeleteButton(final Boolean disableDeleteButton) {
this.disableDeleteButton = disableDeleteButton;
}
public WebMarkupContainer getAlreadyUploadedFiles() {
return alreadyUploadedFiles;
}
}