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