001/* 002 * Copyright 2021 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.cms.duplicate.contents; 017 018import java.io.IOException; 019import java.util.Collection; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.LinkedHashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Map.Entry; 026import java.util.Set; 027import java.util.stream.Collectors; 028import java.util.stream.Stream; 029 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.quartz.JobExecutionContext; 033 034import org.ametys.cms.content.ContentHelper; 035import org.ametys.cms.contenttype.ContentType; 036import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 037import org.ametys.cms.contenttype.ContentTypesHelper; 038import org.ametys.cms.duplicate.contents.DuplicateContentsManager.Status; 039import org.ametys.cms.repository.Content; 040import org.ametys.cms.schedule.AbstractSendingMailSchedulable; 041import org.ametys.core.schedule.Schedulable; 042import org.ametys.core.schedule.progression.ContainerProgressionTracker; 043import org.ametys.core.ui.mail.StandardMailBodyHelper; 044import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder; 045import org.ametys.runtime.i18n.I18nizableText; 046import org.ametys.runtime.i18n.I18nizableTextParameter; 047 048/** 049 * A {@link Schedulable} job for detecting duplicates. 050 */ 051public class DuplicateContentsDetectionSchedulable extends AbstractSendingMailSchedulable 052{ 053 054 private static final Object _HAS_CONFIGURATION_ERRORS = "hasConfigurationErrors"; 055 056 /** The duplicates manager */ 057 protected DuplicateContentsManager _duplicatesManager; 058 059 /** The content types helper */ 060 protected ContentTypesHelper _contentTypesHelper; 061 062 /** The content type extension point */ 063 protected ContentTypeExtensionPoint _cTypeEP; 064 065 /** The content helper */ 066 protected ContentHelper _contentHelper; 067 068 @Override 069 public void service(ServiceManager manager) throws ServiceException 070 { 071 super.service(manager); 072 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 073 _duplicatesManager = (DuplicateContentsManager) manager.lookup(DuplicateContentsManager.ROLE); 074 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 075 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 076 } 077 078 @Override 079 protected void _doExecute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 080 { 081 Map<String, Object> duplicatesInfo = _duplicatesManager.searchDuplicates(); 082 _duplicatesManager.logConfigurationErrors(getLogger()); 083 084 context.put(DuplicateContentsManager.NO_DUPLICATE_CONTENTS_CONTENT_TYPE_KEY, duplicatesInfo.get(DuplicateContentsManager.NO_DUPLICATE_CONTENTS_CONTENT_TYPE_KEY)); 085 context.put(DuplicateContentsManager.DUPLICATE_CONTENTS_KEY, duplicatesInfo.get(DuplicateContentsManager.DUPLICATE_CONTENTS_KEY)); 086 context.put(DuplicateContentsManager.NEAR_DUPLICATE_CONTENTS_KEY, duplicatesInfo.get(DuplicateContentsManager.NEAR_DUPLICATE_CONTENTS_KEY)); 087 context.put(DuplicateContentsManager.STATUS_KEY, duplicatesInfo.get(DuplicateContentsManager.STATUS_KEY)); 088 context.put(_HAS_CONFIGURATION_ERRORS, _duplicatesManager.hasConfigurationErrors()); 089 090 if ((boolean) duplicatesInfo.get(DuplicateContentsManager.NO_DUPLICATE_CONTENTS_CONTENT_TYPE_KEY)) 091 { 092 throw new IllegalArgumentException("Missing or badly configured duplicate-contents.xml file inside WEB-INF/param folder"); 093 } 094 } 095 096 @Override 097 protected I18nizableText _getSuccessMailSubject(JobExecutionContext context) throws Exception 098 { 099 @SuppressWarnings("unchecked") 100 Map<Content, List<Content>> duplicatesMap = (Map<Content, List<Content>>) context.get(DuplicateContentsManager.DUPLICATE_CONTENTS_KEY); 101 @SuppressWarnings("unchecked") 102 Map<Content, List<Content>> nearDuplicatesMap = (Map<Content, List<Content>>) context.get(DuplicateContentsManager.NEAR_DUPLICATE_CONTENTS_KEY); 103 int resultsCount = duplicatesMap.size() + nearDuplicatesMap.size(); 104 Map<String, I18nizableTextParameter> i18nParameters = new HashMap<>(); 105 i18nParameters.put("resultsCount", new I18nizableText(String.valueOf(resultsCount))); 106 107 return new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_SUBJECT", i18nParameters); 108 } 109 110 @Override 111 protected boolean _isMailBodyInHTML(JobExecutionContext context) 112 { 113 return true; 114 } 115 116 @Override 117 protected String _getSuccessMailBody(JobExecutionContext context) throws Exception 118 { 119 @SuppressWarnings("unchecked") 120 Map<Content, List<Content>> duplicatesMap = (Map<Content, List<Content>>) context.get(DuplicateContentsManager.DUPLICATE_CONTENTS_KEY); 121 @SuppressWarnings("unchecked") 122 Map<Content, List<Content>> nearDuplicatesMap = (Map<Content, List<Content>>) context.get(DuplicateContentsManager.NEAR_DUPLICATE_CONTENTS_KEY); 123 @SuppressWarnings("unchecked") 124 Map<Content, DuplicateContentsManager.Status> tooComplexMap = (Map<Content, DuplicateContentsManager.Status>) context.get(DuplicateContentsManager.STATUS_KEY); 125 126 boolean hasConfigurationErrors = (boolean) context.get(_HAS_CONFIGURATION_ERRORS); 127 128 return _createBody(duplicatesMap, nearDuplicatesMap, tooComplexMap, hasConfigurationErrors); 129 130 } 131 132 /** 133 * Create the body of the email 134 * @param duplicatesMap map linking a content to his duplicates 135 * @param nearDuplicatesMap map linking a content to his near duplicates 136 * @param tooComplexMap map linking a content to his search status (used to know if a query failed because the fuzzy query was too complex for example) 137 * @param hasErrors true if the configuration have some errors 138 * @return the body 139 * @throws IOException if failed to build HTML email body for report 140 */ 141 protected String _createBody(Map<Content, List<Content>> duplicatesMap, Map<Content, List<Content>> nearDuplicatesMap, Map<Content, DuplicateContentsManager.Status> tooComplexMap, boolean hasErrors) throws IOException 142 { 143 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 144 .withTitle(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_BODY_TITLE")); 145 146 // Intro 147 bodyBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_BODY_INTRO")); 148 149 // Duplicate and near duplicate lists 150 if (duplicatesMap.isEmpty() && nearDuplicatesMap.isEmpty() && tooComplexMap.isEmpty()) 151 { 152 bodyBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_BODY_NO_DUPLICATE_CONTENTS")); 153 } 154 else 155 { 156 Map<String, I18nizableTextParameter> introI18nParameters = new HashMap<>(); 157 introI18nParameters.put("duplicateCount", new I18nizableText(String.valueOf(duplicatesMap.size()))); 158 introI18nParameters.put("nearDuplicateCount", new I18nizableText(String.valueOf(nearDuplicatesMap.size()))); 159 160 bodyBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_BODY_DETECTED", introI18nParameters)); 161 162 bodyBuilder.addMessage(_displayContents(duplicatesMap, nearDuplicatesMap, tooComplexMap)); 163 164 } 165 if (hasErrors) 166 { 167 bodyBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_FOOTER_ERROR_OR_WARN")); 168 } 169 170 return bodyBuilder.build(); 171 } 172 173 /** 174 * Create StringBuilder used to display contents 175 * @param duplicatesMap map linking a content to his duplicates 176 * @param nearDuplicatesMap map linking a content to his near duplicates 177 * @param tooComplexMap map linking a content to his search status (used to know if a query failed because the fuzzy query was too complex for example) 178 * @return the StringBuilder used to display contents 179 */ 180 protected String _displayContents(Map<Content, List<Content>> duplicatesMap, Map<Content, List<Content>> nearDuplicatesMap, 181 Map<Content, DuplicateContentsManager.Status> tooComplexMap) 182 { 183 Set<String> contentTypes = _duplicatesManager.getContentTypeIds(); 184 185 StringBuilder body = new StringBuilder(); 186 body.append("<ul>"); 187 for (String contentTypeId : contentTypes) 188 { 189 Set<Content> tooComplexMapKeySet = tooComplexMap.entrySet() 190 .stream() 191 .filter(entry -> entry.getValue().equals(Status.TOO_COMPLEX)) 192 .map(Entry::getKey) 193 .collect(Collectors.toSet()); 194 195 Set<Content> allMapKeySet = Stream.of(duplicatesMap.keySet(), nearDuplicatesMap.keySet(), tooComplexMapKeySet) 196 .flatMap(Collection::stream) 197 .filter(content -> _contentTypesHelper.isInstanceOf(content, contentTypeId)) 198 .sorted(Comparator.comparing(Content::getTitle, String.CASE_INSENSITIVE_ORDER)) 199 .collect(Collectors.toCollection(LinkedHashSet::new)); 200 if (!allMapKeySet.isEmpty()) 201 { 202 body.append("<li>"); 203 ContentType contentType = _cTypeEP.getExtension(contentTypeId); 204 body.append(_i18nUtils.translate(contentType.getLabel())); 205 body.append("<ul>"); 206 // Duplicate list 207 for (Content content : allMapKeySet) 208 { 209 body.append(_displayDuplicateContent(duplicatesMap, nearDuplicatesMap, tooComplexMap, content)); 210 211 // Remove contents from maps, in case of contents with multiple content types appear twice 212 duplicatesMap.remove(content); 213 nearDuplicatesMap.remove(content); 214 tooComplexMap.remove(content); 215 } 216 body.append("</ul>"); 217 body.append("</li>"); 218 } 219 220 } 221 body.append("</ul>"); 222 return body.toString(); 223 } 224 225 /** 226 * Create StringBuilder used to display a content 227 * @param duplicatesMap map linking a content to his duplicates 228 * @param nearDuplicatesMap map linking a content to his near duplicates 229 * @param tooComplexMap map linking a content to his search status (used to know if a query failed because the fuzzy query was too complex for example) 230 * @param content the content 231 * @return the StringBuilder used to display a content 232 */ 233 protected StringBuilder _displayDuplicateContent(Map<Content, List<Content>> duplicatesMap, Map<Content, List<Content>> nearDuplicatesMap, 234 Map<Content, DuplicateContentsManager.Status> tooComplexMap, Content content) 235 { 236 StringBuilder body = new StringBuilder(); 237 body.append("<li>"); 238 body.append(_renderContentTitle(content)); 239 240 body.append("<ul>"); 241 List<Content> duplicates = duplicatesMap.get(content); 242 if (duplicates != null) 243 { 244 body.append("<li>"); 245 body.append(_i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_BODY_LIST_DUPLICATE_CONTENTS"))); 246 body.append("<ul>"); 247 for (Content duplicate : duplicates) 248 { 249 body.append("<li>"); 250 body.append(_renderContentTitle(duplicate)); 251 body.append("</li>"); 252 } 253 body.append("</ul>"); 254 body.append("</li>"); 255 } 256 257 List<Content> nearDuplicates = nearDuplicatesMap.get(content); 258 if (nearDuplicates != null) 259 { 260 body.append("<li>"); 261 body.append(_i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_BODY_LIST_NEAR_DUPLICATE_CONTENTS"))); 262 body.append("<ul>"); 263 for (Content nearDuplicate : nearDuplicates) 264 { 265 body.append("<li>"); 266 body.append(_renderContentTitle(nearDuplicate)); 267 body.append("</li>"); 268 } 269 body.append("</ul>"); 270 body.append("</li>"); 271 } 272 273 if (tooComplexMap.get(content) == Status.TOO_COMPLEX) 274 { 275 body.append("<li>"); 276 body.append(_i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_BODY_TOO_COMPLEX"))); 277 body.append("</li>"); 278 } 279 280 body.append("</ul>"); 281 body.append("</li>"); 282 return body; 283 } 284 285 /** 286 * Render the content title 287 * @param content The content 288 * @return the link 289 */ 290 protected String _renderContentTitle(Content content) 291 { 292 return _renderCmsToolLink(content); 293 } 294 295 /** 296 * Render the cms tool link 297 * @param content The content 298 * @return the link 299 */ 300 protected String _renderCmsToolLink(Content content) 301 { 302 String contentBOUrl = _contentHelper.getContentBOUrl(content, Map.of()); 303 return "<a href=\"" + contentBOUrl + "\">" + content.getTitle() + "</a>"; 304 } 305 306 @Override 307 protected I18nizableText _getErrorMailSubject(JobExecutionContext context) throws Exception 308 { 309 return new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_ERROR_MAIL_SUBJECT"); 310 } 311 312 @Override 313 protected String _getErrorMailBody(JobExecutionContext context, Throwable throwable) throws Exception 314 { 315 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 316 .withTitle(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_ERROR_MAIL_BODY_TITLE")); 317 318 if ((boolean) context.get(DuplicateContentsManager.NO_DUPLICATE_CONTENTS_CONTENT_TYPE_KEY)) 319 { 320 bodyBuilder.withMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_MAIL_BODY_NO_CONTENT_TYPE")); 321 } 322 else 323 { 324 bodyBuilder.withMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUPLICATE_CONTENTS_GLOBAL_DETECTION_ERROR_MAIL_BODY")); 325 } 326 327 return bodyBuilder.build(); 328 } 329}