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}