001/*
002 *  Copyright 2017 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.odf.clientsideelement;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Map;
023
024import javax.jcr.RepositoryException;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028
029import org.ametys.cms.repository.Content;
030import org.ametys.cms.repository.WorkflowAwareContent;
031import org.ametys.core.observation.Event;
032import org.ametys.core.observation.ObservationManager;
033import org.ametys.core.ui.Callable;
034import org.ametys.odf.EducationalPathHelper;
035import org.ametys.odf.EducationalPathHelper.EducationalPathReference;
036import org.ametys.odf.ODFHelper;
037import org.ametys.odf.ProgramItem;
038import org.ametys.odf.observation.OdfObservationConstants;
039import org.ametys.runtime.i18n.I18nizableText;
040import org.ametys.runtime.model.ModelHelper;
041
042/**
043 * Set the attribute of type 'content' of a content, with another content.
044 * If content is a {@link ProgramItem}, additional check will be done on catalog and content language
045 */
046public class SetContentAttributeClientSideElement extends org.ametys.cms.clientsideelement.relations.SetContentAttributeClientSideElement
047{
048    /** The ODF helper */
049    protected ODFHelper _odfHelper;
050    /** The educational path helper */
051    protected EducationalPathHelper _educationalHelper;
052    /** The observation manager */
053    protected ObservationManager _observationManager;
054    
055    @Override
056    public void service(ServiceManager manager) throws ServiceException
057    {
058        super.service(manager);
059        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
060        _educationalHelper = (EducationalPathHelper) manager.lookup(EducationalPathHelper.ROLE);
061        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
062    }
063    
064    @Override
065    @Callable
066    public Map<String, Object> setContentAttribute(List<String> contentIdsToReference, Map<String, Integer> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove,
067            String attributePath, List<String> workflowActionIds, Map<String, Object> additionalParams)
068    {
069        Map<String, Object> result = super.setContentAttribute(contentIdsToReference, contentIdsToEdit, contentsToEditToRemove, attributePath, workflowActionIds, additionalParams);
070        
071        if (Boolean.TRUE.equals(result.get("success")))
072        {
073            // Notify observers for educational paths that were changed or removed
074            @SuppressWarnings("unchecked")
075            List<String> contentsInError = result.containsKey("errorIds") ? (List<String>) result.get("errorIds") : List.of();
076            
077            _notifyObserverIfHierarchyChanged(contentIdsToReference, contentIdsToEdit, contentsToEditToRemove, contentsInError);
078        }
079        
080        return result;
081    }
082    
083    /**
084     * Notify observers if the hierarchy of program item has been changed
085     * @param contentIdsToReference The list of content identifiers that will be added as values in the content field
086     * @param contentIdsToEdit The map {key: content identifiers to edit and that will have an attribute of type content modified; value: the new position if attribute is multiple and it is a reorder of values. May be null or equals to -1 if it is not a reorder}
087     * @param contentsToEditToRemove  The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove"
088     * @param contentsInError the ids of contents in error
089     */
090    protected void _notifyObserverIfHierarchyChanged(List<String> contentIdsToReference, Map<String, Integer> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, List<String> contentsInError)
091    {
092        if (!contentsToEditToRemove.isEmpty())
093        {
094            // 'move' or 'remove' mode
095            for (Map<String, String> removeObject : contentsToEditToRemove)
096            {
097                String contentIdToEdit = removeObject.get("contentId");
098                String valueToRemove = removeObject.get("valueToRemove");
099                
100                if (!contentsInError.contains(contentIdToEdit))
101                {
102                    Content removedContent = (Content) _resolver.resolveById(valueToRemove);
103                    Content contentToEdit = (Content) _resolver.resolveById(contentIdToEdit);
104                    
105                    if (removedContent instanceof ProgramItem programItem && contentToEdit instanceof ProgramItem oldParentProgramItem)
106                    {
107                        // A program item has been detached from its parent
108                        Map<String, Object> eventParams = new HashMap<>();
109                        eventParams.put(OdfObservationConstants.ARGS_PROGRAM_ITEM, programItem);
110                        eventParams.put(OdfObservationConstants.ARGS_PROGRAM_ITEM_ID, programItem.getId());
111                        eventParams.put(OdfObservationConstants.ARGS_OLD_PARENT_PROGRAM_ITEM_ID, oldParentProgramItem.getId());
112                        
113                        if (contentIdsToReference.contains(valueToRemove))
114                        {
115                            // move case: the program item has been detached and appended to new parent(s)
116                            List<String> newParentProgramItems = contentIdsToEdit.entrySet().stream()
117                                .filter(e -> e.getValue() == null || e.getValue() == -1) // ignore reorder
118                                .map(e -> e.getKey())
119                                .filter(id -> !contentsInError.contains(id))
120                                .map(id -> _resolver.resolveById(id))
121                                .filter(ProgramItem.class::isInstance)
122                                .map(ProgramItem.class::cast)
123                                .map(ProgramItem::getId)
124                                .toList();
125                            
126                            if (!newParentProgramItems.isEmpty())
127                            {
128                                eventParams.put(OdfObservationConstants.ARGS_NEW_PARENT_PROGRAM_ITEM_IDS, newParentProgramItems);
129                            }
130                        }
131                        
132                        _observationManager.notify(new Event(OdfObservationConstants.EVENT_PROGRAM_ITEM_HIERARCHY_CHANGED, _currentUserProvider.getUser(), eventParams));
133                    }
134                }
135            }
136        }
137        else if (!contentIdsToReference.isEmpty())
138        {
139            // 'append' mode
140            for (String contentIdToReference : contentIdsToReference)
141            {
142                Content appendedContent = (Content) _resolver.resolveById(contentIdToReference);
143                
144                if (appendedContent instanceof ProgramItem programItem)
145                {
146                    List<String> newParentProgramItems = contentIdsToEdit.entrySet().stream()
147                            .filter(e -> e.getValue() == null || e.getValue() == -1) // ignore reorder
148                            .map(e -> e.getKey())
149                            .filter(id -> !contentsInError.contains(id))
150                            .map(id -> _resolver.resolveById(id))
151                            .filter(ProgramItem.class::isInstance)
152                            .map(ProgramItem.class::cast)
153                            .map(ProgramItem::getId)
154                            .toList();
155                    
156                    if (!newParentProgramItems.isEmpty())
157                    {
158                        // The program item has been appended to new parent(s)
159                        Map<String, Object> eventParams = new HashMap<>();
160                        eventParams.put(OdfObservationConstants.ARGS_PROGRAM_ITEM, programItem);
161                        eventParams.put(OdfObservationConstants.ARGS_NEW_PARENT_PROGRAM_ITEM_IDS, newParentProgramItems);
162                        
163                        _observationManager.notify(new Event(OdfObservationConstants.EVENT_PROGRAM_ITEM_HIERARCHY_CHANGED, _currentUserProvider.getUser(), eventParams));
164                    }
165                }
166            }
167        }
168    }
169    
170    /**
171     * Get the educational paths that reference the program items to removed
172     * @param contentIdsToReference The list of content identifiers that will be added as values in the content field
173     * @param contentsToEditToRemove  The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove"
174     * @return a Map with program items referencing the given program items in a educational path attribute as key and the list of educational path references as value
175     */
176    @Callable
177    public Map<String, Object> getEducationalPathReferences(List<String> contentIdsToReference, List<Map<String, String>> contentsToEditToRemove)
178    {
179        Map<String, Object> result = new HashMap<>();
180        
181        for (Map<String, String> removeObject : contentsToEditToRemove)
182        {
183            String contentIdToEdit = removeObject.get("contentId");
184            String valueToRemove = removeObject.get("valueToRemove");
185            
186            Content contentToEdit = (Content) _resolver.resolveById(contentIdToEdit);
187            Content contentToRemove = (Content) _resolver.resolveById(valueToRemove);
188            
189            if (contentToEdit instanceof ProgramItem oldParentProgramItem && contentToRemove instanceof ProgramItem programItemToRemove)
190            {
191                result.put("contentToRemove", Map.of("id", contentToRemove.getId(), "title", contentToRemove.getTitle()));
192                
193                List<Map<String, Object>> references2json = new ArrayList<>();
194                
195                try
196                {
197                    // When a program item is detached from its parent, all educational paths refering it AND its old parent become obsolete
198                    Map<ProgramItem, List<EducationalPathReference>> educationalPathReferences = _educationalHelper.getEducationalPathReferences(programItemToRemove, oldParentProgramItem);
199                    for (ProgramItem programItem : educationalPathReferences.keySet())
200                    {
201                        Map<String, Object> json = new HashMap<>();
202                        json.put("id", programItem.getId());
203                        json.put("code", programItem.getCode());
204                        json.put("title", ((Content) programItem).getTitle());
205                        
206                        List<Map<String, Object>> edutionalPathsToJson = new ArrayList<>();
207                        List<EducationalPathReference> references = educationalPathReferences.get(programItem);
208                        for (EducationalPathReference reference : references)
209                        {
210                            String repeaterDefinitionPath = ModelHelper.getDefinitionPathFromDataPath(reference.repeaterEntryPath());
211                            I18nizableText repeaterLabel = ((Content) programItem).getDataHolder().getDefinition(repeaterDefinitionPath).getLabel();
212                            edutionalPathsToJson.add(Map.of("value", reference.value().toString(), "repeaterLabel", repeaterLabel));
213                        }
214                        
215                        json.put("educationalPaths", edutionalPathsToJson);
216                     
217                        references2json.add(json);
218                    }
219                    
220                    result.put("educationalPathReferences", references2json);
221                }
222                catch (RepositoryException e)
223                {
224                    throw new RuntimeException("Unable to get the educational path attributes that refer the moved contents", e);
225                }
226            }
227        }
228        
229        return result;
230    }
231    
232    @Override
233    protected Map<WorkflowAwareContent, Integer> _filterContentsToEdit(Map<WorkflowAwareContent, Integer> contentsToEdit, List<String> contentIdsToReference, List<I18nizableText> errorMessages, List<String> errorIds, Map<String, Object> additionalParams)
234    {
235        Map<WorkflowAwareContent, Integer> validContents = new LinkedHashMap<>();
236        Map<WorkflowAwareContent, Integer> filteredContents = super._filterContentsToEdit(contentsToEdit, contentIdsToReference, errorMessages, errorIds, additionalParams);
237        
238        @SuppressWarnings("unchecked")
239        List<Content> refContents = (List<Content>) _resolve(contentIdsToReference);
240        
241        for (Map.Entry<WorkflowAwareContent, Integer> contentEntry : filteredContents.entrySet())
242        {
243            WorkflowAwareContent content = contentEntry.getKey();
244            
245            for (Content refContent : refContents)
246            {
247                if (!_odfHelper.isRelationCompatible(refContent, content, errorMessages, additionalParams))
248                {
249                    errorIds.add(content.getId());
250                }
251                else
252                {
253                    validContents.put(content, contentEntry.getValue());
254                }
255            }
256        }
257        
258        return validContents;
259    }
260    
261}