/* * Copyright (c) 2012 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.server.templates.war.components; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.zip.ZipOutputStream; import org.apache.commons.io.FileUtils; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.form.CheckBox; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.upload.FileUpload; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.Model; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.spring.injection.annot.SpringBean; import org.apache.wicket.util.lang.Bytes; import org.apache.wicket.util.string.StringValue; import org.apache.wicket.util.upload.FileUploadBase.SizeLimitExceededException; import org.apache.wicket.util.upload.FileUploadException; import org.apache.wicket.validation.IValidatable; import org.apache.wicket.validation.IValidator; import org.apache.wicket.validation.ValidationError; import com.google.common.io.ByteStreams; import com.tinkerpop.blueprints.impls.orient.OrientGraph; import de.agilecoders.wicket.extensions.javascript.jasny.FileUploadField; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.core.io.project.ProjectInfo; import eu.esdihumboldt.hale.server.db.orient.DatabaseHelper; import eu.esdihumboldt.hale.server.model.Template; import eu.esdihumboldt.hale.server.model.User; import eu.esdihumboldt.hale.server.templates.TemplateProject; import eu.esdihumboldt.hale.server.templates.TemplateScavenger; import eu.esdihumboldt.hale.server.templates.war.pages.NewTemplatePage; import eu.esdihumboldt.hale.server.templates.war.pages.TemplatePage; import eu.esdihumboldt.hale.server.webapp.BaseWebApplication; import eu.esdihumboldt.hale.server.webapp.components.bootstrap.BootstrapFeedbackPanel; import eu.esdihumboldt.hale.server.webapp.util.UserUtil; import eu.esdihumboldt.util.Pair; import eu.esdihumboldt.util.blueprints.entities.NonUniqueResultException; import eu.esdihumboldt.util.io.IOUtils; import eu.esdihumboldt.util.scavenger.ScavengerException; /** * Upload form for new templates or updating templates. * * @author Simon Templer */ public class TemplateUploadForm extends Panel { private static final long serialVersionUID = -8077630706189091706L; private static final ALogger log = ALoggerFactory.getLogger(TemplateUploadForm.class); private final FileUploadField file; @SpringBean private TemplateScavenger templates; private final String templateId; private CheckBox updateInfo; /** * Constructor * * @param id the component ID * @param templateId the identifier of the template to update, or * <code>null</code> to create a new template */ public TemplateUploadForm(String id, String templateId) { super(id); this.templateId = templateId; Form<Void> form = new Form<Void>("upload") { private static final long serialVersionUID = 716487990605324922L; @Override protected void onSubmit() { List<FileUpload> uploads = file.getFileUploads(); if (uploads != null && !uploads.isEmpty()) { final boolean newTemplate = TemplateUploadForm.this.templateId == null; final String templateId; final File dir; File oldContent = null; if (newTemplate) { // attempt to reserve template ID Pair<String, File> template; try { template = templates.reserveResource(determinePreferredId(uploads)); } catch (ScavengerException e) { error(e.getMessage()); return; } templateId = template.getFirst(); dir = template.getSecond(); } else { templateId = TemplateUploadForm.this.templateId; dir = new File(templates.getHuntingGrounds(), templateId); // archive old content try { Path tmpFile = Files.createTempFile("hale-template", ".zip"); try (OutputStream out = Files.newOutputStream(tmpFile); ZipOutputStream zos = new ZipOutputStream(out)) { IOUtils.zipDirectory(dir, zos); } oldContent = tmpFile.toFile(); } catch (IOException e) { log.error("Error saving old template content to archive", e); } // delete old content try { FileUtils.cleanDirectory(dir); } catch (IOException e) { log.error("Error deleting old template content", e); } } try { for (FileUpload upload : uploads) { if (isZipFile(upload)) { // extract uploaded file IOUtils.extract(dir, new BufferedInputStream(upload.getInputStream())); } else { // copy uploaded file File target = new File(dir, upload.getClientFileName()); ByteStreams.copy(upload.getInputStream(), new FileOutputStream( target)); } } // trigger scan after upload if (newTemplate) { templates.triggerScan(); } else { templates.forceUpdate(templateId); } TemplateProject ref = templates.getReference(templateId); if (ref != null && ref.isValid()) { info("Successfully uploaded project"); boolean infoUpdate = (updateInfo != null) ? (updateInfo .getModelObject()) : (false); onUploadSuccess(this, templateId, ref.getProjectInfo(), infoUpdate); } else { if (newTemplate) { templates.releaseResourceId(templateId); } else { restoreContent(dir, oldContent); } error((ref != null) ? (ref.getNotValidMessage()) : ("Uploaded files could not be loaded as HALE project")); } } catch (Exception e) { if (newTemplate) { templates.releaseResourceId(templateId); } else { restoreContent(dir, oldContent); } log.error("Error while uploading file", e); error("Error saving the file"); } } else { warn("Please provide a file for upload"); } } @Override protected void onFileUploadException(FileUploadException e, Map<String, Object> model) { if (e instanceof SizeLimitExceededException) { final String msg = "Only files up to " + bytesToString(getMaxSize(), Locale.US) + " can be uploaded."; error(msg); } else { final String msg = "Error uploading the file: " + e.getLocalizedMessage(); error(msg); log.warn(msg, e); } } }; add(form); // multipart always needed for uploads form.setMultiPart(true); // max size for upload if (UserUtil.isAdmin()) { // admin max upload size form.setMaxSize(Bytes.megabytes(100)); } else { // normal user max upload size // TODO differentiate between logged in and anonymous user? form.setMaxSize(Bytes.megabytes(15)); } // Add file input field for multiple files form.add(file = new FileUploadField("file")); file.add(new IValidator<List<FileUpload>>() { private static final long serialVersionUID = -5668788086384105101L; @Override public void validate(IValidatable<List<FileUpload>> validatable) { if (validatable.getValue().isEmpty()) { validatable.error(new ValidationError("No source files specified.")); } } }); // add anonym/recaptcha panel boolean loggedIn = UserUtil.getLogin() != null; WebMarkupContainer anonym = new WebMarkupContainer("anonym"); if (loggedIn) { anonym.add(new WebMarkupContainer("recaptcha")); } else { anonym.add(new RecaptchaPanel("recaptcha")); } anonym.add(new BookmarkablePageLink<>("login", ((BaseWebApplication) getApplication()) .getLoginPageClass())); anonym.setVisible(!loggedIn); form.add(anonym); // update panel WebMarkupContainer update = new WebMarkupContainer("update"); update.setVisible(templateId != null); updateInfo = new CheckBox("updateInfo", Model.of(true)); update.add(updateInfo); form.add(update); // feedback panel form.add(new BootstrapFeedbackPanel("feedback")); } /** * Determine the preferred resource identifier based on the uploaded files. * * @param uploads the uploaded files * @return the preferred identifier or <code>null</code> */ protected String determinePreferredId(List<FileUpload> uploads) { if (uploads != null && !uploads.isEmpty()) { String filename = uploads.iterator().next().getClientFileName(); // strip extension int i = filename.lastIndexOf('.'); if (i > 0) { filename = filename.substring(0, i); } return filename; } return null; } /** * Restore the old content of a template from an archive. * * @param dir the template directory * @param oldContent the archive with the old content */ protected void restoreContent(File dir, File oldContent) { try { FileUtils.cleanDirectory(dir); } catch (IOException e) { log.error("Error deleting new invalid template content", e); } if (oldContent != null) { // try to restore old content try (InputStream in = new BufferedInputStream(new FileInputStream(oldContent))) { IOUtils.extract(dir, in); } catch (IOException e) { log.error("Error restoring old template content", e); } oldContent.delete(); oldContent = null; templates.forceUpdate(templateId); } } /** * Determines if a file upload is a ZIP file. * * @param upload the file upload * @return if the file upload is a ZIP file and should be extracted to the * target directory */ protected boolean isZipFile(FileUpload upload) { String lowerFileName = upload.getClientFileName().toLowerCase(); if (lowerFileName.endsWith(".hale")) { // do not extract .hale files return false; } switch (upload.getContentType()) { case "application/zip": case "application/x-zip": case "application/x-zip-compressed": return true; } // by default extract .zip and .halez files return lowerFileName.endsWith(".zip") || lowerFileName.endsWith(".halez"); } /** * Called after a successful upload. * * @param form the form * @param templateId the template identifier * @param projectInfo the project info * @param updateInfo if for an updated template, the template information * should be updated from the project */ protected void onUploadSuccess(Form<?> form, String templateId, ProjectInfo projectInfo, boolean updateInfo) { boolean newTemplate = TemplateUploadForm.this.templateId == null; OrientGraph graph = DatabaseHelper.getGraph(); try { Template template = Template.getByTemplateId(graph, templateId); if (template == null) { form.error("Template could not be created"); return; } if (newTemplate) { // created template was a new template // associate user as owner to template String login = UserUtil.getLogin(); if (login != null) { User user = User.getByLogin(graph, login); graph.addEdge(null, template.getV(), user.getV(), "owner"); } // forward to page to fill in template information setResponsePage(new NewTemplatePage(templateId)); } else { // created template already existed // set last updated template.setLastUpdate(new Date()); // update template info from project info if (updateInfo) { template.setName(projectInfo.getName()); template.setAuthor(projectInfo.getAuthor()); template.setDescription(projectInfo.getDescription()); } // forward to template page setResponsePage(TemplatePage.class, new PageParameters().set(0, templateId)); } } catch (NonUniqueResultException e) { form.error("Internal error"); log.error("Duplicate template or user"); } finally { graph.shutdown(); } } /** * Convert {@link Bytes} to a string, produces a prettier output than * {@link Bytes#toString(Locale)} * * @param bytes the bytes * @param locale the locale * * @return the converted string */ public static String bytesToString(Bytes bytes, Locale locale) { if (bytes.bytes() >= 0) { if (bytes.terabytes() >= 1.0) { return unitString(bytes.terabytes(), "TB", locale); } if (bytes.gigabytes() >= 1.0) { return unitString(bytes.gigabytes(), "GB", locale); } if (bytes.megabytes() >= 1.0) { return unitString(bytes.megabytes(), "MB", locale); } if (bytes.kilobytes() >= 1.0) { return unitString(bytes.kilobytes(), "KB", locale); } return Long.toString(bytes.bytes()) + " bytes"; } else { return "N/A"; } } private static String unitString(final double value, final String units, final Locale locale) { return StringValue.valueOf(value, locale) + " " + units; } }