001/*
002 * (C) Copyright 2007 Nuxeo SAS (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 *     Nuxeo - initial API and implementation
016 *
017 * $Id$
018 */
019
020package org.nuxeo.ecm.platform.comment.impl;
021
022import java.io.Serializable;
023import java.text.SimpleDateFormat;
024import java.util.ArrayList;
025import java.util.Calendar;
026import java.util.Collections;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.ecm.core.api.CoreInstance;
035import org.nuxeo.ecm.core.api.CoreSession;
036import org.nuxeo.ecm.core.api.DocumentModel;
037import org.nuxeo.ecm.core.api.DocumentRef;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.api.NuxeoPrincipal;
040import org.nuxeo.ecm.core.api.PathRef;
041import org.nuxeo.ecm.core.api.PropertyException;
042import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
043import org.nuxeo.ecm.core.api.security.ACE;
044import org.nuxeo.ecm.core.api.security.ACL;
045import org.nuxeo.ecm.core.api.security.ACP;
046import org.nuxeo.ecm.core.api.security.SecurityConstants;
047import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
048import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
049import org.nuxeo.ecm.core.event.Event;
050import org.nuxeo.ecm.core.event.EventProducer;
051import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
052import org.nuxeo.ecm.platform.comment.api.CommentConstants;
053import org.nuxeo.ecm.platform.comment.api.CommentConverter;
054import org.nuxeo.ecm.platform.comment.api.CommentEvents;
055import org.nuxeo.ecm.platform.comment.api.CommentManager;
056import org.nuxeo.ecm.platform.comment.service.CommentServiceConfig;
057import org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants;
058import org.nuxeo.ecm.platform.relations.api.Graph;
059import org.nuxeo.ecm.platform.relations.api.RelationManager;
060import org.nuxeo.ecm.platform.relations.api.Resource;
061import org.nuxeo.ecm.platform.relations.api.ResourceAdapter;
062import org.nuxeo.ecm.platform.relations.api.Statement;
063import org.nuxeo.ecm.platform.relations.api.impl.QNameResourceImpl;
064import org.nuxeo.ecm.platform.relations.api.impl.ResourceImpl;
065import org.nuxeo.ecm.platform.relations.api.impl.StatementImpl;
066import org.nuxeo.ecm.platform.relations.jena.JenaGraph;
067import org.nuxeo.ecm.platform.usermanager.UserManager;
068import org.nuxeo.runtime.api.Framework;
069
070/**
071 * @author <a href="mailto:glefter@nuxeo.com">George Lefter</a>
072 */
073public class CommentManagerImpl implements CommentManager {
074
075    private static final Log log = LogFactory.getLog(CommentManagerImpl.class);
076
077    final SimpleDateFormat timeFormat = new SimpleDateFormat("dd-HHmmss.S");
078
079    final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM");
080
081    final CommentServiceConfig config;
082
083    final CommentConverter commentConverter;
084
085    public static final String COMMENTS_DIRECTORY = "Comments";
086
087    public CommentManagerImpl(CommentServiceConfig config) {
088        this.config = config;
089        commentConverter = config.getCommentConverter();
090    }
091
092    public List<DocumentModel> getComments(DocumentModel docModel) {
093        Map<String, Object> ctxMap = Collections.<String, Object> singletonMap(
094                ResourceAdapter.CORE_SESSION_CONTEXT_KEY, docModel.getCoreSession());
095        RelationManager relationManager = Framework.getService(RelationManager.class);
096        Graph graph = relationManager.getGraphByName(config.graphName);
097        Resource docResource = relationManager.getResource(config.documentNamespace, docModel, ctxMap);
098        if (docResource == null) {
099            throw new NuxeoException("Could not adapt document model to relation resource ; "
100                    + "check the service relation adapters configuration");
101        }
102
103        // FIXME AT: why no filter on the predicate?
104        List<Statement> statementList = graph.getStatements(null, null, docResource);
105        if (graph instanceof JenaGraph) {
106            // XXX AT: BBB for when repository name was not included in the
107            // resource uri
108            Resource oldDocResource = new QNameResourceImpl(config.documentNamespace, docModel.getId());
109            statementList.addAll(graph.getStatements(null, null, oldDocResource));
110        }
111
112        List<DocumentModel> commentList = new ArrayList<DocumentModel>();
113        for (Statement stmt : statementList) {
114            QNameResourceImpl subject = (QNameResourceImpl) stmt.getSubject();
115
116            DocumentModel commentDocModel = (DocumentModel) relationManager.getResourceRepresentation(
117                    config.commentNamespace, subject, ctxMap);
118            if (commentDocModel == null) {
119                // XXX AT: maybe user cannot see the comment
120                log.warn("Could not adapt comment relation subject to a document "
121                        + "model; check the service relation adapters configur  ation");
122                continue;
123            }
124            commentList.add(commentDocModel);
125        }
126
127        CommentSorter sorter = new CommentSorter(true);
128        Collections.sort(commentList, sorter);
129
130        return commentList;
131    }
132
133    public DocumentModel createComment(DocumentModel docModel, String comment, String author) {
134        try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
135            DocumentModel commentDM = session.createDocumentModel(CommentsConstants.COMMENT_DOC_TYPE);
136            commentDM.setPropertyValue(CommentsConstants.COMMENT_TEXT, comment);
137            commentDM.setPropertyValue(CommentsConstants.COMMENT_AUTHOR, author);
138            commentDM.setPropertyValue(CommentsConstants.COMMENT_CREATION_DATE, Calendar.getInstance());
139            commentDM = internalCreateComment(session, docModel, commentDM, null);
140            session.save();
141
142            return commentDM;
143        }
144    }
145
146    public DocumentModel createComment(DocumentModel docModel, String comment) {
147        String author = getCurrentUser(docModel);
148        return createComment(docModel, comment, author);
149    }
150
151    /**
152     * If the author property on comment is not set, retrieve the author name from the session
153     *
154     * @param docModel The document model that holds the session id
155     * @param comment The comment to update
156     */
157    private static String updateAuthor(DocumentModel docModel, DocumentModel comment) {
158        // update the author if not set
159        String author = (String) comment.getProperty("comment", "author");
160        if (author == null) {
161            log.debug("deprecated use of createComment: the client should set the author property on document");
162            author = getCurrentUser(docModel);
163            comment.setProperty("comment", "author", author);
164        }
165        return author;
166    }
167
168    public DocumentModel createComment(DocumentModel docModel, DocumentModel comment) {
169        try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
170            DocumentModel doc = internalCreateComment(session, docModel, comment, null);
171            session.save();
172            doc.detach(true);
173            return doc;
174        }
175    }
176
177    protected DocumentModel internalCreateComment(CoreSession session, DocumentModel docModel, DocumentModel comment,
178            String path) {
179        String author = updateAuthor(docModel, comment);
180        DocumentModel createdComment;
181
182        createdComment = createCommentDocModel(session, docModel, comment, path);
183
184        RelationManager relationManager = Framework.getService(RelationManager.class);
185
186        Resource commentRes = relationManager.getResource(config.commentNamespace, createdComment, null);
187
188        Resource documentRes = relationManager.getResource(config.documentNamespace, docModel, null);
189
190        if (commentRes == null || documentRes == null) {
191            throw new NuxeoException("Could not adapt document model to relation resource ; "
192                    + "check the service relation adapters configuration");
193        }
194
195        Resource predicateRes = new ResourceImpl(config.predicateNamespace);
196
197        Statement stmt = new StatementImpl(commentRes, predicateRes, documentRes);
198        relationManager.getGraphByName(config.graphName).add(stmt);
199
200        UserManager userManager = Framework.getService(UserManager.class);
201        if (userManager != null) {
202            // null in tests
203            NuxeoPrincipal principal = userManager.getPrincipal(author);
204            if (principal != null) {
205                notifyEvent(session, docModel, CommentEvents.COMMENT_ADDED, null, createdComment, principal);
206            }
207        }
208
209        return createdComment;
210    }
211
212    private DocumentModel createCommentDocModel(CoreSession mySession, DocumentModel docModel, DocumentModel comment,
213            String path) {
214
215        String domainPath;
216        updateAuthor(docModel, comment);
217
218        String[] pathList = getCommentPathList(comment);
219
220        if (path == null) {
221            domainPath = docModel.getPath().segment(0);
222        } else {
223            domainPath = path;
224        }
225        if (mySession == null) {
226            return null;
227        }
228
229        // TODO GR upgrade this code. It can't work if current user
230        // doesn't have admin rights
231
232        DocumentModel parent = mySession.getDocument(new PathRef(domainPath));
233        for (String name : pathList) {
234            String pathStr = parent.getPathAsString();
235
236            PathRef ref = new PathRef(pathStr, name);
237            if (mySession.exists(ref)) {
238                parent = mySession.getDocument(ref);
239                if (!parent.isFolder()) {
240                    throw new NuxeoException(parent.getPathAsString() + " is not folderish");
241                }
242            } else {
243                DocumentModel dm = mySession.createDocumentModel(pathStr, name, "HiddenFolder");
244                dm.setProperty("dublincore", "title", name);
245                dm.setProperty("dublincore", "description", "");
246                dm.setProperty("dublincore", "created", Calendar.getInstance());
247                dm = mySession.createDocument(dm);
248                setFolderPermissions(dm);
249                parent = dm;
250            }
251        }
252
253        String pathStr = parent.getPathAsString();
254        String commentName = getCommentName(docModel, comment);
255        CommentConverter converter = config.getCommentConverter();
256        PathSegmentService pss = Framework.getService(PathSegmentService.class);
257        DocumentModel commentDocModel = mySession.createDocumentModel(comment.getType());
258        commentDocModel.setProperty("dublincore", "title", commentName);
259        converter.updateDocumentModel(commentDocModel, comment);
260        commentDocModel.setPathInfo(pathStr, pss.generatePathSegment(commentDocModel));
261        commentDocModel = mySession.createDocument(commentDocModel);
262        setCommentPermissions(commentDocModel);
263        log.debug("created comment with id=" + commentDocModel.getId());
264
265        return commentDocModel;
266    }
267
268    private static void notifyEvent(CoreSession session, DocumentModel docModel, String eventType,
269            DocumentModel parent, DocumentModel child, NuxeoPrincipal principal) {
270
271        DocumentEventContext ctx = new DocumentEventContext(session, principal, docModel);
272        Map<String, Serializable> props = new HashMap<String, Serializable>();
273        if (parent != null) {
274            props.put(CommentConstants.PARENT_COMMENT, parent);
275        }
276        props.put(CommentConstants.COMMENT_DOCUMENT, child);
277        props.put(CommentConstants.COMMENT, (String) child.getProperty("comment", "text"));
278        // Keep comment_text for compatibility
279        props.put(CommentConstants.COMMENT_TEXT, (String) child.getProperty("comment", "text"));
280        props.put("category", CommentConstants.EVENT_COMMENT_CATEGORY);
281        ctx.setProperties(props);
282        Event event = ctx.newEvent(eventType);
283
284        EventProducer evtProducer = Framework.getService(EventProducer.class);
285        evtProducer.fireEvent(event);
286        // send also a synchronous Seam message so the CommentManagerActionBean
287        // can rebuild its list
288        // Events.instance().raiseEvent(eventType, docModel);
289    }
290
291    private static void setFolderPermissions(DocumentModel dm) {
292        ACP acp = new ACPImpl();
293        ACE grantAddChildren = new ACE("members", SecurityConstants.ADD_CHILDREN, true);
294        ACE grantRemoveChildren = new ACE("members", SecurityConstants.REMOVE_CHILDREN, true);
295        ACE grantRemove = new ACE("members", SecurityConstants.REMOVE, true);
296        ACL acl = new ACLImpl();
297        acl.setACEs(new ACE[] { grantAddChildren, grantRemoveChildren, grantRemove });
298        acp.addACL(acl);
299        dm.setACP(acp, true);
300    }
301
302    private static void setCommentPermissions(DocumentModel dm) {
303        ACP acp = new ACPImpl();
304        ACE grantRead = new ACE(SecurityConstants.EVERYONE, SecurityConstants.READ, true);
305        ACE grantRemove = new ACE("members", SecurityConstants.REMOVE, true);
306        ACL acl = new ACLImpl();
307        acl.setACEs(new ACE[] { grantRead, grantRemove });
308        acp.addACL(acl);
309        dm.setACP(acp, true);
310    }
311
312    private String[] getCommentPathList(DocumentModel comment) {
313        String[] pathList = new String[2];
314        pathList[0] = COMMENTS_DIRECTORY;
315
316        pathList[1] = dateFormat.format(getCommentTimeStamp(comment));
317        return pathList;
318    }
319
320    /**
321     * @deprecated if the caller is remote, we cannot obtain the session
322     */
323    @Deprecated
324    private static String getCurrentUser(DocumentModel target) {
325        CoreSession userSession = target.getCoreSession();
326        if (userSession == null) {
327            throw new NuxeoException("userSession is null, do not invoke this method when the user is not local");
328        }
329        return userSession.getPrincipal().getName();
330    }
331
332    private String getCommentName(DocumentModel target, DocumentModel comment) {
333        String author = (String) comment.getProperty("comment", "author");
334        if (author == null) {
335            author = getCurrentUser(target);
336        }
337        Date creationDate = getCommentTimeStamp(comment);
338        return "COMMENT-" + author + '-' + timeFormat.format(creationDate.getTime());
339    }
340
341    private static Date getCommentTimeStamp(DocumentModel comment) {
342        Calendar creationDate;
343        try {
344            creationDate = (Calendar) comment.getProperty("dublincore", "created");
345        } catch (PropertyException e) {
346            creationDate = null;
347        }
348        if (creationDate == null) {
349            creationDate = Calendar.getInstance();
350        }
351        return creationDate.getTime();
352    }
353
354    public void deleteComment(DocumentModel docModel, DocumentModel comment) {
355        NuxeoPrincipal author = comment.getCoreSession() != null ? (NuxeoPrincipal) comment.getCoreSession().getPrincipal()
356                : getAuthor(comment);
357        try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
358            DocumentRef ref = comment.getRef();
359            if (!session.exists(ref)) {
360                throw new NuxeoException("Comment Document does not exist: " + comment.getId());
361            }
362
363            session.removeDocument(ref);
364
365            notifyEvent(session, docModel, CommentEvents.COMMENT_REMOVED, null, comment, author);
366
367            session.save();
368        }
369    }
370
371    public DocumentModel createComment(DocumentModel docModel, DocumentModel parent, DocumentModel child)
372            {
373        try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
374            DocumentModel parentDocModel = session.getDocument(parent.getRef());
375            DocumentModel newComment = internalCreateComment(session, parentDocModel, child, null);
376
377            session.save();
378            return newComment;
379        }
380    }
381
382    private static NuxeoPrincipal getAuthor(DocumentModel docModel) {
383        String[] contributors;
384        try {
385            contributors = (String[]) docModel.getProperty("dublincore", "contributors");
386        } catch (PropertyException e) {
387            log.error("Error building principal for comment author", e);
388            return null;
389        }
390        UserManager userManager = Framework.getService(UserManager.class);
391        return userManager.getPrincipal(contributors[0]);
392    }
393
394    public List<DocumentModel> getComments(DocumentModel docModel, DocumentModel parent) {
395        return getComments(parent);
396    }
397
398    public List<DocumentModel> getDocumentsForComment(DocumentModel comment) {
399        Map<String, Object> ctxMap = Collections.<String, Object> singletonMap(
400                ResourceAdapter.CORE_SESSION_CONTEXT_KEY, comment.getCoreSession());
401        RelationManager relationManager = Framework.getService(RelationManager.class);
402        Graph graph = relationManager.getGraphByName(config.graphName);
403        Resource commentResource = relationManager.getResource(config.commentNamespace, comment, ctxMap);
404        if (commentResource == null) {
405            throw new NuxeoException("Could not adapt document model to relation resource ; "
406                    + "check the service relation adapters configuration");
407        }
408        Resource predicate = new ResourceImpl(config.predicateNamespace);
409
410        List<Statement> statementList = graph.getStatements(commentResource, predicate, null);
411        if (graph instanceof JenaGraph) {
412            // XXX AT: BBB for when repository name was not included in the
413            // resource uri
414            Resource oldDocResource = new QNameResourceImpl(config.commentNamespace, comment.getId());
415            statementList.addAll(graph.getStatements(oldDocResource, predicate, null));
416        }
417
418        List<DocumentModel> docList = new ArrayList<DocumentModel>();
419        for (Statement stmt : statementList) {
420            QNameResourceImpl subject = (QNameResourceImpl) stmt.getObject();
421            DocumentModel docModel = (DocumentModel) relationManager.getResourceRepresentation(
422                    config.documentNamespace, subject, ctxMap);
423            if (docModel == null) {
424                log.warn("Could not adapt comment relation subject to a document "
425                        + "model; check the service relation adapters configuration");
426                continue;
427            }
428            docList.add(docModel);
429        }
430        return docList;
431
432    }
433
434    public DocumentModel createLocatedComment(DocumentModel docModel, DocumentModel comment, String path)
435            {
436        try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
437            DocumentModel createdComment = internalCreateComment(session, docModel, comment, path);
438            session.save();
439            return createdComment;
440        }
441    }
442
443    public DocumentModel getThreadForComment(DocumentModel comment) {
444        List<DocumentModel> threads = getDocumentsForComment(comment);
445        if (threads.size() > 0) {
446            DocumentModel thread = (DocumentModel) threads.get(0);
447            while (thread.getType().equals("Post") || thread.getType().equals(CommentsConstants.COMMENT_DOC_TYPE)) {
448                thread = getThreadForComment(thread);
449            }
450            return thread;
451        }
452        return null;
453    }
454}