/* * 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. * * Contributors: * Anahide Tchertchian * Florent Guillaume */ package org.nuxeo.ecm.platform.ui.web.component.file; import java.io.IOException; import java.util.ArrayList; 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.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.ValidatorException; 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.core.blob.BlobManager; import org.nuxeo.ecm.core.blob.BlobProvider; 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. */ 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); protected final JSFBlobUploaderService uploaderService; // 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)); uploaderService = Framework.getService(JSFBlobUploaderService.class); for (JSFBlobUploader uploader : uploaderService.getJSFBlobUploaders()) { uploader.hookSubComponent(this); } } // 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<String> choices = getAvailableChoices(blob, false); String 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 String 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); String previousChoice = previous.getConvertedChoice(); boolean temp = InputFileChoice.isUploadOrKeepTemp(previousChoice); List<String> 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 if (InputFileChoice.KEEP_TEMP.equals(choice)) { // re-submit stored values if (isLocalValueSet()) { submitted.setBlob(previous.getConvertedBlob()); submitted.setFilename(previous.getConvertedFilename()); } if (getEditFilename()) { validateFilename(context, submitted); } } else if (InputFileChoice.KEEP.equals(choice)) { // re-submit stored values submitted.setBlob(previous.getConvertedBlob()); submitted.setFilename(previous.getConvertedFilename()); if (getEditFilename()) { validateFilename(context, submitted); } } else if (InputFileChoice.isUpload(choice)) { try { uploaderService.getJSFBlobUploader(choice).validateUpload(this, context, submitted); if (isValid()) { submitted.setChoice(InputFileChoice.KEEP_TEMP); } } 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); } } else if (InputFileChoice.DELETE.equals(choice) || InputFileChoice.NONE.equals(choice)) { submitted.setBlob(null); submitted.setFilename(null); } // 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); } } 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) { return; } try { InputFileInfo local = getFileInfoLocalValue(); String choice = local.getConvertedChoice(); // set blob and filename if (InputFileChoice.DELETE.equals(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 if (InputFileChoice.isUploadOrKeepTemp(choice)) { // 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.equals(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 (result != null && result instanceof ELException) { messageStr = result.getMessage(); result = result.getCause(); } FacesMessage message; if (messageStr == null) { 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 | ConverterException e) { FacesMessage message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, MessageFactory.getLabel(context, this)); context.addMessage(getClientId(context), message); setValid(false); } } // rendering methods protected List<String> getAvailableChoices(Blob blob, boolean temp) { List<String> choices = new ArrayList<String>(3); boolean isRequired = isRequired(); if (blob != null) { choices.add(temp ? InputFileChoice.KEEP_TEMP : InputFileChoice.KEEP); } else if (!isRequired) { choices.add(InputFileChoice.NONE); } boolean allowUpdate = true; if (blob != null) { BlobManager blobManager = Framework.getService(BlobManager.class); BlobProvider blobProvider = blobManager.getBlobProvider(blob); if (blobProvider != null && !blobProvider.supportsUserUpdate()) { allowUpdate = false; } } if (allowUpdate) { for (JSFBlobUploader uploader : uploaderService.getJSFBlobUploaders()) { choices.add(uploader.getChoice()); } if (blob != null && !isRequired) { choices.add(InputFileChoice.DELETE); } } return choices; } public Blob getCurrentBlob() { Blob blob = null; InputFileInfo submittedFileInfo = getFileInfoSubmittedValue(); if (submittedFileInfo != null) { String choice = submittedFileInfo.getConvertedChoice(); if (InputFileChoice.isKeepOrKeepTemp(choice)) { // rebuild other info from current value InputFileInfo fileInfo = getFileInfoValue(); blob = fileInfo.getConvertedBlob(); } else { blob = submittedFileInfo.getConvertedBlob(); } } else { InputFileInfo fileInfo = getFileInfoValue(); blob = fileInfo.getConvertedBlob(); } return blob; } public String getCurrentFilename() { String filename = null; InputFileInfo submittedFileInfo = getFileInfoSubmittedValue(); if (submittedFileInfo != null) { String choice = submittedFileInfo.getConvertedChoice(); if (InputFileChoice.isKeepOrKeepTemp(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(); } return filename; } @Override public void encodeBegin(FacesContext context) throws IOException { notifyPreviousErrors(context); // not ours to close ResponseWriter writer = context.getResponseWriter(); Blob blob = null; try { blob = getCurrentBlob(); } catch (ConverterException e) { // can happen -> ignore, don't break rendering } String filename = null; try { filename = getCurrentFilename(); } catch (ConverterException e) { // can happen -> ignore, don't break rendering } InputFileInfo fileInfo = getFileInfoSubmittedValue(); if (fileInfo == null) { fileInfo = getFileInfoValue(); } String currentChoice = fileInfo.getConvertedChoice(); boolean temp = InputFileChoice.KEEP_TEMP.equals(currentChoice); List<String> 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 (String radioChoice : choices) { String id = radioClientId + radioChoice; 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); if (radioChoice.equals(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(" " + prop.getKey() + "=\"" + prop.getValue() + "\""); } htmlBuffer.append(" />"); writer.write(htmlBuffer.toString()); writer.endElement("td"); writer.startElement("td", this); writer.writeAttribute("class", "fieldColumn", null); String label = (String) ComponentUtils.getAttributeValue(this, radioChoice + "Label", null); if (label == null) { label = ComponentUtils.translate(context, "label.inputFile." + radioChoice + "Choice"); } writer.write("<label for=\"" + id + "\" style=\"float:left\">" + label + "</label>"); writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); if (InputFileChoice.isKeepOrKeepTemp(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); label = (String) ComponentUtils.getAttributeValue(this, "editFilenameLabel", null); if (label == null) { label = ComponentUtils.translate(context, "label.inputFile.editFilename"); } writer.write("<label for=\"" + filenameComp.getId() + "\">" + label + "</label>"); writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); ComponentUtils.encodeComponent(context, filenameComp); } } } else if (InputFileChoice.isUpload(radioChoice)) { String onChange = String.format("document.getElementById('%s').checked='checked'", id); uploaderService.getJSFBlobUploader(radioChoice).encodeBeginUpload(this, context, onChange); } 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 (Boolean.TRUE.equals(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]; } }