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