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