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