001/*
002 * (C) Copyright 2009 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 *     Radu Darlea
018 *     Bogdan Tatar
019 *     Florent Guillaume
020 */
021package org.nuxeo.ecm.platform.tag.web;
022
023import static org.jboss.seam.ScopeType.APPLICATION;
024import static org.jboss.seam.ScopeType.CONVERSATION;
025import static org.jboss.seam.ScopeType.EVENT;
026
027import java.io.Serializable;
028import java.util.ArrayList;
029import java.util.Collections;
030import java.util.List;
031
032import javax.faces.event.ActionEvent;
033
034import org.apache.commons.lang3.StringUtils;
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.jboss.seam.annotations.Factory;
038import org.jboss.seam.annotations.In;
039import org.jboss.seam.annotations.Name;
040import org.jboss.seam.annotations.Observer;
041import org.jboss.seam.annotations.Scope;
042import org.jboss.seam.annotations.intercept.BypassInterceptors;
043import org.jboss.seam.annotations.web.RequestParameter;
044import org.jboss.seam.contexts.Contexts;
045import org.jboss.seam.faces.FacesMessages;
046import org.jboss.seam.international.StatusMessage;
047import org.nuxeo.ecm.core.api.CoreSession;
048import org.nuxeo.ecm.core.api.DocumentModel;
049import org.nuxeo.ecm.core.api.DocumentModelList;
050import org.nuxeo.ecm.core.api.DocumentNotFoundException;
051import org.nuxeo.ecm.core.api.DocumentRef;
052import org.nuxeo.ecm.core.api.DocumentSecurityException;
053import org.nuxeo.ecm.core.api.IdRef;
054import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
055import org.nuxeo.ecm.platform.tag.Tag;
056import org.nuxeo.ecm.platform.tag.TagService;
057import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
058import org.nuxeo.ecm.webapp.helpers.EventNames;
059import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor;
060import org.nuxeo.runtime.api.Framework;
061
062/**
063 * This Seam bean provides support for tagging related actions which can be made on the current document.
064 */
065@Name("tagActions")
066@Scope(CONVERSATION)
067public class TagActionsBean implements Serializable {
068
069    private static final long serialVersionUID = 1L;
070
071    private static final Log log = LogFactory.getLog(TagActionsBean.class);
072
073    public static final String TAG_SEARCH_RESULT_PAGE = "tag_search_results";
074
075    public static final String SELECTION_EDITED = "selectionEdited";
076
077    public static final String DOCUMENTS_IMPORTED = "documentImported";
078
079    @In(create = true, required = false)
080    protected transient CoreSession documentManager;
081
082    @In(create = true)
083    protected NavigationContext navigationContext;
084
085    @In(create = true, required = false)
086    protected transient FacesMessages facesMessages;
087
088    @In(create = true)
089    protected transient ResourcesAccessor resourcesAccessor;
090
091    protected String listLabel;
092
093    // protected LRUCachingMap<String, Boolean> tagModifyCheckCache = new
094    // LRUCachingMap<String, Boolean>(
095    // 1);
096
097    /**
098     * Keeps the tagging information that will be performed on the current document document.
099     */
100    private String tagLabel;
101
102    /**
103     * Controls the presence of the tagging text field in UI.
104     */
105    private boolean addTag;
106
107    @RequestParameter
108    protected Boolean canSelectNewTag;
109
110    @Factory(value = "tagServiceEnabled", scope = APPLICATION)
111    public boolean isTagServiceEnabled() {
112        return getTagService() != null;
113    }
114
115    protected TagService getTagService() {
116        TagService tagService = Framework.getService(TagService.class);
117        return tagService.isEnabled() ? tagService : null;
118    }
119
120    /**
121     * Returns the list with distinct public tags (or owned by user) that are applied on the current document.
122     */
123    @Factory(value = "currentDocumentTags", scope = EVENT)
124    public List<Tag> getDocumentTags() {
125        DocumentModel currentDocument = navigationContext.getCurrentDocument();
126        if (currentDocument == null || !getTagService().supportsTag(documentManager, currentDocument.getId())) {
127            return new ArrayList<Tag>(0);
128        } else {
129            String docId = currentDocument.getId();
130            List<Tag> tags = getTagService().getDocumentTags(documentManager, docId, null);
131            Collections.sort(tags, Tag.LABEL_COMPARATOR);
132            return tags;
133        }
134    }
135
136    /**
137     * Performs the tagging on the current document.
138     */
139    public String addTagging() {
140        tagLabel = cleanLabel(tagLabel);
141        String messageKey;
142        if (StringUtils.isBlank(tagLabel)) {
143            messageKey = "message.add.new.tagging.not.empty";
144        } else {
145            DocumentModel currentDocument = navigationContext.getCurrentDocument();
146            String docId = currentDocument.getId();
147
148            TagService tagService = getTagService();
149            tagService.tag(documentManager, docId, tagLabel);
150            if (currentDocument.isVersion()) {
151                DocumentModel liveDocument = documentManager.getSourceDocument(currentDocument.getRef());
152                if (!liveDocument.isCheckedOut()) {
153                    tagService.tag(documentManager, liveDocument.getId(), tagLabel);
154                }
155            } else if (!currentDocument.isCheckedOut()) {
156                DocumentRef ref = documentManager.getBaseVersion(currentDocument.getRef());
157                if (ref instanceof IdRef) {
158                    tagService.tag(documentManager, ref.toString(), tagLabel);
159                }
160            }
161            messageKey = "message.add.new.tagging";
162            // force invalidation
163            Contexts.getEventContext().remove("currentDocumentTags");
164        }
165        facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get(messageKey), tagLabel);
166        reset();
167        return null;
168    }
169
170    /**
171     * Removes a tagging from the current document.
172     */
173    public String removeTagging(String label) {
174        DocumentModel currentDocument = navigationContext.getCurrentDocument();
175        String docId = currentDocument.getId();
176
177        TagService tagService = getTagService();
178        tagService.untag(documentManager, docId, label);
179
180        if (currentDocument.isVersion()) {
181            DocumentModel liveDocument = documentManager.getSourceDocument(currentDocument.getRef());
182            if (!liveDocument.isCheckedOut()) {
183                tagService.untag(documentManager, liveDocument.getId(), label);
184            }
185        } else if (!currentDocument.isCheckedOut()) {
186            DocumentRef ref = documentManager.getBaseVersion(currentDocument.getRef());
187            if (ref instanceof IdRef) {
188                tagService.untag(documentManager, ref.toString(), label);
189            }
190        }
191
192        reset();
193        // force invalidation
194        Contexts.getEventContext().remove("currentDocumentTags");
195        facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get("message.remove.tagging"),
196                label);
197        return null;
198    }
199
200    /**
201     * Returns tag cloud info for the whole repository. For performance reasons, the security on underlying documents is
202     * not tested.
203     */
204    @Factory(value = "tagCloudOnAllDocuments", scope = EVENT)
205    public List<Tag> getPopularCloudOnAllDocuments() {
206        List<Tag> cloud = getTagService().getTagCloud(documentManager, null, null, Boolean.TRUE); // logarithmic 0-100
207                                                                                                  // normalization
208        // change weight to a font size
209        double min = 100;
210        double max = 200;
211        for (Tag tag : cloud) {
212            tag.setWeight((long) (min + tag.getWeight() * (max - min) / 100));
213        }
214        Collections.sort(cloud, Tag.LABEL_COMPARATOR);
215        // Collections.sort(cloud, Tag.WEIGHT_COMPARATOR);
216        return cloud;
217    }
218
219    public String listDocumentsForTag(String listLabel) {
220        this.listLabel = listLabel;
221        return TAG_SEARCH_RESULT_PAGE;
222    }
223
224    @Factory(value = "taggedDocuments", scope = EVENT)
225    public DocumentModelList getChildrenSelectModel() {
226        if (StringUtils.isBlank(listLabel)) {
227            return new DocumentModelListImpl(0);
228        } else {
229            List<String> ids = getTagService().getTagDocumentIds(documentManager, listLabel);
230            DocumentModelList docs = new DocumentModelListImpl(ids.size());
231            DocumentModel doc = null;
232            for (String id : ids) {
233                try {
234                    doc = documentManager.getDocument(new IdRef(id));
235                } catch (DocumentNotFoundException e) {
236                    log.error(e.getMessage(), e);
237                } catch (DocumentSecurityException e) {
238                    // user don't have access to the document
239                    doc = null;
240                }
241                if (doc != null) {
242                    docs.add(doc);
243                    doc = null;
244                }
245            }
246            return docs;
247        }
248    }
249
250    public String getListLabel() {
251        return listLabel;
252    }
253
254    public void setListLabel(String listLabel) {
255        this.listLabel = listLabel;
256    }
257
258    /**
259     * Returns <b>true</b> if the current logged user has permission to modify a tag that is applied on the current
260     * document.
261     */
262    public boolean canModifyTag(Tag tag) {
263        return tag != null;
264    }
265
266    /**
267     * Resets the fields that are used for managing actions related to tagging.
268     */
269    public void reset() {
270        tagLabel = null;
271    }
272
273    /**
274     * Used to decide whether the tagging UI field is shown or not.
275     */
276    public void showAddTag(ActionEvent event) {
277        this.addTag = !this.addTag;
278    }
279
280    public String getTagLabel() {
281        return tagLabel;
282    }
283
284    /**
285     * @since 7.1
286     */
287    public void setTagLabel(final String tagLabel) {
288        this.tagLabel = tagLabel;
289    }
290
291    public boolean getAddTag() {
292        return addTag;
293    }
294
295    public void setAddTag(boolean addTag) {
296        this.addTag = addTag;
297    }
298
299    public List<Tag> getSuggestions(Object input) {
300        String label = (String) input;
301        List<Tag> tags = getTagService().getSuggestions(documentManager, label, null);
302        Collections.sort(tags, Tag.LABEL_COMPARATOR);
303        if (tags.size() > 10) {
304            tags = tags.subList(0, 10);
305        }
306
307        // add the typed tag as first suggestion if we can add new tag
308        label = cleanLabel(label);
309        if (Boolean.TRUE.equals(canSelectNewTag) && !tags.contains(new Tag(label, 0))) {
310            tags.add(0, new Tag(label, -1));
311        }
312
313        return tags;
314    }
315
316    protected static String cleanLabel(String label) {
317        label = label.toLowerCase(); // lowercase
318        label = label.replace(" ", ""); // no spaces
319        label = label.replace("\\", ""); // dubious char
320        label = label.replace("'", ""); // dubious char
321        label = label.replace("%", ""); // dubious char
322        return label;
323    }
324
325    @SuppressWarnings("unchecked")
326    @Observer({ SELECTION_EDITED, DOCUMENTS_IMPORTED })
327    public void addTagsOnEvent(List<DocumentModel> documents, DocumentModel docModel) {
328        List<String> tags = (List<String>) docModel.getContextData("bulk_tags");
329        if (tags != null && !tags.isEmpty()) {
330            TagService tagService = Framework.getService(TagService.class);
331            for (DocumentModel doc : documents) {
332                for (String tag : tags) {
333                    tagService.tag(documentManager, doc.getId(), tag);
334                }
335            }
336        }
337    }
338
339    @Observer(value = { EventNames.DOCUMENT_SELECTION_CHANGED }, create = false)
340    @BypassInterceptors
341    public void documentChanged() {
342        addTag = false;
343    }
344
345}