001/* 002 * Copyright 2004 The Apache Software Foundation. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Anahide Tchertchian 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.platform.ui.web.component.file; 021 022import java.io.IOException; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027 028import javax.el.ELException; 029import javax.el.ValueExpression; 030import javax.faces.FacesException; 031import javax.faces.application.Application; 032import javax.faces.application.FacesMessage; 033import javax.faces.component.EditableValueHolder; 034import javax.faces.component.NamingContainer; 035import javax.faces.component.UIComponent; 036import javax.faces.component.UIInput; 037import javax.faces.component.html.HtmlInputText; 038import javax.faces.context.FacesContext; 039import javax.faces.context.ResponseWriter; 040import javax.faces.convert.ConverterException; 041import javax.faces.event.ValueChangeEvent; 042import javax.faces.validator.ValidatorException; 043 044import org.apache.commons.lang.StringUtils; 045import org.apache.commons.logging.Log; 046import org.apache.commons.logging.LogFactory; 047import org.nuxeo.ecm.core.api.Blob; 048import org.nuxeo.ecm.platform.ui.web.application.NuxeoResponseStateManagerImpl; 049import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; 050import org.nuxeo.runtime.api.Framework; 051 052import com.sun.faces.util.MessageFactory; 053 054/** 055 * UIInput file that handles complex validation. 056 * <p> 057 * Attribute value is the file to be uploaded. Its submitted value as well as filename are handled by sub components. 058 * Rendering and validation of subcomponents are handled here. 059 */ 060public class UIInputFile extends UIInput implements NamingContainer { 061 062 public static final String COMPONENT_TYPE = UIInputFile.class.getName(); 063 064 public static final String COMPONENT_FAMILY = "javax.faces.Input"; 065 066 protected static final String CHOICE_FACET_NAME = "choice"; 067 068 protected static final String UPLOAD_FACET_NAME = "upload"; 069 070 protected static final String DEFAULT_DOWNLOAD_FACET_NAME = "default_download"; 071 072 protected static final String DOWNLOAD_FACET_NAME = "download"; 073 074 protected static final String EDIT_FILENAME_FACET_NAME = "edit_filename"; 075 076 protected static final Log log = LogFactory.getLog(UIInputFile.class); 077 078 protected final JSFBlobUploaderService uploaderService; 079 080 // value for filename, will disappear when it's part of the blob 081 protected String filename; 082 083 // used to decide whether filename can be edited 084 protected Boolean editFilename; 085 086 protected String onchange; 087 088 protected String onclick; 089 090 protected String onselect; 091 092 public UIInputFile() { 093 // initiate sub components 094 FacesContext faces = FacesContext.getCurrentInstance(); 095 Application app = faces.getApplication(); 096 ComponentUtils.initiateSubComponent(this, DEFAULT_DOWNLOAD_FACET_NAME, 097 app.createComponent(UIOutputFile.COMPONENT_TYPE)); 098 ComponentUtils.initiateSubComponent(this, EDIT_FILENAME_FACET_NAME, 099 app.createComponent(HtmlInputText.COMPONENT_TYPE)); 100 uploaderService = Framework.getService(JSFBlobUploaderService.class); 101 for (JSFBlobUploader uploader : uploaderService.getJSFBlobUploaders()) { 102 uploader.hookSubComponent(this); 103 } 104 } 105 106 // component will render itself 107 @Override 108 public String getRendererType() { 109 return null; 110 } 111 112 // getters and setters 113 114 /** 115 * Override value so that an {@link InputFileInfo} structure is given instead of the "value" attribute resolution. 116 */ 117 @Override 118 public Object getValue() { 119 Object localValue = getLocalValue(); 120 if (localValue != null) { 121 return localValue; 122 } else { 123 Blob blob = null; 124 Object originalValue = super.getValue(); 125 String mimeType = null; 126 if (originalValue instanceof Blob) { 127 blob = (Blob) originalValue; 128 mimeType = blob.getMimeType(); 129 } 130 List<String> choices = getAvailableChoices(blob, false); 131 String choice = choices.get(0); 132 return new InputFileInfo(choice, blob, getFilename(), mimeType); 133 } 134 } 135 136 public String getFilename() { 137 if (filename != null) { 138 return filename; 139 } 140 ValueExpression ve = getValueExpression("filename"); 141 if (ve != null) { 142 try { 143 return (String) ve.getValue(getFacesContext().getELContext()); 144 } catch (ELException e) { 145 throw new FacesException(e); 146 } 147 } else { 148 return null; 149 } 150 } 151 152 public void setFilename(String filename) { 153 this.filename = filename; 154 } 155 156 public Boolean getEditFilename() { 157 if (editFilename != null) { 158 return editFilename; 159 } 160 ValueExpression ve = getValueExpression("editFilename"); 161 if (ve != null) { 162 try { 163 return !Boolean.FALSE.equals(ve.getValue(getFacesContext().getELContext())); 164 } catch (ELException e) { 165 throw new FacesException(e); 166 } 167 } else { 168 // default value 169 return false; 170 } 171 } 172 173 public void setEditFilename(Boolean editFilename) { 174 this.editFilename = editFilename; 175 } 176 177 public InputFileInfo getFileInfoValue() { 178 return (InputFileInfo) getValue(); 179 } 180 181 public InputFileInfo getFileInfoLocalValue() { 182 return (InputFileInfo) getLocalValue(); 183 } 184 185 public InputFileInfo getFileInfoSubmittedValue() { 186 return (InputFileInfo) getSubmittedValue(); 187 } 188 189 protected String getStringValue(String name, String defaultValue) { 190 ValueExpression ve = getValueExpression(name); 191 if (ve != null) { 192 try { 193 return (String) ve.getValue(getFacesContext().getELContext()); 194 } catch (ELException e) { 195 throw new FacesException(e); 196 } 197 } else { 198 return defaultValue; 199 } 200 } 201 202 public String getOnchange() { 203 if (onchange != null) { 204 return onchange; 205 } 206 return getStringValue("onchange", null); 207 } 208 209 public void setOnchange(String onchange) { 210 this.onchange = onchange; 211 } 212 213 public String getOnclick() { 214 if (onclick != null) { 215 return onclick; 216 } 217 return getStringValue("onclick", null); 218 } 219 220 public void setOnclick(String onclick) { 221 this.onclick = onclick; 222 } 223 224 public String getOnselect() { 225 if (onselect != null) { 226 return onselect; 227 } 228 return getStringValue("onselect", null); 229 } 230 231 public void setOnselect(String onselect) { 232 this.onselect = onselect; 233 } 234 235 // handle submitted values 236 237 @Override 238 public void decode(FacesContext context) { 239 if (context == null) { 240 throw new IllegalArgumentException(); 241 } 242 243 // Force validity back to "true" 244 setValid(true); 245 246 // decode the radio button, other input components will decode 247 // themselves 248 Map<String, String> requestMap = context.getExternalContext().getRequestParameterMap(); 249 String radioClientId = getClientId(context) + NamingContainer.SEPARATOR_CHAR + CHOICE_FACET_NAME; 250 String choice = requestMap.get(radioClientId); 251 // other submitted values will be handled at validation time 252 InputFileInfo submitted = new InputFileInfo(choice, null, null, null); 253 setSubmittedValue(submitted); 254 } 255 256 /** 257 * Process validation. Sub components are already validated. 258 */ 259 @Override 260 public void validate(FacesContext context) { 261 if (context == null) { 262 throw new IllegalArgumentException(); 263 } 264 265 // Submitted value == null means "the component was not submitted 266 // at all"; validation should not continue 267 InputFileInfo submitted = getFileInfoSubmittedValue(); 268 if (submitted == null) { 269 return; 270 } 271 272 InputFileInfo previous = getFileInfoValue(); 273 274 // validate choice 275 String choice; 276 try { 277 choice = submitted.getConvertedChoice(); 278 } catch (ConverterException ce) { 279 ComponentUtils.addErrorMessage(context, this, ce.getMessage()); 280 setValid(false); 281 return; 282 } 283 if (choice == null) { 284 ComponentUtils.addErrorMessage(context, this, "error.inputFile.choiceRequired"); 285 setValid(false); 286 return; 287 } 288 submitted.setChoice(choice); 289 String previousChoice = previous.getConvertedChoice(); 290 boolean temp = InputFileChoice.isUploadOrKeepTemp(previousChoice); 291 List<String> choices = getAvailableChoices(previous.getBlob(), temp); 292 if (!choices.contains(choice)) { 293 ComponentUtils.addErrorMessage(context, this, "error.inputFile.invalidChoice"); 294 setValid(false); 295 return; 296 } 297 298 // validate choice in respect to other submitted values 299 if (InputFileChoice.KEEP_TEMP.equals(choice)) { 300 // re-submit stored values 301 if (isLocalValueSet()) { 302 submitted.setBlob(previous.getConvertedBlob()); 303 submitted.setFilename(previous.getConvertedFilename()); 304 } 305 if (getEditFilename()) { 306 validateFilename(context, submitted); 307 } 308 } else if (InputFileChoice.KEEP.equals(choice)) { 309 // re-submit stored values 310 submitted.setBlob(previous.getConvertedBlob()); 311 submitted.setFilename(previous.getConvertedFilename()); 312 if (getEditFilename()) { 313 validateFilename(context, submitted); 314 } 315 } else if (InputFileChoice.isUpload(choice)) { 316 try { 317 uploaderService.getJSFBlobUploader(choice).validateUpload(this, context, submitted); 318 if (isValid()) { 319 submitted.setChoice(InputFileChoice.KEEP_TEMP); 320 } 321 } catch (ValidatorException e) { 322 // set file to null: blob is null but file is not required 323 submitted.setBlob(null); 324 submitted.setFilename(null); 325 submitted.setChoice(InputFileChoice.NONE); 326 } 327 } else if (InputFileChoice.DELETE.equals(choice) || InputFileChoice.NONE.equals(choice)) { 328 submitted.setBlob(null); 329 submitted.setFilename(null); 330 } 331 332 // will need this to call declared validators 333 super.validateValue(context, submitted); 334 335 // If our value is valid, store the new value, erase the 336 // "submitted" value, and emit a ValueChangeEvent if appropriate 337 if (isValid()) { 338 setValue(submitted); 339 setSubmittedValue(null); 340 if (compareValues(previous, submitted)) { 341 queueEvent(new ValueChangeEvent(this, previous, submitted)); 342 } 343 } 344 } 345 346 public void validateFilename(FacesContext context, InputFileInfo submitted) { 347 // validate filename 348 UIComponent filenameFacet = getFacet(EDIT_FILENAME_FACET_NAME); 349 if (filenameFacet instanceof EditableValueHolder) { 350 EditableValueHolder filenameComp = (EditableValueHolder) filenameFacet; 351 submitted.setFilename(filenameComp.getLocalValue()); 352 String filename; 353 try { 354 filename = submitted.getConvertedFilename(); 355 } catch (ConverterException ce) { 356 ComponentUtils.addErrorMessage(context, this, ce.getMessage()); 357 setValid(false); 358 return; 359 } 360 submitted.setFilename(filename); 361 } 362 } 363 364 public void updateFilename(FacesContext context, String newFilename) { 365 // set filename by hand after validation 366 ValueExpression ve = getValueExpression("filename"); 367 if (ve != null) { 368 ve.setValue(context.getELContext(), newFilename); 369 } 370 } 371 372 @Override 373 public void updateModel(FacesContext context) { 374 if (context == null) { 375 throw new IllegalArgumentException(); 376 } 377 378 if (!isValid() || !isLocalValueSet()) { 379 return; 380 } 381 ValueExpression ve = getValueExpression("value"); 382 if (ve == null) { 383 return; 384 } 385 try { 386 InputFileInfo local = getFileInfoLocalValue(); 387 String choice = local.getConvertedChoice(); 388 // set blob and filename 389 if (InputFileChoice.DELETE.equals(choice)) { 390 // set filename first to avoid error in case it maps the blob filename 391 ValueExpression vef = getValueExpression("filename"); 392 if (vef != null) { 393 vef.setValue(context.getELContext(), local.getConvertedFilename()); 394 } 395 ve.setValue(context.getELContext(), local.getConvertedBlob()); 396 setValue(null); 397 setLocalValueSet(false); 398 } else if (InputFileChoice.isUploadOrKeepTemp(choice)) { 399 // set blob first to avoid error in case the filename maps the blob filename 400 ve.setValue(context.getELContext(), local.getConvertedBlob()); 401 setValue(null); 402 setLocalValueSet(false); 403 ValueExpression vef = getValueExpression("filename"); 404 if (vef != null) { 405 vef.setValue(context.getELContext(), local.getConvertedFilename()); 406 } 407 } else if (InputFileChoice.KEEP.equals(choice)) { 408 // reset local value 409 setValue(null); 410 setLocalValueSet(false); 411 if (getEditFilename()) { 412 // set filename 413 ValueExpression vef = getValueExpression("filename"); 414 if (vef != null) { 415 vef.setValue(context.getELContext(), local.getConvertedFilename()); 416 } 417 } 418 } 419 return; 420 } catch (ELException e) { 421 String messageStr = e.getMessage(); 422 Throwable result = e.getCause(); 423 while (result != null && result instanceof ELException) { 424 messageStr = result.getMessage(); 425 result = result.getCause(); 426 } 427 FacesMessage message; 428 if (messageStr == null) { 429 message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, MessageFactory.getLabel(context, this)); 430 } else { 431 message = new FacesMessage(FacesMessage.SEVERITY_ERROR, messageStr, messageStr); 432 } 433 context.addMessage(getClientId(context), message); 434 setValid(false); 435 } catch (IllegalArgumentException | ConverterException e) { 436 FacesMessage message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, 437 MessageFactory.getLabel(context, this)); 438 context.addMessage(getClientId(context), message); 439 setValid(false); 440 } 441 } 442 443 // rendering methods 444 445 protected List<String> getAvailableChoices(Object value, boolean temp) { 446 List<String> choices = new ArrayList<String>(3); 447 boolean hasFile = value != null; 448 boolean isRequired = isRequired(); 449 if (hasFile) { 450 choices.add(temp ? InputFileChoice.KEEP_TEMP : InputFileChoice.KEEP); 451 } else if (!isRequired) { 452 choices.add(InputFileChoice.NONE); 453 } 454 for (JSFBlobUploader uploader : uploaderService.getJSFBlobUploaders()) { 455 choices.add(uploader.getChoice()); 456 } 457 if (hasFile && !isRequired) { 458 choices.add(InputFileChoice.DELETE); 459 } 460 return choices; 461 } 462 463 public Blob getCurrentBlob() { 464 Blob blob = null; 465 InputFileInfo submittedFileInfo = getFileInfoSubmittedValue(); 466 if (submittedFileInfo != null) { 467 String choice = submittedFileInfo.getConvertedChoice(); 468 if (InputFileChoice.isKeepOrKeepTemp(choice)) { 469 // rebuild other info from current value 470 InputFileInfo fileInfo = getFileInfoValue(); 471 blob = fileInfo.getConvertedBlob(); 472 } else { 473 blob = submittedFileInfo.getConvertedBlob(); 474 } 475 } else { 476 InputFileInfo fileInfo = getFileInfoValue(); 477 blob = fileInfo.getConvertedBlob(); 478 } 479 return blob; 480 } 481 482 public String getCurrentFilename() { 483 String filename = null; 484 InputFileInfo submittedFileInfo = getFileInfoSubmittedValue(); 485 if (submittedFileInfo != null) { 486 String choice = submittedFileInfo.getConvertedChoice(); 487 if (InputFileChoice.isKeepOrKeepTemp(choice)) { 488 // rebuild it in case it's supposed to be kept 489 InputFileInfo fileInfo = getFileInfoValue(); 490 filename = fileInfo.getConvertedFilename(); 491 } else { 492 filename = submittedFileInfo.getConvertedFilename(); 493 } 494 } else { 495 InputFileInfo fileInfo = getFileInfoValue(); 496 filename = fileInfo.getConvertedFilename(); 497 } 498 return filename; 499 } 500 501 @Override 502 public void encodeBegin(FacesContext context) throws IOException { 503 504 notifyPreviousErrors(context); 505 506 // not ours to close 507 @SuppressWarnings("resource") 508 ResponseWriter writer = context.getResponseWriter(); 509 Blob blob = null; 510 try { 511 blob = getCurrentBlob(); 512 } catch (ConverterException e) { 513 // can happen -> ignore, don't break rendering 514 } 515 String filename = null; 516 try { 517 filename = getCurrentFilename(); 518 } catch (ConverterException e) { 519 // can happen -> ignore, don't break rendering 520 } 521 InputFileInfo fileInfo = getFileInfoSubmittedValue(); 522 if (fileInfo == null) { 523 fileInfo = getFileInfoValue(); 524 } 525 String currentChoice = fileInfo.getConvertedChoice(); 526 boolean temp = InputFileChoice.KEEP_TEMP.equals(currentChoice); 527 List<String> choices = getAvailableChoices(blob, temp); 528 529 String radioClientId = getClientId(context) + NamingContainer.SEPARATOR_CHAR + CHOICE_FACET_NAME; 530 writer.startElement("table", this); 531 writer.writeAttribute("class", "dataInput", null); 532 writer.startElement("tbody", this); 533 writer.writeAttribute("class", getAttributes().get("styleClass"), null); 534 for (String radioChoice : choices) { 535 String id = radioClientId + radioChoice; 536 writer.startElement("tr", this); 537 writer.startElement("td", this); 538 writer.writeAttribute("class", "radioColumn", null); 539 Map<String, String> props = new HashMap<String, String>(); 540 props.put("type", "radio"); 541 props.put("name", radioClientId); 542 props.put("id", id); 543 props.put("value", radioChoice); 544 if (radioChoice.equals(currentChoice)) { 545 props.put("checked", "checked"); 546 } 547 String onchange = getOnchange(); 548 if (onchange != null) { 549 props.put("onchange", onchange); 550 } 551 String onclick = getOnclick(); 552 if (onclick != null) { 553 props.put("onclick", onclick); 554 } 555 String onselect = getOnselect(); 556 if (onselect != null) { 557 props.put("onselect", onselect); 558 } 559 StringBuffer htmlBuffer = new StringBuffer(); 560 htmlBuffer.append("<input"); 561 for (Map.Entry<String, String> prop : props.entrySet()) { 562 htmlBuffer.append(String.format(" %s=\"%s\"", prop.getKey(), prop.getValue())); 563 } 564 htmlBuffer.append(" />"); 565 writer.write(htmlBuffer.toString()); 566 writer.endElement("td"); 567 writer.startElement("td", this); 568 writer.writeAttribute("class", "fieldColumn", null); 569 String html = "<label for=\"%s\" style=\"float:left\">%s</label>"; 570 String label = (String) ComponentUtils.getAttributeValue(this, radioChoice + "Label", null); 571 if (label == null) { 572 label = ComponentUtils.translate(context, "label.inputFile." + radioChoice + "Choice"); 573 } 574 writer.write(String.format(html, id, label)); 575 writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); 576 if (InputFileChoice.isKeepOrKeepTemp(radioChoice)) { 577 UIComponent downloadFacet = getFacet(DOWNLOAD_FACET_NAME); 578 if (downloadFacet != null) { 579 // redefined in template 580 ComponentUtils.encodeComponent(context, downloadFacet); 581 } else { 582 downloadFacet = getFacet(DEFAULT_DOWNLOAD_FACET_NAME); 583 if (downloadFacet != null) { 584 UIOutputFile downloadComp = (UIOutputFile) downloadFacet; 585 downloadComp.setQueryParent(true); 586 ComponentUtils.copyValues(this, downloadComp, new String[] { "downloadLabel", "iconRendered" }); 587 ComponentUtils.copyLinkValues(this, downloadComp); 588 ComponentUtils.encodeComponent(context, downloadComp); 589 } 590 } 591 if (getEditFilename()) { 592 writer.write("<br />"); 593 UIComponent filenameFacet = getFacet(EDIT_FILENAME_FACET_NAME); 594 if (filenameFacet instanceof HtmlInputText) { 595 HtmlInputText filenameComp = (HtmlInputText) filenameFacet; 596 filenameComp.setValue(filename); 597 filenameComp.setLocalValueSet(false); 598 String onClick = "document.getElementById('%s').checked='checked'"; 599 filenameComp.setOnclick(String.format(onClick, id)); 600 writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); 601 html = "<label for=\"%s\">%s</label>"; 602 label = (String) ComponentUtils.getAttributeValue(this, "editFilenameLabel", null); 603 if (label == null) { 604 label = ComponentUtils.translate(context, "label.inputFile.editFilename"); 605 } 606 writer.write(String.format(html, filenameComp.getId(), label)); 607 writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); 608 ComponentUtils.encodeComponent(context, filenameComp); 609 } 610 } 611 } else if (InputFileChoice.isUpload(radioChoice)) { 612 String onClick = String.format("document.getElementById('%s').checked='checked'", id); 613 uploaderService.getJSFBlobUploader(radioChoice).encodeBeginUpload(this, context, onClick); 614 } 615 writer.endElement("td"); 616 writer.endElement("tr"); 617 } 618 writer.endElement("tbody"); 619 writer.endElement("table"); 620 writer.flush(); 621 } 622 623 /** 624 * @since 6.1.1 625 */ 626 private void notifyPreviousErrors(FacesContext context) { 627 final Object hasError = context.getAttributes().get(NuxeoResponseStateManagerImpl.MULTIPART_SIZE_ERROR_FLAG); 628 final String componentId = (String) context.getAttributes().get( 629 NuxeoResponseStateManagerImpl.MULTIPART_SIZE_ERROR_COMPONENT_ID); 630 if (Boolean.TRUE.equals(hasError)) { 631 if (StringUtils.isBlank(componentId)) { 632 ComponentUtils.addErrorMessage(context, this, "error.inputFile.maxRequestSize", 633 new Object[] { Framework.getProperty("nuxeo.jsf.maxRequestSize") }); 634 } else if (componentId.equals(getFacet(UPLOAD_FACET_NAME).getClientId())) { 635 ComponentUtils.addErrorMessage(context, this, "error.inputFile.maxSize", 636 new Object[] { Framework.getProperty("nuxeo.jsf.maxFileSize") }); 637 } 638 } 639 } 640 641 // state holder 642 643 @Override 644 public Object saveState(FacesContext context) { 645 Object[] values = new Object[6]; 646 values[0] = super.saveState(context); 647 values[1] = filename; 648 values[2] = editFilename; 649 values[3] = onchange; 650 values[4] = onclick; 651 values[5] = onselect; 652 return values; 653 } 654 655 @Override 656 public void restoreState(FacesContext context, Object state) { 657 Object[] values = (Object[]) state; 658 super.restoreState(context, values[0]); 659 filename = (String) values[1]; 660 editFilename = (Boolean) values[2]; 661 onchange = (String) values[3]; 662 onclick = (String) values[4]; 663 onselect = (String) values[5]; 664 } 665 666}