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