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}