001/* 002 * Copyright 2017 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.odf.workflow; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.TreeSet; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.lang3.ArrayUtils; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.repository.WorkflowAwareContent; 038import org.ametys.cms.workflow.ContentWorkflowHelper; 039import org.ametys.core.ui.Callable; 040import org.ametys.odf.ODFHelper; 041import org.ametys.odf.ProgramItem; 042import org.ametys.odf.course.Course; 043import org.ametys.odf.course.CourseFactory; 044import org.ametys.odf.courselist.CourseListFactory; 045import org.ametys.odf.orgunit.OrgUnit; 046import org.ametys.odf.orgunit.OrgUnitFactory; 047import org.ametys.odf.person.PersonFactory; 048import org.ametys.odf.program.AbstractProgram; 049import org.ametys.odf.program.ContainerFactory; 050import org.ametys.odf.program.ProgramFactory; 051import org.ametys.odf.program.SubProgramFactory; 052import org.ametys.plugins.repository.AmetysObjectResolver; 053import org.ametys.plugins.repository.UnknownAmetysObjectException; 054import org.ametys.plugins.workflow.support.WorkflowProvider; 055import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 056import org.ametys.runtime.plugin.component.AbstractLogEnabled; 057 058import com.opensymphony.workflow.InvalidActionException; 059import com.opensymphony.workflow.WorkflowException; 060import com.opensymphony.workflow.spi.Step; 061 062/** 063 * Helper for ODF contents on their workflow 064 * 065 */ 066public class ODFWorkflowHelper extends AbstractLogEnabled implements Component, Serviceable 067{ 068 /** The component role. */ 069 public static final String ROLE = ODFWorkflowHelper.class.getName(); 070 071 /** The validate step id */ 072 public static final int VALIDATED_STEP_ID = 3; 073 074 /** Constant for storing the result map into the transient variables map. */ 075 protected static final String CONTENTS_IN_ERROR_KEY = "contentsInError"; 076 077 /** Constant for storing the result map into the transient variables map. */ 078 protected static final String VALIDATED_CONTENTS_KEY = "validatedContents"; 079 080 /** The Ametys object resolver */ 081 protected AmetysObjectResolver _resolver; 082 /** The workflow provider */ 083 protected WorkflowProvider _workflowProvider; 084 /** The ODF helper */ 085 protected ODFHelper _odfHelper; 086 /** The workflow helper for contents */ 087 protected ContentWorkflowHelper _contentWorkflowHelper; 088 089 @Override 090 public void service(ServiceManager manager) throws ServiceException 091 { 092 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 093 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 094 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 095 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 096 } 097 098 /** 099 * Check if the contents can be validated. A content can be validated only if all its referenced contents are already validated. 100 * @param contentIds The id of contents to check 101 * @return A map with success key to true if all contents can be validated. A map with the invalidated contents otherwise. 102 */ 103 @Callable 104 public Map<String, Object> canValidContent(List<String> contentIds) 105 { 106 Map<String, Object> result = new HashMap<>(); 107 108 List<Map<String, Object>> contentsInError = new ArrayList<>(); 109 110 for (String contentId : contentIds) 111 { 112 Set<Content> invalidatedContents = new TreeSet<>(new ContentTypeComparator()); 113 114 WorkflowAwareContent content = _resolver.resolveById(contentId); 115 _checkValidateStep(content, invalidatedContents); 116 117 // Remove initial content from invalidated contents 118 invalidatedContents.remove(content); 119 120 if (!invalidatedContents.isEmpty()) 121 { 122 List<Map<String, Object>> invalidatedContentsAsJson = invalidatedContents.stream() 123 .map(c -> _content2Json(c)) 124 .collect(Collectors.toList()); 125 126 Map<String, Object> contentInError = new HashMap<>(); 127 contentInError.put("id", content.getId()); 128 contentInError.put("code", ((ProgramItem) content).getCode()); 129 contentInError.put("title", content.getTitle()); 130 contentInError.put("invalidatedContents", invalidatedContentsAsJson); 131 contentsInError.add(contentInError); 132 } 133 } 134 135 result.put("contentsInError", contentsInError); 136 result.put("success", contentsInError.isEmpty()); 137 return result; 138 } 139 140 /** 141 * Get the global validation status of a content 142 * @param contentId the id of content 143 * @return the result 144 */ 145 @Callable 146 public Map<String, Object> getGlobalValidationStatus(String contentId) 147 { 148 Map<String, Object> result = new HashMap<>(); 149 150 WorkflowAwareContent waContent = _resolver.resolveById(contentId); 151 152 // Order invalidated contents by types 153 Set<Content> invalidatedContents = getInvalidatedContents(waContent); 154 155 List<Map<String, Object>> invalidatedContentsAsJson = invalidatedContents.stream() 156 .map(c -> _content2Json(c)) 157 .collect(Collectors.toList()); 158 159 result.put("invalidatedContents", invalidatedContentsAsJson); 160 result.put("globalValidated", invalidatedContents.isEmpty()); 161 162 return result; 163 } 164 165 /** 166 * Get the invalidated contents referenced by a ODF content 167 * @param content the initial ODF content 168 * @return the set of referenced invalidated contents 169 */ 170 public Set<Content> getInvalidatedContents(WorkflowAwareContent content) 171 { 172 Set<Content> invalidatedContents = new TreeSet<>(new ContentTypeComparator()); 173 174 _checkValidateStep(content, invalidatedContents); 175 176 return invalidatedContents; 177 } 178 179 /** 180 * Determines if a content is already in validated step 181 * @param content The content to test 182 * @return true if the content is already validated 183 */ 184 public boolean isInValidatedStep (WorkflowAwareContent content) 185 { 186 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content); 187 long workflowId = content.getWorkflowId(); 188 189 List<Step> steps = workflow.getCurrentSteps(workflowId); 190 for (Step step : steps) 191 { 192 if (step.getStepId() == VALIDATED_STEP_ID) 193 { 194 return true; 195 } 196 } 197 198 return false; 199 } 200 201 private void _checkValidateStep (WorkflowAwareContent content, Set<Content> invalidatedContents) 202 { 203 if (!isInValidatedStep(content)) 204 { 205 invalidatedContents.add(content); 206 } 207 208 if (content instanceof ProgramItem) 209 { 210 // Check the structure recursively 211 List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content); 212 for (ProgramItem child : children) 213 { 214 WorkflowAwareContent waChild = (WorkflowAwareContent) child; 215 _checkValidateStep(waChild, invalidatedContents); 216 } 217 } 218 219 // Validate others referenced contents 220 if (content instanceof AbstractProgram) 221 { 222 _checkReferencedContents(((AbstractProgram) content).getOrgUnits(), invalidatedContents); 223 _checkReferencedContents(((AbstractProgram) content).getContacts(), invalidatedContents); 224 } 225 else if (content instanceof Course) 226 { 227 _checkReferencedContents(((Course) content).getOrgUnits(), invalidatedContents); 228 _checkReferencedContents(((Course) content).getContacts(), invalidatedContents); 229 } 230 else if (content instanceof OrgUnit) 231 { 232 _checkReferencedContents(((OrgUnit) content).getContacts(), invalidatedContents); 233 } 234 } 235 236 private void _checkReferencedContents (Collection<String> refContentIds, Set<Content> invalidatedContent) 237 { 238 for (String id : refContentIds) 239 { 240 try 241 { 242 if (StringUtils.isNotEmpty(id)) 243 { 244 WorkflowAwareContent refContent = _resolver.resolveById(id); 245 _checkValidateStep(refContent, invalidatedContent); 246 } 247 } 248 catch (UnknownAmetysObjectException e) 249 { 250 // Nothing 251 } 252 } 253 } 254 255 /** 256 * Global validation on a contents. 257 * Validate the contents with their whole structure and the others referenced contacts and orgunits. 258 * @param contentIds the id of contents to validation recursively 259 * @return the result for each initial contents 260 */ 261 @Callable 262 public Map<String, Object> globalValidate(List<String> contentIds) 263 { 264 Map<String, Object> result = new HashMap<>(); 265 266 for (String contentId : contentIds) 267 { 268 Map<String, Object> contentResult = new HashMap<>(); 269 270 contentResult.put(CONTENTS_IN_ERROR_KEY, new HashSet<Content>()); 271 contentResult.put(VALIDATED_CONTENTS_KEY, new HashSet<String>()); 272 273 WorkflowAwareContent waContent = _resolver.resolveById(contentId); 274 275 _validateRecursively(waContent, contentResult); 276 277 @SuppressWarnings("unchecked") 278 Set<Content> contentsInError = (Set<Content>) contentResult.get(CONTENTS_IN_ERROR_KEY); 279 List<Map<String, Object>> contentsInErrorAsJson = contentsInError.stream() 280 .map(c -> _content2Json(c)) 281 .collect(Collectors.toList()); 282 283 contentResult.put(CONTENTS_IN_ERROR_KEY, contentsInErrorAsJson); 284 285 result.put(contentId, contentResult); 286 } 287 288 return result; 289 } 290 291 /** 292 * Get the JSON representation of the content 293 * @param content the content 294 * @return the content properties 295 */ 296 protected Map<String, Object> _content2Json(Content content) 297 { 298 Map<String, Object> content2json = new HashMap<>(); 299 content2json.put("title", content.getTitle()); 300 content2json.put("id", content.getId()); 301 302 if (content instanceof ProgramItem) 303 { 304 content2json.put("code", ((ProgramItem) content).getCode()); 305 } 306 else if (content instanceof OrgUnit) 307 { 308 content2json.put("code", ((OrgUnit) content).getUAICode()); 309 } 310 311 return content2json; 312 } 313 314 /** 315 * Validate the referenced contents recursively 316 * @param content The validated content 317 * @param result the result object to fill during process 318 */ 319 protected void _validateRecursively (WorkflowAwareContent content, Map<String, Object> result) 320 { 321 @SuppressWarnings("unchecked") 322 Set<String> validatedContentIds = (Set<String>) result.get(VALIDATED_CONTENTS_KEY); 323 @SuppressWarnings("unchecked") 324 Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY); 325 326 if (!isInValidatedStep(content)) 327 { 328 // Validate content itself 329 if (!_doValidateWorkflowAction(content, ValidateODFContentFunction.VALIDATE_ACTION_ID)) 330 { 331 contentsInError.add(content); 332 } 333 else 334 { 335 validatedContentIds.add(content.getId()); 336 } 337 } 338 339 if (content instanceof ProgramItem) 340 { 341 // Validate the structure recursively 342 List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content); 343 for (ProgramItem child : children) 344 { 345 _validateRecursively((WorkflowAwareContent) child, result); 346 } 347 } 348 349 // Validate others referenced contents 350 if (content instanceof AbstractProgram) 351 { 352 _validateReferencedContents(((AbstractProgram) content).getOrgUnits(), result); 353 _validateReferencedContents(((AbstractProgram) content).getContacts(), result); 354 } 355 else if (content instanceof Course) 356 { 357 _validateReferencedContents(((Course) content).getOrgUnits(), result); 358 _validateReferencedContents(((Course) content).getContacts(), result); 359 } 360 else if (content instanceof OrgUnit) 361 { 362 _validateReferencedContents(((OrgUnit) content).getContacts(), result); 363 } 364 } 365 366 /** 367 * Validate the list of referenced contents 368 * @param refContentIds The id of contents to validate 369 * @param result the result object to fill during process 370 */ 371 protected void _validateReferencedContents (Collection<String> refContentIds, Map<String, Object> result) 372 { 373 @SuppressWarnings("unchecked") 374 Set<String> validatedContentIds = (Set<String>) result.get(VALIDATED_CONTENTS_KEY); 375 @SuppressWarnings("unchecked") 376 Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY); 377 378 for (String id : refContentIds) 379 { 380 try 381 { 382 if (StringUtils.isNotEmpty(id)) 383 { 384 WorkflowAwareContent content = _resolver.resolveById(id); 385 if (!isInValidatedStep(content)) 386 { 387 if (!_doValidateWorkflowAction (content, ValidateODFContentFunction.VALIDATE_ACTION_ID)) 388 { 389 contentsInError.add(content); 390 } 391 else 392 { 393 validatedContentIds.add(content.getId()); 394 } 395 } 396 } 397 } 398 catch (UnknownAmetysObjectException e) 399 { 400 // Nothing 401 } 402 } 403 } 404 405 /** 406 * Validate a content 407 * @param content The content to validate 408 * @param actionId The id of validate action 409 * @return true if the validation success 410 */ 411 protected boolean _doValidateWorkflowAction (WorkflowAwareContent content, int actionId) 412 { 413 try 414 { 415 _contentWorkflowHelper.doAction(content, actionId, new HashMap<>()); 416 return true; 417 } 418 catch (InvalidActionException e) 419 { 420 getLogger().warn("Unable to validate content \"{}\" ({}): mandatory metadata are probably missing or the content is locked", content.getTitle(), content.getId(), e); 421 return false; 422 } 423 catch (WorkflowException e) 424 { 425 getLogger().warn("Failed to validate content \"{}\" ({})", content.getTitle(), content.getId(), e); 426 return false; 427 } 428 } 429 430 class ContentTypeComparator implements Comparator<Content> 431 { 432 String[] _orderedContentTypes = new String[] { 433 ProgramFactory.PROGRAM_CONTENT_TYPE, 434 SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, 435 ContainerFactory.CONTAINER_CONTENT_TYPE, 436 CourseListFactory.COURSE_LIST_CONTENT_TYPE, 437 CourseFactory.COURSE_CONTENT_TYPE, 438 OrgUnitFactory.ORGUNIT_CONTENT_TYPE, 439 PersonFactory.PERSON_CONTENT_TYPE 440 }; 441 442 @Override 443 public int compare(Content c1, Content c2) 444 { 445 if (c1 == c2) 446 { 447 return 0; 448 } 449 450 String cTypeId1 = c1.getTypes()[0]; 451 String cTypeId2 = c2.getTypes()[0]; 452 453 int i1 = ArrayUtils.indexOf(_orderedContentTypes, cTypeId1); 454 int i2 = ArrayUtils.indexOf(_orderedContentTypes, cTypeId2); 455 456 if (i1 == i2) 457 { 458 // order by title for content of same type 459 int compareTo = c1.getTitle().compareTo(c2.getTitle()); 460 if (compareTo == 0) 461 { 462 // for content of same title, order by id to do not return 0 to add it in TreeSet 463 // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 464 return c1.getId().compareTo(c2.getId()); 465 } 466 else 467 { 468 return compareTo; 469 } 470 } 471 472 return i1 != -1 && i1 < i2 ? -1 : 1; 473 } 474 } 475 476}