/* * Copyright 2004 The Apache Software Foundation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.nuxeo.ecm.platform.ui.web.component.file; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.el.ELException; import javax.el.ValueExpression; import javax.faces.FacesException; import javax.faces.application.Application; import javax.faces.application.FacesMessage; import javax.faces.component.EditableValueHolder; import javax.faces.component.NamingContainer; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.component.html.HtmlInputFile; import javax.faces.component.html.HtmlInputText; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.convert.ConverterException; import javax.faces.event.ValueChangeEvent; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; import javax.servlet.http.Part; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.platform.ui.web.application.NuxeoResponseStateManagerImpl; import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; import org.nuxeo.runtime.api.Framework; import com.sun.faces.util.MessageFactory; /** * UIInput file that handles complex validation. * <p> * Attribute value is the file to be uploaded. Its submitted value as well as * filename are handled by sub components. Rendering and validation of * subcomponents are handled here. * * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a> */ public class UIInputFile extends UIInput implements NamingContainer { public static final String COMPONENT_TYPE = UIInputFile.class.getName(); public static final String COMPONENT_FAMILY = "javax.faces.Input"; protected static final String CHOICE_FACET_NAME = "choice"; protected static final String UPLOAD_FACET_NAME = "upload"; protected static final String DEFAULT_DOWNLOAD_FACET_NAME = "default_download"; protected static final String DOWNLOAD_FACET_NAME = "download"; protected static final String EDIT_FILENAME_FACET_NAME = "edit_filename"; protected static final Log log = LogFactory.getLog(UIInputFile.class); // value for filename, will disappear when it's part of the blob protected String filename; // used to decide whether filename can be edited protected Boolean editFilename; protected String onchange; protected String onclick; protected String onselect; public UIInputFile() { // initiate sub components FacesContext faces = FacesContext.getCurrentInstance(); Application app = faces.getApplication(); ComponentUtils.initiateSubComponent(this, DEFAULT_DOWNLOAD_FACET_NAME, app.createComponent(UIOutputFile.COMPONENT_TYPE)); ComponentUtils.initiateSubComponent(this, EDIT_FILENAME_FACET_NAME, app.createComponent(HtmlInputText.COMPONENT_TYPE)); ComponentUtils.initiateSubComponent(this, UPLOAD_FACET_NAME, app.createComponent(HtmlInputFile.COMPONENT_TYPE)); } // component will render itself @Override public String getRendererType() { return null; } // getters and setters /** * Override value so that an {@link InputFileInfo} structure is given * instead of the "value" attribute resolution. */ @Override public Object getValue() { Object localValue = getLocalValue(); if (localValue != null) { return localValue; } else { Blob blob = null; Object originalValue = super.getValue(); String mimeType = null; if (originalValue instanceof Blob) { blob = (Blob) originalValue; mimeType = blob.getMimeType(); } List<InputFileChoice> choices = getAvailableChoices(blob, false); InputFileChoice choice = choices.get(0); return new InputFileInfo(choice, blob, getFilename(), mimeType); } } public String getFilename() { if (filename != null) { return filename; } ValueExpression ve = getValueExpression("filename"); if (ve != null) { try { return (String) ve.getValue(getFacesContext().getELContext()); } catch (ELException e) { throw new FacesException(e); } } else { return null; } } public void setFilename(String filename) { this.filename = filename; } public Boolean getEditFilename() { if (editFilename != null) { return editFilename; } ValueExpression ve = getValueExpression("editFilename"); if (ve != null) { try { return !Boolean.FALSE.equals(ve.getValue(getFacesContext().getELContext())); } catch (ELException e) { throw new FacesException(e); } } else { // default value return false; } } public void setEditFilename(Boolean editFilename) { this.editFilename = editFilename; } public InputFileInfo getFileInfoValue() { return (InputFileInfo) getValue(); } public InputFileInfo getFileInfoLocalValue() { return (InputFileInfo) getLocalValue(); } public InputFileInfo getFileInfoSubmittedValue() { return (InputFileInfo) getSubmittedValue(); } protected String getStringValue(String name, String defaultValue) { ValueExpression ve = getValueExpression(name); if (ve != null) { try { return (String) ve.getValue(getFacesContext().getELContext()); } catch (ELException e) { throw new FacesException(e); } } else { return defaultValue; } } public String getOnchange() { if (onchange != null) { return onchange; } return getStringValue("onchange", null); } public void setOnchange(String onchange) { this.onchange = onchange; } public String getOnclick() { if (onclick != null) { return onclick; } return getStringValue("onclick", null); } public void setOnclick(String onclick) { this.onclick = onclick; } public String getOnselect() { if (onselect != null) { return onselect; } return getStringValue("onselect", null); } public void setOnselect(String onselect) { this.onselect = onselect; } // handle submitted values @Override public void decode(FacesContext context) { if (context == null) { throw new IllegalArgumentException(); } // Force validity back to "true" setValid(true); // decode the radio button, other input components will decode // themselves Map<String, String> requestMap = context.getExternalContext().getRequestParameterMap(); String radioClientId = getClientId(context) + NamingContainer.SEPARATOR_CHAR + CHOICE_FACET_NAME; String choice = requestMap.get(radioClientId); // other submitted values will be handled at validation time InputFileInfo submitted = new InputFileInfo(choice, null, null, null); setSubmittedValue(submitted); } /** * Process validation. Sub components are already validated. */ @Override public void validate(FacesContext context) { if (context == null) { throw new IllegalArgumentException(); } // Submitted value == null means "the component was not submitted // at all"; validation should not continue InputFileInfo submitted = getFileInfoSubmittedValue(); if (submitted == null) { return; } InputFileInfo previous = getFileInfoValue(); // validate choice InputFileChoice choice; try { choice = submitted.getConvertedChoice(); } catch (ConverterException ce) { ComponentUtils.addErrorMessage(context, this, ce.getMessage()); setValid(false); return; } if (choice == null) { ComponentUtils.addErrorMessage(context, this, "error.inputFile.choiceRequired"); setValid(false); return; } submitted.setChoice(choice); InputFileChoice previousChoice = previous.getConvertedChoice(); boolean temp = InputFileChoice.tempKeep == previousChoice || InputFileChoice.upload == previousChoice; List<InputFileChoice> choices = getAvailableChoices(previous.getBlob(), temp); if (!choices.contains(choice)) { ComponentUtils.addErrorMessage(context, this, "error.inputFile.invalidChoice"); setValid(false); return; } // validate choice in respect to other submitted values switch (choice) { case tempKeep: // re-submit stored values if (isLocalValueSet()) { submitted.setBlob(previous.getConvertedBlob()); submitted.setFilename(previous.getConvertedFilename()); } if (getEditFilename()) { validateFilename(context, submitted); } break; case keep: // re-submit stored values submitted.setBlob(previous.getConvertedBlob()); submitted.setFilename(previous.getConvertedFilename()); if (getEditFilename()) { validateFilename(context, submitted); } break; case upload: try { validateBlob(context, submitted); if (isValid()) { submitted.setChoice(InputFileChoice.tempKeep); } } catch (ValidatorException e) { // set file to null: blob is null but file is not required submitted.setBlob(null); submitted.setFilename(null); submitted.setChoice(InputFileChoice.none); } break; case delete: submitted.setBlob(null); submitted.setFilename(null); break; case none: submitted.setBlob(null); submitted.setFilename(null); break; } // will need this to call declared validators super.validateValue(context, submitted); // If our value is valid, store the new value, erase the // "submitted" value, and emit a ValueChangeEvent if appropriate if (isValid()) { setValue(submitted); setSubmittedValue(null); if (compareValues(previous, submitted)) { queueEvent(new ValueChangeEvent(this, previous, submitted)); } } } public void validateFilename(FacesContext context, InputFileInfo submitted) { // validate filename UIComponent filenameFacet = getFacet(EDIT_FILENAME_FACET_NAME); if (filenameFacet instanceof EditableValueHolder) { EditableValueHolder filenameComp = (EditableValueHolder) filenameFacet; submitted.setFilename(filenameComp.getLocalValue()); String filename; try { filename = submitted.getConvertedFilename(); } catch (ConverterException ce) { ComponentUtils.addErrorMessage(context, this, ce.getMessage()); setValid(false); return; } submitted.setFilename(filename); } } /** * Validates submitted blob. * <p> * Throws ValidatorException as a flag when blob is null and file is not * required to set choice back to "no file" (see NXP-1732). * * @throws ValidatorException */ public void validateBlob(FacesContext context, InputFileInfo submitted) throws ValidatorException { // validate blob UIComponent uploadFacet = getFacet(UPLOAD_FACET_NAME); if (uploadFacet instanceof HtmlInputFile) { HtmlInputFile uploadComp = (HtmlInputFile) uploadFacet; Object submittedFile = uploadComp.getSubmittedValue(); if (submittedFile instanceof Part) { Part file = (Part) submittedFile; try { submitted.setBlob(file.getInputStream()); } catch (IOException e) { ComponentUtils.addErrorMessage(context, this, e.getMessage()); setValid(false); return; } submitted.setFilename(retrieveFilename(file)); submitted.setMimeType(file.getContentType()); } Blob blob = null; try { blob = submitted.getConvertedBlob(); } catch (ConverterException ce) { ComponentUtils.addErrorMessage(context, this, ce.getMessage()); setValid(false); return; } if (blob == null) { Map<String, String> requestParameters = context.getExternalContext().getRequestParameterMap(); String message = requestParameters.containsKey("AJAXREQUEST") ? InputFileInfo.INVALID_WITH_AJAX_MESSAGE : InputFileInfo.INVALID_FILE_MESSAGE; ComponentUtils.addErrorMessage(context, this, message); setValid(false); return; } // get new filename String filename; try { filename = submitted.getConvertedFilename(); } catch (ConverterException ce) { ComponentUtils.addErrorMessage(context, this, ce.getMessage()); setValid(false); return; } submitted.setBlob(blob); submitted.setFilename(filename); } } // protected method waiting for servlet-api improvements protected String retrieveFilename(Part part) { for (String cd : part.getHeader("content-disposition").split(";")) { if (cd.trim().startsWith("filename")) { String filename = cd.substring(cd.indexOf('=') + 1).trim().replace( "\"", ""); return filename; } } return null; } public void updateFilename(FacesContext context, String newFilename) { // set filename by hand after validation ValueExpression ve = getValueExpression("filename"); if (ve != null) { ve.setValue(context.getELContext(), newFilename); } } @Override public void updateModel(FacesContext context) { if (context == null) { throw new IllegalArgumentException(); } if (!isValid() || !isLocalValueSet()) { return; } ValueExpression ve = getValueExpression("value"); if (ve != null) { try { InputFileInfo local = getFileInfoLocalValue(); InputFileChoice choice = local.getConvertedChoice(); // set file name if ((InputFileChoice.keep == choice && getEditFilename()) || InputFileChoice.upload == choice || InputFileChoice.delete == choice || InputFileChoice.tempKeep == choice) { } // set blob and filename if (InputFileChoice.upload == choice || InputFileChoice.delete == choice || InputFileChoice.tempKeep == choice) { if (InputFileChoice.delete == choice) { // set filename first to avoid error in case it maps // the blob filename ValueExpression vef = getValueExpression("filename"); if (vef != null) { vef.setValue(context.getELContext(), local.getConvertedFilename()); } ve.setValue(context.getELContext(), local.getConvertedBlob()); setValue(null); setLocalValueSet(false); } else { // set blob first to avoid error in case the filename // maps the blob filename ve.setValue(context.getELContext(), local.getConvertedBlob()); setValue(null); setLocalValueSet(false); ValueExpression vef = getValueExpression("filename"); if (vef != null) { vef.setValue(context.getELContext(), local.getConvertedFilename()); } } } else if (InputFileChoice.keep == choice) { // reset local value setValue(null); setLocalValueSet(false); if (getEditFilename()) { // set filename ValueExpression vef = getValueExpression("filename"); if (vef != null) { vef.setValue(context.getELContext(), local.getConvertedFilename()); } } } return; } catch (ELException e) { String messageStr = e.getMessage(); Throwable result = e.getCause(); while (null != result && result.getClass().isAssignableFrom(ELException.class)) { messageStr = result.getMessage(); result = result.getCause(); } FacesMessage message; if (null == messageStr) { message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, MessageFactory.getLabel(context, this)); } else { message = new FacesMessage(FacesMessage.SEVERITY_ERROR, messageStr, messageStr); } context.addMessage(getClientId(context), message); setValid(false); } catch (IllegalArgumentException e) { FacesMessage message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, MessageFactory.getLabel(context, this)); context.addMessage(getClientId(context), message); setValid(false); } catch (Exception e) { FacesMessage message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, MessageFactory.getLabel(context, this)); context.addMessage(getClientId(context), message); setValid(false); } } } // rendering methods protected List<InputFileChoice> getAvailableChoices(Object value, boolean temp) { List<InputFileChoice> choices = new ArrayList<InputFileChoice>(); boolean hasFile = value != null; boolean isRequired = isRequired(); if (!hasFile && isRequired) { choices.add(InputFileChoice.upload); } else if (!hasFile && !isRequired) { choices.add(InputFileChoice.none); choices.add(InputFileChoice.upload); } else if (hasFile && isRequired) { if (temp) { choices.add(InputFileChoice.tempKeep); } else { choices.add(InputFileChoice.keep); } choices.add(InputFileChoice.upload); } else if (hasFile && !isRequired) { if (temp) { choices.add(InputFileChoice.tempKeep); } else { choices.add(InputFileChoice.keep); } choices.add(InputFileChoice.upload); choices.add(InputFileChoice.delete); } return choices; } public Blob getCurrentBlob() { Blob blob = null; try { InputFileInfo submittedFileInfo = getFileInfoSubmittedValue(); if (submittedFileInfo != null) { InputFileChoice choice = submittedFileInfo.getConvertedChoice(); if (InputFileChoice.keep == choice || InputFileChoice.tempKeep == choice) { // rebuild other info from current value InputFileInfo fileInfo = getFileInfoValue(); blob = fileInfo.getConvertedBlob(); } else { blob = submittedFileInfo.getConvertedBlob(); } } else { InputFileInfo fileInfo = getFileInfoValue(); blob = fileInfo.getConvertedBlob(); } } catch (Exception e) { log.error(e); } return blob; } public String getCurrentFilename() { String filename = null; try { InputFileInfo submittedFileInfo = getFileInfoSubmittedValue(); if (submittedFileInfo != null) { InputFileChoice choice = submittedFileInfo.getConvertedChoice(); if (InputFileChoice.keep == choice || InputFileChoice.tempKeep == choice) { // rebuild it in case it's supposed to be kept InputFileInfo fileInfo = getFileInfoValue(); filename = fileInfo.getConvertedFilename(); } else { filename = submittedFileInfo.getConvertedFilename(); } } else { InputFileInfo fileInfo = getFileInfoValue(); filename = fileInfo.getConvertedFilename(); } } catch (Exception e) { log.error(e); } return filename; } @Override public void encodeBegin(FacesContext context) throws IOException { notifyPreviousErrors(context); ResponseWriter writer = context.getResponseWriter(); Blob blob = getCurrentBlob(); String filename = getCurrentFilename(); InputFileInfo fileInfo = getFileInfoSubmittedValue(); if (fileInfo == null) { fileInfo = getFileInfoValue(); } InputFileChoice currentChoice = fileInfo.getConvertedChoice(); boolean temp = InputFileChoice.tempKeep == currentChoice; List<InputFileChoice> choices = getAvailableChoices(blob, temp); String radioClientId = getClientId(context) + NamingContainer.SEPARATOR_CHAR + CHOICE_FACET_NAME; writer.startElement("table", this); writer.writeAttribute("class", "dataInput", null); writer.startElement("tbody", this); writer.writeAttribute("class", getAttributes().get("styleClass"), null); for (InputFileChoice radioChoice : choices) { String id = radioClientId + radioChoice.name(); writer.startElement("tr", this); writer.startElement("td", this); writer.writeAttribute("class", "radioColumn", null); Map<String, String> props = new HashMap<String, String>(); props.put("type", "radio"); props.put("name", radioClientId); props.put("id", id); props.put("value", radioChoice.name()); if (radioChoice == currentChoice) { props.put("checked", "checked"); } String onchange = getOnchange(); if (onchange != null) { props.put("onchange", onchange); } String onclick = getOnclick(); if (onclick != null) { props.put("onclick", onclick); } String onselect = getOnselect(); if (onselect != null) { props.put("onselect", onselect); } StringBuffer htmlBuffer = new StringBuffer(); htmlBuffer.append("<input"); for (Map.Entry<String, String> prop : props.entrySet()) { htmlBuffer.append(String.format(" %s=\"%s\"", prop.getKey(), prop.getValue())); } htmlBuffer.append(" />"); writer.write(htmlBuffer.toString()); writer.endElement("td"); writer.startElement("td", this); writer.writeAttribute("class", "fieldColumn", null); String html = "<label for=\"%s\" style=\"float:left\">%s</label>"; String label = (String) ComponentUtils.getAttributeValue(this, radioChoice + "Label", null); if (label == null) { label = ComponentUtils.translate(context, "label.inputFile." + radioChoice + "Choice"); } writer.write(String.format(html, id, label)); writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); if (InputFileChoice.keep == radioChoice || InputFileChoice.tempKeep == radioChoice) { UIComponent downloadFacet = getFacet(DOWNLOAD_FACET_NAME); if (downloadFacet != null) { // redefined in template ComponentUtils.encodeComponent(context, downloadFacet); } else { downloadFacet = getFacet(DEFAULT_DOWNLOAD_FACET_NAME); if (downloadFacet != null) { UIOutputFile downloadComp = (UIOutputFile) downloadFacet; downloadComp.setQueryParent(true); ComponentUtils.copyValues( this, downloadComp, new String[] { "downloadLabel", "iconRendered" }); ComponentUtils.copyLinkValues(this, downloadComp); ComponentUtils.encodeComponent(context, downloadComp); } } if (getEditFilename()) { writer.write("<br />"); UIComponent filenameFacet = getFacet(EDIT_FILENAME_FACET_NAME); if (filenameFacet instanceof HtmlInputText) { HtmlInputText filenameComp = (HtmlInputText) filenameFacet; filenameComp.setValue(filename); filenameComp.setLocalValueSet(false); String onClick = "document.getElementById('%s').checked='checked'"; filenameComp.setOnclick(String.format(onClick, id)); writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); html = "<label for=\"%s\">%s</label>"; label = (String) ComponentUtils.getAttributeValue(this, "editFilenameLabel", null); if (label == null) { label = ComponentUtils.translate(context, "label.inputFile.editFilename"); } writer.write(String.format(html, filenameComp.getId(), label)); writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); ComponentUtils.encodeComponent(context, filenameComp); } } } else if (InputFileChoice.upload == radioChoice) { // encode validators info long sizeMax = 0L; String sizeConstraint = null; List<String> authorizedExtensions = new ArrayList<String>(); List<String> unauthorizedExtensions = new ArrayList<String>(); boolean hidden = false; for (Validator val : getValidators()) { if (val instanceof InputFileSizeValidator) { InputFileSizeValidator sizeVal = (InputFileSizeValidator) val; long currentSizeMax = sizeVal.getMaxSizeBytes(); if (currentSizeMax > sizeMax) { sizeMax = currentSizeMax; sizeConstraint = sizeVal.getMaxSize(); } } else if (val instanceof InputFileMimetypeValidator) { InputFileMimetypeValidator extVal = (InputFileMimetypeValidator) val; hidden = extVal.isHidden(); if (extVal.isAuthorized()) { authorizedExtensions.addAll(Arrays.asList(extVal.getExtensions())); } else { unauthorizedExtensions.addAll(Arrays.asList(extVal.getExtensions())); } } } List<String> constraints = new ArrayList<String>(); if (sizeConstraint != null) { constraints.add(ComponentUtils.translate(context, "label.inputFile.maxSize", sizeConstraint)); } if (!hidden && (!authorizedExtensions.isEmpty() || !unauthorizedExtensions.isEmpty())) { if (!authorizedExtensions.isEmpty()) { constraints.add(ComponentUtils.translate(context, "label.inputFile.authorizedExtensions", StringUtils.join( authorizedExtensions.toArray(), ", "))); } if (!unauthorizedExtensions.isEmpty()) { constraints.add(ComponentUtils.translate( context, "label.inputFile.unauthorizedExtensions", StringUtils.join( unauthorizedExtensions.toArray(), ", "))); } } if (constraints.size() > 0) { writer.write("("); writer.write(StringUtils.join(constraints.toArray(), ", ")); writer.write(")"); writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); } // encode upload component UIComponent uploadFacet = getFacet(UPLOAD_FACET_NAME); if (uploadFacet instanceof HtmlInputFile) { HtmlInputFile uploadComp = (HtmlInputFile) uploadFacet; String onClick = "document.getElementById('%s').checked='checked'"; uploadComp.setOnclick(String.format(onClick, id)); // TODO: add size limit info ComponentUtils.encodeComponent(context, uploadComp); } } writer.endElement("td"); writer.endElement("tr"); } writer.endElement("tbody"); writer.endElement("table"); writer.flush(); } /** * @since 6.1.1 */ private void notifyPreviousErrors(FacesContext context) { final Object hasError = context.getAttributes().get( NuxeoResponseStateManagerImpl.MULTIPART_SIZE_ERROR_FLAG); final String componentId = (String) context.getAttributes().get( NuxeoResponseStateManagerImpl.MULTIPART_SIZE_ERROR_COMPONENT_ID); if (hasError != null && (boolean) hasError) { if (StringUtils.isBlank(componentId)) { ComponentUtils.addErrorMessage( context, this, "error.inputFile.maxRequestSize", new Object[] { Framework.getProperty("nuxeo.jsf.maxRequestSize") }); } else if (componentId.equals(getFacet(UPLOAD_FACET_NAME).getClientId())) { ComponentUtils.addErrorMessage( context, this, "error.inputFile.maxSize", new Object[] { Framework.getProperty("nuxeo.jsf.maxFileSize") }); } } } // state holder @Override public Object saveState(FacesContext context) { Object[] values = new Object[6]; values[0] = super.saveState(context); values[1] = filename; values[2] = editFilename; values[3] = onchange; values[4] = onclick; values[5] = onselect; return values; } @Override public void restoreState(FacesContext context, Object state) { Object[] values = (Object[]) state; super.restoreState(context, values[0]); filename = (String) values[1]; editFilename = (Boolean) values[2]; onchange = (String) values[3]; onclick = (String) values[4]; onselect = (String) values[5]; } }