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