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}