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.Errors; 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> _childMetadataNames; 063 064 private Set<String> _parentMetadataNames; 065 066 @Override 067 public void service(ServiceManager smanager) throws ServiceException 068 { 069 _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE); 070 } 071 072 @Override 073 public void validate(Content content, Errors errors) 074 { 075 // Nothing 076 } 077 078 public void configure(Configuration configuration) throws ConfigurationException 079 { 080 _childMetadataNames = new HashSet<>(); 081 _parentMetadataNames = new HashSet<>(); 082 083 Configuration[] children = configuration.getChildren("childMetadata"); 084 for (Configuration childConf : children) 085 { 086 _childMetadataNames.add(childConf.getAttribute("name")); 087 } 088 089 children = configuration.getChildren("parentMetadata"); 090 for (Configuration childConf : children) 091 { 092 _parentMetadataNames.add(childConf.getAttribute("name")); 093 } 094 } 095 096 @Override 097 public void validate(Content content, Map<String, Object> values, View view, Errors errors) 098 { 099 List<ProgramItem> newParentProgramItems = new ArrayList<>(); 100 101 // Get the new parents from values 102 Set<String> parentMetadataNames = getParentMetadataNames(); 103 for (String parentMetadataName : parentMetadataNames) 104 { 105 if (view.hasModelViewItem(parentMetadataName) && content instanceof ProgramItem) 106 { 107 ElementDefinition parentDefinition = (ElementDefinition) content.getDefinition(parentMetadataName); 108 Object value = values.get(parentMetadataName); 109 Object[] valueToValidate = (Object[]) DataHolderHelper.getValueFromSynchronizableValue(value, content, parentDefinition, Optional.of(parentMetadataName), SynchronizationContext.newInstance()); 110 111 newParentProgramItems = Optional.ofNullable(valueToValidate) 112 .map(Stream::of) 113 .orElseGet(Stream::of) 114 .map(parentDefinition.getType()::castValue) 115 .map(ContentValue.class::cast) 116 .map(ContentValue::getContent) 117 .filter(ProgramItem.class::isInstance) 118 .map(ProgramItem.class::cast) 119 .collect(Collectors.toList()); 120 121 } 122 } 123 124 Set<String> names = getChildMetadataNames(); 125 for (String name : names) 126 { 127 if (view.hasModelViewItem(name) && content instanceof ProgramItem) 128 { 129 List<String> childContentIds = _odfHelper.getChildProgramItems((ProgramItem) content).stream() 130 .map(ProgramItem::getId) 131 .collect(Collectors.toList()); 132 133 ElementDefinition childDefinition = (ElementDefinition) content.getDefinition(name); 134 135 Object value = values.get(name); 136 Object[] valueToValidate = (Object[]) DataHolderHelper.getValueFromSynchronizableValue(value, content, childDefinition, Optional.of(name), SynchronizationContext.newInstance()); 137 Mode mode = _getMode(value); 138 139 // Check duplicate child and infinite loop is not necessary when value is removed 140 if (Mode.REMOVE != mode) 141 { 142 ContentValue[] contentValues = Optional.ofNullable(valueToValidate) 143 .map(Stream::of) 144 .orElseGet(Stream::of) 145 .map(childDefinition.getType()::castValue) 146 .toArray(ContentValue[]::new); 147 for (ContentValue contentValue : contentValues) 148 { 149 Content childContent = contentValue.getContent(); 150 151 // Check duplicate child only on APPEND mode 152 if (Mode.APPEND == mode) 153 { 154 if (childContentIds.contains(childContent.getId())) 155 { 156 _addError(errors, childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_DUPLICATE_CHILD_ERROR"); 157 } 158 } 159 160 if (childContent instanceof ProgramItem) 161 { 162 if (!checkAncestors((ProgramItem) childContent, newParentProgramItems) || !checkAncestors((ProgramItem) content, (ProgramItem) childContent)) 163 { 164 _addError(errors, childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_HIERARCHY_ERROR"); 165 } 166 } 167 } 168 } 169 } 170 } 171 } 172 173 private Mode _getMode(Object value) 174 { 175 return value instanceof SynchronizableValue ? ((SynchronizableValue) value).getMode() : Mode.REPLACE; 176 } 177 178 /** 179 * Get the names of child metadata to be checked 180 * @return the child metadata's names 181 */ 182 protected Set<String> getChildMetadataNames() 183 { 184 return _childMetadataNames; 185 } 186 187 /** 188 * Get the names of parent metadata to be checked 189 * @return the parent metadata's names 190 */ 191 protected Set<String> getParentMetadataNames() 192 { 193 return _parentMetadataNames; 194 } 195 196 /** 197 * Check if the hierarchy of a program item will be still valid if adding the given program item as child. 198 * Return false if the {@link ProgramItem} to add is in the hierarchy of the given {@link ProgramItem} 199 * @param programItem The content to start search 200 * @param childProgramItem The child program item to search in ancestors 201 * @return true if child program item is already part of the hierarchy (ascendant search) 202 */ 203 protected boolean checkAncestors(ProgramItem programItem, ProgramItem childProgramItem) 204 { 205 if (programItem.equals(childProgramItem)) 206 { 207 return false; 208 } 209 210 List<? extends ProgramItem> parents = new ArrayList<>(); 211 if (programItem instanceof Course) 212 { 213 parents = ((Course) programItem).getParentCourseLists(); 214 } 215 else if (programItem instanceof CourseList) 216 { 217 parents = ((CourseList) programItem).getParentCourses(); 218 } 219 else if (programItem instanceof ProgramPart) 220 { 221 parents = ((ProgramPart) programItem).getProgramPartParents(); 222 } 223 224 return checkAncestors(childProgramItem, parents); 225 } 226 227 /** 228 * Check if the hierarchy of a program item will be still valid if adding the given program item as child. 229 * Return false if the {@link ProgramItem} to add is in the hierarchy of the given {@link ProgramItem} 230 * @param childProgramItem The child program item to add 231 * @param parentProgramItems The parent program items of the target 232 * @return true if child program item is already part of the hierarchy (ascendant search) 233 */ 234 protected boolean checkAncestors(ProgramItem childProgramItem, List<? extends ProgramItem> parentProgramItems) 235 { 236 for (ProgramItem parent : parentProgramItems) 237 { 238 if (parent.equals(childProgramItem)) 239 { 240 return false; 241 } 242 else if (!checkAncestors(parent, childProgramItem)) 243 { 244 return false; 245 } 246 } 247 248 return true; 249 } 250 251 /** 252 * Add an error 253 * @param errors The list of errors 254 * @param childDefinition The child metadata definition 255 * @param content The content being edited 256 * @param childContent The content to add as child content 257 * @param catalog the i18n catalog 258 * @param i18nKey the i18n key for the error message 259 */ 260 protected void _addError(Errors errors, ElementDefinition childDefinition, Content content, Content childContent, String catalog, String i18nKey) 261 { 262 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 263 i18nParams.put("title", new I18nizableText(content.getTitle())); 264 i18nParams.put("childTitle", new I18nizableText(childContent.getTitle())); 265 i18nParams.put("fieldLabel", childDefinition.getLabel()); 266 errors.addError(new I18nizableText(catalog, i18nKey, i18nParams)); 267 } 268}