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