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}