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}