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; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import org.apache.avalon.framework.configuration.Configurable; 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.avalon.framework.service.Serviceable; 035import org.apache.commons.io.FilenameUtils; 036import org.apache.commons.lang3.StringUtils; 037 038import org.ametys.cms.repository.Content; 039import org.ametys.cms.repository.ContentQueryHelper; 040import org.ametys.cms.repository.WorkflowAwareContent; 041import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 042import org.ametys.cms.workflow.ContentWorkflowHelper; 043import org.ametys.cms.workflow.EditContentFunction; 044import org.ametys.plugins.repository.AmetysObjectIterable; 045import org.ametys.plugins.repository.AmetysObjectResolver; 046import org.ametys.plugins.repository.jcr.NameHelper; 047import org.ametys.plugins.repository.query.expression.AndExpression; 048import org.ametys.plugins.repository.query.expression.Expression.Operator; 049import org.ametys.plugins.repository.query.expression.StringExpression; 050import org.ametys.plugins.workflow.AbstractWorkflowComponent; 051import org.ametys.runtime.plugin.component.AbstractLogEnabled; 052 053import com.opensymphony.workflow.WorkflowException; 054 055/** 056 * Abstract {@link ContentImporter} class which provides base importer configuration and logic.<br> 057 * Configuration options: 058 * <ul> 059 * <li>Importer priority</li> 060 * <li>Allowed extensions, without leading dot and comma-separated</li> 061 * <li>Content types and mixins of the created contents</li> 062 * <li>Language of the created contents</li> 063 * <li>Content workflow name and creation action ID</li> 064 * </ul><br> 065 * Example configuration handled by the configure method: 066 * <pre> 067 * <extension point="org.ametys.plugins.contentio.ContentImporterExtensionPoint" 068 * id="my.content.importer" 069 * class="..."> 070 * <priority>500</priority> 071 * <extensions>ext,ext2</extensions> 072 * <content-creation> 073 * <content-types>My.ContentType.1,My.ContentType.2</content-types> 074 * <mixins>My.Mixin.1,My.Mixin.2</mixins> 075 * <language>en</language> 076 * <workflow name="content" createActionId="1" editActionId="2"/> 077 * </content-creation> 078 * </extension> 079 * </pre> 080 */ 081public abstract class AbstractContentImporter extends AbstractLogEnabled implements ContentImporter, Serviceable, Configurable 082{ 083 084 /** The default importer priority. */ 085 protected static final int DEFAULT_PRIORITY = 5000; 086 087 /** Map used to store the mapping from "local" ID to content ID, when actually imported. */ 088 protected static final String _CONTENT_ID_MAP_KEY = AbstractContentImporter.class.getName() + "$contentIdMap"; 089 090 /** Map used to store the content references, indexed by content and metadata path. */ 091 protected static final String _CONTENT_LINK_MAP_KEY = AbstractContentImporter.class.getName() + "$contentLinkMap"; 092 093 /** Map used to store the content repeater sizes. */ 094 protected static final String _CONTENT_REPEATER_SIZE_MAP = AbstractContentImporter.class.getName() + "$contentRepeaterSizeMap"; 095 096 /** The AmetysObject resolver. */ 097 protected AmetysObjectResolver _resolver; 098 099 /** The content workflow helper. */ 100 protected ContentWorkflowHelper _contentWorkflowHelper; 101 102 /** The importer priority. */ 103 protected int _priority = DEFAULT_PRIORITY; 104 105 /** The allowed extensions. */ 106 protected Set<String> _extensions; 107 108 /** The imported contents' types. */ 109 protected String[] _contentTypes; 110 111 /** The imported contents' mixins. */ 112 protected String[] _mixins; 113 114 /** The importer contents' language. */ 115 protected String _language; 116 117 /** The importer contents' workflow name. */ 118 protected String _workflowName; 119 120 /** The importer contents' initial action ID. */ 121 protected int _initialActionId; 122 123 /** The importer contents' edition action ID. */ 124 protected int _editActionId; 125 126 @Override 127 public void service(ServiceManager manager) throws ServiceException 128 { 129 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 130 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 131 } 132 133 @Override 134 public void configure(Configuration configuration) throws ConfigurationException 135 { 136 _priority = configuration.getChild("priority").getValueAsInteger(DEFAULT_PRIORITY); 137 138 configureExtensions(configuration.getChild("extensions")); 139 140 configureContentCreation(configuration.getChild("content-creation")); 141 } 142 143 /** 144 * Configure the allowed extensions. 145 * @param configuration the extension configuration. 146 * @throws ConfigurationException if an error occurs. 147 */ 148 protected void configureExtensions(Configuration configuration) throws ConfigurationException 149 { 150 _extensions = new HashSet<>(); 151 152 String extensionsStr = configuration.getValue(""); 153 154 if (StringUtils.isBlank(extensionsStr)) 155 { 156 _extensions.addAll(getDefaultExtensions()); 157 } 158 else 159 { 160 for (String ext : StringUtils.split(extensionsStr, ", ")) 161 { 162 String extension = ext.trim(); 163 if (extension.startsWith(".")) 164 { 165 extension = extension.substring(1); 166 } 167 168 _extensions.add(extension); 169 } 170 } 171 } 172 173 /** 174 * Configure the content creation parameters. 175 * @param configuration the content creation configuration. 176 * @throws ConfigurationException if an error occurs. 177 */ 178 protected void configureContentCreation(Configuration configuration) throws ConfigurationException 179 { 180 String typesStr = configuration.getChild("content-types").getValue(); 181 _contentTypes = StringUtils.split(typesStr, ", "); 182 183 String mixins = configuration.getChild("mixins").getValue(""); // mixins can be empty 184 _mixins = StringUtils.split(mixins, ", "); 185 186 _language = configuration.getChild("language").getValue(); 187 188 configureWorkflow(configuration); 189 } 190 191 /** 192 * Configure the content workflow. 193 * @param configuration the content creation configuration. 194 * @throws ConfigurationException if an error occurs. 195 */ 196 protected void configureWorkflow(Configuration configuration) throws ConfigurationException 197 { 198 Configuration wfConf = configuration.getChild("workflow"); 199 200 _workflowName = wfConf.getAttribute("name"); 201 202 _initialActionId = wfConf.getAttributeAsInteger("createActionId"); 203 _editActionId = wfConf.getAttributeAsInteger("editActionId"); 204 } 205 206 @Override 207 public int getPriority() 208 { 209 return _priority; 210 } 211 212 /** 213 * Get the default allowed extensions. 214 * @return the default allowed extensions, without leading dots. Cannot be null. 215 */ 216 protected Collection<String> getDefaultExtensions() 217 { 218 return Collections.emptySet(); 219 } 220 221 /** 222 * Test if the given filename has a supported extension. 223 * @param name the name, can't be null. 224 * @return true if the extension is supported, false otherwise. 225 * @throws IOException if an error occurs. 226 */ 227 protected boolean isExtensionValid(String name) throws IOException 228 { 229 return _extensions.isEmpty() || _extensions.contains(FilenameUtils.getExtension(name)); 230 } 231 232 /** 233 * The content types of a created content. 234 * @param params the import parameters. 235 * @return the content types of a created content. 236 */ 237 protected String[] getContentTypes(Map<String, Object> params) 238 { 239 return _contentTypes; 240 } 241 242 /** 243 * The mixins of a created content. 244 * @param params the import parameters. 245 * @return The mixins of a created content. 246 */ 247 protected String[] getMixins(Map<String, Object> params) 248 { 249 return _mixins; 250 } 251 252 /** 253 * The language of a created content. 254 * @param params the import parameters. 255 * @return The language of a created content. 256 */ 257 protected String getLanguage(Map<String, Object> params) 258 { 259 return _language; 260 } 261 262 /** 263 * The workflow name of a created content. 264 * @param params the import parameters. 265 * @return The workflow name of a created content. 266 */ 267 protected String getWorkflowName(Map<String, Object> params) 268 { 269 return _workflowName; 270 } 271 272 /** 273 * The workflow creation action ID of a created content. 274 * @param params the import parameters. 275 * @return The workflow creation action ID of a created content. 276 */ 277 protected int getInitialActionId(Map<String, Object> params) 278 { 279 return _initialActionId; 280 } 281 282 /** 283 * The workflow action ID used to edit a content. 284 * @param params the import parameters. 285 * @return The workflow action ID used to edit a content. 286 */ 287 protected int getEditActionId(Map<String, Object> params) 288 { 289 return _editActionId; 290 } 291 292 /** 293 * Get the map used to store the mapping from "local" ID (defined in the import file) 294 * to the AmetysObject ID of the contents, when actually imported. 295 * @param params the import parameters. 296 * @return the content "local to repository" ID map. 297 */ 298 protected Map<String, String> getContentIdMap(Map<String, Object> params) 299 { 300 // Get or create the map in the global parameters. 301 @SuppressWarnings("unchecked") 302 Map<String, String> contentIdMap = (Map<String, String>) params.get(_CONTENT_ID_MAP_KEY); 303 if (contentIdMap == null) 304 { 305 contentIdMap = new HashMap<>(); 306 params.put(_CONTENT_ID_MAP_KEY, contentIdMap); 307 } 308 309 return contentIdMap; 310 } 311 312 /** 313 * Get the map used to store the content references. 314 * The Map is shaped like: referencing content -> local metadata path -> content references. 315 * @param params the import parameters. 316 * @return the content reference map. 317 */ 318 protected Map<Content, Map<String, Object>> getContentRefMap(Map<String, Object> params) 319 { 320 // Get or create the map in the global parameters. 321 @SuppressWarnings("unchecked") 322 Map<Content, Map<String, Object>> contentRefMap = (Map<Content, Map<String, Object>>) params.get(_CONTENT_LINK_MAP_KEY); 323 if (contentRefMap == null) 324 { 325 contentRefMap = new HashMap<>(); 326 params.put(_CONTENT_LINK_MAP_KEY, contentRefMap); 327 } 328 329 return contentRefMap; 330 } 331 332 /** 333 * Add a content reference to the map. 334 * @param content The referencing content. 335 * @param metadataPath The path of the metadata which holds the content references. 336 * @param reference The content reference. 337 * @param params The import parameters. 338 */ 339 protected void addContentReference(Content content, String metadataPath, ContentReference reference, Map<String, Object> params) 340 { 341 addContentReference(getContentRefMap(params), content, metadataPath, reference); 342 } 343 344 /** 345 * Add a content reference to the map. 346 * @param contentRefMap The content reference map. 347 * @param content The referencing content. 348 * @param metadataPath The path of the metadata which holds the content references. 349 * @param reference The content reference. 350 */ 351 protected void addContentReference(Map<Content, Map<String, Object>> contentRefMap, Content content, String metadataPath, ContentReference reference) 352 { 353 Map<String, Object> contentReferences; 354 if (contentRefMap.containsKey(content)) 355 { 356 contentReferences = contentRefMap.get(content); 357 } 358 else 359 { 360 contentReferences = new HashMap<>(); 361 contentRefMap.put(content, contentReferences); 362 } 363 364 contentReferences.put(metadataPath, reference); 365 } 366 367 /** 368 * Add content references to the map. 369 * @param contentRefMap The content reference map. 370 * @param content The referencing content. 371 * @param metadataPath The path of the metadata which holds the content references. 372 * @param references the content reference list. 373 */ 374 protected void addContentReferences(Map<Content, Map<String, Object>> contentRefMap, Content content, String metadataPath, List<ContentReference> references) 375 { 376 Map<String, Object> contentReferences; 377 if (contentRefMap.containsKey(content)) 378 { 379 contentReferences = contentRefMap.get(content); 380 } 381 else 382 { 383 contentReferences = new HashMap<>(); 384 contentRefMap.put(content, contentReferences); 385 } 386 387 contentReferences.put(metadataPath, references); 388 } 389 390 /** 391 * Get the map used to store the repeater sizes. 392 * The Map is shaped like: referencing content -> local metadata path -> content references. 393 * @param params the import parameters. 394 * @return the content reference map. 395 */ 396 protected Map<Content, Map<String, Integer>> getContentRepeaterSizeMap(Map<String, Object> params) 397 { 398 // Get or create the map in the global parameters. 399 @SuppressWarnings("unchecked") 400 Map<Content, Map<String, Integer>> contentRepeaterSizeMap = (Map<Content, Map<String, Integer>>) params.get(_CONTENT_REPEATER_SIZE_MAP); 401 if (contentRepeaterSizeMap == null) 402 { 403 contentRepeaterSizeMap = new HashMap<>(); 404 params.put(_CONTENT_REPEATER_SIZE_MAP, contentRepeaterSizeMap); 405 } 406 407 return contentRepeaterSizeMap; 408 } 409 410 /** 411 * Set a repeater size in the map (needed to execute the edit content function). 412 * @param content The content containing the repeater. 413 * @param metadataPath The repeater metadata path. 414 * @param repeaterSize The repeater size. 415 * @param params The import parameters. 416 */ 417 protected void setRepeaterSize(Content content, String metadataPath, int repeaterSize, Map<String, Object> params) 418 { 419 Map<Content, Map<String, Integer>> contentRepeaterSizeMap = getContentRepeaterSizeMap(params); 420 421 Map<String, Integer> repeaters; 422 if (contentRepeaterSizeMap.containsKey(content)) 423 { 424 repeaters = contentRepeaterSizeMap.get(content); 425 } 426 else 427 { 428 repeaters = new HashMap<>(); 429 contentRepeaterSizeMap.put(content, repeaters); 430 } 431 432 repeaters.put(metadataPath, repeaterSize); 433 } 434 435 /** 436 * Create a content. 437 * @param title the content title. 438 * @param params the import parameters. 439 * @return the created content. 440 * @throws WorkflowException if an error occurs. 441 */ 442 protected Content createContent(String title, Map<String, Object> params) throws WorkflowException 443 { 444 String[] contentTypes = getContentTypes(params); 445 String[] mixins = getMixins(params); 446 String language = getLanguage(params); 447 String workflowName = getWorkflowName(params); 448 int initialActionId = getInitialActionId(params); 449 450 return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, params); 451 } 452 453 /** 454 * Create a content. 455 * @param title the content title. 456 * @param contentTypes the content types. 457 * @param mixins the content mixins. 458 * @param language the content language. 459 * @param params the import parameters. 460 * @return the created content. 461 * @throws WorkflowException if an error occurs. 462 */ 463 protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, Map<String, Object> params) throws WorkflowException 464 { 465 String workflowName = getWorkflowName(params); 466 int initialActionId = getInitialActionId(params); 467 468 return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, params); 469 } 470 471 /** 472 * Create a content. 473 * @param title the content title. 474 * @param contentTypes the content types. 475 * @param mixins the content mixins. 476 * @param language the content language. 477 * @param workflowName the content workflow name. 478 * @param initialActionId the content create action ID. 479 * @param params the import parameters. 480 * @return the created content. 481 * @throws WorkflowException if an error occurs. 482 */ 483 protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, String workflowName, int initialActionId, Map<String, Object> params) throws WorkflowException 484 { 485 String name; 486 try 487 { 488 name = NameHelper.filterName(title); 489 } 490 catch (Exception e) 491 { 492 // Ignore the exception, just provide a valid start. 493 name = "content-" + title; 494 } 495 496 Map<String, Object> result = _contentWorkflowHelper.createContent(workflowName, initialActionId, name, title, contentTypes, mixins, language); 497 498 return (Content) result.get(AbstractContentWorkflowComponent.CONTENT_KEY); 499 } 500 501 /** 502 * Restore content references. 503 * @param params The import parameters. 504 */ 505 protected void restoreContentReferences(Map<String, Object> params) 506 { 507 Map<Content, Map<String, Object>> contentRefMap = getContentRefMap(params); 508 Map<Content, Map<String, Integer>> contentRepeaterSizeMap = getContentRepeaterSizeMap(params); 509 int editActionId = getEditActionId(params); 510 511 for (Content content : contentRefMap.keySet()) 512 { 513 if (content instanceof WorkflowAwareContent) 514 { 515 Map<String, Object> contentReferences = contentRefMap.get(content); 516 Map<String, Integer> repeaters = contentRepeaterSizeMap.get(content); 517 518 Map<String, Object> values = new HashMap<>(); 519 520 // Fill the value map with the content references. 521 setReferenceMetadatas(contentReferences, values, repeaters, params); 522 523 try 524 { 525 if (!values.isEmpty()) 526 { 527 Map<String, Object> contextParameters = new HashMap<>(); 528 contextParameters.put(EditContentFunction.QUIT, true); 529 contextParameters.put(EditContentFunction.FORM_RAW_VALUES, values); 530 531 Map<String, Object> inputs = new HashMap<>(); 532 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 533 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 534 535 _contentWorkflowHelper.doAction((WorkflowAwareContent) content, editActionId, inputs); 536 } 537 } 538 catch (WorkflowException e) 539 { 540 // TODO Throw exception? 541 getLogger().warn("An error occurred restoring content references in content {}", content, e); 542 } 543 } 544 } 545 } 546 547 /** 548 * Fill the value map with the content references. 549 * @param contentReferences The list of content references indexed by metadata path. 550 * @param values The value map passed to the EditContentFunction class. 551 * @param repeaters The repeater sizes for this content. 552 * @param params The import parameters. 553 */ 554 protected void setReferenceMetadatas(Map<String, Object> contentReferences, Map<String, Object> values, Map<String, Integer> repeaters, Map<String, Object> params) 555 { 556 for (String metadataPath : contentReferences.keySet()) 557 { 558 Object value = contentReferences.get(metadataPath); 559 String metaKey = EditContentFunction.FORM_ELEMENTS_PREFIX + metadataPath; 560 561 if (value instanceof List<?>) 562 { 563 // Multiple value 564 @SuppressWarnings("unchecked") 565 List<ContentReference> references = (List<ContentReference>) value; 566 List<String> contentIds = new ArrayList<>(references.size()); 567 for (ContentReference reference : references) 568 { 569 String refContentId = getReferencedContentId(reference, params); 570 if (refContentId != null) 571 { 572 contentIds.add(refContentId); 573 } 574 } 575 576 if (!contentIds.isEmpty()) 577 { 578 values.put(metaKey, contentIds); 579 } 580 } 581 else if (value instanceof ContentReference) 582 { 583 // Single value. 584 String refContentId = getReferencedContentId((ContentReference) value, params); 585 if (refContentId != null) 586 { 587 values.put(metaKey, refContentId); 588 } 589 } 590 } 591 592 if (repeaters != null) 593 { 594 for (String repeaterPath : repeaters.keySet()) 595 { 596 Integer size = repeaters.get(repeaterPath); 597 if (size > 0) 598 { 599 String sizeKey = EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + repeaterPath + "/size"; 600 values.put(sizeKey, repeaters.get(repeaterPath).toString()); 601 } 602 } 603 } 604 } 605 606 /** 607 * Get the content ID from a content reference. 608 * @param contentRef The content reference. 609 * @param params The import parameters. 610 * @return the content ID if it was found, or null otherwise. 611 */ 612 protected String getReferencedContentId(ContentReference contentRef, Map<String, Object> params) 613 { 614 int refType = contentRef.getType(); 615 if (refType == ContentReference.TYPE_LOCAL_ID) 616 { 617 String localId = (String) contentRef.getValue(); 618 String contentId = getContentIdMap(params).get(localId); 619 if (StringUtils.isNotEmpty(contentId) && _resolver.hasAmetysObjectForId(contentId)) 620 { 621 return contentId; 622 } 623 } 624 else if (refType == ContentReference.TYPE_CONTENT_ID) 625 { 626 String contentId = (String) contentRef.getValue(); 627 if (StringUtils.isNotEmpty(contentId) && _resolver.hasAmetysObjectForId(contentId)) 628 { 629 return contentId; 630 } 631 } 632 else if (refType == ContentReference.TYPE_CONTENT_VALUES) 633 { 634 @SuppressWarnings("unchecked") 635 Map<String, String> values = (Map<String, String>) contentRef.getValue(); 636 Content content = getContentFromProperties(values); 637 if (content != null) 638 { 639 return content.getId(); 640 } 641 } 642 643 return null; 644 } 645 646 /** 647 * Search a content from a map of its metadata values. 648 * @param propertyValues The metadata values. 649 * @return The Content if found, null otherwise. 650 */ 651 protected Content getContentFromProperties(Map<String, String> propertyValues) 652 { 653 Content content = null; 654 655 AndExpression expression = new AndExpression(); 656 for (String property : propertyValues.keySet()) 657 { 658 String value = propertyValues.get(property); 659 expression.add(new StringExpression(property, Operator.EQ, value)); 660 } 661 662 String query = ContentQueryHelper.getContentXPathQuery(expression); 663 664 AmetysObjectIterable<Content> contents = _resolver.query(query); 665 Iterator<Content> it = contents.iterator(); 666 667 if (it.hasNext()) 668 { 669 content = it.next(); 670 671 if (it.hasNext()) 672 { 673 content = null; 674 } 675 } 676 677 return content; 678 } 679 680 /** 681 * Class representing a reference to a content in an import file. 682 */ 683 public class ContentReference 684 { 685 /** 686 * The referenced content doesn't exist in the repository, it's in the import file. 687 * The reference value is the content ID in the import file. 688 */ 689 public static final int TYPE_LOCAL_ID = 1; 690 691 /** 692 * The referenced content exits in the repository and its ID is known. 693 * The reference value is the content ID in the repository (AmetysObject ID). 694 */ 695 public static final int TYPE_CONTENT_ID = 2; 696 697 /** 698 * The referenced content exits in the repository. Its ID is not known, 699 * but it can be identified by one or several of its metadata. 700 * The reference value is a Map of metadata name -> value. 701 */ 702 public static final int TYPE_CONTENT_VALUES = 3; 703 704 /** The reference type. */ 705 private int _type; 706 707 /** The reference value, depends on the reference type. */ 708 private Object _value; 709 710 /** 711 * Build a content reference. 712 * @param type the reference type. 713 * @param value the reference value. 714 */ 715 public ContentReference(int type, Object value) 716 { 717 this._type = type; 718 this._value = value; 719 } 720 721 /** 722 * Get the type. 723 * @return the type 724 */ 725 public int getType() 726 { 727 return _type; 728 } 729 730 /** 731 * Set the type. 732 * @param type the type to set 733 */ 734 public void setType(int type) 735 { 736 this._type = type; 737 } 738 739 /** 740 * Get the value. 741 * @return the value 742 */ 743 public Object getValue() 744 { 745 return _value; 746 } 747 748 /** 749 * Set the value. 750 * @param value the value to set 751 */ 752 public void setValue(Object value) 753 { 754 this._value = value; 755 } 756 } 757}