001/*
002 * (C) Copyright 2009-2010 Nuxeo SA (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 *     Radu Darlea
018 *     Catalin Baican
019 *     Florent Guillaume
020 */
021
022package org.nuxeo.ecm.platform.tag;
023
024import java.io.Serializable;
025import java.util.ArrayList;
026import java.util.Calendar;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033
034import org.nuxeo.ecm.core.api.CoreSession;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.DocumentRef;
037import org.nuxeo.ecm.core.api.IdRef;
038import org.nuxeo.ecm.core.api.IterableQueryResult;
039import org.nuxeo.ecm.core.api.NuxeoException;
040import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
041import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
042import org.nuxeo.ecm.core.event.Event;
043import org.nuxeo.ecm.core.event.EventService;
044import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
045import org.nuxeo.ecm.core.query.sql.NXQL;
046import org.nuxeo.ecm.platform.query.api.PageProvider;
047import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
048import org.nuxeo.ecm.platform.query.api.PageProviderService;
049import org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider;
050import org.nuxeo.runtime.api.Framework;
051import org.nuxeo.runtime.model.DefaultComponent;
052
053/**
054 * The implementation of the tag service.
055 */
056public class TagServiceImpl extends DefaultComponent implements TagService {
057
058    public static final String NXTAG = TagQueryMaker.NXTAG;
059
060    protected enum PAGE_PROVIDERS {
061        //
062        GET_DOCUMENT_IDS_FOR_TAG,
063        //
064        GET_FIRST_TAGGING_FOR_DOC_AND_TAG_AND_USER,
065        //
066        GET_FIRST_TAGGING_FOR_DOC_AND_TAG,
067        //
068        GET_TAGS_FOR_DOCUMENT,
069        // core version: should keep on querying VCS
070        GET_TAGS_FOR_DOCUMENT_CORE,
071        //
072        GET_DOCUMENTS_FOR_TAG,
073        //
074        GET_TAGS_FOR_DOCUMENT_AND_USER,
075        // core version: should keep on querying VCS
076        GET_TAGS_FOR_DOCUMENT_AND_USER_CORE,
077        //
078        GET_DOCUMENTS_FOR_TAG_AND_USER,
079        //
080        GET_TAGS_TO_COPY_FOR_DOCUMENT,
081        //
082        GET_TAG_SUGGESTIONS,
083        //
084        GET_TAG_SUGGESTIONS_FOR_USER,
085        //
086        GET_TAGGED_DOCUMENTS_UNDER,
087        //
088        GET_ALL_TAGS,
089        //
090        GET_ALL_TAGS_FOR_USER,
091        //
092        GET_TAGS_FOR_DOCUMENTS,
093        //
094        GET_TAGS_FOR_DOCUMENTS_AND_USER,
095    }
096
097    @Override
098    public boolean isEnabled() {
099        return true;
100    }
101
102    protected static String cleanLabel(String label, boolean allowEmpty, boolean allowPercent) {
103        if (label == null) {
104            if (allowEmpty) {
105                return null;
106            }
107            throw new NuxeoException("Invalid empty tag");
108        }
109        label = label.toLowerCase(); // lowercase
110        label = label.replace(" ", ""); // no spaces
111        label = label.replace("\\", ""); // dubious char
112        label = label.replace("'", ""); // dubious char
113        if (!allowPercent) {
114            label = label.replace("%", ""); // dubious char
115        }
116        if (label.length() == 0) {
117            throw new NuxeoException("Invalid empty tag");
118        }
119        return label;
120    }
121
122    protected static String cleanUsername(String username) {
123        return username == null ? null : username.replace("'", "");
124    }
125
126    public void tag(CoreSession session, String docId, String label, String username) {
127        UnrestrictedAddTagging r = new UnrestrictedAddTagging(session, docId, label, username);
128        r.runUnrestricted();
129        fireUpdateEvent(session, docId);
130    }
131
132    protected void fireUpdateEvent(CoreSession session, String docId) {
133        DocumentRef documentRef = new IdRef(docId);
134        if (session.exists(documentRef)) {
135            DocumentModel documentModel = session.getDocument(documentRef);
136            DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), documentModel);
137            Event event = ctx.newEvent(DocumentEventTypes.DOCUMENT_TAG_UPDATED);
138            Framework.getLocalService(EventService.class).fireEvent(event);
139        }
140    }
141
142    protected static class UnrestrictedAddTagging extends UnrestrictedSessionRunner {
143        private final String docId;
144
145        private final String label;
146
147        private final String username;
148
149        protected UnrestrictedAddTagging(CoreSession session, String docId, String label, String username)
150                {
151            super(session);
152            this.docId = docId;
153            this.label = cleanLabel(label, false, false);
154            this.username = cleanUsername(username);
155        }
156
157        @Override
158        public void run() {
159            // Find tag
160            List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session,
161                    label);
162            String tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null;
163            Calendar date = Calendar.getInstance();
164            if (tagId == null) {
165                // no tag found, create it
166                DocumentModel tag = session.createDocumentModel(null, label, TagConstants.TAG_DOCUMENT_TYPE);
167                tag.setPropertyValue("dc:created", date);
168                tag.setPropertyValue(TagConstants.TAG_LABEL_FIELD, label);
169                tag = session.createDocument(tag);
170                tagId = tag.getId();
171            }
172            // Check if tagging already exists for user.
173            if (username != null) {
174                res = getItems(PAGE_PROVIDERS.GET_FIRST_TAGGING_FOR_DOC_AND_TAG_AND_USER.name(), session, docId, tagId,
175                        username);
176            } else {
177                res = getItems(PAGE_PROVIDERS.GET_FIRST_TAGGING_FOR_DOC_AND_TAG.name(), session, docId, tagId);
178            }
179            if (res != null && !res.isEmpty()) {
180                // tagging already exists
181                return;
182            }
183            // Add tagging to the document.
184            DocumentModel tagging = session.createDocumentModel(null, label, TagConstants.TAGGING_DOCUMENT_TYPE);
185            tagging.setPropertyValue("dc:created", date);
186            if (username != null) {
187                tagging.setPropertyValue("dc:creator", username);
188            }
189            tagging.setPropertyValue(TagConstants.TAGGING_SOURCE_FIELD, docId);
190            tagging.setPropertyValue(TagConstants.TAGGING_TARGET_FIELD, tagId);
191            session.createDocument(tagging);
192            session.save();
193        }
194    }
195
196    public void untag(CoreSession session, String docId, String label, String username) {
197        UnrestrictedRemoveTagging r = new UnrestrictedRemoveTagging(session, docId, label, username);
198        r.runUnrestricted();
199        if (label != null) {
200            fireUpdateEvent(session, docId);
201        }
202    }
203
204    protected static class UnrestrictedRemoveTagging extends UnrestrictedSessionRunner {
205
206        private final String docId;
207
208        private final String label;
209
210        private final String username;
211
212        protected UnrestrictedRemoveTagging(CoreSession session, String docId, String label, String username)
213                {
214            super(session);
215            this.docId = docId;
216            this.label = cleanLabel(label, true, false);
217            this.username = cleanUsername(username);
218        }
219
220        @Override
221        public void run() {
222            String tagId = null;
223            if (label != null) {
224                // Find tag
225                List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session,
226                        label);
227                tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null;
228                if (tagId == null) {
229                    // tag not found
230                    return;
231                }
232            }
233            // Find taggings for user.
234            Set<String> taggingIds = new HashSet<String>();
235            String query = String.format("SELECT ecm:uuid FROM Tagging " + "WHERE relation:source = '%s'", docId);
236            if (tagId != null) {
237                query += String.format(" AND relation:target = '%s'", tagId);
238            }
239            if (username != null) {
240                query += String.format(" AND dc:creator = '%s'", username);
241            }
242            IterableQueryResult res = session.queryAndFetch(query, NXQL.NXQL);
243            try {
244                for (Map<String, Serializable> map : res) {
245                    taggingIds.add((String) map.get(NXQL.ECM_UUID));
246                }
247            } finally {
248                res.close();
249            }
250            // Remove taggings
251            for (String taggingId : taggingIds) {
252                session.removeDocument(new IdRef(taggingId));
253            }
254            if (!taggingIds.isEmpty()) {
255                session.save();
256            }
257        }
258    }
259
260    public List<Tag> getDocumentTags(CoreSession session, String docId, String username) {
261        return getDocumentTags(session, docId, username, true);
262    }
263
264    public List<Tag> getDocumentTags(CoreSession session, String docId, String username, boolean useCore)
265            {
266        UnrestrictedGetDocumentTags r = new UnrestrictedGetDocumentTags(session, docId, username, useCore);
267        r.runUnrestricted();
268        return r.tags;
269    }
270
271    protected static class UnrestrictedGetDocumentTags extends UnrestrictedSessionRunner {
272
273        protected final String docId;
274
275        protected final String username;
276
277        protected final List<Tag> tags;
278
279        protected final boolean useCore;
280
281        protected UnrestrictedGetDocumentTags(CoreSession session, String docId, String username, boolean useCore)
282                {
283            super(session);
284            this.docId = docId;
285            this.username = cleanUsername(username);
286            this.useCore = useCore;
287            tags = new ArrayList<Tag>();
288        }
289
290        @Override
291        public void run() {
292            List<Map<String, Serializable>> res;
293            if (username == null) {
294                String ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT.name();
295                if (useCore) {
296                    ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT_CORE.name();
297                }
298                res = getItems(ppName, session, docId);
299            } else {
300                String ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT_AND_USER.name();
301                if (useCore) {
302                    ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT_AND_USER_CORE.name();
303                }
304                res = getItems(ppName, session, docId, username);
305            }
306            if (res != null) {
307                for (Map<String, Serializable> map : res) {
308                    String label = (String) map.get(TagConstants.TAG_LABEL_FIELD);
309                    tags.add(new Tag(label, 0));
310                }
311            }
312        }
313    }
314
315    @Override
316    public void removeTags(CoreSession session, String docId) {
317        untag(session, docId, null, null);
318    }
319
320    @Override
321    public void copyTags(CoreSession session, String srcDocId, String dstDocId) {
322        copyTags(session, srcDocId, dstDocId, false);
323    }
324
325    protected void copyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags)
326            {
327        if (removeExistingTags) {
328            removeTags(session, dstDocId);
329        }
330
331        UnrestrictedCopyTags r = new UnrestrictedCopyTags(session, srcDocId, dstDocId);
332        r.runUnrestricted();
333    }
334
335    protected static class UnrestrictedCopyTags extends UnrestrictedSessionRunner {
336
337        protected final String srcDocId;
338
339        protected final String dstDocId;
340
341        protected UnrestrictedCopyTags(CoreSession session, String srcDocId, String dstDocId) {
342            super(session);
343            this.srcDocId = srcDocId;
344            this.dstDocId = dstDocId;
345        }
346
347        @Override
348        public void run() {
349            Set<String> existingTags = new HashSet<>();
350            List<Map<String, Serializable>> dstTagsRes = getItems(PAGE_PROVIDERS.GET_TAGS_TO_COPY_FOR_DOCUMENT.name(),
351                    session, dstDocId);
352            if (dstTagsRes != null) {
353                for (Map<String, Serializable> map : dstTagsRes) {
354                    existingTags.add(String.format("%s/%s", map.get("tag:label"), map.get("dc:creator")));
355                }
356            }
357
358            List<Map<String, Serializable>> srcTagsRes = getItems(PAGE_PROVIDERS.GET_TAGS_TO_COPY_FOR_DOCUMENT.name(),
359                    session, srcDocId);
360            if (srcTagsRes != null) {
361                boolean docCreated = false;
362                for (Map<String, Serializable> map : srcTagsRes) {
363                    String key = String.format("%s/%s", map.get("tag:label"), map.get("dc:creator"));
364                    if (!existingTags.contains(key)) {
365                        DocumentModel tagging = session.createDocumentModel(null, (String) map.get("tag:label"),
366                                TagConstants.TAGGING_DOCUMENT_TYPE);
367                        tagging.setPropertyValue("dc:created", map.get("dc:created"));
368                        tagging.setPropertyValue("dc:creator", map.get("dc:creator"));
369                        tagging.setPropertyValue(TagConstants.TAGGING_SOURCE_FIELD, dstDocId);
370                        tagging.setPropertyValue(TagConstants.TAGGING_TARGET_FIELD, map.get("relation:target"));
371                        session.createDocument(tagging);
372                        docCreated = true;
373                    }
374                }
375                if (docCreated) {
376                    session.save();
377                }
378            }
379        }
380    }
381
382    @Override
383    public void replaceTags(CoreSession session, String srcDocId, String dstDocId) {
384        copyTags(session, srcDocId, dstDocId, true);
385    }
386
387    public List<String> getTagDocumentIds(CoreSession session, String label, String username) {
388        UnrestrictedGetTagDocumentIds r = new UnrestrictedGetTagDocumentIds(session, label, username);
389        r.runUnrestricted();
390        return r.docIds;
391    }
392
393    protected static class UnrestrictedGetTagDocumentIds extends UnrestrictedSessionRunner {
394
395        protected final String label;
396
397        protected final String username;
398
399        protected final List<String> docIds;
400
401        protected UnrestrictedGetTagDocumentIds(CoreSession session, String label, String username)
402                {
403            super(session);
404            this.label = cleanLabel(label, false, false);
405            this.username = cleanUsername(username);
406            docIds = new ArrayList<String>();
407        }
408
409        @Override
410        public void run() {
411            List<Map<String, Serializable>> res;
412            if (username == null) {
413                res = getItems(PAGE_PROVIDERS.GET_DOCUMENTS_FOR_TAG.name(), session, label);
414            } else {
415                res = getItems(PAGE_PROVIDERS.GET_DOCUMENTS_FOR_TAG_AND_USER.name(), session, label, username);
416            }
417            if (res != null) {
418                for (Map<String, Serializable> map : res) {
419                    docIds.add((String) map.get(TagConstants.TAGGING_SOURCE_FIELD));
420                }
421            }
422        }
423    }
424
425    public List<Tag> getTagCloud(CoreSession session, String docId, String username, Boolean normalize)
426            {
427        UnrestrictedGetDocumentCloud r = new UnrestrictedGetDocumentCloud(session, docId, username, normalize);
428        r.runUnrestricted();
429        return r.cloud;
430    }
431
432    protected static class UnrestrictedGetDocumentCloud extends UnrestrictedSessionRunner {
433
434        protected final String docId;
435
436        protected final String username;
437
438        protected final List<Tag> cloud;
439
440        protected final Boolean normalize;
441
442        protected UnrestrictedGetDocumentCloud(CoreSession session, String docId, String username, Boolean normalize)
443                {
444            super(session);
445            this.docId = docId;
446            this.username = cleanUsername(username);
447            this.normalize = normalize;
448            cloud = new ArrayList<Tag>();
449        }
450
451        @Override
452        public void run() {
453            List<Map<String, Serializable>> res;
454            if (docId == null) {
455                if (username == null) {
456                    res = getItems(PAGE_PROVIDERS.GET_ALL_TAGS.name(), session);
457                } else {
458                    res = getItems(PAGE_PROVIDERS.GET_ALL_TAGS_FOR_USER.name(), session, username);
459                }
460            } else {
461                // find all docs under docid
462                String path = session.getDocument(new IdRef(docId)).getPathAsString();
463                path = path.replace("'", "");
464                List<String> docIds = new ArrayList<String>();
465                docIds.add(docId);
466                List<Map<String, Serializable>> docRes = getItems(PAGE_PROVIDERS.GET_TAGGED_DOCUMENTS_UNDER.name(),
467                        session, path);
468                if (docRes != null) {
469                    for (Map<String, Serializable> map : docRes) {
470                        docIds.add((String) map.get(NXQL.ECM_UUID));
471                    }
472                }
473
474                if (username == null) {
475                    res = getItems(PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENTS.name(), session, docIds);
476                } else {
477                    res = getItems(PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENTS_AND_USER.name(), session, docIds, username);
478                }
479            }
480
481            int min = 999999, max = 0;
482            if (res != null) {
483                for (Map<String, Serializable> map : res) {
484                    String label = (String) map.get(TagConstants.TAG_LABEL_FIELD);
485                    int weight = ((Long) map.get(TagConstants.TAGGING_SOURCE_FIELD)).intValue();
486                    if (weight == 0) {
487                        // shouldn't happen
488                        continue;
489                    }
490                    if (weight > max) {
491                        max = weight;
492                    }
493                    if (weight < min) {
494                        min = weight;
495                    }
496                    Tag weightedTag = new Tag(label, weight);
497                    cloud.add(weightedTag);
498                }
499            }
500            if (normalize != null) {
501                normalizeCloud(cloud, min, max, !normalize.booleanValue());
502            }
503        }
504    }
505
506    public static void normalizeCloud(List<Tag> cloud, int min, int max, boolean linear) {
507        if (min == max) {
508            for (Tag tag : cloud) {
509                tag.setWeight(100);
510            }
511            return;
512        }
513        double nmin;
514        double diff;
515        if (linear) {
516            nmin = min;
517            diff = max - min;
518        } else {
519            nmin = Math.log(min);
520            diff = Math.log(max) - nmin;
521        }
522        for (Tag tag : cloud) {
523            long weight = tag.getWeight();
524            double norm;
525            if (linear) {
526                norm = (weight - nmin) / diff;
527            } else {
528                norm = (Math.log(weight) - nmin) / diff;
529            }
530            tag.setWeight(Math.round(100 * norm));
531        }
532    }
533
534    public List<Tag> getSuggestions(CoreSession session, String label, String username) {
535        UnrestrictedGetTagSuggestions r = new UnrestrictedGetTagSuggestions(session, label, username);
536        r.runUnrestricted();
537        return r.tags;
538    }
539
540    protected static class UnrestrictedGetTagSuggestions extends UnrestrictedSessionRunner {
541
542        protected final String label;
543
544        protected final String username;
545
546        protected final List<Tag> tags;
547
548        protected UnrestrictedGetTagSuggestions(CoreSession session, String label, String username)
549                {
550            super(session);
551            label = cleanLabel(label, false, true);
552            if (!label.contains("%")) {
553                label += "%";
554            }
555            this.label = label;
556            this.username = cleanUsername(username);
557            tags = new ArrayList<Tag>();
558        }
559
560        @Override
561        public void run() {
562            List<Map<String, Serializable>> res;
563            if (username == null) {
564                res = getItems(PAGE_PROVIDERS.GET_TAG_SUGGESTIONS.name(), session, label);
565            } else {
566                res = getItems(PAGE_PROVIDERS.GET_TAG_SUGGESTIONS_FOR_USER.name(), session, label, username);
567            }
568            if (res != null) {
569                for (Map<String, Serializable> map : res) {
570                    String label = (String) map.get(TagConstants.TAG_LABEL_FIELD);
571                    tags.add(new Tag(label, 0));
572                }
573            }
574            // XXX should sort on tag weight
575            Collections.sort(tags, Tag.LABEL_COMPARATOR);
576        }
577    }
578
579    /**
580     * Returns results from calls to {@link CoreSession#queryAndFetch(String, String, Object...)} using page providers.
581     *
582     * @since 6.0
583     */
584    @SuppressWarnings("unchecked")
585    protected static List<Map<String, Serializable>> getItems(String pageProviderName, CoreSession session,
586            Object... params) {
587        PageProviderService ppService = Framework.getService(PageProviderService.class);
588        if (ppService == null) {
589            throw new RuntimeException("Missing PageProvider service");
590        }
591        Map<String, Serializable> props = new HashMap<String, Serializable>();
592        // first retrieve potential props from definition
593        PageProviderDefinition def = ppService.getPageProviderDefinition(pageProviderName);
594        if (def != null) {
595            Map<String, String> defProps = def.getProperties();
596            if (defProps != null) {
597                props.putAll(defProps);
598            }
599        }
600        props.put(CoreQueryAndFetchPageProvider.CORE_SESSION_PROPERTY, (Serializable) session);
601        PageProvider<Map<String, Serializable>> pp = (PageProvider<Map<String, Serializable>>) ppService.getPageProvider(
602                pageProviderName, null, null, null, props, params);
603        if (pp == null) {
604            throw new NuxeoException("Page provider not found: " + pageProviderName);
605        }
606        return pp.getCurrentPage();
607    }
608}