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