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