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