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}