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; 034 035import org.ametys.cms.data.Binary; 036import org.ametys.cms.data.RichText; 037import org.ametys.cms.repository.Content; 038import org.ametys.cms.repository.ReactionableObject.ReactionType; 039import org.ametys.cms.repository.comment.AbstractComment; 040import org.ametys.cms.repository.comment.RichTextComment; 041import org.ametys.cms.transformation.RichTextTransformer; 042import org.ametys.cms.transformation.docbook.DocbookTransformer; 043import org.ametys.core.user.CurrentUserProvider; 044import org.ametys.core.user.User; 045import org.ametys.core.user.UserIdentity; 046import org.ametys.core.user.UserManager; 047import org.ametys.core.user.population.PopulationContextHelper; 048import org.ametys.plugins.core.user.UserHelper; 049import org.ametys.plugins.repository.AmetysRepositoryException; 050import org.ametys.plugins.repository.tag.TaggableAmetysObject; 051import org.ametys.plugins.workspaces.members.ProjectMemberManager; 052import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint; 053import org.ametys.runtime.model.type.DataContext; 054import org.ametys.runtime.plugin.component.AbstractLogEnabled; 055 056/** 057 * This class represents a result column 058 */ 059public class WorkspaceObjectJSONHelper extends AbstractLogEnabled implements Serviceable 060{ 061 /** Attribute to know if error occurred while handling rich text */ 062 public static final String ATTRIBUTE_FOR_RICHTEXT_ERROR = "contentError"; 063 064 /** The user helper */ 065 protected UserHelper _userHelper; 066 067 /** The tag provider extension point */ 068 protected ProjectTagProviderExtensionPoint _tagProviderExtensionPoint; 069 070 /** The project member manager */ 071 protected ProjectMemberManager _projectMemberManager; 072 073 /** The user manager */ 074 protected UserManager _userManager; 075 076 /** The current user provider */ 077 protected CurrentUserProvider _currentUserProvider; 078 079 /** The population context helper */ 080 protected PopulationContextHelper _populationContextHelper; 081 082 /** Source resolver */ 083 protected SourceResolver _sourceResolver; 084 085 /** Rich text transformer */ 086 protected RichTextTransformer _richTextTransformer; 087 088 public void service(ServiceManager manager) throws ServiceException 089 { 090 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 091 _tagProviderExtensionPoint = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE); 092 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 093 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 094 _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE); 095 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 096 _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE); 097 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 098 } 099 100 /** 101 * return a list of comments in JSON format 102 * @param <T> type of the value to retrieve 103 * @param comments the comments to translate as JSON 104 * @param lang the current language 105 * @param siteName The current site name 106 * @return list of comments 107 */ 108 protected <T extends AbstractComment> List<Map<String, Object>> _commentsToJson(List<T> comments, String lang, String siteName) 109 { 110 return _commentsToJson(comments, lang, siteName, null, true); 111 } 112 113 /** 114 * 115 * return a list of comments in JSON format 116 * @param <T> type of the value to retrieve 117 * @param comments the comments to translate as JSON 118 * @param lang the current language 119 * @param siteName The current site name 120 * @param lastReadDate Last read date to check if the comment is unread 121 * @param parseCommentcontent True to parse comment content 122 * @return list of comments 123 */ 124 protected <T extends AbstractComment> List<Map<String, Object>> _commentsToJson(List<T> comments, String lang, String siteName, ZonedDateTime lastReadDate, boolean parseCommentcontent) 125 { 126 List<Map<String, Object>> json = new ArrayList<>(); 127 128 for (T comment : comments) 129 { 130 json.add(_commentToJson(comment, lang, siteName, lastReadDate, parseCommentcontent)); 131 } 132 133 return json; 134 } 135 /** 136 * Transform the rich text as rendering string 137 * @param content the content 138 * @return the rendering string 139 * @throws IOException if I/O error occurred. 140 */ 141 protected String richTextToRendering(RichText content) throws IOException 142 { 143 Source src = null; 144 try (InputStream contentIs = content.getInputStream()) 145 { 146 147 Map<String, Object> parameters = new HashMap<>(); 148 parameters.put("source", contentIs); 149 parameters.put("dataContext", DataContext.newInstance()); 150 parameters.put("level", 2); 151 152 src = _sourceResolver.resolveURI("cocoon://plugins/workspaces/convert/docbook2htmlrendering", null, parameters); 153 try (InputStream is = src.getInputStream()) 154 { 155 String renderingText = IOUtils.toString(is, "UTF-8"); 156 157 // FIXME Try to find a better workaround 158 renderingText = StringUtils.replace(renderingText, " xmlns:i18n=\"http://apache.org/cocoon/i18n/2.1\"", ""); 159 renderingText = StringUtils.replace(renderingText, " xmlns:ametys=\"org.ametys.cms.transformation.xslt.AmetysXSLTHelper\"", ""); 160 renderingText = StringUtils.replace(renderingText, " xmlns:docbook=\"http://docbook.org/ns/docbook\"", ""); 161 162 renderingText = StringUtils.substringAfter(renderingText, "<xml>"); 163 renderingText = StringUtils.substringBefore(renderingText, "</xml>"); 164 return renderingText; 165 } 166 } 167 finally 168 { 169 _sourceResolver.release(src); 170 } 171 } 172 173 /** 174 * Transform the rich text as simple text 175 * @param content the content 176 * @return the abstract string 177 * @throws IOException if an error occurred 178 */ 179 public String richTextToSimpleText(RichText content) throws IOException 180 { 181 Source src = null; 182 try (InputStream contentIs = content.getInputStream()) 183 { 184 185 Map<String, Object> parameters = new HashMap<>(); 186 parameters.put("source", contentIs); 187 parameters.put("dataContext", DataContext.newInstance()); 188 189 src = _sourceResolver.resolveURI("cocoon://plugins/workspaces/convert/docbook2simpletext", null, parameters); 190 try (InputStream is = src.getInputStream()) 191 { 192 String abstractText = IOUtils.toString(is, "UTF-8"); 193 abstractText = StringUtils.substringAfter(abstractText, "<xml>"); 194 abstractText = StringUtils.substringBefore(abstractText, "</xml>"); 195 // FIXME Try to find a better workaround 196 abstractText = StringUtils.replace(abstractText, " xmlns:i18n=\"http://apache.org/cocoon/i18n/2.1\"", ""); 197 abstractText = StringUtils.replace(abstractText, " xmlns:ametys=\"org.ametys.cms.transformation.xslt.AmetysXSLTHelper\"", ""); 198 return abstractText; 199 } 200 } 201 finally 202 { 203 _sourceResolver.release(src); 204 } 205 } 206 207 /** 208 * return a list of comments in JSON format 209 * @param <T> type of the value to retrieve 210 * @param comment the comment to translate as JSON 211 * @param lang the current language 212 * @param siteName The current site name 213 * @param lastReadDate Last read date to check if the comment is unread 214 * @param parseCommentcontent True to parse comment content 215 * @return list of comments 216 */ 217 protected <T extends AbstractComment> Map<String, Object> _commentToJson(T comment, String lang, String siteName, ZonedDateTime lastReadDate, boolean parseCommentcontent) 218 { 219 Map<String, Object> commentJson = new HashMap<>(); 220 commentJson.put("id", comment.getId()); 221 commentJson.put("subComments", _commentsToJson(comment.getSubComment(true, true), lang, siteName)); 222 commentJson.put("isDeleted", comment.isDeleted()); 223 commentJson.put("creationDate", comment.getCreationDate()); 224 commentJson.put("isReported", comment.getReportsCount() > 0); 225 226 if (comment.isSubComment()) 227 { 228 T parent = comment.getCommentParent(); 229 commentJson.put("parentId", parent.getId()); 230 User author = _userManager.getUser(parent.getAuthor()); 231 commentJson.put("parentAuthor", _authorToJSON(parent, author, lang)); 232 } 233 234 if (!comment.isDeleted()) 235 { 236 commentJson.put("text", comment.getContent()); 237 commentJson.put("isEdited", comment.isEdited()); 238 commentJson.put("nbLike", comment.getReactionUsers(ReactionType.LIKE).size()); 239 List<String> userLikes = comment.getReactionUsers(ReactionType.LIKE) 240 .stream() 241 .map(UserIdentity::userIdentityToString) 242 .collect(Collectors.toList()); 243 commentJson.put("userLikes", userLikes); 244 245 commentJson.put("accepted", comment.isAccepted()); 246 247 UserIdentity currentUser = _currentUserProvider.getUser(); 248 commentJson.put("isLiked", userLikes.contains(UserIdentity.userIdentityToString(currentUser))); 249 250 User author = _userManager.getUser(comment.getAuthor()); 251 252 commentJson.put("author", _authorToJSON(comment, author, lang)); 253 commentJson.put("canHandle", author != null ? author.getIdentity().equals(currentUser) : false); 254 } 255 256 if (lastReadDate != null && comment.getCreationDate().isAfter(lastReadDate)) 257 { 258 commentJson.put("unread", true); 259 } 260 261 if (comment instanceof RichTextComment richTextComment && richTextComment.hasRichTextContent() && parseCommentcontent) 262 { 263 264 RichText richText = richTextComment.getRichTextContent(); 265 StringBuilder result = new StringBuilder(2048); 266 try 267 { 268 _richTextTransformer.transformForEditing(richText, DataContext.newInstance(), result); 269 commentJson.put(RichTextComment.ATTRIBUTE_CONTENT_FOR_EDITING, result.toString()); 270 commentJson.put(RichTextComment.ATTRIBUTE_CONTENT_ABSTRACT, richTextToSimpleText(richText)); 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}