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.DocumentModelList; 042import org.nuxeo.ecm.core.api.NuxeoException; 043import org.nuxeo.ecm.core.api.PropertyException; 044import org.nuxeo.ecm.core.api.impl.DocumentModelImpl; 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, new DocumentModelImpl("Folder")); 155 pathProcessing(); 156 return config.getOutcome(); 157 } 158 159 @SuppressWarnings("unchecked") 160 public boolean isSelected() { 161 if (config.hasContentViewSupport()) { 162 DocumentModel searchDoc = getContentViewSearchDocumentModel(); 163 if (searchDoc != null) { 164 String fieldName = config.getFieldName(); 165 String schemaName = config.getSchemaName(); 166 if (config.isMultiselect()) { 167 List<Object> values = (List<Object>) searchDoc.getProperty(schemaName, fieldName); 168 return values.contains(path); 169 } else { 170 return path.equals(searchDoc.getProperty(schemaName, fieldName)); 171 } 172 } else { 173 log.error("Cannot check if node is selected: " + "search document model is null"); 174 } 175 } else { 176 log.error(String.format("Cannot check if node is selected on tree '%s': no " + "content view available", 177 identifier)); 178 } 179 return false; 180 } 181 182 public int getChildCount() { 183 if (isLastLevel()) { 184 return 0; 185 } 186 return getChildrenEntries().size(); 187 } 188 189 public List<DirectoryTreeNode> getChildren() { 190 if (children != null) { 191 // return last computed state 192 return children; 193 } 194 children = new ArrayList<DirectoryTreeNode>(); 195 if (isLastLevel()) { 196 return children; 197 } 198 String schema = getDirectorySchema(); 199 DocumentModelList results = getChildrenEntries(); 200 FacesContext context = FacesContext.getCurrentInstance(); 201 for (DocumentModel result : results) { 202 String childIdendifier = result.getId(); 203 String childDescription = translate(context, (String) result.getProperty(schema, LABEL_FIELD_ID)); 204 String childPath; 205 if ("".equals(path)) { 206 childPath = childIdendifier; 207 } else { 208 childPath = path + '/' + childIdendifier; 209 } 210 children.add(new DirectoryTreeNode(level + 1, config, childIdendifier, childDescription, childPath, 211 getDirectoryService())); 212 } 213 214 // sort children 215 Comparator<? super DirectoryTreeNode> cmp = new FieldComparator(); 216 Collections.sort(children, cmp); 217 218 return children; 219 } 220 221 private class FieldComparator implements Comparator<DirectoryTreeNode> { 222 223 @Override 224 public int compare(DirectoryTreeNode o1, DirectoryTreeNode o2) { 225 return ObjectUtils.compare(o1.getDescription(), o2.getDescription()); 226 } 227 } 228 229 protected static String translate(FacesContext context, String label) { 230 String bundleName = context.getApplication().getMessageBundle(); 231 Locale locale = context.getViewRoot().getLocale(); 232 label = I18NUtils.getMessageString(bundleName, label, null, locale); 233 return label; 234 } 235 236 protected DocumentModelList getChildrenEntries() { 237 if (childrenEntries != null) { 238 // memorized directory lookup since directory content is not 239 // suppose to change 240 // XXX: use the cache manager instead of field caching strategy 241 return childrenEntries; 242 } 243 try (Session session = getDirectorySession()) { 244 if (level == 0) { 245 String schemaName = getDirectorySchema(); 246 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 247 Schema schema = schemaManager.getSchema(schemaName); 248 if (schema.hasField(PARENT_FIELD_ID)) { 249 // filter on empty parent 250 Map<String, Serializable> filter = new HashMap<String, Serializable>(); 251 filter.put(PARENT_FIELD_ID, ""); 252 childrenEntries = session.query(filter); 253 } else { 254 childrenEntries = session.getEntries(); 255 } 256 } else { 257 Map<String, Serializable> filter = new HashMap<String, Serializable>(); 258 String[] bitsOfPath = path.split("/"); 259 filter.put(PARENT_FIELD_ID, bitsOfPath[level - 1]); 260 childrenEntries = session.query(filter); 261 } 262 return childrenEntries; 263 } 264 } 265 266 public String getDescription() { 267 if (level == 0) { 268 return translate(FacesContext.getCurrentInstance(), description); 269 } 270 return description; 271 } 272 273 public String getIdentifier() { 274 return identifier; 275 } 276 277 public String getPath() { 278 return path; 279 } 280 281 public String getType() { 282 return type; 283 } 284 285 public boolean isLeaf() { 286 return leaf || isLastLevel() || getChildCount() == 0; 287 } 288 289 public void setDescription(String description) { 290 this.description = description; 291 } 292 293 public void setIdentifier(String identifier) { 294 this.identifier = identifier; 295 } 296 297 public void setLeaf(boolean leaf) { 298 this.leaf = leaf; 299 } 300 301 public void setType(String type) { 302 this.type = type; 303 } 304 305 protected DirectoryService getDirectoryService() { 306 if (directoryService == null) { 307 directoryService = DirectoryHelper.getDirectoryService(); 308 } 309 return directoryService; 310 } 311 312 protected String getDirectoryName() { 313 String name = config.getDirectories()[level]; 314 if (name == null) { 315 throw new NuxeoException("could not find directory name for level=" + level); 316 } 317 return name; 318 } 319 320 protected String getDirectorySchema() { 321 return getDirectoryService().getDirectorySchema(getDirectoryName()); 322 } 323 324 protected Session getDirectorySession() { 325 return getDirectoryService().open(getDirectoryName()); 326 } 327 328 protected void lookupContentView() { 329 if (contentView != null) { 330 return; 331 } 332 SeamContextHelper seamContextHelper = new SeamContextHelper(); 333 ContentViewActions cva = (ContentViewActions) seamContextHelper.get("contentViewActions"); 334 contentView = cva.getContentView(config.getContentView()); 335 if (contentView == null) { 336 throw new NuxeoException("no content view registered as " + config.getContentView()); 337 } 338 } 339 340 protected DocumentModel getContentViewSearchDocumentModel() { 341 lookupContentView(); 342 if (contentView != null) { 343 return contentView.getSearchDocumentModel(); 344 } 345 return null; 346 } 347 348 protected boolean isLastLevel() { 349 return config.getDirectories().length == level; 350 } 351 352 public void pathProcessing() throws DirectoryException { 353 if (config.isMultiselect()) { 354 // no breadcrumbs management with multiselect 355 return; 356 } 357 String aPath = null; 358 if (config.hasContentViewSupport()) { 359 DocumentModel searchDoc = getContentViewSearchDocumentModel(); 360 if (searchDoc != null) { 361 aPath = (String) searchDoc.getProperty(config.getSchemaName(), config.getFieldName()); 362 } else { 363 log.error("Cannot perform path preprocessing: " + "search document model is null"); 364 } 365 } 366 if (aPath != null && aPath != "") { 367 String[] bitsOfPath = aPath.split("/"); 368 String myPath = ""; 369 String property = ""; 370 for (int b = 0; b < bitsOfPath.length; b++) { 371 String dirName = config.getDirectories()[b]; 372 if (dirName == null) { 373 throw new DirectoryException("Could not find directory name for key=" + b); 374 } 375 try (Session session = getDirectoryService().open(dirName)) { 376 DocumentModel docMod = session.getEntry(bitsOfPath[b]); 377 try { 378 // take first schema: directory entries only have one 379 final String schemaName = docMod.getSchemas()[0]; 380 property = (String) docMod.getProperty(schemaName, LABEL_FIELD_ID); 381 } catch (PropertyException e) { 382 throw new DirectoryException(e); 383 } 384 myPath = myPath + property + '/'; 385 } 386 } 387 Events.instance().raiseEvent("PATH_PROCESSED", myPath); 388 } else { 389 Events.instance().raiseEvent("PATH_PROCESSED", ""); 390 } 391 } 392 393 /** 394 * @deprecated since 6.0, use {@link #isOpen()} instead 395 */ 396 @Deprecated 397 public boolean isOpened() { 398 return isOpen(); 399 } 400 401 public boolean isOpen() { 402 if (open == null) { 403 final TreeActions treeActionBean = (TreeActionsBean) Component.getInstance("treeActions"); 404 if (!treeActionBean.isNodeExpandEvent()) { 405 if (!config.isMultiselect() && config.hasContentViewSupport()) { 406 DocumentModel searchDoc = getContentViewSearchDocumentModel(); 407 if (searchDoc != null) { 408 String fieldName = config.getFieldName(); 409 String schemaName = config.getSchemaName(); 410 Object value = searchDoc.getProperty(schemaName, fieldName); 411 if (value instanceof String) { 412 open = Boolean.valueOf(((String) value).startsWith(path)); 413 } 414 } else { 415 log.error("Cannot check if node is opened: " + "search document model is null"); 416 } 417 } else { 418 log.error(String.format("Cannot check if node is opened on tree '%s': no " 419 + "content view available", identifier)); 420 } 421 } 422 } 423 return Boolean.TRUE.equals(open); 424 } 425 426 public void setOpen(boolean open) { 427 this.open = Boolean.valueOf(open); 428 } 429 430}