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}