001/*
002 *  Copyright 2022 Anyware Services
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 */
016package org.ametys.plugins.workspaces.util;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.time.ZonedDateTime;
021import java.util.ArrayList;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.io.IOUtils;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.excalibur.source.Source;
034import org.apache.excalibur.source.SourceResolver;
035import org.apache.excalibur.xml.sax.SAXParser;
036import org.xml.sax.InputSource;
037
038import org.ametys.cms.content.RichTextHandler;
039import org.ametys.cms.data.Binary;
040import org.ametys.cms.data.RichText;
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.repository.ReactionableObject.ReactionType;
043import org.ametys.cms.repository.comment.AbstractComment;
044import org.ametys.cms.repository.comment.Comment;
045import org.ametys.cms.repository.comment.CommentsDAO;
046import org.ametys.cms.repository.comment.RichTextComment;
047import org.ametys.cms.transformation.RichTextTransformer;
048import org.ametys.core.user.CurrentUserProvider;
049import org.ametys.core.user.User;
050import org.ametys.core.user.UserIdentity;
051import org.ametys.core.user.UserManager;
052import org.ametys.core.user.population.PopulationContextHelper;
053import org.ametys.plugins.core.user.UserHelper;
054import org.ametys.plugins.repository.AmetysRepositoryException;
055import org.ametys.plugins.repository.tag.TaggableAmetysObject;
056import org.ametys.plugins.workspaces.members.ProjectMemberManager;
057import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint;
058import org.ametys.runtime.model.type.DataContext;
059import org.ametys.runtime.plugin.component.AbstractLogEnabled;
060
061/**
062 * Common helper for workspace objects
063 */
064public class WorkspaceObjectJSONHelper extends AbstractLogEnabled implements Serviceable
065{
066    /** Attribute to know if error occurred while handling rich text */
067    public static final String ATTRIBUTE_FOR_RICHTEXT_ERROR = "contentError";
068
069    /** The user helper */
070    protected UserHelper _userHelper;
071
072    /** The tag provider extension point */
073    protected ProjectTagProviderExtensionPoint _tagProviderExtensionPoint;
074
075    /** The project member manager */
076    protected ProjectMemberManager _projectMemberManager;
077
078    /** The user manager */
079    protected UserManager _userManager;
080
081    /** The current user provider */
082    protected CurrentUserProvider _currentUserProvider;
083
084    /** The population context helper */
085    protected PopulationContextHelper _populationContextHelper;
086
087    /** Source resolver */
088    protected SourceResolver _sourceResolver;
089
090    /** Rich text transformer */
091    protected RichTextTransformer _richTextTransformer;
092    /** The service manager */
093    protected ServiceManager _smanager;
094    
095    /** The comments DAO */
096    protected CommentsDAO _commentsDAO;
097
098    public void service(ServiceManager manager) throws ServiceException
099    {
100        _smanager = manager;
101        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
102        _tagProviderExtensionPoint = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE);
103        _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE);
104        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
105        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
106        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
107        _richTextTransformer = (RichTextTransformer) manager.lookup(ThreadDocbookTransformer.ROLE);
108        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
109        _commentsDAO = (CommentsDAO) manager.lookup(CommentsDAO.ROLE);
110    }
111    /**
112     * return a list of comments in JSON format
113     * @param <T> type of the value to retrieve
114     * @param comments the comments to translate as JSON
115     * @param lang the current language
116     * @param siteName The current site name
117     * @return list of comments
118     */
119    protected <T extends AbstractComment> List<Map<String, Object>> _commentsToJson(List<T> comments, String lang, String siteName)
120    {
121        return _commentsToJson(comments, lang, siteName, null, true);
122    }
123
124    /**
125     *
126     * return a list of comments in JSON format
127     * @param <T> type of the value to retrieve
128     * @param comments the comments to translate as JSON
129     * @param lang the current language
130     * @param siteName The current site name
131     * @param lastReadDate Last read date to check if the comment is unread
132     * @param parseCommentcontent True to parse comment content
133     * @return list of comments
134     */
135    protected <T extends AbstractComment> List<Map<String, Object>> _commentsToJson(List<T> comments, String lang, String siteName, ZonedDateTime lastReadDate, boolean parseCommentcontent)
136    {
137        List<Map<String, Object>> json = new ArrayList<>();
138
139        for (T comment : comments)
140        {
141            json.add(_commentToJson(comment, lang, siteName, lastReadDate, parseCommentcontent));
142        }
143
144        return json;
145    }
146    /**
147     * Transform the rich text as rendering string
148     * @param content the content
149     * @return the rendering string
150     * @throws IOException if I/O error occurred.
151     */
152    public String richTextToRendering(RichText content) throws IOException
153    {
154        Source src = null;
155        try (InputStream contentIs = content.getInputStream())
156        {
157
158            Map<String, Object> parameters = new HashMap<>();
159            parameters.put("source", contentIs);
160            parameters.put("dataContext", DataContext.newInstance());
161            parameters.put("level", 2);
162
163            src = _sourceResolver.resolveURI("cocoon://plugins/workspaces/convert/docbook2htmlrendering", null, parameters);
164            try (InputStream is = src.getInputStream())
165            {
166                String renderingText = IOUtils.toString(is, "UTF-8");
167
168                // FIXME Try to find a better workaround
169                renderingText = StringUtils.replace(renderingText, " xmlns:i18n=\"http://apache.org/cocoon/i18n/2.1\"", "");
170                renderingText = StringUtils.replace(renderingText, " xmlns:ametys=\"org.ametys.cms.transformation.xslt.AmetysXSLTHelper\"", "");
171                renderingText = StringUtils.replace(renderingText, " xmlns:docbook=\"http://docbook.org/ns/docbook\"", "");
172                renderingText = StringUtils.replace(renderingText, " xmlns:project=\"org.ametys.plugins.workspaces.project.helper.ProjectXsltHelper\"", "");
173
174                renderingText = StringUtils.substringAfter(renderingText, "<xml>");
175                renderingText = StringUtils.substringBefore(renderingText, "</xml>");
176                return renderingText;
177            }
178        }
179        finally
180        {
181            _sourceResolver.release(src);
182        }
183    }
184
185    /**
186     * Transform the rich text as simple text (without HTML tag)
187     * @param richText the content
188     * @param maxLength the max length for content excerpt. Set to 0 to not truncate content.
189     * @return the simple text, truncated if maxLength > 0
190     */
191    public String richTextToSimpleText(RichText richText, int maxLength)
192    {
193        SAXParser saxParser = null;
194        try (InputStream is = richText.getInputStream())
195        {
196            RichTextHandler txtHandler = new RichTextHandler(maxLength);
197            saxParser = (SAXParser) _smanager.lookup(SAXParser.ROLE);
198            saxParser.parse(new InputSource(is), txtHandler);
199            
200            return txtHandler.getValue();
201        }
202        catch (Exception e)
203        {
204            getLogger().error("Cannot extract simple text from richtext", e);
205        }
206        finally
207        {
208            _smanager.release(saxParser);
209        }
210        
211        return "";
212    }
213
214    /**
215     * return a list of comments in JSON format
216     * @param <T> type of the value to retrieve
217     * @param comment the comment to translate as JSON
218     * @param lang the current language
219     * @param siteName The current site name
220     * @param lastReadDate Last read date to check if the comment is unread
221     * @param parseCommentcontent True to parse comment content
222     * @return list of comments
223     */
224    protected <T extends AbstractComment> Map<String, Object> _commentToJson(T comment, String lang, String siteName, ZonedDateTime lastReadDate, boolean parseCommentcontent)
225    {
226        Map<String, Object> commentJson = new HashMap<>();
227        commentJson.put("id", comment.getId());
228        commentJson.put("subComments", _commentsToJson(comment.getSubComment(true, true), lang, siteName));
229        commentJson.put("isDeleted", comment.isDeleted());
230        commentJson.put("creationDate", comment.getCreationDate());
231        commentJson.put("isReported", comment.getReportsCount() > 0);
232
233        if (comment.isSubComment())
234        {
235            T parent = comment.getCommentParent();
236            commentJson.put("parentId", parent.getId());
237            User author = _userManager.getUser(parent.getAuthor());
238            commentJson.put("parentAuthor", _authorToJSON(parent, author, lang));
239        }
240
241        if (!comment.isDeleted())
242        {
243            commentJson.put("text", comment.getContent());
244            
245            if (comment instanceof Comment mentionComment)
246            {
247                
248                List<Map<String, Object>> mentionedUsers2json = mentionComment.extractMentions()
249                        .stream()
250                        .map(user -> _userHelper.user2json(user))
251                        .toList();
252                commentJson.put("mentions", mentionedUsers2json);
253            }
254            
255            commentJson.put("isEdited", comment.isEdited());
256            commentJson.put("nbLike", comment.getReactionUsers(ReactionType.LIKE).size());
257            List<String> userLikes = comment.getReactionUsers(ReactionType.LIKE)
258                .stream()
259                .map(UserIdentity::userIdentityToString)
260                .collect(Collectors.toList());
261            commentJson.put("userLikes", userLikes);
262
263            commentJson.put("accepted", comment.isAccepted());
264
265            UserIdentity currentUser = _currentUserProvider.getUser();
266            commentJson.put("isLiked", userLikes.contains(UserIdentity.userIdentityToString(currentUser)));
267
268            User author = _userManager.getUser(comment.getAuthor());
269
270            commentJson.put("author", _authorToJSON(comment, author, lang));
271            commentJson.put("canHandle", author != null ? author.getIdentity().equals(currentUser) : false);
272        }
273
274        if (lastReadDate != null && comment.getCreationDate().isAfter(lastReadDate))
275        {
276            commentJson.put("unread", true);
277        }
278
279        if (comment instanceof RichTextComment richTextComment && richTextComment.hasRichTextContent() && parseCommentcontent)
280        {
281
282            RichText richText = richTextComment.getRichTextContent();
283            StringBuilder result = new StringBuilder(2048);
284            try
285            {
286                _richTextTransformer.transformForEditing(richText, DataContext.newInstance(), result);
287                commentJson.put(RichTextComment.ATTRIBUTE_CONTENT_FOR_EDITING, result.toString());
288                commentJson.put(RichTextComment.ATTRIBUTE_CONTENT_FOR_RENDERING, richTextToRendering(richText));
289            }
290            catch (AmetysRepositoryException | IOException e)
291            {
292                commentJson.put(ATTRIBUTE_FOR_RICHTEXT_ERROR, true);
293                getLogger().error("Unable to transform the rich text value into a string", e);
294            }
295
296        }
297        return commentJson;
298    }
299    
300    /**
301     * Author to JSON
302     * @param <T> type of the value to retrieve
303     * @param comment the comment
304     * @param author the author
305     * @param lang the current language
306     * @return map representing the author
307     */
308    protected <T extends AbstractComment> Map<String, Object> _authorToJSON(T comment, User author, String lang)
309    {
310        Map<String, Object> jsonAuthor = new HashMap<>();
311        jsonAuthor.put("name", Optional.ofNullable(author).map(User::getFullName).orElse(comment.getAuthorName()));
312        if (author != null)
313        {
314            UserIdentity userIdentity = author.getIdentity();
315            jsonAuthor.put("id", UserIdentity.userIdentityToString(userIdentity));
316            jsonAuthor.put("login", userIdentity.getLogin());
317            jsonAuthor.put("populationId", userIdentity.getPopulationId());
318            Content member = _projectMemberManager.getUserContent(lang, author);
319            if (member != null)
320            {
321                if (member.hasValue("function"))
322                {
323                    jsonAuthor.put("function", member.getValue("function"));
324                }
325                if (member.hasValue("organisation-accronym"))
326                {
327                    jsonAuthor.put("organisationAcronym", member.getValue("organisation-accronym"));
328                }
329            }
330        }
331        return jsonAuthor;
332    }
333
334    /**
335     * return a binary in JSON format
336     * @param binary the binary
337     * @return the binary in JSON format
338     */
339    protected Map<String, Object> _binaryToJson(Binary binary)
340    {
341        Map<String, Object> json  = new HashMap<>();
342
343        try (InputStream is = binary.getInputStream())
344        {
345            json.put("id", binary.getFilename());
346            json.put("name", binary.getFilename());
347            json.put("type", binary.getMimeType());
348            json.put("size", binary.getLength());
349        }
350        catch (Exception e)
351        {
352            getLogger().error("An error occurred reading binary {}", binary.getFilename(), e);
353        }
354        return json;
355    }
356
357    /**
358     * return a list of tags in JSON format
359     * @param taggableAmetysObject the {@link TaggableAmetysObject}
360     * @param siteName The current site name
361     * @return a list of tags
362     */
363    protected List<String> _getTags(TaggableAmetysObject taggableAmetysObject, String siteName)
364    {
365        return taggableAmetysObject.getTags()
366                .stream()
367                .filter(tag -> _tagProviderExtensionPoint.hasTag(tag, Map.of("siteName", siteName)))
368                .collect(Collectors.toList());
369    }
370}