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