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