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