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.lang3.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) || InputFileChoice.KEEP.equals(choice)) { 302 if (isLocalValueSet() || InputFileChoice.KEEP.equals(choice)) { 303 // re-submit stored values 304 submitted.setInfo(previous); 305 } 306 if (getEditFilename()) { 307 validateFilename(context, submitted); 308 } 309 } else if (InputFileChoice.isUpload(choice)) { 310 try { 311 uploaderService.getJSFBlobUploader(choice).validateUpload(this, context, submitted); 312 if (isValid()) { 313 submitted.setChoice(InputFileChoice.KEEP_TEMP); 314 } else { 315 // re-submit stored values 316 submitted.setInfo(previous); 317 } 318 } catch (ValidatorException e) { 319 // re-submit stored values 320 submitted.setInfo(previous); 321 } 322 } else if (InputFileChoice.DELETE.equals(choice) || InputFileChoice.NONE.equals(choice)) { 323 submitted.setInfo(null); 324 } 325 326 // will need this to call declared validators 327 super.validateValue(context, submitted); 328 329 // If our value is valid, store the new value, erase the 330 // "submitted" value, and emit a ValueChangeEvent if appropriate 331 if (isValid()) { 332 setValue(submitted); 333 setSubmittedValue(null); 334 if (compareValues(previous, submitted)) { 335 queueEvent(new ValueChangeEvent(this, previous, submitted)); 336 } 337 } 338 } 339 340 public void validateFilename(FacesContext context, InputFileInfo submitted) { 341 // validate filename 342 UIComponent filenameFacet = getFacet(EDIT_FILENAME_FACET_NAME); 343 if (filenameFacet instanceof EditableValueHolder) { 344 EditableValueHolder filenameComp = (EditableValueHolder) filenameFacet; 345 submitted.setFilename(filenameComp.getLocalValue()); 346 String filename; 347 try { 348 filename = submitted.getConvertedFilename(); 349 } catch (ConverterException ce) { 350 ComponentUtils.addErrorMessage(context, this, ce.getMessage()); 351 setValid(false); 352 return; 353 } 354 submitted.setFilename(filename); 355 } 356 } 357 358 public void updateFilename(FacesContext context, String newFilename) { 359 // set filename by hand after validation 360 ValueExpression ve = getValueExpression("filename"); 361 if (ve != null) { 362 ve.setValue(context.getELContext(), newFilename); 363 } 364 } 365 366 @Override 367 public void updateModel(FacesContext context) { 368 if (context == null) { 369 throw new IllegalArgumentException(); 370 } 371 372 if (!isValid() || !isLocalValueSet()) { 373 return; 374 } 375 ValueExpression ve = getValueExpression("value"); 376 if (ve == null) { 377 return; 378 } 379 try { 380 InputFileInfo local = getFileInfoLocalValue(); 381 String choice = local.getConvertedChoice(); 382 // set blob and filename 383 if (InputFileChoice.DELETE.equals(choice)) { 384 // set filename first to avoid error in case it maps the blob filename 385 ValueExpression vef = getValueExpression("filename"); 386 if (vef != null) { 387 vef.setValue(context.getELContext(), local.getConvertedFilename()); 388 } 389 ve.setValue(context.getELContext(), local.getConvertedBlob()); 390 setValue(null); 391 setLocalValueSet(false); 392 } else if (InputFileChoice.isUploadOrKeepTemp(choice)) { 393 // set blob first to avoid error in case the filename maps the blob filename 394 ve.setValue(context.getELContext(), local.getConvertedBlob()); 395 setValue(null); 396 setLocalValueSet(false); 397 ValueExpression vef = getValueExpression("filename"); 398 if (vef != null) { 399 vef.setValue(context.getELContext(), local.getConvertedFilename()); 400 } 401 } else if (InputFileChoice.KEEP.equals(choice)) { 402 // reset local value 403 setValue(null); 404 setLocalValueSet(false); 405 if (getEditFilename()) { 406 // set filename 407 ValueExpression vef = getValueExpression("filename"); 408 if (vef != null) { 409 vef.setValue(context.getELContext(), local.getConvertedFilename()); 410 } 411 } 412 } 413 return; 414 } catch (ELException e) { 415 String messageStr = e.getMessage(); 416 Throwable result = e.getCause(); 417 while (result != null && result instanceof ELException) { 418 messageStr = result.getMessage(); 419 result = result.getCause(); 420 } 421 FacesMessage message; 422 if (messageStr == null) { 423 message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, MessageFactory.getLabel(context, this)); 424 } else { 425 message = new FacesMessage(FacesMessage.SEVERITY_ERROR, messageStr, messageStr); 426 } 427 context.addMessage(getClientId(context), message); 428 setValid(false); 429 } catch (IllegalArgumentException | ConverterException e) { 430 FacesMessage message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, 431 MessageFactory.getLabel(context, this)); 432 context.addMessage(getClientId(context), message); 433 setValid(false); 434 } 435 } 436 437 // rendering methods 438 439 protected List<String> getAvailableChoices(Blob blob, boolean temp) { 440 List<String> choices = new ArrayList<String>(3); 441 boolean isRequired = isRequired(); 442 if (blob != null) { 443 choices.add(temp ? InputFileChoice.KEEP_TEMP : InputFileChoice.KEEP); 444 } else if (!isRequired) { 445 choices.add(InputFileChoice.NONE); 446 } 447 boolean allowUpdate = true; 448 if (blob != null) { 449 BlobManager blobManager = Framework.getService(BlobManager.class); 450 BlobProvider blobProvider = blobManager.getBlobProvider(blob); 451 if (blobProvider != null && !blobProvider.supportsUserUpdate()) { 452 allowUpdate = false; 453 } 454 } 455 if (allowUpdate) { 456 for (JSFBlobUploader uploader : uploaderService.getJSFBlobUploaders()) { 457 choices.add(uploader.getChoice()); 458 } 459 if (blob != null && !isRequired) { 460 choices.add(InputFileChoice.DELETE); 461 } 462 } 463 return choices; 464 } 465 466 public Blob getCurrentBlob() { 467 Blob blob = null; 468 InputFileInfo submittedFileInfo = getFileInfoSubmittedValue(); 469 if (submittedFileInfo != null) { 470 String choice = submittedFileInfo.getConvertedChoice(); 471 if (InputFileChoice.isKeepOrKeepTemp(choice)) { 472 // rebuild other info from current value 473 InputFileInfo fileInfo = getFileInfoValue(); 474 blob = fileInfo.getConvertedBlob(); 475 } else { 476 blob = submittedFileInfo.getConvertedBlob(); 477 } 478 } else { 479 InputFileInfo fileInfo = getFileInfoValue(); 480 blob = fileInfo.getConvertedBlob(); 481 } 482 return blob; 483 } 484 485 public String getCurrentFilename() { 486 String filename = null; 487 InputFileInfo submittedFileInfo = getFileInfoSubmittedValue(); 488 if (submittedFileInfo != null) { 489 String choice = submittedFileInfo.getConvertedChoice(); 490 if (InputFileChoice.isKeepOrKeepTemp(choice)) { 491 // rebuild it in case it's supposed to be kept 492 InputFileInfo fileInfo = getFileInfoValue(); 493 filename = fileInfo.getConvertedFilename(); 494 } else { 495 filename = submittedFileInfo.getConvertedFilename(); 496 } 497 } else { 498 InputFileInfo fileInfo = getFileInfoValue(); 499 filename = fileInfo.getConvertedFilename(); 500 } 501 return filename; 502 } 503 504 @Override 505 public void encodeBegin(FacesContext context) throws IOException { 506 507 notifyPreviousErrors(context); 508 509 // not ours to close 510 ResponseWriter writer = context.getResponseWriter(); 511 Blob blob = null; 512 try { 513 blob = getCurrentBlob(); 514 } catch (ConverterException e) { 515 // can happen -> ignore, don't break rendering 516 } 517 String filename = null; 518 try { 519 filename = getCurrentFilename(); 520 } catch (ConverterException e) { 521 // can happen -> ignore, don't break rendering 522 } 523 InputFileInfo fileInfo = getFileInfoSubmittedValue(); 524 if (fileInfo == null) { 525 fileInfo = getFileInfoValue(); 526 } 527 String currentChoice = fileInfo.getConvertedChoice(); 528 boolean temp = InputFileChoice.KEEP_TEMP.equals(currentChoice); 529 List<String> choices = getAvailableChoices(blob, temp); 530 531 String radioClientId = getClientId(context) + NamingContainer.SEPARATOR_CHAR + CHOICE_FACET_NAME; 532 writer.startElement("table", this); 533 writer.writeAttribute("class", "dataInput", null); 534 writer.startElement("tbody", this); 535 writer.writeAttribute("class", getAttributes().get("styleClass"), null); 536 for (String radioChoice : choices) { 537 String id = radioClientId + radioChoice; 538 writer.startElement("tr", this); 539 writer.startElement("td", this); 540 writer.writeAttribute("class", "radioColumn", null); 541 Map<String, String> props = new HashMap<String, String>(); 542 props.put("type", "radio"); 543 props.put("name", radioClientId); 544 props.put("id", id); 545 props.put("value", radioChoice); 546 if (radioChoice.equals(currentChoice)) { 547 props.put("checked", "checked"); 548 } 549 String onchange = getOnchange(); 550 if (onchange != null) { 551 props.put("onchange", onchange); 552 } 553 String onclick = getOnclick(); 554 if (onclick != null) { 555 props.put("onclick", onclick); 556 } 557 String onselect = getOnselect(); 558 if (onselect != null) { 559 props.put("onselect", onselect); 560 } 561 StringBuffer htmlBuffer = new StringBuffer(); 562 htmlBuffer.append("<input"); 563 for (Map.Entry<String, String> prop : props.entrySet()) { 564 htmlBuffer.append(" " + prop.getKey() + "=\"" + prop.getValue() + "\""); 565 } 566 htmlBuffer.append(" />"); 567 writer.write(htmlBuffer.toString()); 568 writer.endElement("td"); 569 writer.startElement("td", this); 570 writer.writeAttribute("class", "fieldColumn", null); 571 String label = (String) ComponentUtils.getAttributeValue(this, radioChoice + "Label", null); 572 if (label == null) { 573 label = ComponentUtils.translate(context, "label.inputFile." + radioChoice + "Choice"); 574 } 575 writer.write("<label for=\"" + id + "\" style=\"float:left\">" + label + "</label>"); 576 writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); 577 if (InputFileChoice.isKeepOrKeepTemp(radioChoice)) { 578 UIComponent downloadFacet = getFacet(DOWNLOAD_FACET_NAME); 579 if (downloadFacet != null) { 580 // redefined in template 581 ComponentUtils.encodeComponent(context, downloadFacet); 582 } else { 583 downloadFacet = getFacet(DEFAULT_DOWNLOAD_FACET_NAME); 584 if (downloadFacet != null) { 585 UIOutputFile downloadComp = (UIOutputFile) downloadFacet; 586 downloadComp.setQueryParent(true); 587 ComponentUtils.copyValues(this, downloadComp, new String[] { "downloadLabel", "iconRendered" }); 588 ComponentUtils.copyLinkValues(this, downloadComp); 589 ComponentUtils.encodeComponent(context, downloadComp); 590 } 591 } 592 if (getEditFilename()) { 593 writer.write("<br />"); 594 UIComponent filenameFacet = getFacet(EDIT_FILENAME_FACET_NAME); 595 if (filenameFacet instanceof HtmlInputText) { 596 HtmlInputText filenameComp = (HtmlInputText) filenameFacet; 597 filenameComp.setValue(filename); 598 filenameComp.setLocalValueSet(false); 599 String onClick = "document.getElementById('%s').checked='checked'"; 600 filenameComp.setOnclick(String.format(onClick, id)); 601 writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); 602 label = (String) ComponentUtils.getAttributeValue(this, "editFilenameLabel", null); 603 if (label == null) { 604 label = ComponentUtils.translate(context, "label.inputFile.editFilename"); 605 } 606 writer.write("<label for=\"" + filenameComp.getId() + "\">" + label + "</label>"); 607 writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); 608 ComponentUtils.encodeComponent(context, filenameComp); 609 } 610 } 611 } else if (InputFileChoice.isUpload(radioChoice)) { 612 String onChange = String.format("document.getElementById('%s').checked='checked'", id); 613 uploaderService.getJSFBlobUploader(radioChoice).encodeBeginUpload(this, context, onChange); 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}