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.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) {
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     * Gets the doc id to use with the tag service for a given document.
138     * <p>
139     * Proxies are not tagged directly, their underlying document is.
140     *
141     * @deprecated since 5.7.3. The proxy is tagged itself.
142     */
143    @Deprecated
144    public static String getDocIdForTag(DocumentModel doc) {
145        return doc.isProxy() ? doc.getSourceId() : doc.getId();
146    }
147
148    /**
149     * Performs the tagging on the current document.
150     */
151    public String addTagging() {
152        tagLabel = cleanLabel(tagLabel);
153        String messageKey;
154        if (StringUtils.isBlank(tagLabel)) {
155            messageKey = "message.add.new.tagging.not.empty";
156        } else {
157            DocumentModel currentDocument = navigationContext.getCurrentDocument();
158            String docId = currentDocument.getId();
159
160            TagService tagService = getTagService();
161            tagService.tag(documentManager, docId, tagLabel, null);
162            if (currentDocument.isVersion()) {
163                DocumentModel liveDocument = documentManager.getSourceDocument(currentDocument.getRef());
164                if (!liveDocument.isCheckedOut()) {
165                    tagService.tag(documentManager, liveDocument.getId(), tagLabel, null);
166                }
167            } else if (!currentDocument.isCheckedOut()) {
168                DocumentRef ref = documentManager.getBaseVersion(currentDocument.getRef());
169                if (ref instanceof IdRef) {
170                    tagService.tag(documentManager, ref.toString(), tagLabel, null);
171                }
172            }
173            messageKey = "message.add.new.tagging";
174            // force invalidation
175            Contexts.getEventContext().remove("currentDocumentTags");
176        }
177        facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get(messageKey), tagLabel);
178        reset();
179        return null;
180    }
181
182    /**
183     * Removes a tagging from the current document.
184     */
185    public String removeTagging(String label) {
186        DocumentModel currentDocument = navigationContext.getCurrentDocument();
187        String docId = currentDocument.getId();
188
189        TagService tagService = getTagService();
190        tagService.untag(documentManager, docId, label, null);
191
192        if (currentDocument.isVersion()) {
193            DocumentModel liveDocument = documentManager.getSourceDocument(currentDocument.getRef());
194            if (!liveDocument.isCheckedOut()) {
195                tagService.untag(documentManager, liveDocument.getId(), label, null);
196            }
197        } else if (!currentDocument.isCheckedOut()) {
198            DocumentRef ref = documentManager.getBaseVersion(currentDocument.getRef());
199            if (ref instanceof IdRef) {
200                tagService.untag(documentManager, ref.toString(), label, null);
201            }
202        }
203
204        reset();
205        // force invalidation
206        Contexts.getEventContext().remove("currentDocumentTags");
207        facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get("message.remove.tagging"),
208                label);
209        return null;
210    }
211
212    /**
213     * Returns tag cloud info for the whole repository. For performance reasons, the security on underlying documents is
214     * not tested.
215     */
216    @Factory(value = "tagCloudOnAllDocuments", scope = EVENT)
217    public List<Tag> getPopularCloudOnAllDocuments() {
218        List<Tag> cloud = getTagService().getTagCloud(documentManager, null, null, Boolean.TRUE); // logarithmic 0-100
219                                                                                                  // normalization
220        // change weight to a font size
221        double min = 100;
222        double max = 200;
223        for (Tag tag : cloud) {
224            tag.setWeight((long) (min + tag.getWeight() * (max - min) / 100));
225        }
226        Collections.sort(cloud, Tag.LABEL_COMPARATOR);
227        // Collections.sort(cloud, Tag.WEIGHT_COMPARATOR);
228        return cloud;
229    }
230
231    public String listDocumentsForTag(String listLabel) {
232        this.listLabel = listLabel;
233        return TAG_SEARCH_RESULT_PAGE;
234    }
235
236    @Factory(value = "taggedDocuments", scope = EVENT)
237    public DocumentModelList getChildrenSelectModel() {
238        if (StringUtils.isBlank(listLabel)) {
239            return new DocumentModelListImpl(0);
240        } else {
241            List<String> ids = getTagService().getTagDocumentIds(documentManager, listLabel, null);
242            DocumentModelList docs = new DocumentModelListImpl(ids.size());
243            DocumentModel doc = null;
244            for (String id : ids) {
245                try {
246                    doc = documentManager.getDocument(new IdRef(id));
247                } catch (DocumentNotFoundException e) {
248                    log.error(e, e);
249                }
250                if (doc != null) {
251                    docs.add(doc);
252                    doc = null;
253                }
254            }
255            return docs;
256        }
257    }
258
259    public String getListLabel() {
260        return listLabel;
261    }
262
263    public void setListLabel(String listLabel) {
264        this.listLabel = listLabel;
265    }
266
267    /**
268     * Returns <b>true</b> if the current logged user has permission to modify a tag that is applied on the current
269     * document.
270     */
271    public boolean canModifyTag(Tag tag) {
272        return tag != null;
273    }
274
275    /**
276     * Resets the fields that are used for managing actions related to tagging.
277     */
278    public void reset() {
279        tagLabel = null;
280    }
281
282    /**
283     * Used to decide whether the tagging UI field is shown or not.
284     */
285    public void showAddTag(ActionEvent event) {
286        this.addTag = !this.addTag;
287    }
288
289    public String getTagLabel() {
290        return tagLabel;
291    }
292
293    /**
294     * @since 7.1
295     */
296    public void setTagLabel(final String tagLabel) {
297        this.tagLabel = tagLabel;
298    }
299
300    public boolean getAddTag() {
301        return addTag;
302    }
303
304    public void setAddTag(boolean addTag) {
305        this.addTag = addTag;
306    }
307
308    public List<Tag> getSuggestions(Object input) {
309        String label = (String) input;
310        List<Tag> tags = getTagService().getSuggestions(documentManager, label, null);
311        Collections.sort(tags, Tag.LABEL_COMPARATOR);
312        if (tags.size() > 10) {
313            tags = tags.subList(0, 10);
314        }
315
316        // add the typed tag as first suggestion if we can add new tag
317        label = cleanLabel(label);
318        if (Boolean.TRUE.equals(canSelectNewTag) && !tags.contains(new Tag(label, 0))) {
319            tags.add(0, new Tag(label, -1));
320        }
321
322        return tags;
323    }
324
325    protected static String cleanLabel(String label) {
326        label = label.toLowerCase(); // lowercase
327        label = label.replace(" ", ""); // no spaces
328        label = label.replace("\\", ""); // dubious char
329        label = label.replace("'", ""); // dubious char
330        label = label.replace("%", ""); // dubious char
331        return label;
332    }
333
334    @SuppressWarnings("unchecked")
335    @Observer({ SELECTION_EDITED, DOCUMENTS_IMPORTED })
336    public void addTagsOnEvent(List<DocumentModel> documents, DocumentModel docModel) {
337        List<String> tags = (List<String>) docModel.getContextData(ScopeType.REQUEST, "bulk_tags");
338        if (tags != null && !tags.isEmpty()) {
339            TagService tagService = Framework.getLocalService(TagService.class);
340            String username = documentManager.getPrincipal().getName();
341            for (DocumentModel doc : documents) {
342                for (String tag : tags) {
343                    tagService.tag(documentManager, doc.getId(), tag, username);
344                }
345            }
346        }
347    }
348
349    @Observer(value = { EventNames.DOCUMENT_SELECTION_CHANGED }, create = false)
350    @BypassInterceptors
351    public void documentChanged() {
352        addTag = false;
353    }
354
355}