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 */ 020 021package org.nuxeo.ecm.platform.tag; 022 023import java.io.Serializable; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import org.nuxeo.ecm.core.api.CoreInstance; 031import org.nuxeo.ecm.core.api.CoreSession; 032import org.nuxeo.ecm.core.api.DocumentModel; 033import org.nuxeo.ecm.core.api.DocumentRef; 034import org.nuxeo.ecm.core.api.DocumentSecurityException; 035import org.nuxeo.ecm.core.api.IdRef; 036import org.nuxeo.ecm.core.api.NuxeoException; 037import org.nuxeo.ecm.core.api.event.DocumentEventTypes; 038import org.nuxeo.ecm.core.api.security.SecurityConstants; 039import org.nuxeo.ecm.core.event.Event; 040import org.nuxeo.ecm.core.event.EventService; 041import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 042import org.nuxeo.ecm.platform.query.api.PageProvider; 043import org.nuxeo.ecm.platform.query.api.PageProviderDefinition; 044import org.nuxeo.ecm.platform.query.api.PageProviderService; 045import org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider; 046import org.nuxeo.runtime.api.Framework; 047 048/** 049 * @since 9.3 050 */ 051public abstract class AbstractTagService implements TagService { 052 053 protected enum PAGE_PROVIDERS { 054 GET_DOCUMENT_IDS_FOR_FACETED_TAG, 055 // 056 GET_DOCUMENT_IDS_FOR_TAG, 057 // 058 GET_FIRST_TAGGING_FOR_DOC_AND_TAG_AND_USER, 059 // 060 GET_FIRST_TAGGING_FOR_DOC_AND_TAG, 061 // 062 GET_TAGS_FOR_DOCUMENT, 063 // core version: should keep on querying VCS 064 GET_TAGS_FOR_DOCUMENT_CORE, 065 // 066 GET_DOCUMENTS_FOR_TAG, 067 // 068 GET_TAGS_FOR_DOCUMENT_AND_USER, 069 // core version: should keep on querying VCS 070 GET_TAGS_FOR_DOCUMENT_AND_USER_CORE, 071 // 072 GET_DOCUMENTS_FOR_TAG_AND_USER, 073 // 074 GET_TAGS_TO_COPY_FOR_DOCUMENT, 075 // 076 GET_FACETED_TAG_SUGGESTIONS, 077 // 078 GET_TAG_SUGGESTIONS, 079 // 080 GET_TAG_SUGGESTIONS_FOR_USER, 081 // 082 GET_TAGGED_DOCUMENTS_UNDER, 083 // 084 GET_ALL_TAGS, 085 // 086 GET_ALL_TAGS_FOR_USER, 087 // 088 GET_TAGS_FOR_DOCUMENTS, 089 // 090 GET_TAGS_FOR_DOCUMENTS_AND_USER, 091 } 092 093 @Override 094 public boolean isEnabled() { 095 return true; 096 } 097 098 @Override 099 public void tag(CoreSession session, String docId, String label) throws DocumentSecurityException { 100 String cleanLabel = cleanLabel(label, true, false); 101 String username = cleanUsername(session.getPrincipal().getName()); 102 CoreInstance.doPrivileged(session, s -> { 103 doTag(s, docId, cleanLabel, username); 104 }); 105 fireUpdateEvent(session, docId); 106 } 107 108 @Override 109 public void tag(CoreSession session, String docId, String label, String username) { 110 tag(session, docId, label); 111 } 112 113 @Override 114 public void untag(CoreSession session, String docId, String label) 115 throws DocumentSecurityException { 116 // There's two allowed cases here: 117 // - document doesn't exist, we're here after documentRemoved event 118 // - regular case: check if user can remove this tag on document 119 if (!session.exists(new IdRef(docId)) || canUntag(session, docId, label)) { 120 String cleanLabel = cleanLabel(label, true, false); 121 CoreInstance.doPrivileged(session, s -> { 122 doUntag(s, docId, cleanLabel); 123 }); 124 if (label != null) { 125 fireUpdateEvent(session, docId); 126 } 127 } else { 128 String principalName = session.getPrincipal().getName(); 129 throw new DocumentSecurityException("User '" + principalName + "' is not allowed to remove tag '" + label 130 + "' on document '" + docId + "'"); 131 } 132 } 133 134 @Override 135 public void untag(CoreSession session, String docId, String label, String username) 136 throws DocumentSecurityException { 137 untag(session, docId, label); 138 } 139 140 @Override 141 public boolean canUntag(CoreSession session, String docId, String label) { 142 return session.hasPermission(new IdRef(docId), SecurityConstants.WRITE); 143 } 144 145 @Override 146 public Set<String> getTags(CoreSession session, String docId) { 147 return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTags(s, docId)); 148 } 149 150 @Override 151 public List<Tag> getDocumentTags(CoreSession session, String docId, String username) { 152 return getTags(session, docId).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList()); 153 } 154 155 @Override 156 public List<Tag> getDocumentTags(CoreSession session, String docId, String username, boolean useCore) { 157 return getTags(session, docId).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList()); 158 } 159 160 @Override 161 public void removeTags(CoreSession session, String docId) { 162 untag(session, docId, null); 163 } 164 165 @Override 166 public void copyTags(CoreSession session, String srcDocId, String dstDocId) { 167 copyTags(session, srcDocId, dstDocId, false); 168 } 169 170 protected void copyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags) { 171 CoreInstance.doPrivileged(session, s -> { 172 doCopyTags(s, srcDocId, dstDocId, removeExistingTags); 173 }); 174 } 175 176 @Override 177 public void replaceTags(CoreSession session, String srcDocId, String dstDocId) { 178 copyTags(session, srcDocId, dstDocId, true); 179 } 180 181 @Override 182 public List<String> getTagDocumentIds(CoreSession session, String label) { 183 return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTagDocumentIds(s, label)); 184 } 185 186 @Override 187 public List<String> getTagDocumentIds(CoreSession session, String label, String username) { 188 return getTagDocumentIds(session, label); 189 } 190 191 @Override 192 public Set<String> getSuggestions(CoreSession session, String label) { 193 label = cleanLabel(label, true, true); 194 if (!label.contains("%")) { 195 label += "%"; 196 } 197 // effectively final for lambda 198 String l = label; 199 return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTagSuggestions(s, l)); 200 } 201 202 @Override 203 public List<Tag> getSuggestions(CoreSession session, String label, String username) { 204 return getSuggestions(session, label).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList()); 205 } 206 207 public abstract void doTag(CoreSession session, String docId, String label, String username); 208 209 public abstract void doUntag(CoreSession session, String docId, String label); 210 211 public abstract Set<String> doGetTags(CoreSession session, String docId); 212 213 public abstract void doCopyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags); 214 215 public abstract List<String> doGetTagDocumentIds(CoreSession session, String label); 216 217 public abstract Set<String> doGetTagSuggestions(CoreSession session, String label); 218 219 protected static String cleanLabel(String label, boolean allowEmpty, boolean allowPercent) { 220 if (label == null) { 221 if (allowEmpty) { 222 return null; 223 } 224 throw new NuxeoException("Invalid empty tag"); 225 } 226 label = label.toLowerCase(); // lowercase 227 label = label.replace(" ", ""); // no spaces 228 label = label.replace("\\", ""); // dubious char 229 label = label.replace("'", ""); // dubious char 230 if (!allowPercent) { 231 label = label.replace("%", ""); // dubious char 232 } 233 if (label.length() == 0) { 234 throw new NuxeoException("Invalid empty tag"); 235 } 236 return label; 237 } 238 239 protected static String cleanUsername(String username) { 240 return username == null ? null : username.replace("'", ""); 241 } 242 243 /** 244 * Returns results from calls to {@link CoreSession#queryAndFetch(String, String, Object...)} using page providers. 245 * 246 * @since 6.0 247 */ 248 @SuppressWarnings("unchecked") 249 protected static List<Map<String, Serializable>> getItems(String pageProviderName, CoreSession session, 250 Object... params) { 251 PageProviderService ppService = Framework.getService(PageProviderService.class); 252 if (ppService == null) { 253 throw new RuntimeException("Missing PageProvider service"); 254 } 255 Map<String, Serializable> props = new HashMap<>(); 256 // first retrieve potential props from definition 257 PageProviderDefinition def = ppService.getPageProviderDefinition(pageProviderName); 258 if (def != null) { 259 Map<String, String> defProps = def.getProperties(); 260 if (defProps != null) { 261 props.putAll(defProps); 262 } 263 } 264 props.put(CoreQueryAndFetchPageProvider.CORE_SESSION_PROPERTY, (Serializable) session); 265 PageProvider<Map<String, Serializable>> pp = (PageProvider<Map<String, Serializable>>) ppService.getPageProvider( 266 pageProviderName, null, null, null, props, params); 267 if (pp == null) { 268 throw new NuxeoException("Page provider not found: " + pageProviderName); 269 } 270 return pp.getCurrentPage(); 271 } 272 273 protected void fireUpdateEvent(CoreSession session, String docId) { 274 DocumentRef documentRef = new IdRef(docId); 275 if (session.exists(documentRef)) { 276 DocumentModel documentModel = session.getDocument(documentRef); 277 DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), documentModel); 278 Event event = ctx.newEvent(DocumentEventTypes.DOCUMENT_TAG_UPDATED); 279 Framework.getService(EventService.class).fireEvent(event); 280 } 281 } 282 283}