001/*
002 * (C) Copyright 2018-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 *     Funsho David
018 *     Nuno Cunha <ncunha@nuxeo.com>
019 */
020
021package org.nuxeo.ecm.platform.comment.impl;
022
023import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_PARENT_ID_PROPERTY;
024
025import java.util.Collection;
026import java.util.List;
027import java.util.function.Function;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import org.nuxeo.ecm.core.api.CoreInstance;
032import org.nuxeo.ecm.core.api.CoreSession;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.DocumentRef;
035import org.nuxeo.ecm.core.api.IdRef;
036import org.nuxeo.ecm.core.api.PartialList;
037import org.nuxeo.ecm.platform.comment.api.Comment;
038import org.nuxeo.ecm.platform.comment.api.CommentConstants;
039import org.nuxeo.ecm.platform.comment.api.CommentManager;
040import org.nuxeo.ecm.platform.comment.api.exceptions.CommentNotFoundException;
041import org.nuxeo.ecm.platform.comment.api.exceptions.CommentSecurityException;
042
043/**
044 * @since 10.3
045 */
046public class BridgeCommentManager extends AbstractCommentManager {
047
048    protected final CommentManager first;
049
050    protected final CommentManager second;
051
052    public BridgeCommentManager(CommentManager first, CommentManager second) {
053        this.first = first;
054        this.second = second;
055    }
056
057    /**
058     * @since 11.1
059     */
060    public CommentManager getFirst() {
061        return first;
062    }
063
064    /**
065     * @since 11.1
066     */
067    public CommentManager getSecond() {
068        return second;
069    }
070
071    @Override
072    public List<DocumentModel> getComments(DocumentModel docModel) {
073        return Stream.concat(first.getComments(docModel).stream(), second.getComments(docModel).stream())
074                     .distinct()
075                     .collect(Collectors.toList());
076    }
077
078    @Override
079    public List<DocumentModel> getComments(CoreSession session, DocumentModel docModel)
080            throws CommentSecurityException {
081        return Stream.concat(first.getComments(session, docModel).stream(),
082                second.getComments(session, docModel).stream()).distinct().collect(Collectors.toList());
083    }
084
085    @Override
086    @SuppressWarnings("deprecation")
087    public DocumentModel createComment(DocumentModel docModel, String comment) {
088        return second.createComment(docModel, comment);
089    }
090
091    @Override
092    @SuppressWarnings("removal")
093    public DocumentModel createComment(DocumentModel docModel, String comment, String author) {
094        return second.createComment(docModel, comment, author);
095    }
096
097    @Override
098    public DocumentModel createComment(DocumentModel docModel, DocumentModel comment) throws CommentSecurityException {
099        return second.createComment(docModel, comment);
100    }
101
102    @Override
103    @SuppressWarnings("removal")
104    public DocumentModel createComment(DocumentModel docModel, DocumentModel parent, DocumentModel child) {
105        return second.createComment(docModel, parent, child);
106    }
107
108    @Override
109    @SuppressWarnings("removal")
110    public void deleteComment(DocumentModel docModel, DocumentModel comment) {
111        if (comment.getPropertyValue(COMMENT_PARENT_ID_PROPERTY) != null) {
112            second.deleteComment(docModel, comment);
113        } else {
114            first.deleteComment(docModel, comment);
115        }
116    }
117
118    @Override
119    @SuppressWarnings("removal")
120    public List<DocumentModel> getDocumentsForComment(DocumentModel comment) {
121        return Stream.concat(first.getDocumentsForComment(comment).stream(),
122                second.getDocumentsForComment(comment).stream()).distinct().collect(Collectors.toList());
123    }
124
125    @Override
126    public DocumentModel getThreadForComment(DocumentModel comment) throws CommentSecurityException {
127        // handle only Relation to Property bridge, this method should have been deprecated in 10.10 due to unused
128        if (comment.getPropertyValue(COMMENT_PARENT_ID_PROPERTY) != null) {
129            if (second instanceof TreeCommentManager) {
130                return first.getThreadForComment(comment);
131            } else {
132                return second.getThreadForComment(comment);
133            }
134        }
135        return first.getThreadForComment(comment);
136    }
137
138    @Override
139    public DocumentModel createLocatedComment(DocumentModel docModel, DocumentModel comment, String path)
140            throws CommentSecurityException {
141        return second.createLocatedComment(docModel, comment, path);
142    }
143
144    @Override
145    public Comment createComment(CoreSession session, Comment comment)
146            throws CommentNotFoundException, CommentSecurityException {
147        return second.createComment(session, comment);
148    }
149
150    @Override
151    public Comment getComment(CoreSession session, String commentId)
152            throws CommentNotFoundException, CommentSecurityException {
153        return second.getComment(session, commentId);
154    }
155
156    @Override
157    public List<Comment> getComments(CoreSession session, String documentId) {
158        return Stream.concat(first.getComments(session, documentId).stream(),
159                second.getComments(session, documentId).stream()).distinct().collect(Collectors.toList());
160    }
161
162    @Override
163    public PartialList<Comment> getComments(CoreSession session, String documentId, Long pageSize,
164            Long currentPageIndex, boolean sortAscending) throws CommentSecurityException {
165        List<Comment> firstComments = first.getComments(session, documentId, pageSize, currentPageIndex, sortAscending);
166        List<Comment> secondComments = second.getComments(session, documentId, pageSize, currentPageIndex,
167                sortAscending);
168        List<Comment> allComments = Stream.concat(firstComments.stream(), secondComments.stream())
169                                          .distinct()
170                                          .collect(Collectors.toList());
171        return new PartialList<>(allComments, allComments.size());
172    }
173
174    @Override
175    public List<Comment> getComments(CoreSession session, Collection<String> documentIds) {
176        List<Comment> firstComments = first.getComments(session, documentIds);
177        List<Comment> secondComments = second.getComments(session, documentIds);
178        return Stream.concat(firstComments.stream(), secondComments.stream()).distinct().collect(Collectors.toList());
179    }
180
181    @Override
182    public Comment updateComment(CoreSession session, String commentId, Comment comment)
183            throws CommentNotFoundException, CommentSecurityException {
184        DocumentRef commentRef = new IdRef(commentId);
185        return CoreInstance.doPrivileged(session, s -> {
186            // retrieve comment to check which service handles it
187            if (!s.exists(commentRef)) {
188                throw new CommentNotFoundException("The comment " + commentId + " does not exist");
189            }
190            if (s.getDocument(commentRef).getPropertyValue(COMMENT_PARENT_ID_PROPERTY) != null) {
191                return second.updateComment(session, commentId, comment);
192            } else {
193                return first.updateComment(session, commentId, comment);
194            }
195        });
196    }
197
198    @Override
199    public void deleteComment(CoreSession session, String commentId)
200            throws CommentNotFoundException, CommentSecurityException {
201        DocumentRef commentRef = new IdRef(commentId);
202        CoreInstance.doPrivileged(session, s -> {
203            // retrieve comment to check which service handles it
204            if (!s.exists(commentRef)) {
205                throw new CommentNotFoundException("The comment " + commentId + " does not exist");
206            }
207            if (s.getDocument(commentRef).getPropertyValue(COMMENT_PARENT_ID_PROPERTY) != null) {
208                second.deleteComment(session, commentId);
209            } else {
210                first.deleteComment(session, commentId);
211            }
212        });
213    }
214
215    @Override
216    public Comment getExternalComment(CoreSession session, String documentId, String entityId)
217            throws CommentNotFoundException, CommentSecurityException {
218        return second.getExternalComment(session, documentId, entityId);
219    }
220
221    @Override
222    public Comment updateExternalComment(CoreSession session, String documentId, String entityId, Comment comment)
223            throws CommentNotFoundException, CommentSecurityException {
224        return second.updateExternalComment(session, documentId, entityId, comment);
225    }
226
227    @Override
228    public void deleteExternalComment(CoreSession session, String documentId, String entityId)
229            throws CommentNotFoundException, CommentSecurityException {
230        second.deleteExternalComment(session, documentId, entityId);
231    }
232
233    @Override
234    public boolean hasFeature(Feature feature) {
235        switch (feature) {
236        case COMMENTS_LINKED_WITH_PROPERTY:
237            return false;
238        default:
239            throw new UnsupportedOperationException(feature.name());
240        }
241    }
242
243    @Override
244    public DocumentRef getTopLevelDocumentRef(CoreSession session, DocumentRef commentIdRef) {
245        return execute(session, commentIdRef, cm -> cm.getTopLevelDocumentRef(session, commentIdRef));
246    }
247
248    @Override
249    protected DocumentModel getTopLevelDocument(CoreSession session, DocumentModel commentDoc) {
250        // this abstract method should be called within an implementation - no need to implement it in the bridge
251        throw new UnsupportedOperationException();
252    }
253
254    @Override
255    protected DocumentModel getCommentedDocument(CoreSession session, DocumentModel commentDoc) {
256        // this abstract method should be called within an implementation - no need to implement it in the bridge
257        throw new UnsupportedOperationException();
258    }
259
260    /**
261     * Executes the given function for a comment document ref, depending on the types of comment managers.
262     * <p>
263     * In some cases, leveraging {@link CommentConstants#COMMENT_PARENT_ID_PROPERTY} is not enough, this is the case
264     * when bridge is used with {@link PropertyCommentManager} and {@link TreeCommentManager}.
265     * <ul>
266     * <li>{@link CommentManagerImpl} (or RelationCommentManager): Comments structures are stored in jenaGraph</li>
267     * <li>{@link PropertyCommentManager}: All comments are stored under a hidden folder and each comment stores its
268     * parent id in {@code comment:parentId} property</li>
269     * <li>{@link TreeCommentManager}: A {@link CommentConstants#COMMENT_ROOT_DOC_TYPE} document is created under the
270     * top level document to store the comments. Replies are then stored directly under their parent (which is a
271     * comment)</li>
272     * </ul>
273     *
274     * @since 11.1
275     */
276    @SuppressWarnings("deprecation")
277    protected <T> T execute(CoreSession s, DocumentRef documentRef, Function<CommentManager, T> function) {
278        return CoreInstance.doPrivileged(s, session -> {
279            DocumentModel documentModel = session.getDocument(documentRef);
280
281            // From `Relation` to `Property`
282            if (first instanceof CommentManagerImpl && second instanceof PropertyCommentManager) {
283                if (documentModel.getPropertyValue(COMMENT_PARENT_ID_PROPERTY) != null) {
284                    // Comment is in property model
285                    return function.apply(second);
286                }
287                // Comment still in relation model
288                return function.apply(first);
289            }
290
291            // From `Property` to `Tree`
292            if (first instanceof PropertyCommentManager && second instanceof TreeCommentManager) {
293                // In this case we cannot just rely on `comment:parentId` but on the type of doc that contains the
294                // comment
295                DocumentRef parentRef = documentModel.getParentRef();
296                // Comment is under property model
297                if (session.getDocument(parentRef).getType().equals("HiddenFolder")) {
298                    return function.apply(first);
299                }
300                // Comment is under tree model
301                return function.apply(second);
302            }
303
304            throw new IllegalArgumentException(String.format(
305                    "Undefined behaviour for document ref: %s, first: %s, second: %s ", documentModel, first, second));
306        });
307    }
308
309}