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