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