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}