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