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