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}