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