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