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.ArrayList;
025import java.util.Calendar;
026import java.util.Collections;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031import java.util.stream.Collectors;
032
033import org.nuxeo.ecm.core.api.CoreSession;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.IdRef;
036import org.nuxeo.ecm.core.api.IterableQueryResult;
037import org.nuxeo.ecm.core.api.NuxeoException;
038import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
039import org.nuxeo.ecm.core.query.sql.NXQL;
040
041/**
042 * Implementation of tag service based on SQL relations
043 *
044 * @deprecated since 9.3, use {@link FacetedTagService} instead
045 */
046@Deprecated
047public class RelationTagService extends AbstractTagService {
048
049    @Override
050    public boolean hasFeature(Feature feature) {
051        switch (feature) {
052        case TAGS_BELONG_TO_DOCUMENT:
053            return false;
054        default:
055            throw new UnsupportedOperationException(feature.name());
056        }
057    }
058
059    @Override
060    public boolean supportsTag(CoreSession session, String docId) {
061        return true;
062    }
063
064    @Override
065    public void doTag(CoreSession session, String docId, String label, String username) {
066        if (session.getDocument(new IdRef(docId)).isProxy()) { // Tags are disabled on proxies
067            throw new NuxeoException("Adding tags is not allowed on proxies");
068        }
069        // Find tag
070        List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session, label);
071        String tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null;
072        Calendar date = Calendar.getInstance();
073        if (tagId == null) {
074            // no tag found, create it
075            DocumentModel tag = session.createDocumentModel(null, label, TagConstants.TAG_DOCUMENT_TYPE);
076            tag.setPropertyValue("dc:created", date);
077            tag.setPropertyValue(TagConstants.TAG_LABEL_FIELD, label);
078            tag = session.createDocument(tag);
079            tagId = tag.getId();
080        }
081        // Check if tagging already exists for user.
082        res = getItems(PAGE_PROVIDERS.GET_FIRST_TAGGING_FOR_DOC_AND_TAG.name(), session, docId, tagId);
083
084        if (res != null && !res.isEmpty()) {
085            // tagging already exists
086            return;
087        }
088        // Add tagging to the document.
089        DocumentModel tagging = session.createDocumentModel(null, label, TagConstants.TAGGING_DOCUMENT_TYPE);
090        tagging.setPropertyValue("dc:created", date);
091        tagging.setPropertyValue("dc:creator", username);
092
093        tagging.setPropertyValue(TagConstants.TAGGING_SOURCE_FIELD, docId);
094        tagging.setPropertyValue(TagConstants.TAGGING_TARGET_FIELD, tagId);
095        session.createDocument(tagging);
096        session.save();
097    }
098
099    @Override
100    public void doUntag(CoreSession session, String docId, String label) {
101        IdRef ref = new IdRef(docId);
102        if (session.exists(ref) && session.getDocument(ref).isProxy()) { // Tags are disabled on proxies
103            throw new NuxeoException("Removing tags is not allowed on proxies");
104        }
105        String tagId = null;
106        if (label != null) {
107            // Find tag
108            List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session,
109                    label);
110            tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null;
111            if (tagId == null) {
112                // tag not found
113                return;
114            }
115        }
116        // Find taggings for user.
117        Set<String> taggingIds = new HashSet<>();
118        String query = String.format("SELECT ecm:uuid FROM Tagging WHERE relation:source = '%s'", docId);
119        if (tagId != null) {
120            query += String.format(" AND relation:target = '%s'", tagId);
121        }
122        try (IterableQueryResult res = session.queryAndFetch(query, NXQL.NXQL)) {
123            for (Map<String, Serializable> map : res) {
124                taggingIds.add((String) map.get(NXQL.ECM_UUID));
125            }
126        }
127        // Remove taggings
128        for (String taggingId : taggingIds) {
129            session.removeDocument(new IdRef(taggingId));
130        }
131        if (!taggingIds.isEmpty()) {
132            session.save();
133        }
134    }
135
136    @Override
137    public Set<String> doGetTags(CoreSession session, String docId) {
138        List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT.name(), session, docId);
139        if (res == null) {
140            return Collections.emptySet();
141        }
142        return res.stream().map(m -> (String) m.get(TagConstants.TAG_LABEL_FIELD)).collect(Collectors.toSet());
143    }
144
145    @Override
146    public void doCopyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags) {
147        if (removeExistingTags) {
148            doUntag(session, dstDocId, null);
149        }
150        Set<String> existingTags = new HashSet<>();
151        List<Map<String, Serializable>> dstTagsRes = getItems(PAGE_PROVIDERS.GET_TAGS_TO_COPY_FOR_DOCUMENT.name(),
152                session, dstDocId);
153        if (dstTagsRes != null) {
154            for (Map<String, Serializable> map : dstTagsRes) {
155                existingTags.add(String.format("%s/%s", map.get("tag:label"), map.get("dc:creator")));
156            }
157        }
158
159        List<Map<String, Serializable>> srcTagsRes = getItems(PAGE_PROVIDERS.GET_TAGS_TO_COPY_FOR_DOCUMENT.name(),
160                session, srcDocId);
161        if (srcTagsRes != null) {
162            boolean docCreated = false;
163            for (Map<String, Serializable> map : srcTagsRes) {
164                String key = String.format("%s/%s", map.get("tag:label"), map.get("dc:creator"));
165                if (!existingTags.contains(key)) {
166                    DocumentModel tagging = session.createDocumentModel(null, (String) map.get("tag:label"),
167                            TagConstants.TAGGING_DOCUMENT_TYPE);
168                    tagging.setPropertyValue("dc:created", map.get("dc:created"));
169                    tagging.setPropertyValue("dc:creator", map.get("dc:creator"));
170                    tagging.setPropertyValue(TagConstants.TAGGING_SOURCE_FIELD, dstDocId);
171                    tagging.setPropertyValue(TagConstants.TAGGING_TARGET_FIELD, map.get("relation:target"));
172                    session.createDocument(tagging);
173                    docCreated = true;
174                }
175            }
176            if (docCreated) {
177                session.save();
178            }
179        }
180    }
181
182    @Override
183    public List<String> doGetTagDocumentIds(CoreSession session, String label) {
184        List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENTS_FOR_TAG.name(), session, label);
185        if (res == null) {
186            return Collections.emptyList();
187        }
188        return res.stream().map(m -> (String) m.get(TagConstants.TAGGING_SOURCE_FIELD)).collect(Collectors.toList());
189    }
190
191    @Override
192    public Set<String> doGetTagSuggestions(CoreSession session, String label) {
193        List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_TAG_SUGGESTIONS.name(), session, label);
194        if (res == null) {
195            return Collections.emptySet();
196        }
197        return res.stream().map(m -> (String) m.get(TagConstants.TAG_LABEL_FIELD)).collect(Collectors.toSet());
198    }
199
200    /**
201     * @since 8.4
202     */
203    @Override
204    public boolean canUntag(CoreSession session, String docId, String label) {
205        boolean canUntag = super.canUntag(session, docId, label);
206        if (!canUntag) {
207            // Else check if desired tag was created by current user
208            UnrestrictedCanRemoveTagging r = new UnrestrictedCanRemoveTagging(session, docId, label);
209            r.runUnrestricted();
210            canUntag = r.canUntag;
211        }
212        return canUntag;
213    }
214
215    protected class UnrestrictedCanRemoveTagging extends UnrestrictedSessionRunner {
216
217        private final String docId;
218
219        private final String label;
220
221        private boolean canUntag;
222
223        protected UnrestrictedCanRemoveTagging(CoreSession session, String docId, String label) {
224            super(session);
225            this.docId = docId;
226            this.label = cleanLabel(label, true, false);
227            this.canUntag = false;
228        }
229
230        @Override
231        public void run() {
232            String tagId = null;
233            if (label != null) {
234                // Find tag
235                List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session,
236                        label);
237                tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null;
238                if (tagId == null) {
239                    // tag not found - so user can untag
240                    canUntag = true;
241                    return;
242                }
243            }
244            // Find creators of tag(s).
245            Set<String> creators = new HashSet<>();
246            String query = String.format("SELECT DISTINCT dc:creator FROM Tagging WHERE relation:source = '%s'", docId);
247            if (tagId != null) {
248                query += String.format(" AND relation:target = '%s'", tagId);
249            }
250            try (IterableQueryResult res = session.queryAndFetch(query, NXQL.NXQL)) {
251                for (Map<String, Serializable> map : res) {
252                    creators.add((String) map.get("dc:creator"));
253                }
254            }
255            // Check if user can untag
256            // - in case of one tag, check if creators contains user
257            // - in case of all tags, check if user is the only creator
258            canUntag = creators.size() == 1 && creators.contains(originatingUsername);
259        }
260    }
261
262    @Override
263    public List<Tag> getTagCloud(CoreSession session, String docId, String username, Boolean normalize) {
264        UnrestrictedGetDocumentCloud r = new UnrestrictedGetDocumentCloud(session, docId, username, normalize);
265        r.runUnrestricted();
266        return r.cloud;
267    }
268
269    protected static class UnrestrictedGetDocumentCloud extends UnrestrictedSessionRunner {
270
271        protected final String docId;
272
273        protected final String username;
274
275        protected final List<Tag> cloud;
276
277        protected final Boolean normalize;
278
279        protected UnrestrictedGetDocumentCloud(CoreSession session, String docId, String username, Boolean normalize) {
280            super(session);
281            this.docId = docId;
282            this.username = cleanUsername(username);
283            this.normalize = normalize;
284            this.cloud = new ArrayList<>();
285        }
286
287        @Override
288        public void run() {
289            List<Map<String, Serializable>> res;
290            if (docId == null) {
291                if (username == null) {
292                    res = getItems(PAGE_PROVIDERS.GET_ALL_TAGS.name(), session);
293                } else {
294                    res = getItems(PAGE_PROVIDERS.GET_ALL_TAGS_FOR_USER.name(), session, username);
295                }
296            } else {
297                // find all docs under docid
298                String path = session.getDocument(new IdRef(docId)).getPathAsString();
299                path = path.replace("'", "");
300                List<String> docIds = new ArrayList<>();
301                docIds.add(docId);
302                List<Map<String, Serializable>> docRes = getItems(PAGE_PROVIDERS.GET_TAGGED_DOCUMENTS_UNDER.name(),
303                        session, path);
304                if (docRes != null) {
305                    for (Map<String, Serializable> map : docRes) {
306                        docIds.add((String) map.get(NXQL.ECM_UUID));
307                    }
308                }
309
310                if (username == null) {
311                    res = getItems(PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENTS.name(), session, docIds);
312                } else {
313                    res = getItems(PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENTS_AND_USER.name(), session, docIds, username);
314                }
315            }
316
317            int min = 999999, max = 0;
318            if (res != null) {
319                for (Map<String, Serializable> map : res) {
320                    String label = (String) map.get(TagConstants.TAG_LABEL_FIELD);
321                    int weight = ((Long) map.get(TagConstants.TAGGING_SOURCE_FIELD)).intValue();
322                    if (weight == 0) {
323                        // shouldn't happen
324                        continue;
325                    }
326                    if (weight > max) {
327                        max = weight;
328                    }
329                    if (weight < min) {
330                        min = weight;
331                    }
332                    Tag weightedTag = new Tag(label, weight);
333                    cloud.add(weightedTag);
334                }
335            }
336            if (normalize != null) {
337                normalizeCloud(cloud, min, max, !normalize.booleanValue());
338            }
339        }
340
341    }
342
343    public static void normalizeCloud(List<Tag> cloud, int min, int max, boolean linear) {
344        if (min == max) {
345            for (Tag tag : cloud) {
346                tag.setWeight(100);
347            }
348            return;
349        }
350        double nmin;
351        double diff;
352        if (linear) {
353            nmin = min;
354            diff = max - min;
355        } else {
356            nmin = Math.log(min);
357            diff = Math.log(max) - nmin;
358        }
359        for (Tag tag : cloud) {
360            long weight = tag.getWeight();
361            double norm;
362            if (linear) {
363                norm = (weight - nmin) / diff;
364            } else {
365                norm = (Math.log(weight) - nmin) / diff;
366            }
367            tag.setWeight(Math.round(100 * norm));
368        }
369    }
370
371}