001/*
002 * (C) Copyright 2007-2018 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 *     Nuxeo - initial API and implementation
018 *
019 * $Id$
020 */
021
022package org.nuxeo.ecm.platform.comment.impl;
023
024import static java.util.stream.Collectors.collectingAndThen;
025import static java.util.stream.Collectors.toList;
026import static org.nuxeo.ecm.platform.comment.api.ExternalEntityConstants.EXTERNAL_ENTITY_FACET;
027import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_ANCESTOR_IDS;
028import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_DOC_TYPE;
029import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_PARENT_ID;
030
031import java.io.Serializable;
032import java.text.SimpleDateFormat;
033import java.util.ArrayList;
034import java.util.Calendar;
035import java.util.Collections;
036import java.util.Comparator;
037import java.util.Date;
038import java.util.List;
039import java.util.Map;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.ecm.core.api.CloseableCoreSession;
044import org.nuxeo.ecm.core.api.CoreInstance;
045import org.nuxeo.ecm.core.api.CoreSession;
046import org.nuxeo.ecm.core.api.DocumentModel;
047import org.nuxeo.ecm.core.api.DocumentRef;
048import org.nuxeo.ecm.core.api.IdRef;
049import org.nuxeo.ecm.core.api.NuxeoException;
050import org.nuxeo.ecm.core.api.PartialList;
051import org.nuxeo.ecm.core.api.PathRef;
052import org.nuxeo.ecm.core.api.PropertyException;
053import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
054import org.nuxeo.ecm.platform.comment.api.Comment;
055import org.nuxeo.ecm.platform.comment.api.CommentConverter;
056import org.nuxeo.ecm.platform.comment.api.CommentEvents;
057import org.nuxeo.ecm.platform.comment.api.Comments;
058import org.nuxeo.ecm.platform.comment.api.ExternalEntity;
059import org.nuxeo.ecm.platform.comment.api.exceptions.CommentNotFoundException;
060import org.nuxeo.ecm.platform.comment.api.exceptions.CommentSecurityException;
061import org.nuxeo.ecm.platform.comment.service.CommentServiceConfig;
062import org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants;
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
079public class CommentManagerImpl extends AbstractCommentManager {
080
081    private static final Log log = LogFactory.getLog(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 session, DocumentModel docModel)
098            throws CommentSecurityException {
099        Map<String, Object> ctxMap = Collections.<String, Object> singletonMap(ResourceAdapter.CORE_SESSION_CONTEXT_KEY,
100                session);
101        RelationManager relationManager = Framework.getService(RelationManager.class);
102        Graph graph = relationManager.getGraph(config.graphName, session);
103        Resource docResource = relationManager.getResource(config.documentNamespace, docModel, ctxMap);
104        if (docResource == null) {
105            throw new NuxeoException("Could not adapt document model to relation resource ; "
106                    + "check the service relation adapters configuration");
107        }
108
109        // FIXME AT: why no filter on the predicate?
110        List<Statement> statementList = graph.getStatements(null, null, docResource);
111        if (graph instanceof JenaGraph) {
112            // XXX AT: BBB for when repository name was not included in the
113            // resource uri
114            Resource oldDocResource = new QNameResourceImpl(config.documentNamespace, docModel.getId());
115            statementList.addAll(graph.getStatements(null, null, oldDocResource));
116        }
117
118        List<DocumentModel> commentList = new ArrayList<DocumentModel>();
119        for (Statement stmt : statementList) {
120            QNameResourceImpl subject = (QNameResourceImpl) stmt.getSubject();
121
122            DocumentModel commentDocModel = (DocumentModel) relationManager.getResourceRepresentation(
123                    config.commentNamespace, subject, ctxMap);
124            if (commentDocModel == null) {
125                // XXX AT: maybe user cannot see the comment
126                log.warn("Could not adapt comment relation subject to a document "
127                        + "model; check the service relation adapters configur  ation");
128                continue;
129            }
130            commentList.add(commentDocModel);
131        }
132
133        CommentSorter sorter = new CommentSorter(true);
134        Collections.sort(commentList, sorter);
135
136        return commentList;
137    }
138
139    @Override
140    public DocumentModel createComment(DocumentModel docModel, String comment, String author) {
141        try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
142            DocumentModel commentDM = session.createDocumentModel(COMMENT_DOC_TYPE);
143            commentDM.setPropertyValue(CommentsConstants.COMMENT_TEXT, comment);
144            commentDM.setPropertyValue(CommentsConstants.COMMENT_AUTHOR, author);
145            commentDM.setPropertyValue(CommentsConstants.COMMENT_CREATION_DATE, Calendar.getInstance());
146            commentDM = internalCreateComment(session, docModel, commentDM, null);
147            session.save();
148
149            return commentDM;
150        }
151    }
152
153    @Override
154    public DocumentModel createComment(DocumentModel docModel, String comment) {
155        String author = getCurrentUser(docModel);
156        return createComment(docModel, comment, author);
157    }
158
159    /**
160     * If the author property on comment is not set, retrieve the author name from the session
161     *
162     * @param docModel The document model that holds the session id
163     * @param comment The comment to update
164     */
165    private static String updateAuthor(DocumentModel docModel, DocumentModel comment) {
166        // update the author if not set
167        String author = (String) comment.getProperty("comment", "author");
168        if (author == null) {
169            log.debug("deprecated use of createComment: the client should set the author property on document");
170            author = getCurrentUser(docModel);
171            comment.setProperty("comment", "author", author);
172        }
173        return author;
174    }
175
176    @Override
177    public DocumentModel createComment(DocumentModel docModel, DocumentModel comment) throws CommentSecurityException {
178        try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
179            comment.setPropertyValue(COMMENT_ANCESTOR_IDS,
180                    (Serializable) computeAncestorIds(session, docModel.getId()));
181            DocumentModel doc = internalCreateComment(session, docModel, comment, null);
182            session.save();
183            doc.detach(true);
184            return doc;
185        }
186    }
187
188    protected DocumentModel internalCreateComment(CoreSession session, DocumentModel docModel, DocumentModel comment,
189            String path) {
190        DocumentModel createdComment;
191
192        createdComment = createCommentDocModel(session, docModel, comment, path);
193
194        RelationManager relationManager = Framework.getService(RelationManager.class);
195
196        Resource commentRes = relationManager.getResource(config.commentNamespace, createdComment, null);
197
198        Resource documentRes = relationManager.getResource(config.documentNamespace, docModel, null);
199
200        if (commentRes == null || documentRes == null) {
201            throw new NuxeoException("Could not adapt document model to relation resource ; "
202                    + "check the service relation adapters configuration");
203        }
204
205        Resource predicateRes = new ResourceImpl(config.predicateNamespace);
206
207        Statement stmt = new StatementImpl(commentRes, predicateRes, documentRes);
208        relationManager.getGraph(config.graphName, session).add(stmt);
209
210        notifyEvent(session, CommentEvents.COMMENT_ADDED, docModel, createdComment);
211
212        return createdComment;
213    }
214
215    private DocumentModel createCommentDocModel(CoreSession mySession, DocumentModel docModel, DocumentModel comment,
216            String path) {
217
218        String domainPath;
219        updateAuthor(docModel, comment);
220
221        String[] pathList = getCommentPathList(comment);
222
223        if (path == null) {
224            domainPath = "/" + docModel.getPath().segment(0);
225        } else {
226            domainPath = path;
227        }
228        if (mySession == null) {
229            return null;
230        }
231
232        // TODO GR upgrade this code. It can't work if current user
233        // doesn't have admin rights
234
235        DocumentModel parent = mySession.getDocument(new PathRef(domainPath));
236        for (String name : pathList) {
237            String pathStr = parent.getPathAsString();
238
239            PathRef ref = new PathRef(pathStr, name);
240            if (mySession.exists(ref)) {
241                parent = mySession.getDocument(ref);
242                if (!parent.isFolder()) {
243                    throw new NuxeoException(parent.getPathAsString() + " is not folderish");
244                }
245            } else {
246                DocumentModel dm = mySession.createDocumentModel(pathStr, name, "HiddenFolder");
247                dm.setProperty("dublincore", "title", name);
248                dm.setProperty("dublincore", "description", "");
249                dm.setProperty("dublincore", "created", Calendar.getInstance());
250                dm = mySession.createDocument(dm);
251                setFolderPermissions(mySession, dm);
252                parent = dm;
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        setCommentPermissions(mySession, commentDocModel);
266        log.debug("created comment with id=" + commentDocModel.getId());
267
268        return commentDocModel;
269    }
270
271    private String[] getCommentPathList(DocumentModel comment) {
272        String[] pathList = new String[2];
273        pathList[0] = COMMENTS_DIRECTORY;
274
275        pathList[1] = dateFormat.format(getCommentTimeStamp(comment));
276        return pathList;
277    }
278
279    /**
280     * @deprecated if the caller is remote, we cannot obtain the session
281     */
282    @Deprecated
283    private static String getCurrentUser(DocumentModel target) {
284        CoreSession userSession = target.getCoreSession();
285        if (userSession == null) {
286            throw new NuxeoException("userSession is null, do not invoke this method when the user is not local");
287        }
288        return userSession.getPrincipal().getName();
289    }
290
291    private String getCommentName(DocumentModel target, DocumentModel comment) {
292        String author = (String) comment.getProperty("comment", "author");
293        if (author == null) {
294            author = getCurrentUser(target);
295        }
296        Date creationDate = getCommentTimeStamp(comment);
297        return "COMMENT-" + author + '-' + timeFormat.format(creationDate.getTime());
298    }
299
300    private static Date getCommentTimeStamp(DocumentModel comment) {
301        Calendar creationDate;
302        try {
303            creationDate = (Calendar) comment.getProperty("dublincore", "created");
304        } catch (PropertyException e) {
305            creationDate = null;
306        }
307        if (creationDate == null) {
308            creationDate = Calendar.getInstance();
309        }
310        return creationDate.getTime();
311    }
312
313    @Override
314    public void deleteComment(DocumentModel docModel, DocumentModel comment) {
315        try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
316            DocumentRef ref = comment.getRef();
317            if (!session.exists(ref)) {
318                throw new NuxeoException("Comment Document does not exist: " + comment.getId());
319            }
320
321            session.removeDocument(ref);
322
323            notifyEvent(session, CommentEvents.COMMENT_REMOVED, docModel, comment);
324
325            session.save();
326        }
327    }
328
329    @Override
330    public DocumentModel createComment(DocumentModel docModel, DocumentModel parent, DocumentModel child) {
331        try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
332            DocumentModel parentDocModel = session.getDocument(parent.getRef());
333            String containerPath = parent.getPath().removeLastSegments(1).toString();
334            DocumentModel newComment = internalCreateComment(session, parentDocModel, child, containerPath);
335
336            session.save();
337            return newComment;
338        }
339    }
340
341    @Override
342    public List<DocumentModel> getDocumentsForComment(DocumentModel comment) {
343        Map<String, Object> ctxMap = Collections.<String, Object> singletonMap(ResourceAdapter.CORE_SESSION_CONTEXT_KEY,
344                comment.getCoreSession());
345        RelationManager relationManager = Framework.getService(RelationManager.class);
346        Graph graph = relationManager.getGraph(config.graphName, comment.getCoreSession());
347        Resource commentResource = relationManager.getResource(config.commentNamespace, comment, ctxMap);
348        if (commentResource == null) {
349            throw new NuxeoException("Could not adapt document model to relation resource ; "
350                    + "check the service relation adapters configuration");
351        }
352        Resource predicate = new ResourceImpl(config.predicateNamespace);
353
354        List<Statement> statementList = graph.getStatements(commentResource, predicate, null);
355        if (graph instanceof JenaGraph) {
356            // XXX AT: BBB for when repository name was not included in the
357            // resource uri
358            Resource oldDocResource = new QNameResourceImpl(config.commentNamespace, comment.getId());
359            statementList.addAll(graph.getStatements(oldDocResource, predicate, null));
360        }
361
362        List<DocumentModel> docList = new ArrayList<DocumentModel>();
363        for (Statement stmt : statementList) {
364            QNameResourceImpl subject = (QNameResourceImpl) stmt.getObject();
365            DocumentModel docModel = (DocumentModel) relationManager.getResourceRepresentation(config.documentNamespace,
366                    subject, ctxMap);
367            if (docModel == null) {
368                log.warn("Could not adapt comment relation subject to a document "
369                        + "model; check the service relation adapters configuration");
370                continue;
371            }
372            docList.add(docModel);
373        }
374        return docList;
375
376    }
377
378    @Override
379    public DocumentModel createLocatedComment(DocumentModel docModel, DocumentModel comment, String path)
380            throws CommentSecurityException {
381        try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
382            DocumentModel createdComment = internalCreateComment(session, docModel, comment, path);
383            session.save();
384            return createdComment;
385        }
386    }
387
388    @Override
389    public DocumentModel getThreadForComment(DocumentModel comment) throws CommentSecurityException {
390        List<DocumentModel> threads = getDocumentsForComment(comment);
391        if (threads.size() > 0) {
392            DocumentModel thread = threads.get(0);
393            while (thread.getType().equals("Post") || thread.getType().equals(COMMENT_DOC_TYPE)) {
394                thread = getThreadForComment(thread);
395            }
396            return thread;
397        }
398        return null;
399    }
400
401    @Override
402    public Comment createComment(CoreSession session, Comment comment)
403            throws CommentNotFoundException, CommentSecurityException {
404        DocumentRef commentRef = new IdRef(comment.getParentId());
405        if (!session.exists(commentRef)) {
406            throw new CommentNotFoundException("The document " + comment.getParentId() + " does not exist.");
407        }
408        DocumentModel docToComment = session.getDocument(commentRef);
409        DocumentModel commentModel = session.createDocumentModel(COMMENT_DOC_TYPE);
410        commentModel.setPropertyValue("dc:created", Calendar.getInstance());
411
412        Comments.commentToDocumentModel(comment, commentModel);
413        if (comment instanceof ExternalEntity) {
414            commentModel.addFacet(EXTERNAL_ENTITY_FACET);
415            Comments.externalEntityToDocumentModel((ExternalEntity) comment, commentModel);
416        }
417
418        DocumentModel createdCommentModel = createComment(docToComment, commentModel);
419        return Comments.newComment(createdCommentModel);
420    }
421
422    @Override
423    public Comment getComment(CoreSession session, String commentId)
424            throws CommentNotFoundException, CommentSecurityException {
425        DocumentRef commentRef = new IdRef(commentId);
426        if (!session.exists(commentRef)) {
427            throw new CommentNotFoundException("The document " + commentId + " does not exist.");
428        }
429        DocumentModel commentModel = session.getDocument(commentRef);
430        return Comments.newComment(commentModel);
431    }
432
433    @Override
434    @SuppressWarnings("unchecked")
435    public PartialList<Comment> getComments(CoreSession session, String documentId, Long pageSize,
436            Long currentPageIndex, boolean sortAscending) throws CommentSecurityException {
437        DocumentRef docRef = new IdRef(documentId);
438        if (!session.exists(docRef)) {
439            return new PartialList<>(Collections.emptyList(), 0);
440        }
441        DocumentModel commentedDoc = session.getDocument(docRef);
442        // do a dummy implementation of pagination for former comment manager implementation
443        List<DocumentModel> comments = getComments(commentedDoc);
444        long maxSize = pageSize == null || pageSize <= 0 ? comments.size() : pageSize;
445        long offset = currentPageIndex == null || currentPageIndex <= 0 ? 0 : currentPageIndex * pageSize;
446        return comments.stream()
447                       .sorted(Comparator.comparing(doc -> (Calendar) doc.getPropertyValue("dc:created")))
448                       .skip(offset)
449                       .limit(maxSize)
450                       .map(Comments::newComment)
451                       .collect(collectingAndThen(toList(), list -> new PartialList<>(list, comments.size())));
452    }
453
454    @Override
455    public Comment updateComment(CoreSession session, String commentId, Comment comment) throws NuxeoException {
456        throw new UnsupportedOperationException("Update a comment is not possible through this implementation");
457    }
458
459    @Override
460    public void deleteComment(CoreSession session, String commentId)
461            throws CommentNotFoundException, CommentSecurityException {
462        DocumentRef commentRef = new IdRef(commentId);
463        if (!session.exists(commentRef)) {
464            throw new CommentNotFoundException("The comment " + commentId + " does not exist.");
465        }
466        DocumentModel comment = session.getDocument(commentRef);
467        DocumentModel commentedDoc = session.getDocument(
468                new IdRef((String) comment.getPropertyValue(COMMENT_PARENT_ID)));
469        deleteComment(commentedDoc, comment);
470    }
471
472    @Override
473    public Comment getExternalComment(CoreSession session, String entityId) throws NuxeoException {
474        throw new UnsupportedOperationException(
475                "Get a comment from its external entity id is not possible through this implementation");
476    }
477
478    @Override
479    public Comment updateExternalComment(CoreSession session, String entityId, Comment comment) throws NuxeoException {
480        throw new UnsupportedOperationException(
481                "Update a comment from its external entity id is not possible through this implementation");
482    }
483
484    @Override
485    public void deleteExternalComment(CoreSession session, String entityId) throws NuxeoException {
486        throw new UnsupportedOperationException(
487                "Delete a comment from its external entity id is not possible through this implementation");
488    }
489
490    @Override
491    public boolean hasFeature(Feature feature) {
492        switch (feature) {
493        case COMMENTS_LINKED_WITH_PROPERTY:
494            return false;
495        default:
496            throw new UnsupportedOperationException(feature.name());
497        }
498    }
499}