001/* 002 * Copyright 2014 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.contentio.in.xml; 017 018import java.io.ByteArrayInputStream; 019import java.io.IOException; 020import java.util.ArrayList; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.regex.Pattern; 027 028import javax.xml.transform.TransformerException; 029 030import org.apache.avalon.framework.configuration.Configuration; 031import org.apache.avalon.framework.configuration.ConfigurationException; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.commons.lang3.LocaleUtils; 035import org.apache.commons.lang3.StringUtils; 036import org.w3c.dom.Document; 037import org.w3c.dom.Node; 038import org.w3c.dom.NodeList; 039 040import org.ametys.cms.contenttype.ContentTypesHelper; 041import org.ametys.cms.contenttype.MetadataDefinition; 042import org.ametys.cms.contenttype.RepeaterDefinition; 043import org.ametys.cms.repository.Content; 044import org.ametys.cms.repository.ModifiableContent; 045import org.ametys.plugins.contentio.ContentImporterHelper; 046import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 047import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 048import org.ametys.plugins.repository.version.VersionableAmetysObject; 049 050import com.opensymphony.workflow.WorkflowException; 051 052/** 053 * Default implementation of an XML content importer. 054 */ 055public class DefaultXmlContentImporter extends AbstractXmlContentImporter 056{ 057 058 /** Default content types key in the import map. */ 059 protected static final String _DEFAULT_CONTENT_TYPES_KEY = DefaultXmlContentImporter.class.getName() + "$defaultContentTypes"; 060 /** Default content mixins key in the import map. */ 061 protected static final String _DEFAULT_CONTENT_MIXINS_KEY = DefaultXmlContentImporter.class.getName() + "$defaultMixins"; 062 /** Default content language key in the import map. */ 063 protected static final String _DEFAULT_CONTENT_LANG_KEY = DefaultXmlContentImporter.class.getName() + "$defaultLanguage"; 064 065 /** The content type helper. */ 066 protected ContentTypesHelper _cTypeHelper; 067 068 /** The XPath expression to match. */ 069 protected String _matchPath; 070 071 /** The XPath value to match. */ 072 protected String _matchValue; 073 074 /** The XPath value to match. */ 075 protected Pattern _matchRegex; 076 077 @Override 078 public void service(ServiceManager serviceManager) throws ServiceException 079 { 080 super.service(serviceManager); 081 _cTypeHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 082 } 083 084 @Override 085 public void configure(Configuration configuration) throws ConfigurationException 086 { 087 super.configure(configuration); 088 089 configureXmlMatch(configuration.getChild("xml").getChild("match")); 090 } 091 092 /** 093 * Configure the matching value. 094 * @param configuration the matching configuration. 095 * @throws ConfigurationException if an error occurs. 096 */ 097 protected void configureXmlMatch(Configuration configuration) throws ConfigurationException 098 { 099 _matchPath = configuration.getAttribute("path"); 100 _matchValue = configuration.getAttribute("value", null); 101 String regex = configuration.getAttribute("regex", null); 102 if (StringUtils.isNotBlank(regex)) 103 { 104 _matchRegex = Pattern.compile(regex); 105 } 106 } 107 108 @Override 109 public boolean supports(Document document) throws IOException 110 { 111 Node node = _xPathProcessor.selectSingleNode(document, _matchPath, getPrefixResolver()); 112 113 if (_matchValue == null && _matchRegex == null) 114 { 115 return node != null; 116 } 117 else if (_matchRegex != null) 118 { 119 String text = getTextContent(node, null, true); 120 return text != null ? _matchRegex.matcher(text).matches() : false; 121 } 122 else 123 { 124 return _matchValue.equals(getTextContent(node, null, true)); 125 } 126 } 127 128 @Override 129 protected Set<String> importContents(Document node, Map<String, Object> params) throws IOException 130 { 131 Set<String> contentIds = new HashSet<>(); 132 133 Node root = node.getFirstChild(); 134 135 if (root != null) 136 { 137 String[] defaultTypes = StringUtils.split(getAttributeValue(root, "default-types", ""), ", "); 138 String[] defaultMixins = StringUtils.split(getAttributeValue(root, "default-mixins", ""), ", "); 139 String defaultLang = getAttributeValue(root, "default-language", getLanguage(params)); 140 if (defaultTypes.length == 0) 141 { 142 defaultTypes = getContentTypes(params); 143 } 144 if (defaultMixins.length == 0) 145 { 146 defaultMixins = getMixins(params); 147 } 148 149 params.put(_DEFAULT_CONTENT_TYPES_KEY, defaultTypes); 150 params.put(_DEFAULT_CONTENT_MIXINS_KEY, defaultMixins); 151 params.put(_DEFAULT_CONTENT_LANG_KEY, defaultLang); 152 153 // Import all the contents. 154 NodeList contents = _xPathProcessor.selectNodeList(root, "content", getPrefixResolver()); 155 156 for (int i = 0; i < contents.getLength(); i++) 157 { 158 Node contentNode = contents.item(i); 159 160 try 161 { 162 Content content = importContent(contentNode, defaultTypes, defaultMixins, defaultLang, params); 163 164 if (content != null) 165 { 166 contentIds.add(content.getId()); 167 } 168 } 169 catch (WorkflowException e) 170 { 171 getLogger().error("Error importing a content.", e); 172 } 173 } 174 175 // Second pass: restore all the content links. 176 restoreContentReferences(params); 177 178 params.remove(_DEFAULT_CONTENT_TYPES_KEY); 179 params.remove(_DEFAULT_CONTENT_MIXINS_KEY); 180 params.remove(_DEFAULT_CONTENT_LANG_KEY); 181 } 182 183 return contentIds; 184 } 185 186 /** 187 * Import a content from a XML node. 188 * @param contentNode the content XML node. 189 * @param defaultTypes the default content types. 190 * @param defaultMixins the default mixins. 191 * @param defaultLang the default content language. 192 * @param params the import parameters. 193 * @return the Content or null if not created. 194 * @throws IOException if an error occurs during the import. 195 * @throws WorkflowException if an error occurs creating the Content. 196 */ 197 protected Content importContent(Node contentNode, String[] defaultTypes, String[] defaultMixins, String defaultLang, Map<String, Object> params) throws IOException, WorkflowException 198 { 199 String localId = getAttributeValue(contentNode, "id", ""); 200 201 String cTypesStr = getAttributeValue(contentNode, "types", ""); 202 String mixinsStr = getAttributeValue(contentNode, "mixins", ""); 203 204 String[] contentTypes = StringUtils.isEmpty(cTypesStr) ? defaultTypes : StringUtils.split(cTypesStr, ", "); 205 String[] contentMixins = StringUtils.isEmpty(mixinsStr) ? defaultMixins : StringUtils.split(mixinsStr, ", "); 206 String language = getAttributeValue(contentNode, "language", defaultLang); 207 208 String title = _xPathProcessor.evaluateAsString(contentNode, "metadata/title", getPrefixResolver()); 209 210 Content content = createContent(title, contentTypes, contentMixins, language, params); 211 212 if (content instanceof ModifiableContent) 213 { 214 importMetadata((ModifiableContent) content, contentNode, "", params); 215 216 ((ModifiableContent) content).saveChanges(); 217 218 if (content instanceof VersionableAmetysObject) 219 { 220 ((VersionableAmetysObject) content).checkpoint(); 221 } 222 } 223 224 // If the content contains a "local" ID (local to the import file), remember it. 225 if (StringUtils.isNotBlank(localId)) 226 { 227 Map<String, String> contentIdMap = getContentIdMap(params); 228 contentIdMap.put(localId, content.getId()); 229 } 230 231 return content; 232 } 233 234 /** 235 * Import metadata from a content node. 236 * @param content the content to populate. 237 * @param contentNode the content DOM node. 238 * @param prefix the metadata prefix. 239 * @param params the import parameters. 240 * @throws IOException if an error occurs. 241 */ 242 protected void importMetadata(ModifiableContent content, Node contentNode, String prefix, Map<String, Object> params) throws IOException 243 { 244 try 245 { 246 Map<String, MetadataDefinition> metadataDefinitions = _cTypeHelper.getMetadataDefinitions(content.getTypes()); 247 248 ModifiableCompositeMetadata meta = content.getMetadataHolder(); 249 250 Node metadataRootNode = _xPathProcessor.selectSingleNode(contentNode, "metadata", getPrefixResolver()); 251 252 if (metadataRootNode != null) 253 { 254 NodeList metaNodes = metadataRootNode.getChildNodes(); 255 for (int i = 0; i < metaNodes.getLength(); i++) 256 { 257 Node metaNode = metaNodes.item(i); 258 if (metaNode.getNodeType() == Node.ELEMENT_NODE) 259 { 260 String metaName = metaNode.getLocalName(); 261 MetadataDefinition metaDef = metadataDefinitions.get(metaName); 262 263 String subMetadataPath = prefix + (StringUtils.isEmpty(prefix) ? "" : "/") + metaName; 264 265 importMetadata(content, meta, metaNode, metaDef, subMetadataPath, params); 266 } 267 } 268 } 269 } 270 catch (ConfigurationException e) 271 { 272 throw new IOException("Error retrieving metadata definitions.", e); 273 } 274 } 275 276 /** 277 * Import metadata from a DOM node. 278 * @param content The content being imported. 279 * @param meta the metadata holder to populate. 280 * @param metaNode the metadata DOM node. 281 * @param metaDef the metadata definition. 282 * @param metadataPath the metadata path. 283 * @param params the import parameters. 284 * @throws IOException if an error occurs. 285 */ 286 protected void importMetadata(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, MetadataDefinition metaDef, String metadataPath, Map<String, Object> params) throws IOException 287 { 288 String name = metaNode.getLocalName(); 289 NodeList valueNodes = _xPathProcessor.selectNodeList(metaNode, "value", getPrefixResolver()); 290 String[] values = new String[valueNodes.getLength()]; 291 for (int i = 0; i < valueNodes.getLength(); i++) 292 { 293 values[i] = valueNodes.item(i).getTextContent(); 294 } 295 296 try 297 { 298 if (metaDef != null) 299 { 300 if (metaDef instanceof RepeaterDefinition) 301 { 302 setRepeater(content, meta, metaNode, (RepeaterDefinition) metaDef, name, metadataPath, params); 303 } 304 else 305 { 306 setMetadata(content, meta, metaNode, metaDef, name, values, metadataPath, params); 307 } 308 } 309 } 310 catch (Exception e) 311 { 312 String message = "The values for metadata '" + name + "' are invalid and will be ignored: " + StringUtils.join(values, ", "); 313 getLogger().warn(message, e); 314 } 315 } 316 317 /** 318 * Set the values of a metadata. 319 * @param content The content being imported. 320 * @param meta the metadata holder. 321 * @param metaNode the metadata DOM node. 322 * @param metaDef the metadata definition. 323 * @param name the metadata name. 324 * @param values the metadata values. 325 * @param metadataPath the metadata path. 326 * @param params the import parameters. 327 * @throws IOException if an error occurs. 328 */ 329 protected void setMetadata(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, MetadataDefinition metaDef, String name, String[] values, String metadataPath, Map<String, Object> params) throws IOException 330 { 331 switch (metaDef.getType()) 332 { 333 case STRING: 334 setStringMetadata(meta, name, metaDef, values); 335 break; 336 case BOOLEAN: 337 setBooleanMetadata(meta, name, metaDef, values); 338 break; 339 case LONG: 340 setLongMetadata(meta, name, metaDef, values); 341 break; 342 case DOUBLE: 343 setDoubleMetadata(meta, name, metaDef, values); 344 break; 345 case DATE: 346 case DATETIME: 347 setDateMetadata(meta, name, metaDef, values); 348 break; 349 case GEOCODE: 350 String latitude = getAttributeValue(metaNode, "latitude", null); 351 String longitude = getAttributeValue(metaNode, "longitude", null); 352 if (latitude != null && longitude != null) 353 { 354 setGeocodeMetadata(meta, name, metaDef, latitude, longitude); 355 } 356 break; 357 case RICH_TEXT: 358 setRichText(meta, metaNode, name); 359 break; 360 case COMPOSITE: 361 setComposite(content, meta, metaNode, metaDef, name, metadataPath, params); 362 break; 363 case BINARY: 364 case FILE: 365 if (values.length > 0) 366 { 367 setBinaryMetadata(meta, name, metaDef, values[0]); 368 } 369 break; 370 case CONTENT: 371 setContentReferences(content, meta, metaNode, name, metaDef, values, metadataPath, params); 372 break; 373 case SUB_CONTENT: 374 setSubContents(meta, metaNode, name, content.getId(), metadataPath, params); 375 break; 376 case MULTILINGUAL_STRING: 377 setMultilingualString(meta, metaNode, name); 378 break; 379 case USER: 380 case REFERENCE: 381 default: 382 break; 383 } 384 } 385 386 /** 387 * Set a composite metadata. 388 * @param content The content being imported. 389 * @param meta the metadata holder. 390 * @param metaNode the metadata DOM node. 391 * @param metaDef the metadata definition. 392 * @param name the metadata name. 393 * @param metadataPath the metadata path. 394 * @param params the import parameters. 395 * @throws IOException if an error occurs. 396 */ 397 protected void setComposite(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, MetadataDefinition metaDef, String name, String metadataPath, Map<String, Object> params) throws IOException 398 { 399 NodeList subMetaNodes = metaNode.getChildNodes(); 400 if (subMetaNodes.getLength() > 0) 401 { 402 ModifiableCompositeMetadata composite = meta.getCompositeMetadata(name, true); 403 for (int i = 0; i < subMetaNodes.getLength(); i++) 404 { 405 Node subMetaNode = subMetaNodes.item(i); 406 if (subMetaNode.getNodeType() == Node.ELEMENT_NODE) 407 { 408 String subMetaName = subMetaNode.getLocalName(); 409 MetadataDefinition childDef = metaDef.getMetadataDefinition(subMetaName); 410 String subMetaPath = metadataPath + "/" + subMetaName; 411 412 importMetadata(content, composite, subMetaNode, childDef, subMetaPath, params); 413 } 414 } 415 } 416 } 417 418 /** 419 * Set a RichText metadata. 420 * @param meta the metadata holder. 421 * @param metaNode the metadata node. 422 * @param name the metadata name. 423 * @throws IOException if an error occurs. 424 */ 425 protected void setMultilingualString(ModifiableCompositeMetadata meta, Node metaNode, String name) throws IOException 426 { 427 NodeList localeNodes = metaNode.getChildNodes(); 428 for (int i = 0; i < localeNodes.getLength(); i++) 429 { 430 Node localeNode = localeNodes.item(i); 431 if (localeNode.getNodeType() == Node.ELEMENT_NODE) 432 { 433 String localeAsStr = localeNode.getNodeName(); 434 String value = localeNode.getNodeValue(); 435 436 try 437 { 438 meta.setMetadata(name, value, LocaleUtils.toLocale(localeAsStr)); 439 } 440 catch (IllegalArgumentException e) 441 { 442 throw new IOException(localeAsStr + " is not a valid locale", e); 443 } 444 } 445 } 446 } 447 448 /** 449 * Set a RichText metadata. 450 * @param meta the metadata holder. 451 * @param metaNode the metadata node. 452 * @param name the metadata name. 453 * @throws IOException if an error occurs. 454 */ 455 protected void setRichText(ModifiableCompositeMetadata meta, Node metaNode, String name) throws IOException 456 { 457 NodeList docbookNodes = metaNode.getChildNodes(); 458 for (int i = 0; i < docbookNodes.getLength(); i++) 459 { 460 Node docbookNode = docbookNodes.item(i); 461 if (docbookNode.getNodeType() == Node.ELEMENT_NODE && "article".equals(docbookNode.getLocalName())) 462 { 463 try 464 { 465 String docbook = ContentImporterHelper.serializeNode(docbookNode); 466 setRichText(meta, name, new ByteArrayInputStream(docbook.getBytes("UTF-8"))); 467 } 468 catch (TransformerException e) 469 { 470 throw new IOException("Error serializing a docbook node.", e); 471 } 472 } 473 } 474 } 475 476 /** 477 * Set a repeater metadata. 478 * @param content The content being imported. 479 * @param meta the metadata holder. 480 * @param metaNode the metadata DOM node. 481 * @param repeaterDef the repeater definition. 482 * @param name the metadata name. 483 * @param metadataPath the metadata path. 484 * @param params the import parameters. 485 * @throws IOException if an error occurs. 486 */ 487 protected void setRepeater(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, RepeaterDefinition repeaterDef, String name, String metadataPath, Map<String, Object> params) throws IOException 488 { 489 ModifiableCompositeMetadata repeaterMeta = meta.getCompositeMetadata(name, true); 490 491 NodeList entryNodes = _xPathProcessor.selectNodeList(metaNode, "entry"); 492 int repeaterSize = entryNodes.getLength(); 493 setRepeaterSize(content, metadataPath, repeaterSize, params); 494 495 for (int i = 0; i < repeaterSize; i++) 496 { 497 Node entryNode = entryNodes.item(i); 498 String entryName = Integer.toString(i + 1); 499 500 ModifiableCompositeMetadata entryMeta = repeaterMeta.getCompositeMetadata(entryName, true); 501 NodeList subMetaNodes = entryNode.getChildNodes(); 502 for (int j = 0; j < subMetaNodes.getLength(); j++) 503 { 504 Node subMetaNode = subMetaNodes.item(j); 505 if (subMetaNode.getNodeType() == Node.ELEMENT_NODE) 506 { 507 String subMetaName = subMetaNode.getLocalName(); 508 MetadataDefinition childDef = repeaterDef.getMetadataDefinition(subMetaName); 509 String subMetaPath = metadataPath + "/" + entryName + "/" + subMetaName; 510 511 importMetadata(content, entryMeta, subMetaNode, childDef, subMetaPath, params); 512 } 513 } 514 } 515 } 516 517 /** 518 * Set a content metadata. 519 * @param content The content being imported. 520 * @param meta the metadata holder. 521 * @param metaNode the metadata DOM node. 522 * @param name the metadata name. 523 * @param metaDef the metadata definition 524 * @param values the values array 525 * @param metadataPath the metadata path. 526 * @param params the import parameters. 527 * @throws IOException if an error occurs. 528 */ 529 protected void setContentReferences(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, String name, MetadataDefinition metaDef, String[] values, String metadataPath, Map<String, Object> params) throws IOException 530 { 531 if (values != null) 532 { 533 Map<Content, Map<String, Object>> contentRefMap = getContentRefMap(params); 534 535 List<ContentReference> references = new ArrayList<>(); 536 537 NodeList valueNodes = _xPathProcessor.selectNodeList(metaNode, "value", getPrefixResolver()); 538 for (int i = 0; i < valueNodes.getLength(); i++) 539 { 540 Node valueNode = valueNodes.item(i); 541 542 String refType = getAttributeValue(valueNode, "type", "local"); 543 String textContent = valueNode.getTextContent(); 544 545 if (refType.equals("local") && StringUtils.isNotBlank(textContent)) 546 { 547 // The reference is local to the file. 548 references.add(new ContentReference(ContentReference.TYPE_LOCAL_ID, textContent.trim())); 549 } 550 else if (refType.equals("repository") && StringUtils.isNotBlank(textContent)) 551 { 552 // The reference point to a content in the repository. 553 references.add(new ContentReference(ContentReference.TYPE_CONTENT_ID, textContent.trim())); 554 } 555 else if (refType.equals("properties")) 556 { 557 // No text content, parse the child nodes. 558 Map<String, String> refValues = new HashMap<>(); 559 NodeList refNodes = _xPathProcessor.selectNodeList(valueNode, "meta", getPrefixResolver()); 560 for (int j = 0; j < refNodes.getLength(); j++) 561 { 562 Node refNode = refNodes.item(j); 563 String metaName = getAttributeValue(refNode, "name", null); 564 String value = getAttributeValue(refNode, "value", null); 565 566 if (StringUtils.isNotBlank(metaName) && value != null) 567 { 568 refValues.put(metaName, value); 569 } 570 } 571 572 references.add(new ContentReference(ContentReference.TYPE_CONTENT_VALUES, refValues)); 573 } 574 } 575 576 if (!references.isEmpty()) 577 { 578 if (metaDef.isMultiple()) 579 { 580 addContentReferences(contentRefMap, content, metadataPath, references); 581 } 582 else 583 { 584 addContentReference(contentRefMap, content, metadataPath, references.get(0)); 585 } 586 } 587 } 588 } 589 590 /** 591 * Set a sub-content metadata. 592 * @param meta the metadata holder. 593 * @param metaNode the metadata DOM node. 594 * @param name the metadata name. 595 * @param parentContentId the parent content ID. 596 * @param metadataPath the metadata path. 597 * @param params the import parameters. 598 * @throws IOException if an error occurs. 599 */ 600 protected void setSubContents(ModifiableCompositeMetadata meta, Node metaNode, String name, String parentContentId, String metadataPath, Map<String, Object> params) throws IOException 601 { 602 NodeList contentNodes = _xPathProcessor.selectNodeList(metaNode, "value/content"); 603 if (contentNodes.getLength() > 0) 604 { 605 ModifiableTraversableAmetysObject subContentMeta = meta.getObjectCollection(name, true); 606 607 for (int i = 0; i < contentNodes.getLength(); i++) 608 { 609 Node contentNode = contentNodes.item(i); 610 importSubContent(subContentMeta, contentNode, parentContentId, metadataPath, params); 611 } 612 } 613 } 614 615 /** 616 * Import a sub-content. 617 * @param subContentMeta the content collection metadata. 618 * @param contentNode the DOM node representing the sub-content. 619 * @param parentContentId the parent content ID. 620 * @param metadataPath the content collection metadata path. 621 * @param params the import parameters. 622 * @return the created Content. 623 * @throws IOException if an error occurs. 624 */ 625 protected Content importSubContent(ModifiableTraversableAmetysObject subContentMeta, Node contentNode, String parentContentId, String metadataPath, Map<String, Object> params) throws IOException 626 { 627 String[] defaultTypes = (String[]) params.get(_DEFAULT_CONTENT_TYPES_KEY); 628 String[] defaultMixins = (String[]) params.get(_DEFAULT_CONTENT_MIXINS_KEY); 629 String defaultLanguage = (String) params.get(_DEFAULT_CONTENT_LANG_KEY); 630 631 String cTypesStr = getAttributeValue(contentNode, "types", ""); 632 String mixinsStr = getAttributeValue(contentNode, "mixins", ""); 633 634 String[] contentTypes = StringUtils.isEmpty(cTypesStr) ? defaultTypes : StringUtils.split(cTypesStr, ", "); 635 String[] mixins = StringUtils.isEmpty(mixinsStr) ? defaultMixins : StringUtils.split(mixinsStr, ", "); 636 String language = getAttributeValue(contentNode, "language", defaultLanguage); 637 638 String title = _xPathProcessor.evaluateAsString(contentNode, "metadata/title"); 639 640 try 641 { 642 Content content = createContent(title, contentTypes, mixins, language, parentContentId, metadataPath, params); 643 644 if (content instanceof ModifiableContent) 645 { 646 importMetadata((ModifiableContent) content, contentNode, metadataPath, params); 647 648 ((ModifiableContent) content).saveChanges(); 649 } 650 651 return content; 652 } 653 catch (WorkflowException e) 654 { 655 getLogger().error("Error creating sub-content.", e); 656 throw new IOException("Error creating sub-content.", e); 657 } 658 } 659 660}