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}