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