001/* 002 * Copyright 2024 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.HashMap; 019import java.util.HashSet; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023import java.util.Set; 024 025import org.apache.avalon.framework.configuration.Configurable; 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.commons.lang3.StringUtils; 032 033import org.ametys.cms.contenttype.validation.AbstractContentValidator; 034import org.ametys.cms.data.holder.DataHolderDisableConditionsEvaluator; 035import org.ametys.cms.repository.Content; 036import org.ametys.odf.ODFHelper; 037import org.ametys.odf.data.EducationalPath; 038import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 039import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 040import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 041import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 042import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 043import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 044import org.ametys.runtime.i18n.I18nizableText; 045import org.ametys.runtime.i18n.I18nizableTextParameter; 046import org.ametys.runtime.model.ModelItem; 047import org.ametys.runtime.model.View; 048import org.ametys.runtime.model.disableconditions.DisableConditionsEvaluator; 049import org.ametys.runtime.parameter.ValidationResult; 050 051/** 052 * This global validation checks if the values of educational path attributs of a repeater entries are unique.<br> 053 * <repeaterPath>, <educationalPathDataName> and <errorI18nKey> configuration are expected. 054 */ 055public class RepeaterWithEducationalPathValidator extends AbstractContentValidator implements Serviceable, Configurable 056{ 057 private DisableConditionsEvaluator<ModelAwareDataHolder> _disableConditionsEvaluator; 058 private String _repeaterPath; 059 private String _educationalPathAttribute; 060 private ODFHelper _odfHelper; 061 private String _errorI18nKey; 062 063 @SuppressWarnings("unchecked") 064 public void service(ServiceManager manager) throws ServiceException 065 { 066 _disableConditionsEvaluator = (DisableConditionsEvaluator<ModelAwareDataHolder>) manager.lookup(DataHolderDisableConditionsEvaluator.ROLE); 067 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 068 } 069 070 public void configure(Configuration configuration) throws ConfigurationException 071 { 072 _repeaterPath = configuration.getChild("repeaterPath").getValue(); 073 _educationalPathAttribute = configuration.getChild("educationalPathAttribute").getValue(); 074 _errorI18nKey = configuration.getChild("errorI18nKey").getValue(); 075 } 076 077 @Override 078 public ValidationResult validate(Content content) 079 { 080 ValidationResult result = new ValidationResult(); 081 082 ModelItem repeaterDefinition = content.getDefinition(_repeaterPath); 083 084 if (content.hasValue(_repeaterPath) && !_disableConditionsEvaluator.evaluateDisableConditions(repeaterDefinition, _repeaterPath, content)) 085 { 086 Set<EducationalPath> paths = new HashSet<>(); 087 088 ModelAwareRepeater repeater = content.getRepeater(_repeaterPath); 089 090 ModelItem contentDataDefinition = content.getDefinition(_repeaterPath + ModelItem.ITEM_PATH_SEPARATOR + _educationalPathAttribute); 091 092 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 093 { 094 String dataPath = _repeaterPath + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR + _educationalPathAttribute; 095 if (!_disableConditionsEvaluator.evaluateDisableConditions(contentDataDefinition, dataPath, content)) 096 { 097 EducationalPath path = entry.getValue(_educationalPathAttribute); 098 099 if (path != null && !paths.add(path)) 100 { 101 // Two entries or more have the same value for education path 102 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 103 i18nParams.put("path", new I18nizableText(_odfHelper.getEducationalPathAsString(path))); 104 result.addError(new I18nizableText(StringUtils.substringBefore(_errorI18nKey, ':'), StringUtils.substringAfter(_errorI18nKey, ':'), i18nParams)); 105 break; // stop iteration 106 } 107 } 108 } 109 } 110 111 return result; 112 } 113 114 @Override 115 public ValidationResult validate(Content content, Map<String, Object> values, View view) 116 { 117 ValidationResult result = new ValidationResult(); 118 119 Object repeater = values.get(_repeaterPath); 120 121 List<Map<String, Object>> repeaterEntries = repeater instanceof SynchronizableRepeater synchronizableRepeater 122 ? synchronizableRepeater.getEntries() 123 : List.of(); // repeater can be an UntouchedValue, if it is disable or non-writable 124 125 if (!repeaterEntries.isEmpty()) 126 { 127 Set<EducationalPath> paths = new HashSet<>(); 128 ModelItem modelItem = content.getDefinition(_repeaterPath + ModelItem.ITEM_PATH_SEPARATOR + _educationalPathAttribute); 129 130 for (int i = 0; i < repeaterEntries.size(); i++) 131 { 132 Map<String, Object> entry = repeaterEntries.get(i); 133 int position = i + 1; 134 135 Optional<String> oldDataPath = Optional.of(repeaterEntries) 136 .filter(SynchronizableRepeater.class::isInstance) 137 .map(SynchronizableRepeater.class::cast) 138 .flatMap(syncRepeater -> syncRepeater.getPreviousPosition(position)) 139 .map(previousPosition -> _repeaterPath + "[" + previousPosition + "]" + ModelItem.ITEM_PATH_SEPARATOR + _educationalPathAttribute); 140 141 EducationalPath path = Optional.ofNullable(_educationalPathAttribute) 142 .map(entry::get) 143 .map(value -> DataHolderHelper.getValueFromSynchronizableValue(value, content, modelItem, oldDataPath, SynchronizationContext.newInstance())) 144 .filter(EducationalPath.class::isInstance) 145 .map(EducationalPath.class::cast) 146 .orElse(null); // value can be an UntouchedValue, if it is disabled or non-writable 147 148 if (path != null && !paths.add(path)) 149 { 150 // Two entries or more have the same value for education path 151 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 152 i18nParams.put("path", new I18nizableText(_odfHelper.getEducationalPathAsString(path))); 153 result.addError(new I18nizableText(StringUtils.substringBefore(_errorI18nKey, ':'), StringUtils.substringAfter(_errorI18nKey, ':'), i18nParams)); 154 break; // stop iteration 155 } 156 } 157 } 158 159 return result; 160 } 161 162}