001/* 002 * Copyright 2010 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.content.consistency; 017 018import java.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026import java.util.concurrent.Callable; 027import java.util.concurrent.CancellationException; 028import java.util.concurrent.ExecutionException; 029import java.util.concurrent.Executor; 030import java.util.concurrent.ExecutorService; 031import java.util.concurrent.Executors; 032import java.util.concurrent.Future; 033import java.util.concurrent.ThreadFactory; 034import java.util.concurrent.atomic.AtomicLong; 035 036import javax.jcr.RepositoryException; 037 038import org.apache.avalon.framework.activity.Initializable; 039import org.apache.avalon.framework.component.Component; 040import org.apache.avalon.framework.context.ContextException; 041import org.apache.avalon.framework.context.Contextualizable; 042import org.apache.avalon.framework.logger.Logger; 043import org.apache.avalon.framework.service.ServiceException; 044import org.apache.avalon.framework.service.ServiceManager; 045import org.apache.avalon.framework.service.Serviceable; 046import org.apache.cocoon.Constants; 047import org.apache.cocoon.environment.Context; 048import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 049 050import org.ametys.cms.content.references.OutgoingReferences; 051import org.ametys.cms.contenttype.ContentType; 052import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 053import org.ametys.cms.repository.Content; 054import org.ametys.cms.repository.ContentQueryHelper; 055import org.ametys.cms.repository.DefaultContent; 056import org.ametys.cms.repository.WorkflowAwareContent; 057import org.ametys.cms.transformation.ConsistencyChecker; 058import org.ametys.cms.transformation.ConsistencyChecker.CHECK; 059import org.ametys.core.engine.BackgroundEngineHelper; 060import org.ametys.core.schedule.progression.ContainerProgressionTracker; 061import org.ametys.core.schedule.progression.SimpleProgressionTracker; 062import org.ametys.core.util.DateUtils; 063import org.ametys.plugins.repository.AmetysObjectIterable; 064import org.ametys.plugins.repository.AmetysObjectResolver; 065import org.ametys.plugins.repository.AmetysRepositoryException; 066import org.ametys.plugins.repository.ModifiableAmetysObject; 067import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 068import org.ametys.plugins.repository.RepositoryConstants; 069import org.ametys.plugins.repository.UnknownAmetysObjectException; 070import org.ametys.plugins.repository.collection.AmetysObjectCollectionFactory; 071import org.ametys.plugins.repository.jcr.NameHelper; 072import org.ametys.plugins.repository.jcr.NameHelper.NameComputationMode; 073import org.ametys.plugins.repository.query.QueryHelper; 074import org.ametys.plugins.repository.query.expression.AndExpression; 075import org.ametys.plugins.repository.query.expression.Expression; 076import org.ametys.plugins.repository.query.expression.Expression.Operator; 077import org.ametys.plugins.repository.query.expression.StringExpression; 078import org.ametys.plugins.repositoryapp.RepositoryProvider; 079import org.ametys.plugins.workflow.support.WorkflowHelper; 080import org.ametys.runtime.model.Model; 081import org.ametys.runtime.model.ModelHelper; 082import org.ametys.runtime.model.View; 083import org.ametys.runtime.model.ViewHelper; 084import org.ametys.runtime.plugin.component.AbstractLogEnabled; 085 086import com.opensymphony.workflow.loader.StepDescriptor; 087 088/** 089 * Manage all operation related to checking the consistency of a content 090 */ 091public class ContentConsistencyManager extends AbstractLogEnabled implements Initializable, Contextualizable, Serviceable, Component 092{ 093 /** The avalon role */ 094 public static final String ROLE = ContentConsistencyManager.class.getName(); 095 096 private static final String __CONSISTENCY_RESULTS_ROOT_NODE_NAME = "consistencyResults"; 097 098 private static ExecutorService __PARALLEL_THREAD_EXECUTOR; 099 private static final int __THREAD_POOL_SIZE_MULTIPLIER = 4; 100 101 /** The ametys object resolver. */ 102 protected AmetysObjectResolver _resolver; 103 104 /** The consistency checker */ 105 protected ConsistencyChecker _consistencyChecker; 106 107 private Context _cocoonContext; 108 private RepositoryProvider _repositoryProvider; 109 private ServiceManager _manager; 110 111 private ContentTypeExtensionPoint _cTypeEP; 112 113 private WorkflowHelper _workflowHelper; 114 115 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 116 { 117 _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 118 } 119 120 public void service(ServiceManager manager) throws ServiceException 121 { 122 _manager = manager; 123 _consistencyChecker = (ConsistencyChecker) manager.lookup(ConsistencyChecker.ROLE); 124 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 125 _repositoryProvider = (RepositoryProvider) manager.lookup(RepositoryProvider.ROLE); 126 _workflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE); 127 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 128 } 129 130 public void initialize() throws Exception 131 { 132 AsyncConsistencyCheckerThreadFactory threadFactory = new AsyncConsistencyCheckerThreadFactory(); 133 // The thread are doing a lot of heavy IO operations, it's worth going over the number of available processors 134 __PARALLEL_THREAD_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * __THREAD_POOL_SIZE_MULTIPLIER, threadFactory); 135 } 136 137 /** 138 * Thread factory for async checker. 139 * Set the thread name format and marks the thread as daemon. 140 */ 141 static class AsyncConsistencyCheckerThreadFactory implements ThreadFactory 142 { 143 private static ThreadFactory _defaultThreadFactory; 144 private static String _nameFormat; 145 private static AtomicLong _count; 146 147 public AsyncConsistencyCheckerThreadFactory() 148 { 149 _defaultThreadFactory = Executors.defaultThreadFactory(); 150 _nameFormat = "ametys-async-consistency-checker-%d"; 151 _count = new AtomicLong(0); 152 } 153 154 public Thread newThread(Runnable r) 155 { 156 Thread thread = _defaultThreadFactory.newThread(r); 157 thread.setName(String.format(_nameFormat, _count.getAndIncrement())); 158 // make the threads low priority daemon to avoid slowing user thread 159 thread.setDaemon(true); 160 thread.setPriority(3); 161 162 return thread; 163 } 164 } 165 166 /** 167 * Getter to provide synthetic access to the manager 168 */ 169 private ServiceManager getManager() 170 { 171 return _manager; 172 } 173 174 /** 175 * Runnable to be used for asynchronous calls 176 */ 177 class AsyncConsistencyChecker implements Callable<String> 178 { 179 /** event to observe */ 180 protected final Logger _logger; 181 private final Content _content; 182 183 public AsyncConsistencyChecker(Content content, org.slf4j.Logger logger) 184 { 185 this._logger = new SLF4JLoggerAdapter(logger); 186 this._content = content; 187 } 188 189 @Override 190 public String call() 191 { 192 Map<String, Object> environmentInformation = null; 193 try 194 { 195 // Create the environment. 196 environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(getManager(), _cocoonContext, _logger); 197 198 return _checkConsistency(_content); 199 } 200 catch (Exception e) 201 { 202 throw new RuntimeException("Content consistency check for content " + _content.getId() + " failed", e); 203 } 204 finally 205 { 206 BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation); 207 } 208 } 209 } 210 211 /** 212 * Check all contents to see if there references are consistent. 213 * All result will also be stored in the consistency result database table for later access. 214 * @param progressionTracker the progression tracker for consistensy check 215 * @return record describing the results of the checks 216 */ 217 public ConsistenciesReport checkAllContents(ContainerProgressionTracker progressionTracker) 218 { 219 // Get the start date to remove outdated results later 220 ZonedDateTime startDate = ZonedDateTime.now(); 221 try (AmetysObjectIterable<Content> contents = _getContents(null)) 222 { 223 ConsistenciesReport checkReport = _checkContents(contents, progressionTracker.getStep(CheckContentConsistencySchedulable.CHECK_CONTENTS)); 224 // the checkContents only remove possible outdated results for the set of content provided as arguments. 225 // We need to remove all the other result : deleted content, deleted link, etc… 226 removeOutdatedResult(startDate, null, progressionTracker.getStep(CheckContentConsistencySchedulable.REMOVE_OUTDATED_RESULT)); 227 return checkReport; 228 } 229 } 230 231 /** 232 * Remove results older than a given date 233 * @param date the threshold 234 * @param filterExpression an expression to filter the content to consider, null to consider all contents 235 * @param progressionTracker the progression tracker for the task 236 */ 237 protected void removeOutdatedResult(ZonedDateTime date, Expression filterExpression, SimpleProgressionTracker progressionTracker) 238 { 239 String xPathQuery = QueryHelper.getXPathQuery(null, ContentConsistencyResult.CONTENT_CONSISTENCY_RESULT_NODETYPE, filterExpression); 240 xPathQuery += "[@ametys:" + ContentConsistencyModel.DATE + " < xs:dateTime('" + DateUtils.zonedDateTimeToString(date) + "')]"; 241 try (AmetysObjectIterable<ContentConsistencyResult> outdatedResults = _resolver.query(xPathQuery)) 242 { 243 progressionTracker.setSize(outdatedResults.getSize()); 244 for (ContentConsistencyResult outdatedResult : outdatedResults) 245 { 246 try 247 { 248 outdatedResult.remove(); 249 outdatedResult.saveChanges(); 250 } 251 catch (AmetysRepositoryException e) 252 { 253 getLogger().warn("Failed to remove outdated result '{}' due to repository error", outdatedResult.getId(), e); 254 } 255 progressionTracker.increment(); 256 } 257 } 258 } 259 260 /** 261 * Record holding the results of a consistency checks on multiple contents 262 * @param results a list of {@link ContentConsistencyResult} for every contents that were checked 263 * @param unchecked a list of content id for every content check that were interrupted, cancelled or failed during execution 264 */ 265 public record ConsistenciesReport(List<String> results, List<String> unchecked) { 266 /** 267 * Returns true if this report contains no elements 268 * @return true if this report contains no elements 269 */ 270 public boolean isEmpty() 271 { 272 return (results == null || results.isEmpty()) 273 && (unchecked == null || unchecked.isEmpty()); 274 } 275 } 276 277 /** 278 * Check all the content provided by the iterable for broken links. 279 * This method will actually provide {@link Callable} to an {@link Executor} that parallelize the checks. 280 * @param contents an iterable of contents to check 281 * @param progressionTracker progression tracker for ckecking conistency in contents 282 * @return record describing the results of the checks 283 */ 284 protected ConsistenciesReport _checkContents(AmetysObjectIterable<Content> contents, SimpleProgressionTracker progressionTracker) 285 { 286 try 287 { 288 List<AsyncConsistencyChecker> checkers = contents.stream() 289 .map(content -> new AsyncConsistencyChecker(content, getLogger())) 290 .toList(); 291 292 // execute all checker and wait for their completion (either success or failure) 293 List<Future<String>> futures = __PARALLEL_THREAD_EXECUTOR.invokeAll(checkers); 294 295 // Refresh the session to retrieve the repository modification from the threads 296 _repositoryProvider.getSession("default").refresh(true); 297 298 Iterator<Future<String>> fIterarot = futures.iterator(); 299 Iterator<Content> cIterator = contents.iterator(); 300 301 // Both iterators should be the same size but we use Math.min() in case it's not 302 long numberOfContents = Math.min(futures.size(), contents.getSize()); 303 progressionTracker.setSize(numberOfContents); 304 305 List<String> done = new ArrayList<>(); 306 List<String> failed = new ArrayList<>(); 307 308 // both iterator should have the same size as the future iterator was mapped from the content iterator 309 while (fIterarot.hasNext() && cIterator.hasNext()) 310 { 311 Future<String> future = fIterarot.next(); 312 Content content = cIterator.next(); 313 314 try 315 { 316 String result = future.get(); 317 if (result != null) 318 { 319 done.add(result); 320 } 321 } 322 catch (CancellationException | InterruptedException | ExecutionException e) 323 { 324 String contentId = content.getId(); 325 getLogger().error("Failed to retrieve result from content consistency checker thread for content {}", contentId, e); 326 failed.add(contentId); 327 } 328 progressionTracker.increment(); 329 } 330 return new ConsistenciesReport(done, failed); 331 } 332 catch (InterruptedException e) 333 { 334 getLogger().error("Content consistency check was interrupted", e); 335 return null; 336 } 337 catch (RepositoryException e1) 338 { 339 getLogger().error("Failed to refresh the session"); 340 return null; 341 } 342 } 343 344 private String _checkConsistency(Content content) 345 { 346 int successCount = 0; 347 int unknownCount = 0; 348 int unauthorizedCount = 0; 349 int notFoundCount = 0; 350 int serverErrorCount = 0; 351 352 Map<String, OutgoingReferences> referencesByPath = content.getOutgoingReferences(); 353 354 for (String dataPath : referencesByPath.keySet()) 355 { 356 OutgoingReferences references = referencesByPath.get(dataPath); 357 for (String referenceType : references.keySet()) 358 { 359 for (String referenceValue : references.get(referenceType)) 360 { 361 CHECK check; 362 try 363 { 364 check = _consistencyChecker.checkConsistency(referenceType, referenceValue, content.getId(), dataPath, false).status(); 365 } 366 catch (Exception e) 367 { 368 check = CHECK.SERVER_ERROR; 369 getLogger().debug("An exception occurred while checking reference value {} at dataPath {} for content {}", referenceType + "#" + referenceValue, dataPath, content.getId(), e); 370 } 371 372 switch (check) 373 { 374 case SUCCESS: 375 successCount++; 376 break; 377 case UNKNOWN: 378 unknownCount++; 379 break; 380 case UNAUTHORIZED: 381 unauthorizedCount++; 382 break; 383 case NOT_FOUND: 384 notFoundCount++; 385 break; 386 case SERVER_ERROR: 387 default: 388 serverErrorCount++; 389 break; 390 } 391 } 392 } 393 } 394 395 // Store the result 396 ContentConsistencyResult result = storeResult((WorkflowAwareContent) content, successCount, unknownCount, unauthorizedCount, notFoundCount, serverErrorCount); 397 398 return result != null ? result.getId() : null; 399 } 400 401 /** 402 * Store the result of the consistency check done on the content 403 * @param content the content checked 404 * @param successCount the number of success 405 * @param unknownCount the number of unknown 406 * @param unauthorizedCount the number of unauthorized 407 * @param notFoundCount the number of not found 408 * @param serverErrorCount the number of server error 409 * @return the result 410 */ 411 protected ContentConsistencyResult storeResult(WorkflowAwareContent content, int successCount, int unknownCount, int unauthorizedCount, int notFoundCount, int serverErrorCount) 412 { 413 // Retrieve a pre existing result for the content or create an new one 414 Optional<ContentConsistencyResult> previousResult = _getExistingResultForContent(content.getId()); 415 if (unknownCount > 0 || unauthorizedCount > 0 || serverErrorCount > 0 || notFoundCount > 0) 416 { 417 ContentConsistencyResult result = previousResult.orElseGet(() -> 418 { 419 ModifiableTraversableAmetysObject root = _getOrCreateRootCollection(); 420 String nodeName = NameHelper.getUniqueAmetysObjectName(root, "consistency-result", NameComputationMode.GENERATED_KEY, false); 421 return root.createChild(nodeName, ContentConsistencyResult.CONTENT_CONSISTENCY_RESULT_NODETYPE); 422 } 423 ); 424 425 Map<String, Object> values = new HashMap<>(); 426 values.put(ContentConsistencyModel.CONTENT_ID, content.getId()); 427 values.put(ContentConsistencyModel.TITLE, content.getTitle()); 428 values.put(ContentConsistencyModel.CONTENT_TYPES, content.getTypes()); 429 values.put(ContentConsistencyModel.CREATION_DATE, content.getCreationDate()); 430 values.put(ContentConsistencyModel.CREATOR, content.getCreator()); 431 Optional.ofNullable(content.getLastMajorValidationDate()).ifPresent(date -> values.put(ContentConsistencyModel.LAST_MAJOR_VALIDATION_DATE, date)); 432 content.getLastMajorValidator().ifPresent(user -> values.put(ContentConsistencyModel.LAST_MAJOR_VALIDATOR, user)); 433 Optional.ofNullable(content.getLastValidationDate()).ifPresent(date -> values.put(ContentConsistencyModel.LAST_VALIDATION_DATE, date)); 434 content.getLastValidator().ifPresent(user -> values.put(ContentConsistencyModel.LAST_VALIDATOR, user)); 435 values.put(ContentConsistencyModel.LAST_CONTRIBUTOR, content.getLastContributor()); 436 values.put(ContentConsistencyModel.LAST_MODIFICATION_DATE, content.getLastModified()); 437 values.put(ContentConsistencyModel.WORKFLOW_STEP, content.getCurrentStepId()); 438 values.put(ContentConsistencyModel.DATE, ZonedDateTime.now()); 439 values.put(ContentConsistencyModel.NOT_FOUND, notFoundCount); 440 values.put(ContentConsistencyModel.SERVER_ERROR, serverErrorCount); 441 values.put(ContentConsistencyModel.SUCCESS, successCount); 442 values.put(ContentConsistencyModel.UNAUTHORIZED, unauthorizedCount); 443 values.put(ContentConsistencyModel.UNKNOWN, unknownCount); 444 445 result.synchronizeValues(values); 446 447 result.saveChanges(); 448 return result; 449 } 450 // Remove old result if there is no error any more 451 else if (previousResult.isPresent()) 452 { 453 ContentConsistencyResult result = previousResult.get(); 454 ModifiableAmetysObject parent = result.getParent(); 455 result.remove(); 456 parent.saveChanges(); 457 } 458 return null; 459 } 460 461 private Optional<ContentConsistencyResult> _getExistingResultForContent(String id) 462 { 463 String xPathQuery = QueryHelper.getXPathQuery(null, ContentConsistencyResult.CONTENT_CONSISTENCY_RESULT_NODETYPE, new StringExpression(ContentConsistencyModel.CONTENT_ID, Operator.EQ, id)); 464 try (AmetysObjectIterable<ContentConsistencyResult> query = _resolver.query(xPathQuery)) 465 { 466 return query.stream().findFirst(); 467 } 468 } 469 470 // Synchronized to avoid creation of multiple root 471 private synchronized ModifiableTraversableAmetysObject _getOrCreateRootCollection() 472 { 473 ModifiableTraversableAmetysObject pluginsRoot = _resolver.resolveByPath("/ametys:plugins"); 474 475 ModifiableTraversableAmetysObject cmsNode = null; 476 if (pluginsRoot.hasChild("cms")) 477 { 478 cmsNode = (ModifiableTraversableAmetysObject) pluginsRoot.getChild("cms"); 479 } 480 else 481 { 482 cmsNode = (ModifiableTraversableAmetysObject) pluginsRoot.createChild("cms", "ametys:unstructured"); 483 } 484 485 ModifiableTraversableAmetysObject resultsCollection = null; 486 if (cmsNode.hasChild(__CONSISTENCY_RESULTS_ROOT_NODE_NAME)) 487 { 488 resultsCollection = (ModifiableTraversableAmetysObject) cmsNode.getChild(__CONSISTENCY_RESULTS_ROOT_NODE_NAME); 489 } 490 else 491 { 492 resultsCollection = (ModifiableTraversableAmetysObject) cmsNode.createChild(__CONSISTENCY_RESULTS_ROOT_NODE_NAME, AmetysObjectCollectionFactory.COLLECTION_NODETYPE); 493 // Save the new node instantly to allow other thread to retrieve the newly created node. 494 cmsNode.saveChanges(); 495 } 496 497 return resultsCollection; 498 } 499 500 /** 501 * Get the contents with inconsistency information. 502 * @param filterExpression an expression to filter content to check, or null to check all contents 503 * @return an iterator on contents. 504 */ 505 protected AmetysObjectIterable<Content> _getContents(Expression filterExpression) 506 { 507 String query = ContentQueryHelper.getContentXPathQuery(filterExpression != null ? new AndExpression(new ConsistencyExpression(), filterExpression) : new ConsistencyExpression()); 508 return _resolver.query(query); 509 } 510 511 /** 512 * Expression which tests if contents have consistency informations. 513 */ 514 public static class ConsistencyExpression implements Expression 515 { 516 public String build() 517 { 518 return new StringBuilder() 519 .append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_ROOT_OUTGOING_REFERENCES) 520 .append('/').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_OUTGOING_REFERENCES) 521 .append("/*") 522 .append("/@").append(RepositoryConstants.NAMESPACE_PREFIX).append(':').append(DefaultContent.METADATA_OUTGOING_REFERENCE_PROPERTY) 523 .toString(); 524 } 525 } 526 527 /** 528 * Serialize a content consistency result as JSON 529 * @param result the results to serialize 530 * @param columns the list of columns to use as "view" 531 * @return a JSON representation of the result 532 */ 533 public Map<String, Object> resultToJSON(ContentConsistencyResult result, List<Map<String, Object>> columns) 534 { 535 // Column can point either to the result object or the linked content. 536 // We need to separate them before serializing 537 538 Collection< ? extends Model> resultModel = result.getModel(); 539 Model[] resultModelArray = resultModel.toArray(size -> new Model[size]); 540 // initialize the view with the content id. Its convenient and we should 541 // always need this value 542 View resultView = View.of(resultModel, ContentConsistencyModel.CONTENT_ID); 543 544 // Do not fail if the content is unavailable, simply ignore the content column 545 Content content = null; 546 Collection<? extends Model> contentModel = null; 547 Model[] contentModelArray = null; 548 View contentView = null; 549 boolean contentViewEmpty = true; 550 try 551 { 552 content = _resolver.resolveById(result.getContentId()); 553 contentModel = content.getModel(); 554 contentModelArray = contentModel.toArray(size -> new Model[size]); 555 contentView = ViewHelper.createEmptyViewItemAccessor(contentModel); 556 } 557 catch (UnknownAmetysObjectException e) 558 { 559 getLogger().warn("Content with id '{}' doesn't exist. Consistency result will be included without content data", result.getContentId(), e); 560 } 561 562 // Separate column in the result view and content view 563 for (Map<String, Object> column : columns) 564 { 565 // if a column is available in the result, use the result value. 566 // else try to use the content 567 String columnItemPath = (String) column.get("path"); 568 if (ModelHelper.hasModelItem(columnItemPath, resultModel)) 569 { 570 ViewHelper.addViewItem(columnItemPath, resultView, resultModelArray); 571 } 572 else if (content != null) 573 { 574 if (ModelHelper.hasModelItem(columnItemPath, contentModel)) 575 { 576 ViewHelper.addViewItem(columnItemPath, contentView, contentModelArray); 577 contentViewEmpty = false; 578 } 579 else 580 { 581 getLogger().info("item path {} is not defined for content {}. It will be ignored in the result.", columnItemPath, content.getId()); 582 } 583 } 584 } 585 586 Map<String, Object> searchResult = result.dataToJSON(resultView); 587 588 // improve serialization for some particular metadata 589 if (resultView.hasModelViewItem(ContentConsistencyModel.WORKFLOW_STEP)) 590 { 591 searchResult.put(ContentConsistencyModel.WORKFLOW_STEP, _workflowStepToJSON(result.getWorkflowStep(), content)); 592 } 593 594 if (resultView.hasModelViewItem(ContentConsistencyModel.CONTENT_TYPES)) 595 { 596 searchResult.put(ContentConsistencyModel.CONTENT_TYPES, _contentTypeToJSON(result.getContentTypes())); 597 } 598 599 if (content != null && !contentViewEmpty) 600 { 601 searchResult.putAll(content.dataToJSON(contentView)); 602 } 603 604 return searchResult; 605 } 606 607 608 private Object _contentTypeToJSON(String[] typeIds) 609 { 610 List<Map<String, Object>> jsonValues = new ArrayList<>(typeIds.length); 611 for (String id : typeIds) 612 { 613 ContentType contentType = _cTypeEP.getExtension(id); 614 Map<String, Object> jsonValue = new HashMap<>(); 615 if (contentType != null) 616 { 617 jsonValue.put("label", contentType.getLabel()); 618 jsonValue.put("smallIcon", contentType.getSmallIcon()); 619 620 String iconGlyph = contentType.getIconGlyph(); 621 if (iconGlyph != null) 622 { 623 jsonValue.put("iconGlyph", iconGlyph); 624 String iconDecorator = contentType.getIconDecorator(); 625 if (iconDecorator != null) 626 { 627 jsonValue.put("iconDecorator", iconDecorator); 628 } 629 } 630 } 631 jsonValues.add(jsonValue); 632 } 633 return jsonValues; 634 } 635 636 private Object _workflowStepToJSON(long value, Content content) 637 { 638 if (content instanceof WorkflowAwareContent) 639 { 640 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 641 int currentStepId = Math.toIntExact(value); 642 643 StepDescriptor stepDescriptor = _workflowHelper.getStepDescriptor(waContent, currentStepId); 644 645 if (stepDescriptor != null) 646 { 647 return _workflowHelper.workflowStep2JSON(stepDescriptor); 648 } 649 } 650 651 return Map.of(); 652 } 653}