001/* 002 * (C) Copyright 2011 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 * Contributors: 016 * Anahide Tchertchian <at@nuxeo.com> 017 * Thomas Roger <troger@nuxeo.com> 018 */ 019 020package org.nuxeo.ecm.webapp.directory; 021 022import java.io.Serializable; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import javax.faces.context.FacesContext; 030 031import org.jboss.seam.ScopeType; 032import org.jboss.seam.annotations.Begin; 033import org.jboss.seam.annotations.Create; 034import org.jboss.seam.annotations.In; 035import org.jboss.seam.annotations.Name; 036import org.jboss.seam.annotations.Observer; 037import org.jboss.seam.annotations.Scope; 038import org.jboss.seam.annotations.intercept.BypassInterceptors; 039import org.jboss.seam.core.Events; 040import org.jboss.seam.faces.FacesMessages; 041import org.jboss.seam.international.StatusMessage; 042import org.nuxeo.ecm.core.api.DocumentModel; 043import org.nuxeo.ecm.core.api.DocumentModelComparator; 044import org.nuxeo.ecm.core.api.DocumentModelList; 045import org.nuxeo.ecm.core.api.NuxeoPrincipal; 046import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; 047import org.nuxeo.ecm.directory.BaseSession; 048import org.nuxeo.ecm.directory.DirectoryDeleteConstraintException; 049import org.nuxeo.ecm.directory.Session; 050import org.nuxeo.ecm.directory.api.DirectoryDeleteConstraint; 051import org.nuxeo.ecm.directory.api.DirectoryService; 052import org.nuxeo.ecm.directory.api.ui.DirectoryUI; 053import org.nuxeo.ecm.directory.api.ui.DirectoryUIManager; 054import org.nuxeo.ecm.platform.actions.ActionContext; 055import org.nuxeo.ecm.platform.actions.ejb.ActionManager; 056import org.nuxeo.ecm.platform.actions.jsf.JSFActionContext; 057import org.nuxeo.ecm.platform.ui.web.directory.ChainSelectBase; 058import org.nuxeo.ecm.platform.ui.web.directory.DirectoryHelper; 059import org.nuxeo.ecm.platform.ui.web.util.SeamContextHelper; 060import org.nuxeo.ecm.webapp.helpers.EventNames; 061 062/** 063 * Manages directories editable by administrators. 064 * 065 * @author Anahide Tchertchian 066 */ 067@Name("directoryUIActions") 068@Scope(ScopeType.CONVERSATION) 069public class DirectoryUIActionsBean implements Serializable { 070 071 private static final long serialVersionUID = 1L; 072 073 public static final String DIRECTORY_DEFAULT_VIEW = "view_directory"; 074 075 @In(create = true) 076 protected transient DirectoryUIManager directoryUIManager; 077 078 // FIXME: use a business delegate 079 protected transient DirectoryService dirService; 080 081 @In(create = true, required = false) 082 protected transient FacesMessages facesMessages; 083 084 @In(create = true) 085 protected Map<String, String> messages; 086 087 @In(create = true, required = false) 088 protected transient ActionManager actionManager; 089 090 @In(create = true) 091 private transient NuxeoPrincipal currentNuxeoPrincipal; 092 093 protected List<String> directoryNames; 094 095 protected DirectoryUI currentDirectoryInfo; 096 097 protected DocumentModelList currentDirectoryEntries; 098 099 protected DocumentModel selectedDirectoryEntry; 100 101 protected boolean showAddForm = false; 102 103 protected DocumentModel creationDirectoryEntry; 104 105 protected String selectedDirectoryName; 106 107 @Begin(join = true) 108 @Create 109 public void initialize() { 110 initDirService(); 111 } 112 113 private void initDirService() { 114 if (dirService == null) { 115 dirService = DirectoryHelper.getDirectoryService(); 116 } 117 } 118 119 public List<String> getDirectoryNames() { 120 if (directoryNames == null) { 121 directoryNames = directoryUIManager.getDirectoryNames(); 122 if (directoryNames.size() > 0) { 123 // preserve selected directory if present 124 if (selectedDirectoryName == null || !directoryNames.contains(selectedDirectoryName)) { 125 selectedDirectoryName = directoryNames.get(0); 126 } 127 selectDirectory(); 128 } 129 } 130 return directoryNames; 131 } 132 133 public String getSelectedDirectoryName() { 134 return selectedDirectoryName; 135 } 136 137 public void setSelectedDirectoryName(String selectedDirectoryName) { 138 this.selectedDirectoryName = selectedDirectoryName; 139 } 140 141 public void selectDirectory() { 142 resetSelectedDirectoryData(); 143 currentDirectoryInfo = directoryUIManager.getDirectoryInfo(selectedDirectoryName); 144 } 145 146 public DirectoryUI getCurrentDirectory() { 147 return currentDirectoryInfo; 148 } 149 150 public DocumentModelList getCurrentDirectoryEntries() { 151 if (currentDirectoryEntries == null) { 152 currentDirectoryEntries = new DocumentModelListImpl(); 153 String dirName = currentDirectoryInfo.getName(); 154 try (Session dirSession = dirService.open(dirName)) { 155 Map<String, Serializable> emptyMap = Collections.emptyMap(); 156 Set<String> emptySet = Collections.emptySet(); 157 DocumentModelList entries = dirSession.query(emptyMap, emptySet, null, true); 158 if (entries != null && !entries.isEmpty()) { 159 currentDirectoryEntries.addAll(entries); 160 } 161 // sort 162 String sortField = currentDirectoryInfo.getSortField(); 163 if (sortField == null) { 164 sortField = dirService.getDirectoryIdField(dirName); 165 } 166 // sort 167 Map<String, String> orderBy = new HashMap<String, String>(); 168 orderBy.put(sortField, DocumentModelComparator.ORDER_ASC); 169 Collections.sort(currentDirectoryEntries, 170 new DocumentModelComparator(dirService.getDirectorySchema(dirName), orderBy)); 171 } 172 } 173 return currentDirectoryEntries; 174 } 175 176 public void resetSelectedDirectoryData() { 177 currentDirectoryInfo = null; 178 currentDirectoryEntries = null; 179 resetSelectedDirectoryEntry(); 180 resetCreateDirectoryEntry(); 181 } 182 183 public boolean getShowAddForm() { 184 return showAddForm; 185 } 186 187 public void toggleShowAddForm() { 188 showAddForm = !showAddForm; 189 } 190 191 public DocumentModel getCreationDirectoryEntry() { 192 if (creationDirectoryEntry == null) { 193 String dirName = currentDirectoryInfo.getName(); 194 String schema = dirService.getDirectorySchema(dirName); 195 creationDirectoryEntry = BaseSession.createEntryModel(null, schema, null, null); 196 } 197 return creationDirectoryEntry; 198 } 199 200 public void createDirectoryEntry() { 201 String dirName = currentDirectoryInfo.getName(); 202 try (Session dirSession = dirService.open(dirName)) { 203 // check if entry already exists 204 String schema = dirService.getDirectorySchema(dirName); 205 String idField = dirService.getDirectoryIdField(dirName); 206 Object id = creationDirectoryEntry.getProperty(schema, idField); 207 if (id instanceof String && dirSession.hasEntry((String) id)) { 208 facesMessages.add(StatusMessage.Severity.ERROR, 209 messages.get("vocabulary.entry.identifier.already.exists")); 210 return; 211 } 212 setParentColumnIfNull(creationDirectoryEntry); 213 dirSession.createEntry(creationDirectoryEntry); 214 215 resetCreateDirectoryEntry(); 216 // invalidate directory entries list 217 currentDirectoryEntries = null; 218 Events.instance().raiseEvent(EventNames.DIRECTORY_CHANGED, dirName); 219 220 facesMessages.add(StatusMessage.Severity.INFO, messages.get("vocabulary.entry.added")); 221 } 222 } 223 224 public void resetCreateDirectoryEntry() { 225 creationDirectoryEntry = null; 226 showAddForm = false; 227 } 228 229 public void selectDirectoryEntry(String entryId) { 230 String dirName = currentDirectoryInfo.getName(); 231 try (Session dirSession = dirService.open(dirName)) { 232 selectedDirectoryEntry = dirSession.getEntry(entryId); 233 } 234 } 235 236 public DocumentModel getSelectedDirectoryEntry() { 237 return selectedDirectoryEntry; 238 } 239 240 public void resetSelectedDirectoryEntry() { 241 selectedDirectoryEntry = null; 242 } 243 244 public void editSelectedDirectoryEntry() { 245 String dirName = currentDirectoryInfo.getName(); 246 try (Session dirSession = dirService.open(dirName)) { 247 setParentColumnIfNull(selectedDirectoryEntry); 248 dirSession.updateEntry(selectedDirectoryEntry); 249 selectedDirectoryEntry = null; 250 // invalidate directory entries list 251 currentDirectoryEntries = null; 252 Events.instance().raiseEvent(EventNames.DIRECTORY_CHANGED, dirName); 253 254 facesMessages.add(StatusMessage.Severity.INFO, messages.get("vocabulary.entry.edited")); 255 } 256 } 257 258 /** 259 * Forces the "parent" column of an "xvocabulary" directory entry to the empty string if null. This is required when 260 * filtering on the parent column, expecting the submitted value {@code ""}, not {@code null}. 261 * <p> 262 * Note that the empty string submitted value is converted to {@code null} because: 263 * <ul> 264 * <li>The {@code javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL} context parameter, used by UIInput, 265 * is set to {@code true}.</li> 266 * <li>In any case, for a "selectOneDirectory" widget relying on UISelectOne, the MenuRenderer converts 267 * RIConstants#NO_VALUE to {@code null}.</li> 268 * </ul> 269 */ 270 protected void setParentColumnIfNull(DocumentModel directoryEntry) { 271 if (directoryEntry != null && directoryEntry.hasSchema(ChainSelectBase.XVOCABULARY_SCHEMA) 272 && directoryEntry.getProperty(ChainSelectBase.XVOCABULARY_SCHEMA, 273 ChainSelectBase.PARENT_COLUMN) == null) { 274 directoryEntry.setProperty(ChainSelectBase.XVOCABULARY_SCHEMA, ChainSelectBase.PARENT_COLUMN, ""); 275 } 276 } 277 278 public void deleteDirectoryEntry(String entryId) { 279 String dirName = currentDirectoryInfo.getName(); 280 List<DirectoryDeleteConstraint> deleteConstraints = currentDirectoryInfo.getDeleteConstraints(); 281 if (deleteConstraints != null && !deleteConstraints.isEmpty()) { 282 for (DirectoryDeleteConstraint deleteConstraint : deleteConstraints) { 283 if (!deleteConstraint.canDelete(dirService, entryId)) { 284 facesMessages.add(StatusMessage.Severity.ERROR, 285 messages.get("feedback.directory.deleteEntry.constraintError")); 286 return; 287 } 288 } 289 } 290 try (Session dirSession = dirService.open(dirName)) { 291 try { 292 dirSession.deleteEntry(entryId); 293 // invalidate directory entries list 294 currentDirectoryEntries = null; 295 Events.instance().raiseEvent(EventNames.DIRECTORY_CHANGED, dirName); 296 facesMessages.add(StatusMessage.Severity.INFO, messages.get("vocabulary.entry.deleted")); 297 } catch (DirectoryDeleteConstraintException e) { 298 facesMessages.add(StatusMessage.Severity.ERROR, 299 messages.get("feedback.directory.deleteEntry.constraintError")); 300 } 301 } 302 } 303 304 public boolean isReadOnly(String directoryName) { 305 boolean isReadOnly; 306 307 try (Session dirSession = dirService.open(directoryName)) { 308 // Check Directory ReadOnly Status 309 boolean dirReadOnly = dirSession.isReadOnly(); 310 311 // Check DirectoryUI ReadOnly Status 312 boolean dirUIReadOnly; 313 DirectoryUI dirInfo = directoryUIManager.getDirectoryInfo(directoryName); 314 if (dirInfo == null) { 315 // assume read-only 316 dirUIReadOnly = true; 317 } else { 318 dirUIReadOnly = Boolean.TRUE.equals(dirInfo.isReadOnly()); 319 } 320 321 isReadOnly = dirReadOnly || dirUIReadOnly; 322 } 323 return isReadOnly; 324 } 325 326 protected ActionContext createDirectoryActionContext() { 327 return createDirectoryActionContext(selectedDirectoryName); 328 } 329 330 protected ActionContext createDirectoryActionContext(String directoryName) { 331 FacesContext faces = FacesContext.getCurrentInstance(); 332 if (faces == null) { 333 throw new IllegalArgumentException("Faces context is null"); 334 } 335 ActionContext ctx = new JSFActionContext(faces); 336 ctx.putLocalVariable("SeamContext", new SeamContextHelper()); 337 ctx.putLocalVariable("directoryName", directoryName); 338 ctx.setCurrentPrincipal(currentNuxeoPrincipal); 339 return ctx; 340 } 341 342 public boolean checkContextualDirectoryFilter(String filterName) { 343 return actionManager.checkFilter(filterName, createDirectoryActionContext()); 344 } 345 346 /** 347 * @since 5.9.1 348 */ 349 public boolean checkContextualDirectoryFilter(String filterName, String directoryName) { 350 return actionManager.checkFilter(filterName, createDirectoryActionContext(directoryName)); 351 } 352 353 @Observer(value = { EventNames.FLUSH_EVENT }, create = false) 354 @BypassInterceptors 355 public void onHotReloadFlush() { 356 directoryNames = null; 357 resetSelectedDirectoryData(); 358 } 359 360}