001/*
002 * (C) Copyright 2007-2020 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 *     Nuxeo - initial API and implementation
018 */
019
020package org.nuxeo.ecm.platform.comment.impl;
021
022import static java.util.Objects.requireNonNullElseGet;
023import static java.util.stream.Collectors.collectingAndThen;
024import static java.util.stream.Collectors.toList;
025import static org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonReader.applyDirtyPropertyValues;
026import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_AUTHOR_PROPERTY;
027import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_CREATION_DATE_PROPERTY;
028import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_DOC_TYPE;
029import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_SCHEMA;
030import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_TEXT_PROPERTY;
031import static org.nuxeo.ecm.platform.comment.api.ExternalEntityConstants.EXTERNAL_ENTITY_FACET;
032
033import java.text.SimpleDateFormat;
034import java.util.ArrayList;
035import java.util.Calendar;
036import java.util.Collection;
037import java.util.Collections;
038import java.util.Comparator;
039import java.util.Date;
040import java.util.List;
041import java.util.Map;
042import java.util.function.Function;
043import java.util.stream.Stream;
044
045import org.apache.logging.log4j.LogManager;
046import org.apache.logging.log4j.Logger;
047import org.nuxeo.ecm.core.api.CoreInstance;
048import org.nuxeo.ecm.core.api.CoreSession;
049import org.nuxeo.ecm.core.api.DocumentModel;
050import org.nuxeo.ecm.core.api.DocumentRef;
051import org.nuxeo.ecm.core.api.IdRef;
052import org.nuxeo.ecm.core.api.NuxeoException;
053import org.nuxeo.ecm.core.api.PartialList;
054import org.nuxeo.ecm.core.api.PathRef;
055import org.nuxeo.ecm.core.api.PropertyException;
056import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
057import org.nuxeo.ecm.platform.comment.api.Comment;
058import org.nuxeo.ecm.platform.comment.api.CommentConverter;
059import org.nuxeo.ecm.platform.comment.api.CommentEvents;
060import org.nuxeo.ecm.platform.comment.api.exceptions.CommentNotFoundException;
061import org.nuxeo.ecm.platform.comment.api.exceptions.CommentSecurityException;
062import org.nuxeo.ecm.platform.comment.service.CommentServiceConfig;
063import org.nuxeo.ecm.platform.relations.api.Graph;
064import org.nuxeo.ecm.platform.relations.api.RelationManager;
065import org.nuxeo.ecm.platform.relations.api.Resource;
066import org.nuxeo.ecm.platform.relations.api.ResourceAdapter;
067import org.nuxeo.ecm.platform.relations.api.Statement;
068import org.nuxeo.ecm.platform.relations.api.impl.QNameResourceImpl;
069import org.nuxeo.ecm.platform.relations.api.impl.ResourceImpl;
070import org.nuxeo.ecm.platform.relations.api.impl.StatementImpl;
071import org.nuxeo.ecm.platform.relations.jena.JenaGraph;
072import org.nuxeo.runtime.api.Framework;
073
074/**
075 * @author <a href="mailto:glefter@nuxeo.com">George Lefter</a>
076 * @deprecated since 10.3, use {@link PropertyCommentManager} instead.
077 */
078@Deprecated(since = "10.3")
079public class CommentManagerImpl extends AbstractCommentManager {
080
081    private static final Logger log = LogManager.getLogger(CommentManagerImpl.class);
082
083    final SimpleDateFormat timeFormat = new SimpleDateFormat("dd-HHmmss.S");
084
085    final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM");
086
087    final CommentServiceConfig config;
088
089    final CommentConverter commentConverter;
090
091    public CommentManagerImpl(CommentServiceConfig config) {
092        this.config = config;
093        commentConverter = config.getCommentConverter();
094    }
095
096    @Override
097    public List<DocumentModel> getComments(CoreSession s, DocumentModel docModel) throws CommentSecurityException {
098        return doPrivileged(s, docModel.getRepositoryName(), session -> {
099            Map<String, Object> ctxMap = Collections.singletonMap(ResourceAdapter.CORE_SESSION_CONTEXT_KEY, session);
100            RelationManager relationManager = Framework.getService(RelationManager.class);
101            Graph graph = relationManager.getGraph(config.graphName, session);
102            Resource docResource = relationManager.getResource(config.documentNamespace, docModel, ctxMap);
103            if (docResource == null) {
104                throw new NuxeoException("Could not adapt document model to relation resource ; "
105                        + "check the service relation adapters configuration");
106            }
107
108            // FIXME AT: why no filter on the predicate?
109            List<Statement> statementList = graph.getStatements(null, null, docResource);
110            if (graph instanceof JenaGraph) {
111                // XXX AT: BBB for when repository name was not included in the
112                // resource uri
113                Resource oldDocResource = new QNameResourceImpl(config.documentNamespace, docModel.getId());
114                statementList.addAll(graph.getStatements(null, null, oldDocResource));
115            }
116
117            List<DocumentModel> commentList = new ArrayList<>();
118            for (Statement stmt : statementList) {
119                QNameResourceImpl subject = (QNameResourceImpl) stmt.getSubject();
120
121                DocumentModel commentDocModel = (DocumentModel) relationManager.getResourceRepresentation(
122                        config.commentNamespace, subject, ctxMap);
123                if (commentDocModel == null) {
124                    // XXX AT: maybe user cannot see the comment
125                    log.warn(
126                            "Could not adapt comment relation subject to a document model; check the service relation adapters configuration");
127                    continue;
128                }
129                commentList.add(commentDocModel);
130            }
131
132            CommentSorter sorter = new CommentSorter(true);
133            commentList.sort(sorter);
134
135            return commentList;
136        });
137    }
138
139    @Override
140    @SuppressWarnings("removal")
141    public DocumentModel createComment(DocumentModel docModel, String comment, String author) {
142        CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName());
143        DocumentModel commentDM = session.createDocumentModel(COMMENT_DOC_TYPE);
144        commentDM.setPropertyValue(COMMENT_TEXT_PROPERTY, comment);
145        commentDM.setPropertyValue(COMMENT_AUTHOR_PROPERTY, author);
146        commentDM.setPropertyValue(COMMENT_CREATION_DATE_PROPERTY, Calendar.getInstance());
147        commentDM = internalCreateComment(session, docModel, commentDM, null);
148        session.save();
149
150        return commentDM;
151    }
152
153    @Override
154    public DocumentModel getThreadForComment(DocumentModel comment) throws CommentSecurityException {
155        List<DocumentModel> threads = getDocumentsForComment(comment);
156        if (threads.size() > 0) {
157            DocumentModel thread = threads.get(0);
158            while (thread.getType().equals("Post") || thread.getType().equals(COMMENT_DOC_TYPE)) {
159                thread = getThreadForComment(thread);
160            }
161            return thread;
162        }
163        return null;
164    }
165
166    @Override
167    public DocumentModel createComment(DocumentModel docModel, String comment) {
168        String author = getCurrentUser(docModel);
169        return createComment(docModel, comment, author);
170    }
171
172    /**
173     * If the author property on comment is not set, retrieve the author name from the session
174     *
175     * @param docModel The document model that holds the session id
176     * @param comment The comment to update
177     */
178    private static String updateAuthor(DocumentModel docModel, DocumentModel comment) {
179        // update the author if not set
180        String author = (String) comment.getProperty("comment", "author");
181        if (author == null) {
182            log.debug("deprecated use of createComment: the client should set the author property on document");
183            author = getCurrentUser(docModel);
184            comment.setProperty("comment", "author", author);
185        }
186        return author;
187    }
188
189    @Override
190    public DocumentModel createComment(DocumentModel docModel, DocumentModel comment) throws CommentSecurityException {
191        CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName());
192        DocumentModel doc = internalCreateComment(session, docModel, comment, null);
193        session.save();
194        doc.detach(true);
195        return doc;
196    }
197
198    protected DocumentModel internalCreateComment(CoreSession session, DocumentModel docModel, DocumentModel comment,
199            String path) {
200        DocumentModel createdComment;
201
202        createdComment = createCommentDocModel(session, docModel, comment, path);
203
204        RelationManager relationManager = Framework.getService(RelationManager.class);
205
206        Resource commentRes = relationManager.getResource(config.commentNamespace, createdComment, null);
207
208        Resource documentRes = relationManager.getResource(config.documentNamespace, docModel, null);
209
210        if (commentRes == null || documentRes == null) {
211            throw new NuxeoException("Could not adapt document model to relation resource ; "
212                    + "check the service relation adapters configuration");
213        }
214
215        Resource predicateRes = new ResourceImpl(config.predicateNamespace);
216
217        Statement stmt = new StatementImpl(commentRes, predicateRes, documentRes);
218        relationManager.getGraph(config.graphName, session).add(stmt);
219
220        notifyEvent(session, CommentEvents.COMMENT_ADDED, docModel, createdComment);
221
222        return createdComment;
223    }
224
225    private DocumentModel createCommentDocModel(CoreSession mySession, DocumentModel docModel, DocumentModel comment,
226            String path) {
227
228        String domainPath;
229        updateAuthor(docModel, comment);
230
231        String[] pathList = getCommentPathList(comment);
232
233        domainPath = requireNonNullElseGet(path, () -> "/" + docModel.getPath().segment(0));
234        if (mySession == null) {
235            return null;
236        }
237
238        // TODO GR upgrade this code. It can't work if current user
239        // doesn't have admin rights
240
241        DocumentModel parent = mySession.getDocument(new PathRef(domainPath));
242        for (String name : pathList) {
243            String pathStr = parent.getPathAsString();
244
245            PathRef ref = new PathRef(pathStr, name);
246            if (mySession.exists(ref)) {
247                parent = mySession.getDocument(ref);
248                if (!parent.isFolder()) {
249                    throw new NuxeoException(parent.getPathAsString() + " is not folderish");
250                }
251            } else {
252                parent = createHiddenFolder(mySession, pathStr, name);
253            }
254        }
255
256        String pathStr = parent.getPathAsString();
257        String commentName = getCommentName(docModel, comment);
258        CommentConverter converter = config.getCommentConverter();
259        PathSegmentService pss = Framework.getService(PathSegmentService.class);
260        DocumentModel commentDocModel = mySession.createDocumentModel(comment.getType());
261        commentDocModel.setProperty("dublincore", "title", commentName);
262        converter.updateDocumentModel(commentDocModel, comment);
263        commentDocModel.setPathInfo(pathStr, pss.generatePathSegment(commentDocModel));
264        commentDocModel = mySession.createDocument(commentDocModel);
265        log.debug("created comment with id={}", commentDocModel.getId());
266
267        return commentDocModel;
268    }
269
270    protected DocumentModel createHiddenFolder(CoreSession session, String parentPath, String name) {
271        DocumentModel dm = session.createDocumentModel(parentPath, name, "HiddenFolder");
272        dm.setProperty("dublincore", "title", name);
273        dm.setProperty("dublincore", "description", "");
274        Framework.doPrivileged(() -> dm.setProperty("dublincore", "created", Calendar.getInstance()));
275        DocumentModel parent = session.createDocument(dm); // change variable name to be effectively final
276        setFolderPermissions(session, parent);
277        return parent;
278    }
279
280    private String[] getCommentPathList(DocumentModel comment) {
281        String[] pathList = new String[2];
282        pathList[0] = COMMENTS_DIRECTORY;
283
284        pathList[1] = dateFormat.format(getCommentTimeStamp(comment));
285        return pathList;
286    }
287
288    /**
289     * @deprecated if the caller is remote, we cannot obtain the session
290     */
291    @Deprecated
292    private static String getCurrentUser(DocumentModel target) {
293        CoreSession userSession = target.getCoreSession();
294        if (userSession == null) {
295            throw new NuxeoException("userSession is null, do not invoke this method when the user is not local");
296        }
297        return userSession.getPrincipal().getName();
298    }
299
300    private String getCommentName(DocumentModel target, DocumentModel comment) {
301        String author = (String) comment.getProperty("comment", "author");
302        if (author == null) {
303            author = getCurrentUser(target);
304        }
305        Date creationDate = getCommentTimeStamp(comment);
306        return "COMMENT-" + author + '-' + timeFormat.format(creationDate.getTime());
307    }
308
309    private static Date getCommentTimeStamp(DocumentModel comment) {
310        Calendar creationDate;
311        try {
312            creationDate = (Calendar) comment.getProperty("dublincore", "created");
313        } catch (PropertyException e) {
314            creationDate = null;
315        }
316        if (creationDate == null) {
317            creationDate = Calendar.getInstance();
318        }
319        return creationDate.getTime();
320    }
321
322    @Override
323    @SuppressWarnings("removal")
324    public void deleteComment(DocumentModel docModel, DocumentModel comment) {
325        CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName());
326        DocumentRef ref = comment.getRef();
327        if (!session.exists(ref)) {
328            throw new NuxeoException("Comment Document does not exist: " + comment.getId());
329        }
330
331        // fetch top level doc before deleting document
332        DocumentModel topLevelDoc = getTopLevelDocument(session, comment);
333        // finally remove the doc and fire event
334        session.removeDocument(ref);
335        notifyEvent(session, CommentEvents.COMMENT_REMOVED, topLevelDoc, docModel, comment);
336
337        session.save();
338    }
339
340    @Override
341    @SuppressWarnings("removal")
342    public DocumentModel createComment(DocumentModel docModel, DocumentModel parent, DocumentModel child) {
343        CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName());
344        DocumentModel parentDocModel = session.getDocument(parent.getRef());
345        String containerPath = parent.getPath().removeLastSegments(1).toString();
346        DocumentModel newComment = internalCreateComment(session, parentDocModel, child, containerPath);
347
348        session.save();
349        return newComment;
350    }
351
352    @Override
353    @SuppressWarnings("removal")
354    public List<DocumentModel> getDocumentsForComment(DocumentModel comment) {
355        return doPrivileged(comment.getCoreSession(), comment.getRepositoryName(), session -> {
356            Map<String, Object> ctxMap = Collections.singletonMap(ResourceAdapter.CORE_SESSION_CONTEXT_KEY, session);
357            RelationManager relationManager = Framework.getService(RelationManager.class);
358            Graph graph = relationManager.getGraph(config.graphName, session);
359            Resource commentResource = relationManager.getResource(config.commentNamespace, comment, ctxMap);
360            if (commentResource == null) {
361                throw new NuxeoException("Could not adapt document model to relation resource ; "
362                        + "check the service relation adapters configuration");
363            }
364            Resource predicate = new ResourceImpl(config.predicateNamespace);
365
366            List<Statement> statementList = graph.getStatements(commentResource, predicate, null);
367            if (graph instanceof JenaGraph) {
368                // XXX AT: BBB for when repository name was not included in the
369                // resource uri
370                Resource oldDocResource = new QNameResourceImpl(config.commentNamespace, comment.getId());
371                statementList.addAll(graph.getStatements(oldDocResource, predicate, null));
372            }
373
374            List<DocumentModel> docList = new ArrayList<>();
375            for (Statement stmt : statementList) {
376                QNameResourceImpl subject = (QNameResourceImpl) stmt.getObject();
377                DocumentModel docModel = (DocumentModel) relationManager.getResourceRepresentation(
378                        config.documentNamespace, subject, ctxMap);
379                if (docModel == null) {
380                    log.warn(
381                            "Could not adapt comment relation subject to a document model; check the service relation adapters configuration");
382                    continue;
383                }
384                // detach the document as it was loaded by a system session, not the user session.
385                docModel.detach(true);
386                docList.add(docModel);
387            }
388            return docList;
389        });
390    }
391
392    /**
393     * @since 11.1
394     */
395    protected List<DocumentModel> doPrivileged(CoreSession session, String repositoryName,
396            Function<CoreSession, List<DocumentModel>> function) {
397        // the comment may be detached therefore without a session.
398        if (session == null) {
399            return CoreInstance.doPrivileged(repositoryName, function);
400        }
401        return CoreInstance.doPrivileged(session, function);
402    }
403
404    @Override
405    public DocumentModel createLocatedComment(DocumentModel docModel, DocumentModel comment, String path)
406            throws CommentSecurityException {
407        CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName());
408        DocumentModel createdComment = internalCreateComment(session, docModel, comment, path);
409        session.save();
410        return createdComment;
411    }
412
413    @Override
414    public Comment createComment(CoreSession session, Comment comment)
415            throws CommentNotFoundException, CommentSecurityException {
416        DocumentRef commentRef = new IdRef(comment.getParentId());
417        if (!session.exists(commentRef)) {
418            throw new CommentNotFoundException("The document " + comment.getParentId() + " does not exist.");
419        }
420        DocumentModel docToComment = session.getDocument(commentRef);
421        DocumentModel commentModel = session.createDocumentModel(COMMENT_DOC_TYPE);
422        commentModel.setPropertyValue("dc:created", Calendar.getInstance());
423
424        fillCommentForCreation(session, comment);
425
426        if (comment.getDocument().hasFacet(EXTERNAL_ENTITY_FACET)) {
427            commentModel.addFacet(EXTERNAL_ENTITY_FACET);
428        }
429        applyDirtyPropertyValues(comment.getDocument(), commentModel);
430
431        DocumentModel createdCommentModel = createComment(docToComment, commentModel);
432        return createdCommentModel.getAdapter(Comment.class);
433    }
434
435    @Override
436    public Comment getComment(CoreSession session, String commentId)
437            throws CommentNotFoundException, CommentSecurityException {
438        DocumentRef commentRef = new IdRef(commentId);
439        if (!session.exists(commentRef)) {
440            throw new CommentNotFoundException("The document " + commentId + " does not exist.");
441        }
442        DocumentModel commentModel = session.getDocument(commentRef);
443        return commentModel.getAdapter(Comment.class);
444    }
445
446    @Override
447    public PartialList<Comment> getComments(CoreSession s, String documentId, Long pageSize, Long currentPageIndex,
448            boolean sortAscending) throws CommentSecurityException {
449        return CoreInstance.doPrivileged(s, session -> {
450            DocumentRef docRef = new IdRef(documentId);
451            if (!session.exists(docRef)) {
452                return new PartialList<Comment>(Collections.emptyList(), 0); // NOSONAR
453            }
454            DocumentModel commentedDoc = session.getDocument(docRef);
455            // do a dummy implementation of pagination for former comment manager implementation
456            List<DocumentModel> comments = getComments(commentedDoc);
457            long maxSize = pageSize == null || pageSize <= 0 ? comments.size() : pageSize;
458            long offset = currentPageIndex == null || currentPageIndex <= 0 ? 0 : currentPageIndex * pageSize;
459            return comments.stream()
460                           .sorted(Comparator.comparing(doc -> (Calendar) doc.getPropertyValue("dc:created")))
461                           .skip(offset)
462                           .limit(maxSize)
463                           .map(doc -> doc.getAdapter(Comment.class))
464                           .collect(collectingAndThen(toList(), list -> new PartialList<>(list, comments.size())));
465        });
466    }
467
468    @Override
469    public List<Comment> getComments(CoreSession session, Collection<String> documentIds) {
470        return documentIds.stream().flatMap(docId -> streamAllComments(session, docId)).collect(toList());
471    }
472
473    protected Stream<Comment> streamAllComments(CoreSession session, String docId) {
474        return getComments(session,
475                docId).stream().flatMap(c -> Stream.concat(Stream.of(c), streamAllComments(session, c.getId())));
476    }
477
478    @Override
479    public Comment updateComment(CoreSession session, String commentId, Comment comment) throws NuxeoException {
480        throw new UnsupportedOperationException("Update a comment is not possible through this implementation");
481    }
482
483    @Override
484    public void deleteComment(CoreSession s, String commentId)
485            throws CommentNotFoundException, CommentSecurityException {
486        DocumentRef commentRef = new IdRef(commentId);
487        CoreInstance.doPrivileged(s, session -> {
488            if (!session.exists(commentRef)) {
489                throw new CommentNotFoundException("The comment " + commentId + " does not exist.");
490            }
491
492            DocumentModel commentDocModel = session.getDocument(commentRef);
493            DocumentModel commentedDocModel = getDocumentsForComment(commentDocModel).get(0);
494            deleteComment(commentedDocModel, commentDocModel);
495        });
496    }
497
498    @Override
499    public Comment getExternalComment(CoreSession session, String documentId, String entityId) throws NuxeoException {
500        throw new UnsupportedOperationException(
501                "Get a comment from its external entity id is not possible through this implementation");
502    }
503
504    @Override
505    public Comment updateExternalComment(CoreSession session, String documentId, String entityId, Comment comment)
506            throws NuxeoException {
507        throw new UnsupportedOperationException(
508                "Update a comment from its external entity id is not possible through this implementation");
509    }
510
511    @Override
512    public void deleteExternalComment(CoreSession session, String documentId, String entityId) throws NuxeoException {
513        throw new UnsupportedOperationException(
514                "Delete a comment from its external entity id is not possible through this implementation");
515    }
516
517    @Override
518    public boolean hasFeature(Feature feature) {
519        switch (feature) {
520        case COMMENTS_LINKED_WITH_PROPERTY:
521        case COMMENTS_ARE_SPECIAL_CHILDREN:
522            return false;
523        default:
524            throw new UnsupportedOperationException(feature.name());
525        }
526    }
527
528    @Override
529    protected DocumentModel getTopLevelDocument(CoreSession s, DocumentModel commentDoc) {
530        return CoreInstance.doPrivileged(s, session -> {
531            DocumentModel documentModel = commentDoc;
532            while (documentModel != null && documentModel.hasSchema(COMMENT_SCHEMA)) {
533                List<DocumentModel> ancestors = getDocumentsForComment(documentModel);
534                documentModel = ancestors.isEmpty() ? null : ancestors.get(0);
535            }
536            return documentModel;
537        });
538    }
539
540    @Override
541    protected DocumentModel getCommentedDocument(CoreSession session, DocumentModel commentDoc) {
542        throw new UnsupportedOperationException();
543    }
544}