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 * George Lefter 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.Collections; 027import java.util.HashMap; 028import java.util.Iterator; 029import java.util.LinkedHashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033 034import javax.el.ValueExpression; 035import javax.faces.application.FacesMessage; 036import javax.faces.component.NamingContainer; 037import javax.faces.component.UIComponent; 038import javax.faces.component.UIInput; 039import javax.faces.component.UISelectItem; 040import javax.faces.component.html.HtmlSelectOneListbox; 041import javax.faces.context.FacesContext; 042 043import org.apache.commons.lang.StringUtils; 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046import org.nuxeo.ecm.core.api.DocumentModel; 047import org.nuxeo.ecm.core.api.DocumentModelList; 048import org.nuxeo.ecm.directory.Session; 049import org.nuxeo.ecm.directory.api.DirectoryService; 050import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; 051 052/** 053 * @author <a href="mailto:glefter@nuxeo.com">George Lefter</a> 054 */ 055public abstract class ChainSelectBase extends UIInput implements NamingContainer { 056 057 private static final Log log = LogFactory.getLog(ChainSelect.class); 058 059 protected static final String DISPLAY_LABEL = "label"; 060 061 protected static final String DISPLAY_ID = "id"; 062 063 protected static final String DISPLAY_IDLABEL = "idAndLabel"; 064 065 protected static final String DEFAULT_KEYSEPARATOR = "/"; 066 067 protected static final String SELECT = "selectListbox"; 068 069 public static final String VOCABULARY_SCHEMA = "vocabulary"; 070 071 /** Directory with a parent column. */ 072 public static final String XVOCABULARY_SCHEMA = "xvocabulary"; 073 074 protected String directoryNames; 075 076 protected String keySeparator = DEFAULT_KEYSEPARATOR; 077 078 protected boolean qualifiedParentKeys = false; 079 080 protected int depth; 081 082 protected String display = DISPLAY_LABEL; 083 084 protected boolean translate; 085 086 protected boolean showObsolete; 087 088 protected String style; 089 090 protected String styleClass; 091 092 protected int listboxSize; 093 094 protected boolean allowBranchSelection; 095 096 protected String reRender; 097 098 private boolean displayValueOnly; 099 100 protected String defaultRootKey; 101 102 protected Map<String, String[]> selectionMap = new HashMap<String, String[]>(); 103 104 protected ChainSelectBase() { 105 HtmlSelectOneListbox select = new HtmlSelectOneListbox(); 106 getFacets().put(SELECT, select); 107 } 108 109 public String getDirectory(int level) { 110 String[] directories = getDirectories(); 111 if (isRecursive()) { 112 return directories[0]; 113 } else { 114 if (level < directories.length) { 115 return directories[level]; 116 } else { 117 return null; 118 } 119 } 120 } 121 122 @Override 123 @SuppressWarnings("unchecked") 124 public void restoreState(FacesContext context, Object state) { 125 Object[] values = (Object[]) state; 126 super.restoreState(context, values[0]); 127 ChainSelectState chainState = (ChainSelectState) values[1]; 128 selectionMap = (Map<String, String[]>) values[2]; 129 130 depth = chainState.getDepth(); 131 display = chainState.getDisplay(); 132 directoryNames = chainState.getDirectoryNames(); 133 keySeparator = chainState.getKeySeparator(); 134 qualifiedParentKeys = chainState.getQualifiedParentKeys(); 135 showObsolete = chainState.getShowObsolete(); 136 listboxSize = chainState.getListboxSize(); 137 style = chainState.getStyle(); 138 styleClass = chainState.getStyleClass(); 139 translate = chainState.getTranslate(); 140 allowBranchSelection = chainState.getAllowBranchSelection(); 141 reRender = chainState.getReRender(); 142 displayValueOnly = chainState.getDisplayValueOnly(); 143 defaultRootKey = chainState.getDefaultRootKey(); 144 } 145 146 @Override 147 public Object saveState(FacesContext context) { 148 ChainSelectState chainState = new ChainSelectState(); 149 chainState.setDepth(depth); 150 chainState.setDisplay(display); 151 chainState.setDirectoryNames(directoryNames); 152 chainState.setKeySeparator(keySeparator); 153 chainState.setQualifiedParentKeys(qualifiedParentKeys); 154 chainState.setShowObsolete(showObsolete); 155 chainState.setStyle(style); 156 chainState.setStyleClass(styleClass); 157 chainState.setTranslate(translate); 158 chainState.setListboxSize(listboxSize); 159 chainState.setAllowBranchSelection(allowBranchSelection); 160 chainState.setReRender(reRender); 161 chainState.setDisplayValueOnly(displayValueOnly); 162 chainState.setDefaultRootKey(defaultRootKey); 163 164 Object[] values = new Object[3]; 165 values[0] = super.saveState(context); 166 values[1] = chainState; 167 values[2] = selectionMap; 168 169 return values; 170 } 171 172 protected HtmlSelectOneListbox getListbox(FacesContext context, int level) { 173 String componentId = getComponentId(level); 174 175 HtmlSelectOneListbox listbox = new HtmlSelectOneListbox(); 176 getChildren().add(listbox); 177 178 listbox.setId(componentId); 179 listbox.getChildren().clear(); 180 181 String reRender = getReRender(); 182 if (reRender == null) { 183 reRender = getId(); 184 } 185 186 UIComponent support = context.getApplication().createComponent("org.ajax4jsf.ajax.Support"); 187 support.getAttributes().put("event", "onchange"); 188 support.getAttributes().put("reRender", reRender); 189 support.getAttributes().put("immediate", Boolean.TRUE); 190 support.getAttributes().put("id", componentId + "_a4jSupport"); 191 listbox.getChildren().add(support); 192 193 return listbox; 194 } 195 196 protected void encodeListbox(FacesContext context, int level, String[] selectedKeys) throws IOException { 197 HtmlSelectOneListbox listbox = getListbox(context, level); 198 listbox.setSize(getListboxSize()); 199 200 List<DirectoryEntry> items; 201 if (level <= selectedKeys.length) { 202 items = getDirectoryEntries(level, selectedKeys); 203 } else { 204 items = new ArrayList<DirectoryEntry>(); 205 } 206 207 UISelectItem emptyItem = new UISelectItem(); 208 emptyItem.setItemLabel(ComponentUtils.translate(context, "label.vocabulary.selectValue")); 209 emptyItem.setItemValue(""); 210 emptyItem.setId(context.getViewRoot().createUniqueId()); 211 listbox.getChildren().add(emptyItem); 212 213 for (DirectoryEntry child : items) { 214 UISelectItem selectItem = new UISelectItem(); 215 String itemValue = child.getId(); 216 String itemLabel = child.getLabel(); 217 itemLabel = computeItemLabel(context, itemValue, itemLabel); 218 219 selectItem.setItemValue(itemValue); 220 selectItem.setItemLabel(itemLabel); 221 selectItem.setId(context.getViewRoot().createUniqueId()); 222 listbox.getChildren().add(selectItem); 223 } 224 225 if (level < selectedKeys.length) { 226 listbox.setValue(selectedKeys[level]); 227 } 228 229 ComponentUtils.encodeComponent(context, listbox); 230 } 231 232 public String[] getDirectories() { 233 return StringUtils.split(getDirectoryNames(), ","); 234 } 235 236 public boolean isRecursive() { 237 return getDirectories().length != getDepth(); 238 } 239 240 /** 241 * Computes the items that should be displayed for the nth listbox, depending on the options that have been selected 242 * in the previous ones. 243 * 244 * @param level the index of the listbox for which to compute the items 245 * @param selectedKeys the keys for the items selected on the previous levels 246 * @return a list of directory items 247 */ 248 public List<DirectoryEntry> getDirectoryEntries(int level, String[] selectedKeys) { 249 250 assert level <= selectedKeys.length; 251 252 List<DirectoryEntry> result = new ArrayList<DirectoryEntry>(); 253 String directoryName = getDirectory(level); 254 255 DirectoryService service = DirectoryHelper.getDirectoryService(); 256 try (Session session = service.open(directoryName)) { 257 String schema = service.getDirectorySchema(directoryName); 258 Map<String, Serializable> filter = new HashMap<String, Serializable>(); 259 260 if (level == 0) { 261 if (schema.equals(XVOCABULARY_SCHEMA)) { 262 filter.put("parent", null); 263 } 264 } else { 265 if (getQualifiedParentKeys()) { 266 Iterator<String> iter = Arrays.asList(selectedKeys).subList(0, level).iterator(); 267 String fullPath = StringUtils.join(iter, getKeySeparator()); 268 filter.put("parent", fullPath); 269 } else { 270 filter.put("parent", selectedKeys[level - 1]); 271 } 272 } 273 274 if (!getShowObsolete()) { 275 filter.put("obsolete", "0"); 276 } 277 278 Set<String> emptySet = Collections.emptySet(); 279 Map<String, String> orderBy = new LinkedHashMap<String, String>(); 280 281 // adding sorting suport 282 if (schema.equals(VOCABULARY_SCHEMA) || schema.equals(XVOCABULARY_SCHEMA)) { 283 orderBy.put("ordering", "asc"); 284 orderBy.put("id", "asc"); 285 } 286 287 DocumentModelList entries = session.query(filter, emptySet, orderBy); 288 for (DocumentModel entry : entries) { 289 DirectoryEntry newNode = new DirectoryEntry(schema, entry); 290 result.add(newNode); 291 } 292 } 293 294 return result; 295 } 296 297 /** 298 * Resolves a list of keys (a selection) to a list of coresponding directory items. Example: [a, b, c] is resolved 299 * to [getNode(a), getNode(b), getNode(c)] 300 * 301 * @param keys 302 * @return 303 */ 304 public List<DirectoryEntry> resolveKeys(String[] keys) { 305 List<DirectoryEntry> result = new ArrayList<DirectoryEntry>(); 306 307 DirectoryService service = DirectoryHelper.getDirectoryService(); 308 for (int level = 0; level < keys.length; level++) { 309 String directoryName = getDirectory(level); 310 try (Session session = service.open(directoryName)) { 311 String schema = service.getDirectorySchema(directoryName); 312 Map<String, Serializable> filter = new HashMap<>(); 313 314 if (level == 0) { 315 if (schema.equals(XVOCABULARY_SCHEMA)) { 316 filter.put("parent", null); 317 } 318 } else { 319 if (getQualifiedParentKeys()) { 320 Iterator<String> iter = Arrays.asList(keys).subList(0, level).iterator(); 321 String fullPath = StringUtils.join(iter, getKeySeparator()); 322 filter.put("parent", fullPath); 323 } else { 324 filter.put("parent", keys[level - 1]); 325 } 326 } 327 filter.put("id", keys[level]); 328 329 DocumentModelList entries = session.query(filter); 330 if (entries == null || entries.isEmpty()) { 331 log.warn("keyList could not be resolved at level " + level); 332 break; 333 } 334 DirectoryEntry node = new DirectoryEntry(schema, entries.get(0)); 335 result.add(node); 336 337 } 338 } 339 return result; 340 } 341 342 public String getComponentId(int level) { 343 String directory = getDirectory(level); 344 if (isRecursive()) { 345 return directory + '_' + level; 346 } else { 347 return directory + '_' + level; 348 } 349 } 350 351 public String getKeySeparator() { 352 ValueExpression ve = getValueExpression("keySeparator"); 353 if (ve != null) { 354 return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 355 } else { 356 return keySeparator; 357 } 358 } 359 360 public void setKeySeparator(String keySeparator) { 361 this.keySeparator = keySeparator; 362 } 363 364 public String getDefaultRootKey() { 365 ValueExpression ve = getValueExpression("defaultRootKey"); 366 if (ve != null) { 367 return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 368 } else { 369 return defaultRootKey; 370 } 371 } 372 373 public void setDefaultRootKey(String defaultRootKey) { 374 this.defaultRootKey = defaultRootKey; 375 } 376 377 public boolean getDisplayValueOnly() { 378 ValueExpression ve = getValueExpression("displayValueOnly"); 379 if (ve != null) { 380 Boolean value = (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 381 return value == null ? false : value; 382 } else { 383 return displayValueOnly; 384 } 385 } 386 387 public void setDisplayValueOnly(boolean displayValueOnly) { 388 this.displayValueOnly = displayValueOnly; 389 } 390 391 public int getListboxSize() { 392 ValueExpression ve = getValueExpression("listboxSize"); 393 if (ve != null) { 394 return (Integer) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 395 } else { 396 return listboxSize; 397 } 398 } 399 400 public void setListboxSize(int listboxSize) { 401 this.listboxSize = listboxSize; 402 } 403 404 public String getDisplay() { 405 ValueExpression ve = getValueExpression("display"); 406 if (ve != null) { 407 return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 408 } else { 409 return display != null ? display : DISPLAY_LABEL; 410 } 411 } 412 413 public void setDisplay(String display) { 414 this.display = display; 415 } 416 417 public boolean getQualifiedParentKeys() { 418 ValueExpression ve = getValueExpression("qualifiedParentKeys"); 419 if (ve != null) { 420 return (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 421 } else { 422 return qualifiedParentKeys; 423 } 424 } 425 426 public String getDirectoryNames() { 427 ValueExpression ve = getValueExpression("directoryNames"); 428 if (ve != null) { 429 return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 430 } else { 431 return directoryNames; 432 } 433 } 434 435 public void setDirectoryNames(String directoryNames) { 436 this.directoryNames = directoryNames; 437 } 438 439 public int getDepth() { 440 int myDepth; 441 ValueExpression ve = getValueExpression("depth"); 442 if (ve != null) { 443 myDepth = (Integer) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 444 } else { 445 myDepth = depth; 446 } 447 448 return myDepth != 0 ? myDepth : getDirectories().length; 449 } 450 451 public void setDepth(int depth) { 452 this.depth = depth; 453 } 454 455 public String getStyle() { 456 ValueExpression ve = getValueExpression("style"); 457 if (ve != null) { 458 return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 459 } else { 460 return style; 461 } 462 } 463 464 public void setStyle(String style) { 465 this.style = style; 466 } 467 468 public String getStyleClass() { 469 ValueExpression ve = getValueExpression("styleClass"); 470 if (ve != null) { 471 return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 472 } else { 473 return styleClass; 474 } 475 } 476 477 public void setStyleClass(String styleClass) { 478 this.styleClass = styleClass; 479 } 480 481 public boolean getTranslate() { 482 ValueExpression ve_translate = getValueExpression("translate"); 483 if (ve_translate != null) { 484 return (Boolean) ve_translate.getValue(FacesContext.getCurrentInstance().getELContext()); 485 } else { 486 return translate; 487 } 488 } 489 490 public void setTranslate(boolean translate) { 491 this.translate = translate; 492 } 493 494 public boolean getShowObsolete() { 495 ValueExpression ve = getValueExpression("showObsolete"); 496 if (ve != null) { 497 return (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 498 } else { 499 return showObsolete; 500 } 501 } 502 503 public void setShowObsolete(boolean showObsolete) { 504 this.showObsolete = showObsolete; 505 } 506 507 public boolean getAllowBranchSelection() { 508 ValueExpression ve = getValueExpression("allowBranchSelection"); 509 if (ve != null) { 510 return (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 511 } else { 512 return allowBranchSelection; 513 } 514 } 515 516 public void setAllowBranchSelection(boolean allowBranchSelection) { 517 this.allowBranchSelection = allowBranchSelection; 518 } 519 520 public String getReRender() { 521 ValueExpression ve = getValueExpression("reRender"); 522 if (ve != null) { 523 return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); 524 } else { 525 return reRender; 526 } 527 } 528 529 public void setReRender(String reRender) { 530 this.reRender = reRender; 531 } 532 533 protected String[] getValueAsArray(String value) { 534 if (value == null) { 535 return new String[0]; 536 } 537 return StringUtils.split(value, getKeySeparator()); 538 } 539 540 protected String getValueAsString(String[] ar) { 541 return StringUtils.join(ar, getKeySeparator()); 542 } 543 544 protected String computeItemLabel(FacesContext context, String id, String label) { 545 boolean translate = getTranslate(); 546 String display = getDisplay(); 547 548 String translatedLabel = label; 549 if (translate) { 550 translatedLabel = ComponentUtils.translate(context, label); 551 } 552 553 if (DISPLAY_ID.equals(display)) { 554 return id; 555 } else if (DISPLAY_LABEL.equals(display)) { 556 return translatedLabel; 557 } else if (DISPLAY_IDLABEL.equals(display)) { 558 return id + ' ' + translatedLabel; 559 } else { 560 throw new RuntimeException( 561 "invalid value for attribute 'display'; should be either 'id', 'label' or 'idAndLabel'"); 562 } 563 } 564 565 public abstract String[] getSelection(); 566 567 protected void decodeSelection(FacesContext context) { 568 List<String> selectedKeyList = new ArrayList<String>(); 569 Map<String, String> parameters = context.getExternalContext().getRequestParameterMap(); 570 571 String[] selection = getSelection(); 572 for (int level = 0; level < getDepth(); level++) { 573 String clientId = getClientId(context) + SEPARATOR_CHAR + getComponentId(level); 574 String value = parameters.get(clientId); 575 if (StringUtils.isEmpty(value)) { 576 break; 577 } 578 selectedKeyList.add(value); 579 580 // compare the old value with the new one; if they differ 581 // the new list of keys is finished 582 if (level >= selection.length) { 583 break; 584 } 585 String oldValue = selection[level]; 586 if (!value.equals(oldValue)) { 587 break; 588 } 589 } 590 selection = selectedKeyList.toArray(new String[selectedKeyList.size()]); 591 setSelection(selection); 592 } 593 594 protected void setSelection(String[] selection) { 595 String clientId = getClientId(FacesContext.getCurrentInstance()); 596 selectionMap.put(clientId, selection); 597 } 598 599 protected boolean validateEntry(FacesContext context, String[] keys) { 600 if (!getAllowBranchSelection() && keys.length != getDepth()) { 601 String messageStr = ComponentUtils.translate(context, "label.chainSelect.incomplete_selection"); 602 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, messageStr, messageStr); 603 context.addMessage(getClientId(context), message); 604 setValid(false); 605 return false; 606 } else { 607 return true; 608 } 609 } 610 611}