001/* 002 * (C) Copyright 2017 Nuxeo (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 * Funsho David 018 */ 019 020package org.nuxeo.ecm.platform.tag; 021 022import static org.nuxeo.ecm.core.api.CoreSession.ALLOW_VERSION_WRITE; 023import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_UUID; 024import static org.nuxeo.ecm.platform.tag.TagConstants.TAG_FACET; 025import static org.nuxeo.ecm.platform.tag.TagConstants.TAG_LIST; 026 027import java.io.Serializable; 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.Set; 034import java.util.stream.Collectors; 035 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.nuxeo.ecm.core.api.CoreSession; 039import org.nuxeo.ecm.core.api.DocumentModel; 040import org.nuxeo.ecm.core.api.IdRef; 041import org.nuxeo.ecm.core.api.NuxeoException; 042import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 043import org.nuxeo.ecm.core.versioning.VersioningService; 044 045/** 046 * Implementation of the tag service based on facet 047 * 048 * @since 9.3 049 */ 050public class FacetedTagService extends AbstractTagService { 051 052 private static final Log log = LogFactory.getLog(FacetedTagService.class); 053 054 public static final String LABEL_PROPERTY = "label"; 055 056 public static final String USERNAME_PROPERTY = "username"; 057 058 /** 059 * Context data to disable versioning, used by NoVersioningFacetedTagFilter. 060 * @since 9.10 061 */ 062 public static final String DISABLE_VERSIONING = "tag.facet.disable.versioning"; 063 064 @Override 065 public boolean hasFeature(Feature feature) { 066 switch (feature) { 067 case TAGS_BELONG_TO_DOCUMENT: 068 return true; 069 default: 070 throw new UnsupportedOperationException(feature.name()); 071 } 072 } 073 074 @Override 075 public boolean supportsTag(CoreSession session, String docId) { 076 return session.getDocument(new IdRef(docId)).hasFacet(TAG_FACET); 077 } 078 079 protected void saveDocument(CoreSession session, DocumentModel doc) { 080 doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, Boolean.TRUE); 081 doc.putContextData(DISABLE_VERSIONING, Boolean.TRUE); 082 session.saveDocument(doc); 083 } 084 085 @Override 086 public void doTag(CoreSession session, String docId, String label, String username) { 087 DocumentModel docModel = session.getDocument(new IdRef(docId)); 088 if (docModel.isProxy()) { 089 throw new NuxeoException("Adding tags is not allowed on proxies"); 090 } 091 List<Map<String, Serializable>> tags = getTags(docModel); 092 if (tags.stream().noneMatch(t -> label.equals(t.get(LABEL_PROPERTY)))) { 093 Map<String, Serializable> tag = new HashMap<>(); 094 tag.put(LABEL_PROPERTY, label); 095 tag.put(USERNAME_PROPERTY, username); 096 tags.add(tag); 097 setTags(docModel, tags); 098 saveDocument(session, docModel); 099 } 100 } 101 102 @Override 103 public void doUntag(CoreSession session, String docId, String label) { 104 DocumentModel docModel = session.getDocument(new IdRef(docId)); 105 if (docModel.isProxy()) { 106 throw new NuxeoException("Removing tags is not allowed on proxies"); 107 } 108 if (docModel.hasFacet(TAG_FACET)) { 109 // If label is null, all the tags are removed 110 if (label == null) { 111 if (!getTags(docModel).isEmpty()) { 112 setTags(docModel, new ArrayList<>()); 113 saveDocument(session, docModel); 114 } 115 } else { 116 List<Map<String, Serializable>> tags = getTags(docModel); 117 Map<String, Serializable> tag = tags.stream() 118 .filter(t -> label.equals(t.get(LABEL_PROPERTY))) 119 .findFirst() 120 .orElse(null); 121 if (tag != null) { 122 tags.remove(tag); 123 setTags(docModel, tags); 124 saveDocument(session, docModel); 125 } 126 } 127 } 128 } 129 130 @Override 131 public boolean canUntag(CoreSession session, String docId, String label) { 132 boolean canUntag = super.canUntag(session, docId, label); 133 if (!canUntag) { 134 // Check also if the current user is the one who applied the tag 135 DocumentModel docModel = session.getDocument(new IdRef(docId)); 136 Map<String, Serializable> tag = getTags(docModel).stream() 137 .filter(t -> label.equals(t.get(LABEL_PROPERTY))) 138 .findFirst() 139 .orElse(null); 140 if (tag != null) { 141 String username = session.getPrincipal().getName(); 142 canUntag = username.equals(tag.get(USERNAME_PROPERTY)); 143 } 144 } 145 return canUntag; 146 } 147 148 @Override 149 public Set<String> doGetTags(CoreSession session, String docId) { 150 DocumentModel docModel = session.getDocument(new IdRef(docId)); 151 List<Map<String, Serializable>> tags = getTags(docModel); 152 return tags.stream().map(t -> (String) t.get(LABEL_PROPERTY)).collect(Collectors.toSet()); 153 } 154 155 @Override 156 public void doCopyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags) { 157 DocumentModel srcDocModel = session.getDocument(new IdRef(srcDocId)); 158 DocumentModel dstDocModel = session.getDocument(new IdRef(dstDocId)); 159 160 if (!dstDocModel.isProxy()) { 161 List<Map<String, Serializable>> srcTags = getTags(srcDocModel); 162 List<Map<String, Serializable>> dstTags; 163 if (removeExistingTags) { 164 dstTags = srcTags; 165 } else { 166 dstTags = getTags(dstDocModel); 167 for (Map<String, Serializable> tag : srcTags) { 168 if (dstTags.stream().noneMatch(t -> tag.get(LABEL_PROPERTY).equals(t.get(LABEL_PROPERTY)))) { 169 dstTags.add(tag); 170 } 171 } 172 } 173 setTags(dstDocModel, dstTags); 174 saveDocument(session, dstDocModel); 175 } 176 } 177 178 @Override 179 public List<String> doGetTagDocumentIds(CoreSession session, String label) { 180 List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_FACETED_TAG.name(), session, 181 label); 182 if (res == null) { 183 return Collections.emptyList(); 184 } 185 return res.stream().map(m -> (String) m.get(ECM_UUID)).collect(Collectors.toList()); 186 } 187 188 @Override 189 public Set<String> doGetTagSuggestions(CoreSession session, String label) { 190 List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_FACETED_TAG_SUGGESTIONS.name(), session, 191 label); 192 if (res == null) { 193 return Collections.emptySet(); 194 } 195 return res.stream().map(m -> (String) m.get(TagConstants.TAG_LIST + "/*1/label")).collect(Collectors.toSet()); 196 } 197 198 @Override 199 public List<Tag> getTagCloud(CoreSession session, String docId, String username, Boolean normalize) { 200 return Collections.emptyList(); 201 } 202 203 @SuppressWarnings("unchecked") 204 protected List<Map<String, Serializable>> getTags(DocumentModel docModel) { 205 try { 206 return (List<Map<String, Serializable>>) docModel.getPropertyValue(TAG_LIST); 207 } catch (PropertyNotFoundException e) { 208 if (log.isWarnEnabled()) { 209 log.warn(String.format( 210 "Getting tags on %s failed since %s is missing on %s document type. This operation will be ignored.", 211 docModel.getPathAsString(), TAG_FACET, docModel.getType())); 212 } 213 return new ArrayList<>(); 214 } 215 } 216 217 protected void setTags(DocumentModel docModel, List<Map<String, Serializable>> tags) { 218 try { 219 if (docModel.isVersion()) { 220 docModel.putContextData(ALLOW_VERSION_WRITE, Boolean.TRUE); 221 } 222 docModel.setPropertyValue(TAG_LIST, (Serializable) tags); 223 } catch (PropertyNotFoundException e) { 224 if (log.isWarnEnabled()) { 225 log.warn(String.format( 226 "Setting tags on %s failed since %s is missing on %s document type. This operation will be ignored.", 227 docModel.getPathAsString(), TAG_FACET, docModel.getType())); 228 } 229 } 230 } 231}