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