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 String cleanLabel = cleanLabel(label, true, false); 184 return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTagDocumentIds(s, cleanLabel)); 185 } 186 187 @Override 188 public List<String> getTagDocumentIds(CoreSession session, String label, String username) { 189 return getTagDocumentIds(session, label); 190 } 191 192 @Override 193 public Set<String> getSuggestions(CoreSession session, String label) { 194 label = cleanLabel(label, true, true); 195 if (!label.contains("%")) { 196 label += "%"; 197 } 198 // effectively final for lambda 199 String l = label; 200 return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTagSuggestions(s, l)); 201 } 202 203 @Override 204 public List<Tag> getSuggestions(CoreSession session, String label, String username) { 205 return getSuggestions(session, label).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList()); 206 } 207 208 public abstract void doTag(CoreSession session, String docId, String label, String username); 209 210 public abstract void doUntag(CoreSession session, String docId, String label); 211 212 public abstract Set<String> doGetTags(CoreSession session, String docId); 213 214 public abstract void doCopyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags); 215 216 public abstract List<String> doGetTagDocumentIds(CoreSession session, String label); 217 218 public abstract Set<String> doGetTagSuggestions(CoreSession session, String label); 219 220 protected static String cleanLabel(String label, boolean allowEmpty, boolean allowPercent) { 221 if (label == null) { 222 if (allowEmpty) { 223 return null; 224 } 225 throw new NuxeoException("Invalid empty tag"); 226 } 227 label = label.toLowerCase(); // lowercase 228 label = label.replace(" ", ""); // no spaces 229 label = label.replace("/", ""); // no slash 230 label = label.replace("\\", ""); // dubious char 231 label = label.replace("'", ""); // dubious char 232 if (!allowPercent) { 233 label = label.replace("%", ""); // dubious char 234 } 235 if (label.length() == 0) { 236 throw new NuxeoException("Invalid empty tag"); 237 } 238 return label; 239 } 240 241 protected static String cleanUsername(String username) { 242 return username == null ? null : username.replace("'", ""); 243 } 244 245 /** 246 * Returns results from calls to {@link CoreSession#queryAndFetch(String, String, Object...)} using page providers. 247 * 248 * @since 6.0 249 */ 250 @SuppressWarnings("unchecked") 251 protected static List<Map<String, Serializable>> getItems(String pageProviderName, CoreSession session, 252 Object... params) { 253 PageProviderService ppService = Framework.getService(PageProviderService.class); 254 if (ppService == null) { 255 throw new RuntimeException("Missing PageProvider service"); 256 } 257 Map<String, Serializable> props = new HashMap<>(); 258 // first retrieve potential props from definition 259 PageProviderDefinition def = ppService.getPageProviderDefinition(pageProviderName); 260 if (def != null) { 261 Map<String, String> defProps = def.getProperties(); 262 if (defProps != null) { 263 props.putAll(defProps); 264 } 265 } 266 props.put(CoreQueryAndFetchPageProvider.CORE_SESSION_PROPERTY, (Serializable) session); 267 PageProvider<Map<String, Serializable>> pp = (PageProvider<Map<String, Serializable>>) ppService.getPageProvider( 268 pageProviderName, null, null, null, props, params); 269 if (pp == null) { 270 throw new NuxeoException("Page provider not found: " + pageProviderName); 271 } 272 return pp.getCurrentPage(); 273 } 274 275 protected void fireUpdateEvent(CoreSession session, String docId) { 276 DocumentRef documentRef = new IdRef(docId); 277 if (session.exists(documentRef)) { 278 DocumentModel documentModel = session.getDocument(documentRef); 279 DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), documentModel); 280 Event event = ctx.newEvent(DocumentEventTypes.DOCUMENT_TAG_UPDATED); 281 Framework.getService(EventService.class).fireEvent(event); 282 } 283 } 284 285}