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;
034import org.apache.excalibur.xml.sax.SAXParser;
035import org.xml.sax.InputSource;
036
037import org.ametys.cms.content.RichTextHandler;
038import org.ametys.cms.data.Binary;
039import org.ametys.cms.data.RichText;
040import org.ametys.cms.repository.Content;
041import org.ametys.cms.repository.ReactionableObject.ReactionType;
042import org.ametys.cms.repository.comment.AbstractComment;
043import org.ametys.cms.repository.comment.RichTextComment;
044import org.ametys.cms.transformation.RichTextTransformer;
045import org.ametys.cms.transformation.docbook.DocbookTransformer;
046import org.ametys.core.user.CurrentUserProvider;
047import org.ametys.core.user.User;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.core.user.UserManager;
050import org.ametys.core.user.population.PopulationContextHelper;
051import org.ametys.plugins.core.user.UserHelper;
052import org.ametys.plugins.repository.AmetysRepositoryException;
053import org.ametys.plugins.repository.tag.TaggableAmetysObject;
054import org.ametys.plugins.workspaces.members.ProjectMemberManager;
055import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint;
056import org.ametys.runtime.model.type.DataContext;
057import org.ametys.runtime.plugin.component.AbstractLogEnabled;
058
059/**
060 * Common helper for workspace objects
061 */
062public class WorkspaceObjectJSONHelper extends AbstractLogEnabled implements Serviceable
063{
064    /** Attribute to know if error occurred while handling rich text */
065    public static final String ATTRIBUTE_FOR_RICHTEXT_ERROR = "contentError";
066
067    /** The user helper */
068    protected UserHelper _userHelper;
069
070    /** The tag provider extension point */
071    protected ProjectTagProviderExtensionPoint _tagProviderExtensionPoint;
072
073    /** The project member manager */
074    protected ProjectMemberManager _projectMemberManager;
075
076    /** The user manager */
077    protected UserManager _userManager;
078
079    /** The current user provider */
080    protected CurrentUserProvider _currentUserProvider;
081
082    /** The population context helper */
083    protected PopulationContextHelper _populationContextHelper;
084
085    /** Source resolver */
086    protected SourceResolver _sourceResolver;
087
088    /** Rich text transformer */
089    protected RichTextTransformer _richTextTransformer;
090    /** The service manager */
091    protected ServiceManager _smanager;
092
093    public void service(ServiceManager manager) throws ServiceException
094    {
095        _smanager = manager;
096        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
097        _tagProviderExtensionPoint = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE);
098        _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE);
099        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
100        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
101        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
102        _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE);
103        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
104    }
105
106    /**
107     * return a list of comments in JSON format
108     * @param <T> type of the value to retrieve
109     * @param comments the comments to translate as JSON
110     * @param lang the current language
111     * @param siteName The current site name
112     * @return list of comments
113     */
114    protected <T extends AbstractComment> List<Map<String, Object>> _commentsToJson(List<T> comments, String lang, String siteName)
115    {
116        return _commentsToJson(comments, lang, siteName, null, true);
117    }
118
119    /**
120     *
121     * return a list of comments in JSON format
122     * @param <T> type of the value to retrieve
123     * @param comments the comments to translate as JSON
124     * @param lang the current language
125     * @param siteName The current site name
126     * @param lastReadDate Last read date to check if the comment is unread
127     * @param parseCommentcontent True to parse comment content
128     * @return list of comments
129     */
130    protected <T extends AbstractComment> List<Map<String, Object>> _commentsToJson(List<T> comments, String lang, String siteName, ZonedDateTime lastReadDate, boolean parseCommentcontent)
131    {
132        List<Map<String, Object>> json = new ArrayList<>();
133
134        for (T comment : comments)
135        {
136            json.add(_commentToJson(comment, lang, siteName, lastReadDate, parseCommentcontent));
137        }
138
139        return json;
140    }
141    /**
142     * Transform the rich text as rendering string
143     * @param content the content
144     * @return the rendering string
145     * @throws IOException if I/O error occurred.
146     */
147    protected String richTextToRendering(RichText content) throws IOException
148    {
149        Source src = null;
150        try (InputStream contentIs = content.getInputStream())
151        {
152
153            Map<String, Object> parameters = new HashMap<>();
154            parameters.put("source", contentIs);
155            parameters.put("dataContext", DataContext.newInstance());
156            parameters.put("level", 2);
157
158            src = _sourceResolver.resolveURI("cocoon://plugins/workspaces/convert/docbook2htmlrendering", null, parameters);
159            try (InputStream is = src.getInputStream())
160            {
161                String renderingText = IOUtils.toString(is, "UTF-8");
162
163                // FIXME Try to find a better workaround
164                renderingText = StringUtils.replace(renderingText, " xmlns:i18n=\"http://apache.org/cocoon/i18n/2.1\"", "");
165                renderingText = StringUtils.replace(renderingText, " xmlns:ametys=\"org.ametys.cms.transformation.xslt.AmetysXSLTHelper\"", "");
166                renderingText = StringUtils.replace(renderingText, " xmlns:docbook=\"http://docbook.org/ns/docbook\"", "");
167
168                renderingText = StringUtils.substringAfter(renderingText, "<xml>");
169                renderingText = StringUtils.substringBefore(renderingText, "</xml>");
170                return renderingText;
171            }
172        }
173        finally
174        {
175            _sourceResolver.release(src);
176        }
177    }
178
179    /**
180     * Transform the rich text as simple text (without HTML tag)
181     * @param richText the content
182     * @param maxLength the max length for content excerpt. Set to 0 to not truncate content.
183     * @return the simple text, truncated if maxLength > 0
184     */
185    public String richTextToSimpleText(RichText richText, int maxLength)
186    {
187        SAXParser saxParser = null;
188        try (InputStream is = richText.getInputStream())
189        {
190            RichTextHandler txtHandler = new RichTextHandler(maxLength);
191            saxParser = (SAXParser) _smanager.lookup(SAXParser.ROLE);
192            saxParser.parse(new InputSource(is), txtHandler);
193            
194            return txtHandler.getValue();
195        }
196        catch (Exception e)
197        {
198            getLogger().error("Cannot extract simple text from richtext", e);
199        }
200        finally
201        {
202            _smanager.release(saxParser);
203        }
204        
205        return "";
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", 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_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}