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}