001/* 002 * (C) Copyright 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: DirectoryTreeNode.java 29611 2008-01-24 16:51:03Z gracinet $ 018 */ 019package org.nuxeo.ecm.webapp.directory; 020 021import java.io.Serializable; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Locale; 028import java.util.Map; 029 030import javax.faces.context.FacesContext; 031 032import org.apache.commons.lang.ObjectUtils; 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import org.jboss.seam.Component; 036import org.jboss.seam.core.Events; 037import org.nuxeo.common.utils.i18n.I18NUtils; 038import org.nuxeo.ecm.core.api.DocumentModel; 039import org.nuxeo.ecm.core.api.DocumentModelList; 040import org.nuxeo.ecm.core.api.NuxeoException; 041import org.nuxeo.ecm.core.api.PropertyException; 042import org.nuxeo.ecm.core.api.impl.DocumentModelImpl; 043import org.nuxeo.ecm.core.schema.SchemaManager; 044import org.nuxeo.ecm.core.schema.types.Schema; 045import org.nuxeo.ecm.directory.DirectoryException; 046import org.nuxeo.ecm.directory.Session; 047import org.nuxeo.ecm.directory.api.DirectoryService; 048import org.nuxeo.ecm.platform.contentview.jsf.ContentView; 049import org.nuxeo.ecm.platform.contentview.seam.ContentViewActions; 050import org.nuxeo.ecm.platform.ui.web.directory.DirectoryHelper; 051import org.nuxeo.ecm.platform.ui.web.util.SeamContextHelper; 052import org.nuxeo.ecm.webapp.helpers.EventNames; 053import org.nuxeo.ecm.webapp.tree.TreeActions; 054import org.nuxeo.ecm.webapp.tree.TreeActionsBean; 055import org.nuxeo.runtime.api.Framework; 056 057/** 058 * Register directory tree configurations to make them available to the DirectoryTreeManagerBean to build 059 * DirectoryTreeNode instances. 060 * 061 * @author <a href="mailto:ogrisel@nuxeo.com">Olivier Grisel</a> 062 */ 063public class DirectoryTreeNode { 064 065 private static final Log log = LogFactory.getLog(DirectoryTreeNode.class); 066 067 public static final String PARENT_FIELD_ID = "parent"; 068 069 private static final String LABEL_FIELD_ID = "label"; 070 071 protected final String path; 072 073 protected final int level; 074 075 protected Boolean open = null; 076 077 protected final DirectoryTreeDescriptor config; 078 079 protected String identifier; 080 081 protected String description; 082 083 protected boolean leaf = false; 084 085 protected String type = "defaultDirectoryTreeNode"; 086 087 protected DirectoryService directoryService; 088 089 protected ContentView contentView; 090 091 protected DocumentModelList childrenEntries; 092 093 protected List<DirectoryTreeNode> children; 094 095 public DirectoryTreeNode(int level, DirectoryTreeDescriptor config, String identifier, String description, 096 String path, DirectoryService directoryService) { 097 this.level = level; 098 this.config = config; 099 this.identifier = identifier; 100 this.description = description; 101 this.path = path; 102 this.directoryService = directoryService; 103 } 104 105 protected List<String> processSelectedValuesOnMultiSelect(String value, List<String> values) { 106 if (values.contains(value)) { 107 values.remove(value); 108 } else { 109 // unselect all previous selection that are either more 110 // generic or more specific 111 List<String> valuesToRemove = new ArrayList<String>(); 112 String valueSlash = value + "/"; 113 for (String existingSelection : values) { 114 String existingSelectionSlash = existingSelection + "/"; 115 if (existingSelectionSlash.startsWith(valueSlash) || valueSlash.startsWith(existingSelectionSlash)) { 116 valuesToRemove.add(existingSelection); 117 } 118 } 119 values.removeAll(valuesToRemove); 120 121 // add the new selection 122 values.add(value); 123 } 124 return values; 125 } 126 127 @SuppressWarnings("unchecked") 128 public String selectNode() { 129 if (config.hasContentViewSupport()) { 130 DocumentModel searchDoc = getContentViewSearchDocumentModel(); 131 if (searchDoc != null) { 132 String fieldName = config.getFieldName(); 133 String schemaName = config.getSchemaName(); 134 if (config.isMultiselect()) { 135 List<String> values = (List<String>) searchDoc.getProperty(schemaName, fieldName); 136 values = processSelectedValuesOnMultiSelect(path, values); 137 searchDoc.setProperty(schemaName, fieldName, values); 138 } else { 139 searchDoc.setProperty(schemaName, fieldName, path); 140 } 141 if (contentView != null) { 142 contentView.refreshPageProvider(); 143 } 144 } else { 145 log.error("Cannot select node: search document model is null"); 146 } 147 } else { 148 log.error(String.format("Cannot select node on tree '%s': no content view available", identifier)); 149 } 150 // raise this event in order to reset the documents lists from 151 // 'conversationDocumentsListsManager' 152 Events.instance().raiseEvent(EventNames.FOLDERISHDOCUMENT_SELECTION_CHANGED, new DocumentModelImpl("Folder")); 153 pathProcessing(); 154 return config.getOutcome(); 155 } 156 157 @SuppressWarnings("unchecked") 158 public boolean isSelected() { 159 if (config.hasContentViewSupport()) { 160 DocumentModel searchDoc = getContentViewSearchDocumentModel(); 161 if (searchDoc != null) { 162 String fieldName = config.getFieldName(); 163 String schemaName = config.getSchemaName(); 164 if (config.isMultiselect()) { 165 List<Object> values = (List<Object>) searchDoc.getProperty(schemaName, fieldName); 166 return values.contains(path); 167 } else { 168 return path.equals(searchDoc.getProperty(schemaName, fieldName)); 169 } 170 } else { 171 log.error("Cannot check if node is selected: " + "search document model is null"); 172 } 173 } else { 174 log.error(String.format("Cannot check if node is selected on tree '%s': no " + "content view available", 175 identifier)); 176 } 177 return false; 178 } 179 180 public int getChildCount() { 181 if (isLastLevel()) { 182 return 0; 183 } 184 return getChildrenEntries().size(); 185 } 186 187 public List<DirectoryTreeNode> getChildren() { 188 if (children != null) { 189 // return last computed state 190 return children; 191 } 192 children = new ArrayList<DirectoryTreeNode>(); 193 if (isLastLevel()) { 194 return children; 195 } 196 String schema = getDirectorySchema(); 197 DocumentModelList results = getChildrenEntries(); 198 FacesContext context = FacesContext.getCurrentInstance(); 199 for (DocumentModel result : results) { 200 String childIdendifier = result.getId(); 201 String childDescription = translate(context, (String) result.getProperty(schema, LABEL_FIELD_ID)); 202 String childPath; 203 if ("".equals(path)) { 204 childPath = childIdendifier; 205 } else { 206 childPath = path + '/' + childIdendifier; 207 } 208 children.add(new DirectoryTreeNode(level + 1, config, childIdendifier, childDescription, childPath, 209 getDirectoryService())); 210 } 211 212 // sort children 213 Comparator<? super DirectoryTreeNode> cmp = new FieldComparator(); 214 Collections.sort(children, cmp); 215 216 return children; 217 } 218 219 private class FieldComparator implements Comparator<DirectoryTreeNode> { 220 221 @Override 222 public int compare(DirectoryTreeNode o1, DirectoryTreeNode o2) { 223 return ObjectUtils.compare(o1.getDescription(), o2.getDescription()); 224 } 225 } 226 227 protected static String translate(FacesContext context, String label) { 228 String bundleName = context.getApplication().getMessageBundle(); 229 Locale locale = context.getViewRoot().getLocale(); 230 label = I18NUtils.getMessageString(bundleName, label, null, locale); 231 return label; 232 } 233 234 protected DocumentModelList getChildrenEntries() { 235 if (childrenEntries != null) { 236 // memorized directory lookup since directory content is not 237 // suppose to change 238 // XXX: use the cache manager instead of field caching strategy 239 return childrenEntries; 240 } 241 try (Session session = getDirectorySession()) { 242 if (level == 0) { 243 String schemaName = getDirectorySchema(); 244 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 245 Schema schema = schemaManager.getSchema(schemaName); 246 if (schema.hasField(PARENT_FIELD_ID)) { 247 // filter on empty parent 248 Map<String, Serializable> filter = new HashMap<String, Serializable>(); 249 filter.put(PARENT_FIELD_ID, ""); 250 childrenEntries = session.query(filter); 251 } else { 252 childrenEntries = session.getEntries(); 253 } 254 } else { 255 Map<String, Serializable> filter = new HashMap<String, Serializable>(); 256 String[] bitsOfPath = path.split("/"); 257 filter.put(PARENT_FIELD_ID, bitsOfPath[level - 1]); 258 childrenEntries = session.query(filter); 259 } 260 return childrenEntries; 261 } 262 } 263 264 public String getDescription() { 265 if (level == 0) { 266 return translate(FacesContext.getCurrentInstance(), description); 267 } 268 return description; 269 } 270 271 public String getIdentifier() { 272 return identifier; 273 } 274 275 public String getPath() { 276 return path; 277 } 278 279 public String getType() { 280 return type; 281 } 282 283 public boolean isLeaf() { 284 return leaf || isLastLevel() || getChildCount() == 0; 285 } 286 287 public void setDescription(String description) { 288 this.description = description; 289 } 290 291 public void setIdentifier(String identifier) { 292 this.identifier = identifier; 293 } 294 295 public void setLeaf(boolean leaf) { 296 this.leaf = leaf; 297 } 298 299 public void setType(String type) { 300 this.type = type; 301 } 302 303 protected DirectoryService getDirectoryService() { 304 if (directoryService == null) { 305 directoryService = DirectoryHelper.getDirectoryService(); 306 } 307 return directoryService; 308 } 309 310 protected String getDirectoryName() { 311 String name = config.getDirectories()[level]; 312 if (name == null) { 313 throw new NuxeoException("could not find directory name for level=" + level); 314 } 315 return name; 316 } 317 318 protected String getDirectorySchema() { 319 return getDirectoryService().getDirectorySchema(getDirectoryName()); 320 } 321 322 protected Session getDirectorySession() { 323 return getDirectoryService().open(getDirectoryName()); 324 } 325 326 protected void lookupContentView() { 327 if (contentView != null) { 328 return; 329 } 330 SeamContextHelper seamContextHelper = new SeamContextHelper(); 331 ContentViewActions cva = (ContentViewActions) seamContextHelper.get("contentViewActions"); 332 contentView = cva.getContentView(config.getContentView()); 333 if (contentView == null) { 334 throw new NuxeoException("no content view registered as " + config.getContentView()); 335 } 336 } 337 338 protected DocumentModel getContentViewSearchDocumentModel() { 339 lookupContentView(); 340 if (contentView != null) { 341 return contentView.getSearchDocumentModel(); 342 } 343 return null; 344 } 345 346 protected boolean isLastLevel() { 347 return config.getDirectories().length == level; 348 } 349 350 public void pathProcessing() throws DirectoryException { 351 if (config.isMultiselect()) { 352 // no breadcrumbs management with multiselect 353 return; 354 } 355 String aPath = null; 356 if (config.hasContentViewSupport()) { 357 DocumentModel searchDoc = getContentViewSearchDocumentModel(); 358 if (searchDoc != null) { 359 aPath = (String) searchDoc.getProperty(config.getSchemaName(), config.getFieldName()); 360 } else { 361 log.error("Cannot perform path preprocessing: " + "search document model is null"); 362 } 363 } 364 if (aPath != null && aPath != "") { 365 String[] bitsOfPath = aPath.split("/"); 366 String myPath = ""; 367 String property = ""; 368 for (int b = 0; b < bitsOfPath.length; b++) { 369 String dirName = config.getDirectories()[b]; 370 if (dirName == null) { 371 throw new DirectoryException("Could not find directory name for key=" + b); 372 } 373 try (Session session = getDirectoryService().open(dirName)) { 374 DocumentModel docMod = session.getEntry(bitsOfPath[b]); 375 try { 376 // take first schema: directory entries only have one 377 final String schemaName = docMod.getSchemas()[0]; 378 property = (String) docMod.getProperty(schemaName, LABEL_FIELD_ID); 379 } catch (PropertyException e) { 380 throw new DirectoryException(e); 381 } 382 myPath = myPath + property + '/'; 383 } 384 } 385 Events.instance().raiseEvent("PATH_PROCESSED", myPath); 386 } else { 387 Events.instance().raiseEvent("PATH_PROCESSED", ""); 388 } 389 } 390 391 /** 392 * @deprecated since 6.0, use {@link #isOpen()} instead 393 */ 394 @Deprecated 395 public boolean isOpened() { 396 return isOpen(); 397 } 398 399 public boolean isOpen() { 400 if (open == null) { 401 final TreeActions treeActionBean = (TreeActionsBean) Component.getInstance("treeActions"); 402 if (!treeActionBean.isNodeExpandEvent()) { 403 if (!config.isMultiselect() && config.hasContentViewSupport()) { 404 DocumentModel searchDoc = getContentViewSearchDocumentModel(); 405 if (searchDoc != null) { 406 String fieldName = config.getFieldName(); 407 String schemaName = config.getSchemaName(); 408 Object value = searchDoc.getProperty(schemaName, fieldName); 409 if (value instanceof String) { 410 open = Boolean.valueOf(((String) value).startsWith(path)); 411 } 412 } else { 413 log.error("Cannot check if node is opened: " + "search document model is null"); 414 } 415 } else { 416 log.error(String.format("Cannot check if node is opened on tree '%s': no " 417 + "content view available", identifier)); 418 } 419 } 420 } 421 return Boolean.TRUE.equals(open); 422 } 423 424 public void setOpen(boolean open) { 425 this.open = Boolean.valueOf(open); 426 } 427 428}