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