001/* 002 * Copyright 2020 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.plugins.repository.data.extractor.xml; 017 018import java.lang.reflect.Array; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026 027import org.apache.commons.lang3.StringUtils; 028import org.apache.commons.lang3.tuple.Pair; 029import org.apache.xpath.XPathAPI; 030import org.w3c.dom.Element; 031 032import org.ametys.core.util.dom.DOMUtils; 033import org.ametys.plugins.repository.data.extractor.ModelAwareValuesExtractor; 034import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 035import org.ametys.plugins.repository.model.CompositeDefinition; 036import org.ametys.plugins.repository.model.RepeaterDefinition; 037import org.ametys.runtime.model.ElementDefinition; 038import org.ametys.runtime.model.Model; 039import org.ametys.runtime.model.ModelHelper; 040import org.ametys.runtime.model.ModelItem; 041import org.ametys.runtime.model.ModelItemContainer; 042import org.ametys.runtime.model.ModelItemGroup; 043import org.ametys.runtime.model.ModelViewItem; 044import org.ametys.runtime.model.ModelViewItemGroup; 045import org.ametys.runtime.model.View; 046import org.ametys.runtime.model.ViewElement; 047import org.ametys.runtime.model.ViewItem; 048import org.ametys.runtime.model.ViewItemContainer; 049import org.ametys.runtime.model.exception.BadDataPathCardinalityException; 050import org.ametys.runtime.model.exception.BadItemTypeException; 051import org.ametys.runtime.model.exception.UndefinedItemPathException; 052import org.ametys.runtime.model.type.ElementType; 053 054/** 055 * This class provides methods to extract values from an XML document, using a model 056 */ 057public class ModelAwareXMLValuesExtractor implements ModelAwareValuesExtractor 058{ 059 /** The DOM element containing the XML values */ 060 protected Element _element; 061 062 /** The getter that retrieves needed additional data by types */ 063 protected XMLValuesExtractorAdditionalDataGetter _additionalDataGetter; 064 065 /** The model of the extracted values */ 066 protected Collection<? extends ModelItemContainer> _modelItemContainers; 067 068 /** 069 * Creates a model aware XML values extractor 070 * @param element the DOM element containing the XML values 071 * @param additionalDataGetter the getter that retrieves needed additional data by types 072 * @param models the model of the extracted values 073 */ 074 public ModelAwareXMLValuesExtractor(Element element, XMLValuesExtractorAdditionalDataGetter additionalDataGetter, Model... models) 075 { 076 this(element, additionalDataGetter, Arrays.asList(models)); 077 } 078 079 /** 080 * Creates a model aware XML values extractor 081 * @param element the DOM element containing the XML values 082 * @param additionalDataGetter the getter that retrieves needed additional data by types 083 * @param modelItemContainers the model of the extracted values 084 */ 085 public ModelAwareXMLValuesExtractor(Element element, XMLValuesExtractorAdditionalDataGetter additionalDataGetter, Collection<? extends ModelItemContainer> modelItemContainers) 086 { 087 _element = element; 088 _additionalDataGetter = additionalDataGetter; 089 _modelItemContainers = modelItemContainers; 090 } 091 092 public Map<String, Object> extractValues() throws Exception 093 { 094 View view = new View(); 095 _fillViewItemContainerFromXML(_element, view, _modelItemContainers); 096 return extractValues(view); 097 } 098 099 /** 100 * Fill the given view item container with the data found in the given element. 101 * @param element The DOM element containing the values 102 * @param viewItemContainer The view item container to fill 103 * @param modelItemContainer The model item containing the items that could be in the element 104 * @throws Exception if an error occurs 105 */ 106 protected void _fillViewItemContainerFromXML(Element element, ViewItemContainer viewItemContainer, ModelItemContainer modelItemContainer) throws Exception 107 { 108 _fillViewItemContainerFromXML(element, viewItemContainer, List.of(modelItemContainer)); 109 } 110 111 /** 112 * Fill the given view item container with the data found in the given element. 113 * @param element The DOM element containing the values 114 * @param viewItemContainer The view item container to fill 115 * @param modelItemContainers The model items containing the items that could be in the element 116 * @throws Exception if an error occurs 117 */ 118 @SuppressWarnings("unchecked") 119 protected void _fillViewItemContainerFromXML(Element element, ViewItemContainer viewItemContainer, Collection<? extends ModelItemContainer> modelItemContainers) throws Exception 120 { 121 List<Element> children = DOMUtils.getUniqueChildElements(element); 122 for (Element child : children) 123 { 124 String dataName = child.getNodeName(); 125 if (ModelHelper.hasModelItem(dataName, modelItemContainers)) 126 { 127 ModelItem modelItem = ModelHelper.getModelItem(dataName, modelItemContainers); 128 ModelViewItem modelViewItem; 129 if (modelItem instanceof ModelItemGroup) 130 { 131 Element groupNode = child; 132 if (modelItem instanceof RepeaterDefinition) 133 { 134 // Use the first entry to create the view - assume that all entries will have the same data 135 groupNode = DOMUtils.getChildElementByTagName(child, "entry"); 136 } 137 138 modelViewItem = new ModelViewItemGroup(); 139 _fillViewItemContainerFromXML(groupNode, (ModelViewItemGroup) modelViewItem, (ModelItemGroup) modelItem); 140 } 141 else 142 { 143 modelViewItem = new ViewElement(); 144 } 145 146 modelViewItem.setDefinition(modelItem); 147 viewItemContainer.addViewItem(modelViewItem); 148 } 149 } 150 151 } 152 153 public Map<String, Object> extractValues(View view) throws Exception 154 { 155 return _extractValues(_element, view, ""); 156 } 157 158 /** 159 * Extracts the values of all items in the given view item container 160 * @param currentElement the DOM element containing the values of the items in the container 161 * @param viewItemContainer the view item container 162 * @param prefix the path of the item represented by the view item container (prefix of all contained items) 163 * @return the values of all items in the given view item containers 164 * @throws Exception if an error occurs 165 */ 166 protected Map<String, Object> _extractValues(Element currentElement, ViewItemContainer viewItemContainer, String prefix) throws Exception 167 { 168 Map<String, Object> values = new HashMap<>(); 169 for (ViewItem viewItem : viewItemContainer.getViewItems()) 170 { 171 if (viewItem instanceof ModelViewItem) 172 { 173 ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition(); 174 String dataName = modelItem.getName(); 175 if (DOMUtils.hasChildElement(currentElement, dataName)) 176 { 177 Object value; 178 if (viewItem instanceof ModelViewItemGroup) 179 { 180 value = _extractGroupValues(currentElement, (ModelViewItemGroup) viewItem, dataName, prefix); 181 } 182 else 183 { 184 value = _extractElementValue(currentElement, (ElementDefinition) modelItem, prefix); 185 } 186 187 values.put(dataName, value); 188 } 189 } 190 else if (viewItem instanceof ViewItemContainer) 191 { 192 values.putAll(_extractValues(currentElement, (ViewItemContainer) viewItem, prefix)); 193 } 194 } 195 196 return values; 197 } 198 199 public <T> T extractValue(String dataPath) throws Exception 200 { 201 // Check that there is an item at the given path 202 if (!ModelHelper.hasModelItem(dataPath, _modelItemContainers)) 203 { 204 throw new UndefinedItemPathException("Unable to retrieve the value at path '" + dataPath + "'. There is no such item defined by the model."); 205 } 206 207 return _extractValue(_modelItemContainers, _element, dataPath, ""); 208 } 209 210 /** 211 * Extracts the value at the given path 212 * @param <T> type of the value to retrieve 213 * @param modelItemContainer The model item containing the item of the value to extract 214 * @param currentElement the DOM element containing the model item container's values 215 * @param relativeDataPath The data path relative to the model item container 216 * @param prefix the path of the item represented by the model item container (prefix of all contained items) 217 * @return the value 218 * @throws Exception if an error occurs 219 */ 220 protected <T> T _extractValue(ModelItemContainer modelItemContainer, Element currentElement, String relativeDataPath, String prefix) throws Exception 221 { 222 return _extractValue(List.of(modelItemContainer), currentElement, relativeDataPath, prefix); 223 } 224 225 /** 226 * Extracts the value at the given path 227 * @param <T> type of the value to retrieve 228 * @param modelItemContainers The model items containing the item of the value to extract 229 * @param currentElement the DOM element containing the model item containers' values 230 * @param relativeDataPath The data path relative to the model item containers 231 * @param prefix the path of the item represented by the model item containers (prefix of all contained items) 232 * @return the value 233 * @throws Exception if an error occurs 234 */ 235 protected <T> T _extractValue(Collection<? extends ModelItemContainer> modelItemContainers, Element currentElement, String relativeDataPath, String prefix) throws Exception 236 { 237 String[] pathSegments = StringUtils.split(relativeDataPath, ModelItem.ITEM_PATH_SEPARATOR); 238 239 if (pathSegments == null || pathSegments.length < 1) 240 { 241 throw new IllegalArgumentException("Unable to extract the value of the data at the given path. This path is empty."); 242 } 243 else if (pathSegments.length == 1) 244 { 245 String dataName = relativeDataPath; 246 ModelItem modelItem = ModelHelper.getModelItem(dataName, modelItemContainers); 247 if (modelItem instanceof ElementDefinition) 248 { 249 return _extractElementValue(currentElement, (ElementDefinition) modelItem, prefix); 250 } 251 else 252 { 253 ModelViewItemGroup modelViewItemGroup = ModelViewItemGroup.of((ModelItemGroup) modelItem); 254 return _extractGroupValues(currentElement, modelViewItemGroup, dataName, prefix); 255 } 256 } 257 else 258 { 259 String firstSegmentDataName = pathSegments[0]; 260 String newRelativeDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 261 String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + firstSegmentDataName : firstSegmentDataName; 262 263 ModelItem modelItem = ModelHelper.getModelItem(firstSegmentDataName, modelItemContainers); 264 265 if (modelItem instanceof RepeaterDefinition) 266 { 267 if (DataHolderHelper.isRepeaterEntryPath(firstSegmentDataName)) 268 { 269 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(firstSegmentDataName); 270 Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(currentElement, repeaterNameAndEntryPosition.getLeft() + "/entry[@name='" + repeaterNameAndEntryPosition.getRight() + "']"); 271 if (repeaterEntryElement != null) 272 { 273 return _extractValue((RepeaterDefinition) modelItem, repeaterEntryElement, newRelativeDataPath, newPrefix); 274 } 275 else 276 { 277 return null; 278 } 279 } 280 else 281 { 282 throw new BadDataPathCardinalityException("Unable to extract the value at path '" + relativeDataPath + "'. The segment '" + pathSegments[0] + "' refers to a repeater but not an entry."); 283 } 284 } 285 else if (modelItem instanceof CompositeDefinition) 286 { 287 Element compositeElement = DOMUtils.getChildElementByTagName(currentElement, firstSegmentDataName); 288 return compositeElement != null ? _extractValue((CompositeDefinition) modelItem, compositeElement, newRelativeDataPath, newPrefix) : null; 289 } 290 else 291 { 292 throw new BadItemTypeException("Unable to extract the value at path '" + relativeDataPath + "'. The segment '" + pathSegments[0] + "' does not represent a group item."); 293 } 294 } 295 } 296 297 /** 298 * Extracts the value of the given element 299 * @param <T> type of the value to retrieve 300 * @param parent the DOM element of the element definition's parent 301 * @param definition the element's definition 302 * @param prefix the path of the element's parent 303 * @return the value 304 * @throws Exception if an error occurs 305 */ 306 @SuppressWarnings("unchecked") 307 protected <T> T _extractElementValue(Element parent, ElementDefinition definition, String prefix) throws Exception 308 { 309 ElementType type = definition.getType(); 310 String dataName = definition.getName(); 311 312 String absoluteDataPath = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName; 313 Optional<Object> additionalData = _additionalDataGetter.getAdditionalData(absoluteDataPath, type); 314 315 Object value = type.valueFromXML(parent, dataName, additionalData); 316 317 if (definition.isMultiple() && type.getManagedClass().isInstance(value)) 318 { 319 // The value is single but should be an array. Create the array with the single value 320 T arrayValue = (T) Array.newInstance(type.getManagedClass(), 1); 321 Array.set(arrayValue, 0, value); 322 return arrayValue; 323 } 324 else if (!definition.isMultiple() && type.getManagedClassArray().isInstance(value)) 325 { 326 // The value is multiple but should be single. Retrieve the first value of the array 327 return Array.getLength(value) > 0 ? (T) Array.get(value, 0) : null; 328 } 329 else 330 { 331 return (T) value; 332 } 333 } 334 335 /** 336 * Extracts the values of the given group 337 * @param <T> type of the values to retrieve (example: {@link Map} for a composite, {@link List} for a repeater 338 * @param parent the DOM element of the group's parent 339 * @param modelViewItemGroup view item corresponding to the group 340 * @param dataName the name of the data to extract 341 * @param prefix the path of the group's parent 342 * @return the value 343 * @throws Exception if an error occurs 344 */ 345 @SuppressWarnings("unchecked") 346 protected <T> T _extractGroupValues(Element parent, ModelViewItemGroup modelViewItemGroup, String dataName, String prefix) throws Exception 347 { 348 ModelItemGroup modelItemGroup = modelViewItemGroup.getDefinition(); 349 String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName; 350 351 if (modelItemGroup instanceof RepeaterDefinition) 352 { 353 if (DataHolderHelper.isRepeaterEntryPath(dataName)) 354 { 355 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataName); 356 Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(parent, repeaterNameAndEntryPosition.getLeft() + "/entry[@name='" + repeaterNameAndEntryPosition.getRight() + "']"); 357 return repeaterEntryElement != null ? (T) _extractValues(repeaterEntryElement, modelViewItemGroup, newPrefix) : null; 358 } 359 else 360 { 361 Element repeaterElement = DOMUtils.getChildElementByTagName(parent, dataName); 362 if (repeaterElement != null) 363 { 364 List<Map<String, Object>> repeaterValues = new ArrayList<>(); 365 int repeaterSize = Integer.valueOf(XPathAPI.eval(repeaterElement, "count(entry)").str()); 366 for (int i = 1; i <= repeaterSize; i++) 367 { 368 Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(repeaterElement, "entry[@name='" + i + "']"); 369 Map<String, Object> repeaterEntryValues = _extractValues(repeaterEntryElement, modelViewItemGroup, newPrefix + "[" + i + "]"); 370 repeaterValues.add(repeaterEntryValues); 371 } 372 return (T) repeaterValues; 373 } 374 else 375 { 376 return null; 377 } 378 } 379 } 380 else 381 { 382 Element compositeElement = DOMUtils.getChildElementByTagName(parent, dataName); 383 return compositeElement != null ? (T) _extractValues(compositeElement, modelViewItemGroup, newPrefix) : null; 384 } 385 } 386}