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