001/* 002 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others. 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 * Nuxeo - initial API and implementation 018 * 019 * $Id$ 020 */ 021 022package org.nuxeo.ecm.platform.ui.web.directory; 023 024import java.io.IOException; 025import java.io.Serializable; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.HashMap; 029import java.util.Iterator; 030import java.util.LinkedHashMap; 031import java.util.List; 032import java.util.Map; 033 034import javax.el.ELException; 035import javax.el.ValueExpression; 036import javax.faces.FacesException; 037import javax.faces.application.FacesMessage; 038import javax.faces.component.UIComponent; 039import javax.faces.component.UIInput; 040import javax.faces.context.FacesContext; 041import javax.faces.context.ResponseWriter; 042 043import org.apache.commons.lang3.StringUtils; 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046import org.nuxeo.ecm.core.api.NuxeoException; 047import org.nuxeo.ecm.platform.ui.web.component.ResettableComponent; 048import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; 049 050import com.sun.faces.facelets.component.UIRepeat; 051 052/** 053 * DOCUMENT ME. 054 * <p> 055 * Refactor me and it's christmas. 056 * 057 * @author <a href="mailto:[email protected]">George Lefter</a> 058 */ 059public class ChainSelect extends UIInput implements ResettableComponent { 060 061 public static final String COMPONENT_TYPE = "nxdirectory.chainSelect"; 062 063 public static final String COMPONENT_FAMILY = "nxdirectory.chainSelect"; 064 065 public static final String DEFAULT_KEY_SEPARATOR = "/"; 066 067 public static final String DEFAULT_PARENT_KEY = null; 068 069 private static final Log log = LogFactory.getLog(ChainSelect.class); 070 071 // Direct access from ChainSelectStatus 072 Map<Integer, NestedChainSelectComponentInfo> compInfos = new HashMap<Integer, NestedChainSelectComponentInfo>(); 073 074 /** 075 * The keys of the selected items in chain controls. 076 */ 077 private List<String> keyList = new ArrayList<String>(); 078 079 private String onchange; 080 081 private Map<String, DirectorySelectItem>[] optionList; 082 083 private Integer size; 084 085 private boolean localize; 086 087 private boolean multiSelect = false; 088 089 private boolean allowRootSelection = false; 090 091 private boolean allowBranchSelection = false; 092 093 private boolean qualifiedParentKeys = false; 094 095 private Selection[] selections; 096 097 // XXX AT: this attribute is useless, value is already there to store that 098 private Selection[] componentValue; 099 100 private Boolean displayValueOnly; 101 102 private String displayValueOnlyStyle; 103 104 private String displayValueOnlyStyleClass; 105 106 private String cssStyle; 107 108 private String cssStyleClass; 109 110 private boolean multiParentSelect = false; 111 112 /** 113 * The index of the last selection box that was selected. 114 */ 115 private int lastSelectedComponentIndex; 116 117 /** 118 * This field is used to separate the levels of on hierarchical vocabulary.This way all parents of a record will be 119 * separated through this field. 120 */ 121 private String keySeparator; 122 123 /** 124 * Value used to filter on parent key when searching for a hierarchical directory roots. 125 * <p> 126 * If not set, will use null. 127 */ 128 protected String defaultRootKey; 129 130 /** 131 * New attribute to handle bad behaviour on ajax re-render, forcing local cache refresh 132 * 133 * @since 5.6 134 */ 135 protected Boolean resetCacheOnUpdate; 136 137 public boolean isAllowBranchSelection() { 138 return allowBranchSelection; 139 } 140 141 public void setAllowBranchSelection(boolean allowBranchSelection) { 142 this.allowBranchSelection = allowBranchSelection; 143 } 144 145 public boolean isAllowRootSelection() { 146 return allowRootSelection; 147 } 148 149 public void setAllowRootSelection(boolean allowRootSelection) { 150 this.allowRootSelection = allowRootSelection; 151 } 152 153 @Override 154 public String getFamily() { 155 return COMPONENT_FAMILY; 156 } 157 158 @Override 159 public String getRendererType() { 160 return null; 161 } 162 163 @Override 164 @SuppressWarnings("unchecked") 165 public void restoreState(FacesContext context, Object state) { 166 Object[] values = (Object[]) state; 167 super.restoreState(context, values[0]); 168 componentValue = (Selection[]) values[1]; 169 optionList = (Map<String, DirectorySelectItem>[]) values[2]; 170 localize = (Boolean) values[3]; 171 size = (Integer) values[4]; 172 multiSelect = (Boolean) values[5]; 173 allowRootSelection = (Boolean) values[6]; 174 allowBranchSelection = (Boolean) values[7]; 175 selections = (Selection[]) values[8]; 176 qualifiedParentKeys = (Boolean) values[9]; 177 displayValueOnly = (Boolean) values[10]; 178 displayValueOnlyStyle = (String) values[11]; 179 displayValueOnlyStyleClass = (String) values[12]; 180 multiParentSelect = (Boolean) values[13]; 181 cssStyle = (String) values[14]; 182 cssStyleClass = (String) values[15]; 183 keySeparator = (String) values[16]; 184 lastSelectedComponentIndex = (Integer) values[17]; 185 compInfos = (Map<Integer, NestedChainSelectComponentInfo>) values[18]; 186 keyList = (List<String>) values[19]; 187 onchange = (String) values[20]; 188 defaultRootKey = (String) values[21]; 189 resetCacheOnUpdate = (Boolean) values[22]; 190 } 191 192 @Override 193 public Object saveState(FacesContext arg0) { 194 Object[] values = new Object[23]; 195 values[0] = super.saveState(arg0); 196 values[1] = componentValue; 197 values[2] = optionList; 198 values[3] = localize; 199 values[4] = size; 200 values[5] = multiSelect; 201 values[6] = allowRootSelection; 202 values[7] = allowBranchSelection; 203 values[8] = selections; 204 values[9] = qualifiedParentKeys; 205 values[10] = displayValueOnly; 206 values[11] = displayValueOnlyStyle; 207 values[12] = displayValueOnlyStyleClass; 208 values[13] = multiParentSelect; 209 values[14] = cssStyle; 210 values[15] = cssStyleClass; 211 values[16] = keySeparator; 212 values[17] = lastSelectedComponentIndex; 213 values[18] = compInfos; 214 values[19] = keyList; 215 values[20] = onchange; 216 values[21] = defaultRootKey; 217 values[22] = resetCacheOnUpdate; 218 return values; 219 } 220 221 public List<String> getSelectionKeyList() { 222 return keyList; 223 } 224 225 public void addToSelectionKeyList(String key) { 226 keyList.add(key); 227 } 228 229 @Override 230 public void decode(FacesContext context) { 231 if (getDisplayValueOnly()) { 232 return; 233 } 234 235 setValid(true); 236 rebuildOptions(); 237 238 if (!multiParentSelect) { 239 componentValue = selections; 240 String[] value = encodeValue(componentValue); 241 if (!multiSelect) { 242 setSubmittedValue(value[0]); 243 } else { 244 if (!multiParentSelect) { 245 // remove the "" entry from the submitted value 246 List<String> list = new ArrayList<String>(Arrays.asList(value)); 247 list.remove(""); 248 value = list.toArray(new String[list.size()]); 249 } 250 setSubmittedValue(value); 251 } 252 } else { 253 String[] value = encodeValue(componentValue); 254 setSubmittedValue(value); 255 } 256 257 // identify the repeat child tag that displays 258 // current added selections to dynamically set 259 // it's iterable value 260 List<UIComponent> children = getChildren(); 261 for (UIComponent child : children) { 262 if (!(child instanceof UIRepeat)) { 263 continue; 264 } 265 UIRepeat component = (UIRepeat) child; 266 if (component.getId().equals("current_selections")) { 267 component.setValue(componentValue); 268 } 269 } 270 } 271 272 public static String format(Object o) { 273 if (o == null) { 274 return "NULL"; 275 } 276 if (o instanceof String[]) { 277 return formatAr((String[]) o); 278 } else if (o instanceof String) { 279 return (String) o; 280 } else { 281 return o.getClass().getName(); 282 } 283 } 284 285 public static String formatAr(String[] ar) { 286 if (ar == null) { 287 return "NULL"; 288 } 289 if (ar.length == 0) { 290 return "[]"; 291 } else { 292 return '[' + StringUtils.join(ar, ", ") + ']'; 293 } 294 } 295 296 @Override 297 public void encodeBegin(FacesContext context) throws IOException { 298 init(); 299 rebuildOptions(); 300 ResponseWriter writer = context.getResponseWriter(); 301 writer.startElement("div", this); 302 if (cssStyle != null) { 303 writer.writeAttribute("style", cssStyle, "style"); 304 } 305 if (cssStyleClass != null) { 306 writer.writeAttribute("class", cssStyleClass, "class"); 307 } 308 writer.writeAttribute("id", getClientId(context), "id"); 309 310 super.encodeBegin(context); 311 } 312 313 @Override 314 public void encodeEnd(FacesContext context) throws IOException { 315 ResponseWriter writer = context.getResponseWriter(); 316 writer.endElement("div"); 317 } 318 319 public Object getProperty(String name) { 320 ValueExpression ve = getValueExpression(name); 321 if (ve != null) { 322 try { 323 return ve.getValue(getFacesContext().getELContext()); 324 } catch (ELException e) { 325 throw new FacesException(e); 326 } 327 } else { 328 Map<String, Object> attrMap = getAttributes(); 329 return attrMap.get(name); 330 } 331 } 332 333 public String getStringProperty(String name, String defaultValue) { 334 String value = (String) getProperty(name); 335 return value != null ? value : defaultValue; 336 } 337 338 public Boolean getBooleanProperty(String name, boolean defaultValue) { 339 Boolean value = (Boolean) getProperty(name); 340 return value != null ? value : Boolean.valueOf(defaultValue); 341 } 342 343 public Boolean getLocalize() { 344 return localize; 345 } 346 347 public void setLocalize(Boolean localize) { 348 this.localize = localize; 349 } 350 351 public String getCssStyle() { 352 return cssStyle; 353 } 354 355 public void setCssStyle(String cssStyle) { 356 this.cssStyle = cssStyle; 357 } 358 359 public String getCssStyleClass() { 360 return cssStyleClass; 361 } 362 363 public void setCSsStyleClass(String cssStyleClass) { 364 this.cssStyleClass = cssStyleClass; 365 } 366 367 public String getOnchange() { 368 if (onchange != null) { 369 return onchange; 370 } 371 ValueExpression ve = getValueExpression("onchange"); 372 if (ve != null) { 373 try { 374 return (String) ve.getValue(getFacesContext().getELContext()); 375 } catch (ELException e) { 376 throw new FacesException(e); 377 } 378 } 379 return null; 380 } 381 382 public void setOnchange(String onchange) { 383 this.onchange = onchange; 384 } 385 386 public Selection getSelection(int i) { 387 if (selections == null) { 388 throw new NuxeoException("ChainSelect is mis-behaving, it's probable you're experiencing issue NXP-5762"); 389 } 390 return selections[i]; 391 } 392 393 public void setSelections(Selection[] sels) { 394 selections = sels; 395 } 396 397 public Integer getSize() { 398 return size; 399 } 400 401 @SuppressWarnings("unchecked") 402 public void setSize(Integer size) { 403 optionList = new LinkedHashMap[size]; 404 this.size = size; 405 } 406 407 public Map<String, DirectorySelectItem> getOptions(int index) { 408 return optionList[index]; 409 } 410 411 public void setOptions(int index, Map<String, DirectorySelectItem> opts) { 412 optionList[index] = opts; 413 } 414 415 /** 416 * If the user changes selection for position k, all options for n>k will be reset. We only have to rebuild options 417 * for position k+1. 418 */ 419 public void rebuildOptions() { 420 // for (int i = 0; i < size; i++) { 421 // if (optionList[i] != null) { 422 // continue; 423 // } 424 // if (i == 0 425 // || (selections.length != 0 && selections[0].getColumnValue(i - 1) != 426 // null)) { 427 // rebuildOptions(i); 428 // } 429 // } 430 } 431 432 public ChainSelectListboxComponent getComponent(UIComponent parent, int i) { 433 ChainSelectListboxComponent c = null; 434 Iterator<UIComponent> children = parent.getFacetsAndChildren(); 435 if (children != null) { 436 UIComponent child = null; 437 while (children.hasNext()) { 438 child = (UIComponent) children.next(); 439 if (child instanceof ChainSelectListboxComponent) { 440 Integer index = ((ChainSelectListboxComponent) child).getIndex(); 441 if (i == index) { 442 c = (ChainSelectListboxComponent) child; 443 break; 444 } 445 } else { 446 // explore subcomps 447 c = getComponent(child, i); 448 if (c != null) { 449 break; 450 } 451 } 452 } 453 } 454 return c; 455 } 456 457 public ChainSelectListboxComponent getComponent(int i) { 458 return getComponent(this, i); 459 } 460 461 public boolean isMultiSelect() { 462 return multiSelect; 463 } 464 465 public void setMultiSelect(boolean multiSelect) { 466 this.multiSelect = multiSelect; 467 } 468 469 public Selection[] getSelections() { 470 return selections; 471 } 472 473 public boolean isQualifiedParentKeys() { 474 return qualifiedParentKeys; 475 } 476 477 public void setQualifiedParentKeys(boolean fullyQualifiedParentKey) { 478 qualifiedParentKeys = fullyQualifiedParentKey; 479 } 480 481 public Boolean getDisplayValueOnly() { 482 if (displayValueOnly != null) { 483 return displayValueOnly; 484 } 485 return false; 486 } 487 488 public void setDisplayValueOnly(Boolean displayValueOnly) { 489 this.displayValueOnly = displayValueOnly; 490 } 491 492 public String getDisplayValueOnlyStyle() { 493 return displayValueOnlyStyle; 494 } 495 496 public void setDisplayValueOnlyStyle(String displayValueOnlyStyle) { 497 this.displayValueOnlyStyle = displayValueOnlyStyle; 498 } 499 500 public String getDisplayValueOnlyStyleClass() { 501 return displayValueOnlyStyleClass; 502 } 503 504 public void setDisplayValueOnlyStyleClass(String displayValueOnlyStyleClass) { 505 this.displayValueOnlyStyleClass = displayValueOnlyStyleClass; 506 } 507 508 public boolean getMultiParentSelect() { 509 return multiParentSelect; 510 } 511 512 public void setMultiParentSelect(boolean multiParentSelect) { 513 this.multiParentSelect = multiParentSelect; 514 if (multiParentSelect) { 515 multiSelect = true; 516 } 517 } 518 519 public String[] encodeValue(Selection[] selections) { 520 String[] keys = new String[selections.length]; 521 for (int i = 0; i < selections.length; i++) { 522 keys[i] = selections[i].getValue(keySeparator); 523 } 524 return keys; 525 } 526 527 private void init() { 528 if (componentValue == null) { 529 Object value = getValue(); 530 if (value == null) { 531 componentValue = new Selection[0]; 532 selections = new Selection[1]; 533 selections[0] = new Selection(new DirectorySelectItem[0]); 534 return; 535 } 536 String[] rows; 537 if (multiSelect) { 538 if (value instanceof String[]) { 539 rows = (String[]) value; 540 } else if (value instanceof Object[]) { 541 Object[] values = (Object[]) value; 542 rows = new String[values.length]; 543 for (int i = 0; i < rows.length; i++) { 544 rows[i] = String.valueOf(values[i]); 545 } 546 } else if (value instanceof List) { 547 List valueList = (List) value; 548 rows = new String[valueList.size()]; 549 for (int i = 0; i < rows.length; i++) { 550 rows[i] = String.valueOf(valueList.get(i)); 551 } 552 } else { 553 rows = new String[] {}; 554 } 555 } else { 556 rows = new String[] { (String) value }; 557 } 558 559 componentValue = new Selection[rows.length]; 560 for (int i = 0; i < rows.length; i++) { 561 String[] columns = StringUtils.split(rows[i], getKeySeparator()); 562 componentValue[i] = createSelection(columns); 563 } 564 565 if (multiParentSelect) { 566 selections = new Selection[1]; 567 selections[0] = new Selection(new DirectorySelectItem[0]); 568 } else { 569 selections = componentValue; 570 } 571 } 572 } 573 574 public Selection createSelection(List<String> columns) { 575 return createSelection(columns.toArray(new String[columns.size()])); 576 } 577 578 public Selection createSelection(String[] columns) { 579 List<String> keyList = new ArrayList<String>(); 580 List<DirectorySelectItem> itemList = new ArrayList<DirectorySelectItem>(); 581 for (int i = 0; i < columns.length; i++) { 582 String id = columns[i]; 583 584 String directoryName = null; 585 VocabularyEntryList directoryValues = null; 586 boolean displayObsoleteEntries = false; 587 588 NestedChainSelectComponentInfo compInfo = compInfos.get(i); 589 if (compInfo != null) { 590 directoryName = compInfo.directoryName; 591 directoryValues = compInfo.directoryValues; 592 displayObsoleteEntries = compInfo.displayObsoleteEntries; 593 } else { 594 // fallback to the old solution 595 ChainSelectListboxComponent comp = getComponent(i); 596 if (comp != null) { 597 directoryName = comp.getStringProperty("directoryName", null); 598 directoryValues = comp.getDirectoryValues(); 599 displayObsoleteEntries = comp.getBooleanProperty("displayObsoleteEntries", false); 600 } 601 } 602 603 Map<String, Serializable> filter = new HashMap<String, Serializable>(); 604 filter.put("id", id); 605 606 if (i == 0) { 607 if (directoryName != null) { 608 if (DirectoryHelper.instance().hasParentColumn(directoryName)) { 609 filter.put("parent", getDefaultRootKey()); 610 } 611 } 612 } else { 613 String parentId; 614 if (qualifiedParentKeys) { 615 parentId = StringUtils.join(keyList.iterator(), getKeySeparator()); 616 } else { 617 parentId = columns[i - 1]; 618 } 619 filter.put("parent", parentId); 620 } 621 622 keyList.add(id); 623 624 if (!displayObsoleteEntries) { 625 filter.put("obsolete", 0); 626 } 627 List<DirectorySelectItem> items = null; 628 if (directoryName != null) { 629 items = DirectoryHelper.instance().getSelectItems(directoryName, filter); 630 } else { 631 items = DirectoryHelper.getSelectItems(directoryValues, filter); 632 } 633 if (items == null) { 634 throw new IllegalStateException(String.format("Item not found: directoryName=%s, filter=%s", 635 directoryName, filter)); 636 } 637 if (items.isEmpty()) { 638 log.warn(String.format("No selection for dir %s ", directoryName)); 639 return new Selection(itemList.toArray(new DirectorySelectItem[0])); 640 } else { 641 if (items.size() != 1) { 642 log.warn(String.format("Too many items (%s) found: directoryName=%s, filter=%s", 643 Integer.toString(items.size()), directoryName, filter)); 644 } 645 itemList.add(items.get(0)); 646 } 647 } 648 return new Selection(itemList.toArray(new DirectorySelectItem[columns.length])); 649 } 650 651 public Selection[] getComponentValue() { 652 return componentValue; 653 } 654 655 public void setComponentValue(Selection[] componentValue) { 656 this.componentValue = componentValue; 657 } 658 659 public int getLastSelectedComponentIndex() { 660 return lastSelectedComponentIndex; 661 } 662 663 public void setLastSelectedComponentIndex(int index) { 664 lastSelectedComponentIndex = index; 665 } 666 667 /** 668 * This structure is needed to keep data for dynamically generated components. 669 */ 670 static class NestedChainSelectComponentInfo { 671 672 String directoryName; 673 674 VocabularyEntryList directoryValues; 675 676 boolean displayObsoleteEntries; 677 678 boolean localize; 679 680 String display; 681 682 } 683 684 public void setCompAtIndex(int index, ChainSelectListboxComponent comp) { 685 686 NestedChainSelectComponentInfo compInfo = new NestedChainSelectComponentInfo(); 687 688 compInfo.directoryName = comp.getStringProperty("directoryName", null); 689 compInfo.directoryValues = comp.getDirectoryValues(); 690 compInfo.displayObsoleteEntries = comp.getBooleanProperty("displayObsoleteEntries", false); 691 compInfo.localize = comp.getBooleanProperty("localize", false); 692 compInfo.display = comp.getDisplay(); 693 694 compInfos.put(index, compInfo); 695 } 696 697 public String getKeySeparator() { 698 return keySeparator != null ? keySeparator : DEFAULT_KEY_SEPARATOR; 699 } 700 701 public void setKeySeparator(String keySeparator) { 702 this.keySeparator = keySeparator; 703 } 704 705 public String getDefaultRootKey() { 706 ValueExpression ve = getValueExpression("defaultRootKey"); 707 if (ve != null) { 708 return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 709 } else { 710 return defaultRootKey; 711 } 712 } 713 714 public void setDefaultRootKey(String defaultRootKey) { 715 this.defaultRootKey = defaultRootKey; 716 } 717 718 @Override 719 public void validateValue(FacesContext context, Object newValue) { 720 super.validateValue(context, newValue); 721 if (!isValid()) { 722 return; 723 } 724 725 if (newValue instanceof String) { 726 String newValueStr = (String) newValue; 727 if (StringUtils.isEmpty(newValueStr)) { 728 return; 729 } 730 731 String[] rows = StringUtils.split(newValueStr, getKeySeparator()); 732 boolean allowBranchSelection = Boolean.TRUE.equals(getBooleanProperty("allowBranchSelection", false)); 733 if (!allowBranchSelection && rows.length != size) { 734 String messageStr = ComponentUtils.translate(context, "label.chainSelect.incomplete_selection"); 735 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, messageStr, messageStr); 736 context.addMessage(getClientId(context), message); 737 setValid(false); 738 } 739 } 740 } 741 742 /** 743 * @since 5.6 744 */ 745 public Boolean getResetCacheOnUpdate() { 746 if (resetCacheOnUpdate != null) { 747 return resetCacheOnUpdate; 748 } 749 ValueExpression ve = getValueExpression("resetCacheOnUpdate"); 750 if (ve != null) { 751 try { 752 return Boolean.valueOf(Boolean.TRUE.equals(ve.getValue(getFacesContext().getELContext()))); 753 } catch (ELException e) { 754 throw new FacesException(e); 755 } 756 } else { 757 // default value 758 return Boolean.FALSE; 759 } 760 } 761 762 /** 763 * @since 5.6 764 */ 765 public void setResetCacheOnUpdate(Boolean resetCacheOnUpdate) { 766 this.resetCacheOnUpdate = resetCacheOnUpdate; 767 } 768 769 /** 770 * Override update method to reset cached value and ensure good re-render in ajax 771 * 772 * @since 5.6 773 */ 774 @Override 775 public void processUpdates(FacesContext context) { 776 super.processUpdates(context); 777 if (Boolean.TRUE.equals(getResetCacheOnUpdate()) && isValid()) { 778 componentValue = new Selection[0]; 779 } 780 } 781 782 /** 783 * Reset the chain select cached model 784 * 785 * @since 5.7 786 */ 787 @Override 788 public void resetCachedModel() { 789 if (getValueExpression("value") != null) { 790 setValue(null); 791 setLocalValueSet(false); 792 } 793 setSubmittedValue(null); 794 setComponentValue(null); 795 } 796 797}