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}