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}