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 Optional<ModelItem> optionalModelItem = _getModelItemFromNodeName(element, child.getNodeName(), modelItemContainers); 125 if (optionalModelItem.isPresent()) 126 { 127 ModelItem modelItem = optionalModelItem.get(); 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(groupNode, "entry"); 136 } 137 138 modelViewItem = new ModelViewItemGroup(); 139 140 if (groupNode != null) 141 { 142 _fillViewItemContainerFromXML(groupNode, (ModelViewItemGroup) modelViewItem, (ModelItemGroup) modelItem); 143 } 144 } 145 else 146 { 147 modelViewItem = new ViewElement(); 148 } 149 150 modelViewItem.setDefinition(modelItem); 151 if (!viewItemContainer.hasModelViewItem(modelViewItem)) 152 { 153 viewItemContainer.addViewItem(modelViewItem); 154 } 155 } 156 } 157 } 158 159 /** 160 * Retrieves the model item corresponding to the given node name 161 * @param parent the DOM parent element 162 * @param nodeName the node name 163 * @param modelItemContainers the model item containers where to search for the model item 164 * @return the model item corresponding to the given node name 165 */ 166 protected Optional<ModelItem> _getModelItemFromNodeName(Element parent, String nodeName, Collection<? extends ModelItemContainer> modelItemContainers) 167 { 168 if (ModelHelper.hasModelItem(nodeName, modelItemContainers)) 169 { 170 return Optional.of(ModelHelper.getModelItem(nodeName, modelItemContainers)); 171 } 172 else 173 { 174 return Optional.empty(); 175 } 176 } 177 178 public Map<String, Object> extractValues(View view) throws Exception 179 { 180 return _extractValues(_element, view, ""); 181 } 182 183 /** 184 * Extracts the values of all items in the given view item container 185 * @param currentElement the DOM element containing the values of the items in the container 186 * @param viewItemContainer the view item container 187 * @param prefix the path of the item represented by the view item container (prefix of all contained items) 188 * @return the values of all items in the given view item containers 189 * @throws Exception if an error occurs 190 */ 191 protected Map<String, Object> _extractValues(Element currentElement, ViewItemContainer viewItemContainer, String prefix) throws Exception 192 { 193 Map<String, Object> values = new HashMap<>(); 194 for (ViewItem viewItem : viewItemContainer.getViewItems()) 195 { 196 if (viewItem instanceof ModelViewItem) 197 { 198 ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition(); 199 String dataName = modelItem.getName(); 200 if (_hasChildForAttribute(currentElement, dataName)) 201 { 202 Object value; 203 if (viewItem instanceof ModelViewItemGroup) 204 { 205 value = _extractGroupValues(currentElement, (ModelViewItemGroup) viewItem, dataName, prefix); 206 } 207 else 208 { 209 value = _extractElementValue(currentElement, (ElementDefinition) modelItem, prefix); 210 } 211 212 values.put(dataName, value); 213 } 214 } 215 else if (viewItem instanceof ViewItemContainer) 216 { 217 values.putAll(_extractValues(currentElement, (ViewItemContainer) viewItem, prefix)); 218 } 219 } 220 221 return values; 222 } 223 224 /** 225 * Checks if the given element contains a child for the given attribute name 226 * @param element the element to check 227 * @param attributeName the name of the attribute to search 228 * @return <code>true</code> if the element contains a child corresponding to the given attribute name, <code>false</code> otherwise 229 */ 230 protected boolean _hasChildForAttribute(Element element, String attributeName) 231 { 232 return DOMUtils.hasChildElement(element, attributeName); 233 } 234 235 public <T> T extractValue(String dataPath) throws Exception 236 { 237 // Check that there is an item at the given path 238 if (!ModelHelper.hasModelItem(dataPath, _modelItemContainers)) 239 { 240 throw new UndefinedItemPathException("Unable to retrieve the value at path '" + dataPath + "'. There is no such item defined by the model."); 241 } 242 243 return _extractValue(_modelItemContainers, _element, dataPath, StringUtils.EMPTY); 244 } 245 246 /** 247 * Extracts the value at the given path 248 * @param <T> type of the value to retrieve 249 * @param modelItemContainer The model item containing the item of the value to extract 250 * @param currentElement the DOM element containing the model item container's values 251 * @param relativeDataPath The data path relative to the model item container 252 * @param prefix the path of the item represented by the model item container (prefix of all contained items) 253 * @return the value 254 * @throws Exception if an error occurs 255 */ 256 protected <T> T _extractValue(ModelItemContainer modelItemContainer, Element currentElement, String relativeDataPath, String prefix) throws Exception 257 { 258 return _extractValue(List.of(modelItemContainer), currentElement, relativeDataPath, prefix); 259 } 260 261 /** 262 * Extracts the value at the given path 263 * @param <T> type of the value to retrieve 264 * @param modelItemContainers The model items containing the item of the value to extract 265 * @param currentElement the DOM element containing the model item containers' values 266 * @param relativeDataPath The data path relative to the model item containers 267 * @param prefix the path of the item represented by the model item containers (prefix of all contained items) 268 * @return the value 269 * @throws Exception if an error occurs 270 */ 271 protected <T> T _extractValue(Collection<? extends ModelItemContainer> modelItemContainers, Element currentElement, String relativeDataPath, String prefix) throws Exception 272 { 273 String[] pathSegments = StringUtils.split(relativeDataPath, ModelItem.ITEM_PATH_SEPARATOR); 274 275 if (pathSegments == null || pathSegments.length < 1) 276 { 277 throw new IllegalArgumentException("Unable to extract the value of the data at the given path. This path is empty."); 278 } 279 else if (pathSegments.length == 1) 280 { 281 String dataName = relativeDataPath; 282 ModelItem modelItem = ModelHelper.getModelItem(dataName, modelItemContainers); 283 if (modelItem instanceof ElementDefinition) 284 { 285 return _extractElementValue(currentElement, (ElementDefinition) modelItem, prefix); 286 } 287 else 288 { 289 ModelViewItemGroup modelViewItemGroup = ModelViewItemGroup.of((ModelItemGroup) modelItem); 290 return _extractGroupValues(currentElement, modelViewItemGroup, dataName, prefix); 291 } 292 } 293 else 294 { 295 String firstSegmentDataName = pathSegments[0]; 296 String newRelativeDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 297 String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + firstSegmentDataName : firstSegmentDataName; 298 299 ModelItem modelItem = ModelHelper.getModelItem(firstSegmentDataName, modelItemContainers); 300 301 if (modelItem instanceof RepeaterDefinition) 302 { 303 if (DataHolderHelper.isRepeaterEntryPath(firstSegmentDataName)) 304 { 305 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(firstSegmentDataName); 306 Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(currentElement, repeaterNameAndEntryPosition.getLeft() + "/entry[@name='" + repeaterNameAndEntryPosition.getRight() + "']"); 307 if (repeaterEntryElement != null) 308 { 309 return _extractValue((RepeaterDefinition) modelItem, repeaterEntryElement, newRelativeDataPath, newPrefix); 310 } 311 else 312 { 313 return null; 314 } 315 } 316 else 317 { 318 throw new BadDataPathCardinalityException("Unable to extract the value at path '" + relativeDataPath + "'. The segment '" + pathSegments[0] + "' refers to a repeater but not an entry."); 319 } 320 } 321 else if (modelItem instanceof CompositeDefinition) 322 { 323 Element compositeElement = DOMUtils.getChildElementByTagName(currentElement, firstSegmentDataName); 324 return compositeElement != null ? _extractValue((CompositeDefinition) modelItem, compositeElement, newRelativeDataPath, newPrefix) : null; 325 } 326 else 327 { 328 throw new BadItemTypeException("Unable to extract the value at path '" + relativeDataPath + "'. The segment '" + pathSegments[0] + "' does not represent a group item."); 329 } 330 } 331 } 332 333 /** 334 * Extracts the value of the given element 335 * @param <T> type of the value to retrieve 336 * @param parent the DOM element of the element definition's parent 337 * @param definition the element's definition 338 * @param prefix the path of the element's parent 339 * @return the value 340 * @throws Exception if an error occurs 341 */ 342 @SuppressWarnings("unchecked") 343 protected <T> T _extractElementValue(Element parent, ElementDefinition definition, String prefix) throws Exception 344 { 345 ElementType type = definition.getType(); 346 String dataName = definition.getName(); 347 348 String absoluteDataPath = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName; 349 Optional<Object> additionalData = _additionalDataGetter.getAdditionalData(absoluteDataPath, type); 350 351 Object value = _extractElementValue(parent, definition, additionalData); 352 353 if (definition.isMultiple() && value != null && !value.getClass().isArray()) 354 { 355 // The value is single but should be an array. Create the array with the single value 356 return (T) new Object[] {value}; 357 } 358 else if (!definition.isMultiple() && value != null && value.getClass().isArray()) 359 { 360 // The value is multiple but should be single. Retrieve the first value of the array 361 return Array.getLength(value) > 0 ? (T) Array.get(value, 0) : null; 362 } 363 else 364 { 365 return (T) value; 366 } 367 } 368 369 /** 370 * Extracts the value of the given element 371 * @param <T> type of the element definition 372 * @param parent the DOM element of the element definition's parent 373 * @param definition the element's definition 374 * @param additionalData the additional data needed to extract the value 375 * @return the value 376 * @throws Exception if an error occurs 377 */ 378 protected <T> Object _extractElementValue(Element parent, ElementDefinition<T> definition, Optional<Object> additionalData) throws Exception 379 { 380 ElementType<T> type = definition.getType(); 381 String dataName = definition.getName(); 382 return type.valueFromXML(parent, dataName, additionalData); 383 } 384 385 /** 386 * Extracts the values of the given group 387 * @param <T> type of the values to retrieve (example: {@link Map} for a composite, {@link List} for a repeater 388 * @param parent the DOM element of the group's parent 389 * @param modelViewItemGroup view item corresponding to the group 390 * @param dataName the name of the data to extract 391 * @param prefix the path of the group's parent 392 * @return the value 393 * @throws Exception if an error occurs 394 */ 395 @SuppressWarnings("unchecked") 396 protected <T> T _extractGroupValues(Element parent, ModelViewItemGroup modelViewItemGroup, String dataName, String prefix) throws Exception 397 { 398 ModelItemGroup modelItemGroup = modelViewItemGroup.getDefinition(); 399 String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName; 400 401 if (modelItemGroup instanceof RepeaterDefinition) 402 { 403 if (DataHolderHelper.isRepeaterEntryPath(dataName)) 404 { 405 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataName); 406 Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(parent, repeaterNameAndEntryPosition.getLeft() + "/entry[@name='" + repeaterNameAndEntryPosition.getRight() + "']"); 407 return repeaterEntryElement != null ? (T) _extractValues(repeaterEntryElement, modelViewItemGroup, newPrefix) : null; 408 } 409 else 410 { 411 Element repeaterElement = DOMUtils.getChildElementByTagName(parent, dataName); 412 if (repeaterElement != null) 413 { 414 List<Map<String, Object>> repeaterValues = new ArrayList<>(); 415 int repeaterSize = Integer.valueOf(XPathAPI.eval(repeaterElement, "count(entry)").str()); 416 for (int i = 1; i <= repeaterSize; i++) 417 { 418 Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(repeaterElement, "entry[@name='" + i + "']"); 419 Map<String, Object> repeaterEntryValues = _extractValues(repeaterEntryElement, modelViewItemGroup, newPrefix + "[" + i + "]"); 420 repeaterValues.add(repeaterEntryValues); 421 } 422 return (T) repeaterValues; 423 } 424 else 425 { 426 return null; 427 } 428 } 429 } 430 else 431 { 432 Element compositeElement = DOMUtils.getChildElementByTagName(parent, dataName); 433 return compositeElement != null ? (T) _extractValues(compositeElement, modelViewItemGroup, newPrefix) : null; 434 } 435 } 436}