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