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}