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