001/* 002 * Copyright 2018 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.validator; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.List; 022import java.util.Map; 023import java.util.Optional; 024import java.util.Set; 025import java.util.stream.Collectors; 026import java.util.stream.Stream; 027 028import org.apache.avalon.framework.configuration.Configurable; 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034 035import org.ametys.cms.contenttype.validation.AbstractContentValidator; 036import org.ametys.cms.data.ContentValue; 037import org.ametys.cms.repository.Content; 038import org.ametys.odf.ODFHelper; 039import org.ametys.odf.ProgramItem; 040import org.ametys.odf.course.Course; 041import org.ametys.odf.courselist.CourseList; 042import org.ametys.odf.program.ProgramPart; 043import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 044import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 045import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode; 046import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 047import org.ametys.runtime.i18n.I18nizableText; 048import org.ametys.runtime.i18n.I18nizableTextParameter; 049import org.ametys.runtime.model.ElementDefinition; 050import org.ametys.runtime.model.View; 051import org.ametys.runtime.parameter.ValidationResult; 052 053/** 054 * Global validator for {@link ProgramItem} content. 055 * Check that structure to not create an infinite loop 056 */ 057public class ProgramItemHierarchyValidator extends AbstractContentValidator implements Serviceable, Configurable 058{ 059 /** The ODF helper */ 060 protected ODFHelper _odfHelper; 061 062 private Set<String> _childAttributeNames; 063 private Set<String> _parentAttributeNames; 064 065 @Override 066 public void service(ServiceManager smanager) throws ServiceException 067 { 068 _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE); 069 } 070 071 @Override 072 public ValidationResult validate(Content content) 073 { 074 // Nothing 075 return ValidationResult.empty(); 076 } 077 078 public void configure(Configuration configuration) throws ConfigurationException 079 { 080 _childAttributeNames = _configureAttributeNames(configuration, "childAttribute"); 081 _parentAttributeNames = _configureAttributeNames(configuration, "parentAttribute"); 082 } 083 084 private Set<String> _configureAttributeNames(Configuration configuration, String childName) throws ConfigurationException 085 { 086 Set<String> attributeNames = new HashSet<>(); 087 088 Configuration[] children = configuration.getChildren(childName); 089 for (Configuration childConf : children) 090 { 091 attributeNames.add(childConf.getAttribute("name")); 092 } 093 094 return attributeNames; 095 } 096 097 @Override 098 public ValidationResult validate(Content content, Map<String, Object> values, View view) 099 { 100 ValidationResult result = new ValidationResult(); 101 102 List<ProgramItem> newParentProgramItems = new ArrayList<>(); 103 104 // Get the new parents from values 105 Set<String> parentAttributeNames = getParentAttributeNames(); 106 for (String parentAttributeName : parentAttributeNames) 107 { 108 if (view.hasModelViewItem(parentAttributeName) && content instanceof ProgramItem) 109 { 110 ElementDefinition parentDefinition = (ElementDefinition) content.getDefinition(parentAttributeName); 111 Object value = values.get(parentAttributeName); 112 Object[] valueToValidate = (Object[]) DataHolderHelper.getValueFromSynchronizableValue(value, content, parentDefinition, Optional.of(parentAttributeName), SynchronizationContext.newInstance()); 113 114 newParentProgramItems = Optional.ofNullable(valueToValidate) 115 .map(Stream::of) 116 .orElseGet(Stream::of) 117 .map(parentDefinition.getType()::castValue) 118 .map(ContentValue.class::cast) 119 .map(ContentValue::getContent) 120 .filter(ProgramItem.class::isInstance) 121 .map(ProgramItem.class::cast) 122 .collect(Collectors.toList()); 123 124 } 125 } 126 127 Set<String> names = getChildAttributeNames(); 128 for (String name : names) 129 { 130 if (view.hasModelViewItem(name) && content instanceof ProgramItem) 131 { 132 List<String> childContentIds = _odfHelper.getChildProgramItems((ProgramItem) content).stream() 133 .map(ProgramItem::getId) 134 .collect(Collectors.toList()); 135 136 ElementDefinition childDefinition = (ElementDefinition) content.getDefinition(name); 137 138 Object value = values.get(name); 139 Object[] valueToValidate = (Object[]) DataHolderHelper.getValueFromSynchronizableValue(value, content, childDefinition, Optional.of(name), SynchronizationContext.newInstance()); 140 Mode mode = _getMode(value); 141 142 // Check duplicate child and infinite loop is not necessary when value is removed 143 if (Mode.REMOVE != mode) 144 { 145 ContentValue[] contentValues = Optional.ofNullable(valueToValidate) 146 .map(Stream::of) 147 .orElseGet(Stream::of) 148 .map(childDefinition.getType()::castValue) 149 .toArray(ContentValue[]::new); 150 for (ContentValue contentValue : contentValues) 151 { 152 Content childContent = contentValue.getContent(); 153 154 // Check duplicate child only on APPEND mode 155 if (Mode.APPEND == mode) 156 { 157 if (childContentIds.contains(childContent.getId())) 158 { 159 I18nizableText errorMessage = _getError(childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_DUPLICATE_CHILD_ERROR"); 160 result.addError(errorMessage); 161 } 162 } 163 164 if (childContent instanceof ProgramItem) 165 { 166 if (!checkAncestors((ProgramItem) childContent, newParentProgramItems) || !checkAncestors((ProgramItem) content, (ProgramItem) childContent)) 167 { 168 I18nizableText errorMessage = _getError(childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_HIERARCHY_ERROR"); 169 result.addError(errorMessage); 170 } 171 } 172 } 173 } 174 } 175 } 176 177 return result; 178 } 179 180 private Mode _getMode(Object value) 181 { 182 return value instanceof SynchronizableValue ? ((SynchronizableValue) value).getMode() : Mode.REPLACE; 183 } 184 185 /** 186 * Get the names of child attribute to be checked 187 * @return the child attribute's names 188 */ 189 protected Set<String> getChildAttributeNames() 190 { 191 return _childAttributeNames; 192 } 193 194 /** 195 * Get the names of parent attribute to be checked 196 * @return the parent attribute's names 197 */ 198 protected Set<String> getParentAttributeNames() 199 { 200 return _parentAttributeNames; 201 } 202 203 /** 204 * Check if the hierarchy of a program item will be still valid if adding the given program item as child. 205 * Return false if the {@link ProgramItem} to add is in the hierarchy of the given {@link ProgramItem} 206 * @param programItem The content to start search 207 * @param childProgramItem The child program item to search in ancestors 208 * @return true if child program item is already part of the hierarchy (ascendant search) 209 */ 210 protected boolean checkAncestors(ProgramItem programItem, ProgramItem childProgramItem) 211 { 212 if (programItem.equals(childProgramItem)) 213 { 214 return false; 215 } 216 217 List<? extends ProgramItem> parents = new ArrayList<>(); 218 if (programItem instanceof Course) 219 { 220 parents = ((Course) programItem).getParentCourseLists(); 221 } 222 else if (programItem instanceof CourseList) 223 { 224 parents = ((CourseList) programItem).getParentCourses(); 225 } 226 else if (programItem instanceof ProgramPart) 227 { 228 parents = ((ProgramPart) programItem).getProgramPartParents(); 229 } 230 231 return checkAncestors(childProgramItem, parents); 232 } 233 234 /** 235 * Check if the hierarchy of a program item will be still valid if adding the given program item as child. 236 * Return false if the {@link ProgramItem} to add is in the hierarchy of the given {@link ProgramItem} 237 * @param childProgramItem The child program item to add 238 * @param parentProgramItems The parent program items of the target 239 * @return true if child program item is already part of the hierarchy (ascendant search) 240 */ 241 protected boolean checkAncestors(ProgramItem childProgramItem, List<? extends ProgramItem> parentProgramItems) 242 { 243 for (ProgramItem parent : parentProgramItems) 244 { 245 if (parent.equals(childProgramItem)) 246 { 247 return false; 248 } 249 else if (!checkAncestors(parent, childProgramItem)) 250 { 251 return false; 252 } 253 } 254 255 return true; 256 } 257 258 /** 259 * Retrieves an error message 260 * @param childDefinition The child attribute definition 261 * @param content The content being edited 262 * @param childContent The content to add as child content 263 * @param catalog the i18n catalog 264 * @param i18nKey the i18n key for the error message 265 * @return the error message 266 */ 267 protected I18nizableText _getError(ElementDefinition childDefinition, Content content, Content childContent, String catalog, String i18nKey) 268 { 269 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 270 i18nParams.put("title", new I18nizableText(content.getTitle())); 271 i18nParams.put("childTitle", new I18nizableText(childContent.getTitle())); 272 i18nParams.put("fieldLabel", childDefinition.getLabel()); 273 return new I18nizableText(catalog, i18nKey, i18nParams); 274 } 275}