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; 017 018import java.io.ByteArrayInputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.net.HttpURLConnection; 022import java.net.URL; 023import java.net.URLDecoder; 024import java.time.LocalDate; 025import java.time.ZoneId; 026import java.time.format.DateTimeFormatter; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.Collections; 030import java.util.Date; 031import java.util.HashMap; 032import java.util.HashSet; 033import java.util.Iterator; 034import java.util.List; 035import java.util.Map; 036import java.util.Set; 037import java.util.regex.Matcher; 038import java.util.regex.Pattern; 039 040import org.apache.avalon.framework.configuration.Configurable; 041import org.apache.avalon.framework.configuration.Configuration; 042import org.apache.avalon.framework.configuration.ConfigurationException; 043import org.apache.avalon.framework.service.ServiceException; 044import org.apache.avalon.framework.service.ServiceManager; 045import org.apache.avalon.framework.service.Serviceable; 046import org.apache.commons.io.FilenameUtils; 047import org.apache.commons.io.IOUtils; 048import org.apache.commons.io.output.ByteArrayOutputStream; 049import org.apache.commons.lang3.StringUtils; 050 051import org.ametys.cms.FilterNameHelper; 052import org.ametys.cms.contenttype.MetadataDefinition; 053import org.ametys.cms.repository.Content; 054import org.ametys.cms.repository.ContentQueryHelper; 055import org.ametys.cms.repository.WorkflowAwareContent; 056import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 057import org.ametys.cms.workflow.ContentWorkflowHelper; 058import org.ametys.cms.workflow.EditContentFunction; 059import org.ametys.core.util.DateUtils; 060import org.ametys.plugins.repository.AmetysObjectIterable; 061import org.ametys.plugins.repository.AmetysObjectResolver; 062import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata; 063import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 064import org.ametys.plugins.repository.metadata.ModifiableRichText; 065import org.ametys.plugins.repository.query.expression.AndExpression; 066import org.ametys.plugins.repository.query.expression.Expression; 067import org.ametys.plugins.repository.query.expression.Expression.Operator; 068import org.ametys.plugins.repository.query.expression.StringExpression; 069import org.ametys.plugins.workflow.AbstractWorkflowComponent; 070import org.ametys.runtime.plugin.component.AbstractLogEnabled; 071 072import com.opensymphony.workflow.WorkflowException; 073 074/** 075 * Abstract {@link ContentImporter} class which provides base importer configuration and logic.<br> 076 * Configuration options: 077 * <ul> 078 * <li>Importer priority</li> 079 * <li>Allowed extensions, without leading dot and comma-separated</li> 080 * <li>Content types and mixins of the created contents</li> 081 * <li>Language of the created contents</li> 082 * <li>Content workflow name and creation action ID</li> 083 * </ul><br> 084 * Example configuration handled by the configure method: 085 * <pre> 086 * <extension point="org.ametys.plugins.contentio.ContentImporterExtensionPoint" 087 * id="my.content.importer" 088 * class="..."> 089 * <priority>500</priority> 090 * <extensions>ext,ext2</extensions> 091 * <content-creation> 092 * <content-types>My.ContentType.1,My.ContentType.2</content-types> 093 * <mixins>My.Mixin.1,My.Mixin.2</mixins> 094 * <language>en</language> 095 * <workflow name="content" createActionId="1" editActionId="2"/> 096 * </content-creation> 097 * </extension> 098 * </pre> 099 */ 100public abstract class AbstractContentImporter extends AbstractLogEnabled implements ContentImporter, Serviceable, Configurable 101{ 102 103 /** The default importer priority. */ 104 protected static final int DEFAULT_PRIORITY = 5000; 105 106 /** Map used to store the mapping from "local" ID to content ID, when actually imported. */ 107 protected static final String _CONTENT_ID_MAP_KEY = AbstractContentImporter.class.getName() + "$contentIdMap"; 108 109 /** Map used to store the content references, indexed by content and metadata path. */ 110 protected static final String _CONTENT_LINK_MAP_KEY = AbstractContentImporter.class.getName() + "$contentLinkMap"; 111 112 /** Map used to store the content repeater sizes. */ 113 protected static final String _CONTENT_REPEATER_SIZE_MAP = AbstractContentImporter.class.getName() + "$contentRepeaterSizeMap"; 114 115 /** The AmetysObject resolver. */ 116 protected AmetysObjectResolver _resolver; 117 118 /** The content workflow helper. */ 119 protected ContentWorkflowHelper _contentWorkflowHelper; 120 121 /** The importer priority. */ 122 protected int _priority = DEFAULT_PRIORITY; 123 124 /** The allowed extensions. */ 125 protected Set<String> _extensions; 126 127 /** The imported contents' types. */ 128 protected String[] _contentTypes; 129 130 /** The imported contents' mixins. */ 131 protected String[] _mixins; 132 133 /** The importer contents' language. */ 134 protected String _language; 135 136 /** The importer contents' workflow name. */ 137 protected String _workflowName; 138 139 /** The importer contents' initial action ID. */ 140 protected int _initialActionId; 141 142 /** The importer contents' edition action ID. */ 143 protected int _editActionId; 144 145 @Override 146 public void service(ServiceManager manager) throws ServiceException 147 { 148 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 149 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 150 } 151 152 @Override 153 public void configure(Configuration configuration) throws ConfigurationException 154 { 155 _priority = configuration.getChild("priority").getValueAsInteger(DEFAULT_PRIORITY); 156 157 configureExtensions(configuration.getChild("extensions")); 158 159 configureContentCreation(configuration.getChild("content-creation")); 160 } 161 162 /** 163 * Configure the allowed extensions. 164 * @param configuration the extension configuration. 165 * @throws ConfigurationException if an error occurs. 166 */ 167 protected void configureExtensions(Configuration configuration) throws ConfigurationException 168 { 169 _extensions = new HashSet<>(); 170 171 String extensionsStr = configuration.getValue(""); 172 173 if (StringUtils.isBlank(extensionsStr)) 174 { 175 _extensions.addAll(getDefaultExtensions()); 176 } 177 else 178 { 179 for (String ext : StringUtils.split(extensionsStr, ", ")) 180 { 181 String extension = ext.trim(); 182 if (extension.startsWith(".")) 183 { 184 extension = extension.substring(1); 185 } 186 187 _extensions.add(extension); 188 } 189 } 190 } 191 192 /** 193 * Configure the content creation parameters. 194 * @param configuration the content creation configuration. 195 * @throws ConfigurationException if an error occurs. 196 */ 197 protected void configureContentCreation(Configuration configuration) throws ConfigurationException 198 { 199 String typesStr = configuration.getChild("content-types").getValue(); 200 _contentTypes = StringUtils.split(typesStr, ", "); 201 202 String mixins = configuration.getChild("mixins").getValue(""); // mixins can be empty 203 _mixins = StringUtils.split(mixins, ", "); 204 205 _language = configuration.getChild("language").getValue(); 206 207 configureWorkflow(configuration); 208 } 209 210 /** 211 * Configure the content workflow. 212 * @param configuration the content creation configuration. 213 * @throws ConfigurationException if an error occurs. 214 */ 215 protected void configureWorkflow(Configuration configuration) throws ConfigurationException 216 { 217 Configuration wfConf = configuration.getChild("workflow"); 218 219 _workflowName = wfConf.getAttribute("name"); 220 221 _initialActionId = wfConf.getAttributeAsInteger("createActionId"); 222 _editActionId = wfConf.getAttributeAsInteger("editActionId"); 223 } 224 225 @Override 226 public int getPriority() 227 { 228 return _priority; 229 } 230 231 /** 232 * Get the default allowed extensions. 233 * @return the default allowed extensions, without leading dots. Cannot be null. 234 */ 235 protected Collection<String> getDefaultExtensions() 236 { 237 return Collections.emptySet(); 238 } 239 240 /** 241 * Test if the given filename has a supported extension. 242 * @param name the name, can't be null. 243 * @return true if the extension is supported, false otherwise. 244 * @throws IOException if an error occurs. 245 */ 246 protected boolean isExtensionValid(String name) throws IOException 247 { 248 return _extensions.isEmpty() || _extensions.contains(FilenameUtils.getExtension(name)); 249 } 250 251 /** 252 * The content types of a created content. 253 * @param params the import parameters. 254 * @return the content types of a created content. 255 */ 256 protected String[] getContentTypes(Map<String, Object> params) 257 { 258 return _contentTypes; 259 } 260 261 /** 262 * The mixins of a created content. 263 * @param params the import parameters. 264 * @return The mixins of a created content. 265 */ 266 protected String[] getMixins(Map<String, Object> params) 267 { 268 return _mixins; 269 } 270 271 /** 272 * The language of a created content. 273 * @param params the import parameters. 274 * @return The language of a created content. 275 */ 276 protected String getLanguage(Map<String, Object> params) 277 { 278 return _language; 279 } 280 281 /** 282 * The workflow name of a created content. 283 * @param params the import parameters. 284 * @return The workflow name of a created content. 285 */ 286 protected String getWorkflowName(Map<String, Object> params) 287 { 288 return _workflowName; 289 } 290 291 /** 292 * The workflow creation action ID of a created content. 293 * @param params the import parameters. 294 * @return The workflow creation action ID of a created content. 295 */ 296 protected int getInitialActionId(Map<String, Object> params) 297 { 298 return _initialActionId; 299 } 300 301 /** 302 * The workflow action ID used to edit a content. 303 * @param params the import parameters. 304 * @return The workflow action ID used to edit a content. 305 */ 306 protected int getEditActionId(Map<String, Object> params) 307 { 308 return _editActionId; 309 } 310 311 /** 312 * Get the map used to store the mapping from "local" ID (defined in the import file) 313 * to the AmetysObject ID of the contents, when actually imported. 314 * @param params the import parameters. 315 * @return the content "local to repository" ID map. 316 */ 317 protected Map<String, String> getContentIdMap(Map<String, Object> params) 318 { 319 // Get or create the map in the global parameters. 320 @SuppressWarnings("unchecked") 321 Map<String, String> contentIdMap = (Map<String, String>) params.get(_CONTENT_ID_MAP_KEY); 322 if (contentIdMap == null) 323 { 324 contentIdMap = new HashMap<>(); 325 params.put(_CONTENT_ID_MAP_KEY, contentIdMap); 326 } 327 328 return contentIdMap; 329 } 330 331 /** 332 * Get the map used to store the content references. 333 * The Map is shaped like: referencing content -> local metadata path -> content references. 334 * @param params the import parameters. 335 * @return the content reference map. 336 */ 337 protected Map<Content, Map<String, Object>> getContentRefMap(Map<String, Object> params) 338 { 339 // Get or create the map in the global parameters. 340 @SuppressWarnings("unchecked") 341 Map<Content, Map<String, Object>> contentRefMap = (Map<Content, Map<String, Object>>) params.get(_CONTENT_LINK_MAP_KEY); 342 if (contentRefMap == null) 343 { 344 contentRefMap = new HashMap<>(); 345 params.put(_CONTENT_LINK_MAP_KEY, contentRefMap); 346 } 347 348 return contentRefMap; 349 } 350 351 /** 352 * Add a content reference to the map. 353 * @param content The referencing content. 354 * @param metadataPath The path of the metadata which holds the content references. 355 * @param reference The content reference. 356 * @param params The import parameters. 357 */ 358 protected void addContentReference(Content content, String metadataPath, ContentReference reference, Map<String, Object> params) 359 { 360 addContentReference(getContentRefMap(params), content, metadataPath, reference); 361 } 362 363 /** 364 * Add a content reference to the map. 365 * @param contentRefMap The content reference map. 366 * @param content The referencing content. 367 * @param metadataPath The path of the metadata which holds the content references. 368 * @param reference The content reference. 369 */ 370 protected void addContentReference(Map<Content, Map<String, Object>> contentRefMap, Content content, String metadataPath, ContentReference reference) 371 { 372 Map<String, Object> contentReferences; 373 if (contentRefMap.containsKey(content)) 374 { 375 contentReferences = contentRefMap.get(content); 376 } 377 else 378 { 379 contentReferences = new HashMap<>(); 380 contentRefMap.put(content, contentReferences); 381 } 382 383 contentReferences.put(metadataPath, reference); 384 } 385 386 /** 387 * Add content references to the map. 388 * @param contentRefMap The content reference map. 389 * @param content The referencing content. 390 * @param metadataPath The path of the metadata which holds the content references. 391 * @param references the content reference list. 392 */ 393 protected void addContentReferences(Map<Content, Map<String, Object>> contentRefMap, Content content, String metadataPath, List<ContentReference> references) 394 { 395 Map<String, Object> contentReferences; 396 if (contentRefMap.containsKey(content)) 397 { 398 contentReferences = contentRefMap.get(content); 399 } 400 else 401 { 402 contentReferences = new HashMap<>(); 403 contentRefMap.put(content, contentReferences); 404 } 405 406 contentReferences.put(metadataPath, references); 407 } 408 409 /** 410 * Get the map used to store the repeater sizes. 411 * The Map is shaped like: referencing content -> local metadata path -> content references. 412 * @param params the import parameters. 413 * @return the content reference map. 414 */ 415 protected Map<Content, Map<String, Integer>> getContentRepeaterSizeMap(Map<String, Object> params) 416 { 417 // Get or create the map in the global parameters. 418 @SuppressWarnings("unchecked") 419 Map<Content, Map<String, Integer>> contentRepeaterSizeMap = (Map<Content, Map<String, Integer>>) params.get(_CONTENT_REPEATER_SIZE_MAP); 420 if (contentRepeaterSizeMap == null) 421 { 422 contentRepeaterSizeMap = new HashMap<>(); 423 params.put(_CONTENT_REPEATER_SIZE_MAP, contentRepeaterSizeMap); 424 } 425 426 return contentRepeaterSizeMap; 427 } 428 429 /** 430 * Set a repeater size in the map (needed to execute the edit content function). 431 * @param content The content containing the repeater. 432 * @param metadataPath The repeater metadata path. 433 * @param repeaterSize The repeater size. 434 * @param params The import parameters. 435 */ 436 protected void setRepeaterSize(Content content, String metadataPath, int repeaterSize, Map<String, Object> params) 437 { 438 Map<Content, Map<String, Integer>> contentRepeaterSizeMap = getContentRepeaterSizeMap(params); 439 440 Map<String, Integer> repeaters; 441 if (contentRepeaterSizeMap.containsKey(content)) 442 { 443 repeaters = contentRepeaterSizeMap.get(content); 444 } 445 else 446 { 447 repeaters = new HashMap<>(); 448 contentRepeaterSizeMap.put(content, repeaters); 449 } 450 451 repeaters.put(metadataPath, repeaterSize); 452 } 453 454 /** 455 * Create a content. 456 * @param title the content title. 457 * @param params the import parameters. 458 * @return the created content. 459 * @throws WorkflowException if an error occurs. 460 */ 461 protected Content createContent(String title, Map<String, Object> params) throws WorkflowException 462 { 463 String[] contentTypes = getContentTypes(params); 464 String[] mixins = getMixins(params); 465 String language = getLanguage(params); 466 String workflowName = getWorkflowName(params); 467 int initialActionId = getInitialActionId(params); 468 469 return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, params); 470 } 471 472 /** 473 * Create a content. 474 * @param title the content title. 475 * @param contentTypes the content types. 476 * @param mixins the content mixins. 477 * @param language the content language. 478 * @param params the import parameters. 479 * @return the created content. 480 * @throws WorkflowException if an error occurs. 481 */ 482 protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, Map<String, Object> params) throws WorkflowException 483 { 484 String workflowName = getWorkflowName(params); 485 int initialActionId = getInitialActionId(params); 486 487 return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, params); 488 } 489 490 /** 491 * Create a content. 492 * @param title the content title. 493 * @param contentTypes the content types. 494 * @param mixins the content mixins. 495 * @param language the content language. 496 * @param parentContentId the parent content ID. 497 * @param parentContentMetadataPath the parent content metadata path. 498 * @param params the import parameters. 499 * @return the created content. 500 * @throws WorkflowException if an error occurs. 501 */ 502 protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, String parentContentId, String parentContentMetadataPath, Map<String, Object> params) throws WorkflowException 503 { 504 String workflowName = getWorkflowName(params); 505 int initialActionId = getInitialActionId(params); 506 507 return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, parentContentId, parentContentMetadataPath, params); 508 } 509 510 /** 511 * Create a content. 512 * @param title the content title. 513 * @param contentTypes the content types. 514 * @param mixins the content mixins. 515 * @param language the content language. 516 * @param workflowName the content workflow name. 517 * @param initialActionId the content create action ID. 518 * @param params the import parameters. 519 * @return the created content. 520 * @throws WorkflowException if an error occurs. 521 */ 522 protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, String workflowName, int initialActionId, Map<String, Object> params) throws WorkflowException 523 { 524 return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, null, null, params); 525 } 526 527 /** 528 * Create a content. 529 * @param title the content title. 530 * @param contentTypes the content types. 531 * @param mixins the content mixins. 532 * @param language the content language. 533 * @param workflowName the content workflow name. 534 * @param initialActionId the content create action ID. 535 * @param parentContentId the parent content ID. 536 * @param parentContentMetadataPath the parent content metadata path. 537 * @param params the import parameters. 538 * @return the created content. 539 * @throws WorkflowException if an error occurs. 540 */ 541 protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, String workflowName, int initialActionId, String parentContentId, String parentContentMetadataPath, Map<String, Object> params) throws WorkflowException 542 { 543 String name; 544 try 545 { 546 name = FilterNameHelper.filterName(title); 547 } 548 catch (Exception e) 549 { 550 // Ignore the exception, just provide a valid start. 551 name = "content-" + title; 552 } 553 554 Map<String, Object> result = _contentWorkflowHelper.createContent(workflowName, initialActionId, name, title, contentTypes, mixins, language, parentContentId, parentContentMetadataPath); 555 556 return (Content) result.get(AbstractContentWorkflowComponent.CONTENT_KEY); 557 } 558 559 /** 560 * Set a string metadata. 561 * @param meta the metadata holder. 562 * @param name the metadata name. 563 * @param metaDef the metadata definition. 564 * @param values the metadata values. 565 */ 566 protected void setStringMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values) 567 { 568 if (values != null) 569 { 570 if (metaDef.isMultiple()) 571 { 572 meta.setMetadata(name, values); 573 } 574 else 575 { 576 meta.setMetadata(name, values[0]); 577 } 578 } 579 } 580 581 /** 582 * Set a boolean metadata. 583 * @param meta the metadata holder. 584 * @param name the metadata name. 585 * @param metaDef the metadata definition. 586 * @param values the metadata values. 587 */ 588 protected void setBooleanMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values) 589 { 590 if (values != null) 591 { 592 if (metaDef.isMultiple()) 593 { 594 boolean[] bValues = new boolean[values.length]; 595 for (int i = 0; i < values.length; i++) 596 { 597 bValues[i] = Boolean.parseBoolean(values[i]); 598 } 599 600 meta.setMetadata(name, bValues); 601 } 602 else 603 { 604 meta.setMetadata(name, Boolean.parseBoolean(values[0])); 605 } 606 } 607 } 608 609 /** 610 * Set a long metadata. 611 * @param meta the metadata holder. 612 * @param name the metadata name. 613 * @param metaDef the metadata definition. 614 * @param values the metadata values. 615 */ 616 protected void setLongMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values) 617 { 618 if (values != null) 619 { 620 if (metaDef.isMultiple()) 621 { 622 long[] lValues = new long[values.length]; 623 for (int i = 0; i < values.length; i++) 624 { 625 lValues[i] = Long.parseLong(values[i]); 626 } 627 628 meta.setMetadata(name, lValues); 629 } 630 else 631 { 632 meta.setMetadata(name, Long.parseLong(values[0])); 633 } 634 } 635 } 636 637 /** 638 * Set a double metadata. 639 * @param meta the metadata holder. 640 * @param name the metadata name. 641 * @param metaDef the metadata definition. 642 * @param values the metadata values. 643 */ 644 protected void setDoubleMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values) 645 { 646 if (values != null) 647 { 648 if (metaDef.isMultiple()) 649 { 650 double[] dValues = new double[values.length]; 651 for (int i = 0; i < values.length; i++) 652 { 653 dValues[i] = Double.parseDouble(values[i]); 654 } 655 656 meta.setMetadata(name, dValues); 657 } 658 else 659 { 660 meta.setMetadata(name, Double.parseDouble(values[0])); 661 } 662 } 663 } 664 665 /** 666 * Set a date or datetime metadata. 667 * @param meta the metadata holder. 668 * @param name the metadata name. 669 * @param metaDef the metadata definition. 670 * @param values the metadata values. 671 */ 672 protected void setDateMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values) 673 { 674 if (values != null) 675 { 676 if (metaDef.isMultiple()) 677 { 678 Date[] dValues = new Date[values.length]; 679 for (int i = 0; i < values.length; i++) 680 { 681 dValues[i] = parseDate(values[i]); 682 } 683 684 meta.setMetadata(name, dValues); 685 } 686 else 687 { 688 meta.setMetadata(name, parseDate(values[0])); 689 } 690 } 691 } 692 693 /** 694 * Parse a String value as a Date.<br> 695 * Allowed formats: 696 * <ul> 697 * <li>yyyy-MM-dd</li> 698 * <li>yyyy-MM-dd'T'HH:mm:ss.SSSZZ</li> 699 * </ul> 700 * @param value the String value. 701 * @return the parsed Date or <code>null</code> if the value can't be parsed. 702 */ 703 protected Date parseDate(String value) 704 { 705 return parseDate(value, false); 706 } 707 708 /** 709 * Parse a String value as a Date.<br> 710 * Allowed formats: 711 * <ul> 712 * <li>yyyy-MM-dd</li> 713 * <li>yyyy-MM-dd'T'HH:mm:ss.SSSZZ</li> 714 * </ul> 715 * @param value the String value. 716 * @param throwException true to throw an exception if the value can't be parsed, false to return null. 717 * @return the parsed Date or <code>null</code> if the value can't be parsed and throwException is false. 718 */ 719 protected Date parseDate(String value, boolean throwException) 720 { 721 Date dateValue = null; 722 723 try 724 { 725 dateValue = Date.from(LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(ZoneId.systemDefault()).toInstant()); 726 } 727 catch (Exception e) 728 { 729 dateValue = DateUtils.parse(value); 730 } 731 732 if (dateValue == null && throwException) 733 { 734 throw new IllegalArgumentException("'" + value + "' could not be cast as a Date."); 735 } 736 737 return dateValue; 738 } 739 740 /** 741 * Set a geocode metadata. 742 * @param meta the metadata holder. 743 * @param name the metadata name. 744 * @param metaDef the metadata definition. 745 * @param latitude the geocode latitude as a String. 746 * @param longitude the geocode longitude as a String. 747 */ 748 protected void setGeocodeMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String latitude, String longitude) 749 { 750 if (StringUtils.isNotEmpty(latitude) && StringUtils.isNotEmpty(longitude)) 751 { 752 double dLat = Double.parseDouble(latitude); 753 double dLong = Double.parseDouble(longitude); 754 755 setGeocodeMetadata(meta, name, metaDef, dLat, dLong); 756 } 757 else 758 { 759 throw new IllegalArgumentException("Invalid geocode values: latitude='" + latitude + "', longitude='" + longitude + "'."); 760 } 761 } 762 763 /** 764 * Set a geocode metadata. 765 * @param meta the metadata holder. 766 * @param name the metadata name. 767 * @param metaDef the metadata definition. 768 * @param latitude the geocode latitude. 769 * @param longitude the geocode longitude. 770 */ 771 protected void setGeocodeMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, double latitude, double longitude) 772 { 773 ModifiableCompositeMetadata geoCode = meta.getCompositeMetadata(name, true); 774 geoCode.setMetadata("longitude", longitude); 775 geoCode.setMetadata("latitude", latitude); 776 } 777 778 /** 779 * Set a file metadata. 780 * @param meta the metadata holder. 781 * @param name the metadata name. 782 * @param metaDef the metadata definition 783 * @param value the value 784 * @throws IOException if an exception occurs when manipulating files 785 */ 786 protected void setBinaryMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String value) throws IOException 787 { 788 if (StringUtils.isNotEmpty(value)) 789 { 790 try 791 { 792 Pattern pattern = Pattern.compile("filename=\"([^\"]+)\""); 793 794 URL url = new URL(value); 795 HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 796 connection.setConnectTimeout(1000); 797 connection.setReadTimeout(2000); 798 799 try (InputStream is = connection.getInputStream()) 800 { 801 String contentType = StringUtils.defaultString(connection.getContentType(), "application/unknown"); 802 String contentEncoding = StringUtils.defaultString(connection.getContentEncoding(), ""); 803 String contentDisposition = StringUtils.defaultString(connection.getHeaderField("Content-Disposition"), ""); 804 String filename = URLDecoder.decode(FilenameUtils.getName(connection.getURL().getPath()), "UTF-8"); 805 if (StringUtils.isEmpty(filename)) 806 { 807 Matcher matcher = pattern.matcher(contentDisposition); 808 if (matcher.matches()) 809 { 810 filename = matcher.group(1); 811 } 812 else 813 { 814 filename = "unknown"; 815 } 816 } 817 818 try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) 819 { 820 IOUtils.copy(is, bos); 821 822 ModifiableBinaryMetadata binaryMeta = meta.getBinaryMetadata(name, true); 823 binaryMeta.setLastModified(new Date()); 824 binaryMeta.setInputStream(new ByteArrayInputStream(bos.toByteArray())); 825 if (StringUtils.isNotEmpty(filename)) 826 { 827 binaryMeta.setFilename(filename); 828 } 829 if (StringUtils.isNotEmpty(contentType)) 830 { 831 binaryMeta.setMimeType(contentType); 832 } 833 if (StringUtils.isNotEmpty(contentEncoding)) 834 { 835 binaryMeta.setEncoding(contentEncoding); 836 } 837 } 838 } 839 } 840 catch (Exception e) 841 { 842 throw new IllegalArgumentException("Unable to fetch file from URL '" + value + "', it will be ignored.", e); 843 } 844 } 845 } 846 847 /** 848 * Set a RichText metadata. 849 * @param meta the metadata holder. 850 * @param name the metadata name. 851 * @param data an input stream on the rich text content. 852 */ 853 protected void setRichText(ModifiableCompositeMetadata meta, String name, InputStream data) 854 { 855 ModifiableRichText richText = meta.getRichText(name, true); 856 857 richText.setEncoding("UTF-8"); 858 richText.setLastModified(new Date()); 859 richText.setMimeType("text/xml"); 860 richText.setInputStream(data); 861 } 862 863 /** 864 * Restore content references. 865 * @param params The import parameters. 866 */ 867 protected void restoreContentReferences(Map<String, Object> params) 868 { 869 Map<Content, Map<String, Object>> contentRefMap = getContentRefMap(params); 870 Map<Content, Map<String, Integer>> contentRepeaterSizeMap = getContentRepeaterSizeMap(params); 871 int editActionId = getEditActionId(params); 872 873 for (Content content : contentRefMap.keySet()) 874 { 875 if (content instanceof WorkflowAwareContent) 876 { 877 Map<String, Object> contentReferences = contentRefMap.get(content); 878 Map<String, Integer> repeaters = contentRepeaterSizeMap.get(content); 879 880 Map<String, Object> values = new HashMap<>(); 881 882 // Fill the value map with the content references. 883 setReferenceMetadatas(contentReferences, values, repeaters, params); 884 885 try 886 { 887 if (!values.isEmpty()) 888 { 889 Map<String, Object> contextParameters = new HashMap<>(); 890 contextParameters.put(EditContentFunction.QUIT, true); 891 contextParameters.put(EditContentFunction.FORM_RAW_VALUES, values); 892 893 Map<String, Object> inputs = new HashMap<>(); 894 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 895 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 896 897 _contentWorkflowHelper.doAction((WorkflowAwareContent) content, editActionId, inputs); 898 } 899 } 900 catch (WorkflowException e) 901 { 902 // TODO Throw exception? 903 getLogger().warn("An error occurred restoring content references in content {}", content, e); 904 } 905 } 906 } 907 } 908 909 /** 910 * Fill the value map with the content references. 911 * @param contentReferences The list of content references indexed by metadata path. 912 * @param values The value map passed to the EditContentFunction class. 913 * @param repeaters The repeater sizes for this content. 914 * @param params The import parameters. 915 */ 916 protected void setReferenceMetadatas(Map<String, Object> contentReferences, Map<String, Object> values, Map<String, Integer> repeaters, Map<String, Object> params) 917 { 918 for (String metadataPath : contentReferences.keySet()) 919 { 920 Object value = contentReferences.get(metadataPath); 921 String metaKey = EditContentFunction.FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.'); 922 923 if (value instanceof List<?>) 924 { 925 // Multiple value 926 @SuppressWarnings("unchecked") 927 List<ContentReference> references = (List<ContentReference>) value; 928 List<String> contentIds = new ArrayList<>(references.size()); 929 for (ContentReference reference : references) 930 { 931 String refContentId = getReferencedContentId(reference, params); 932 if (refContentId != null) 933 { 934 contentIds.add(refContentId); 935 } 936 } 937 938 if (!contentIds.isEmpty()) 939 { 940 values.put(metaKey, contentIds); 941 } 942 } 943 else if (value instanceof ContentReference) 944 { 945 // Single value. 946 String refContentId = getReferencedContentId((ContentReference) value, params); 947 if (refContentId != null) 948 { 949 values.put(metaKey, refContentId); 950 } 951 } 952 } 953 954 if (repeaters != null) 955 { 956 for (String repeaterPath : repeaters.keySet()) 957 { 958 Integer size = repeaters.get(repeaterPath); 959 if (size > 0) 960 { 961 String sizeKey = EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + repeaterPath.replace('/', '.') + ".size"; 962 values.put(sizeKey, repeaters.get(repeaterPath).toString()); 963 } 964 } 965 } 966 } 967 968 /** 969 * Get the content ID from a content reference. 970 * @param contentRef The content reference. 971 * @param params The import parameters. 972 * @return the content ID if it was found, or null otherwise. 973 */ 974 protected String getReferencedContentId(ContentReference contentRef, Map<String, Object> params) 975 { 976 int refType = contentRef.getType(); 977 if (refType == ContentReference.TYPE_LOCAL_ID) 978 { 979 String localId = (String) contentRef.getValue(); 980 String contentId = getContentIdMap(params).get(localId); 981 if (StringUtils.isNotEmpty(contentId) && _resolver.hasAmetysObjectForId(contentId)) 982 { 983 return contentId; 984 } 985 } 986 else if (refType == ContentReference.TYPE_CONTENT_ID) 987 { 988 String contentId = (String) contentRef.getValue(); 989 if (StringUtils.isNotEmpty(contentId) && _resolver.hasAmetysObjectForId(contentId)) 990 { 991 return contentId; 992 } 993 } 994 else if (refType == ContentReference.TYPE_CONTENT_VALUES) 995 { 996 @SuppressWarnings("unchecked") 997 Map<String, String> values = (Map<String, String>) contentRef.getValue(); 998 Content content = getContentFromProperties(values); 999 if (content != null) 1000 { 1001 return content.getId(); 1002 } 1003 } 1004 1005 return null; 1006 } 1007 1008 /** 1009 * Search a content from a map of its metadata values. 1010 * @param propertyValues The metadata values. 1011 * @return The Content if found, null otherwise. 1012 */ 1013 protected Content getContentFromProperties(Map<String, String> propertyValues) 1014 { 1015 Content content = null; 1016 1017 List<Expression> expressions = new ArrayList<>(); 1018 for (String property : propertyValues.keySet()) 1019 { 1020 String value = propertyValues.get(property); 1021 expressions.add(new StringExpression(property, Operator.EQ, value)); 1022 } 1023 1024 Expression[] exprArray = expressions.toArray(new Expression[expressions.size()]); 1025 1026 String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(exprArray)); 1027 1028 AmetysObjectIterable<Content> contents = _resolver.query(query); 1029 Iterator<Content> it = contents.iterator(); 1030 1031 if (it.hasNext()) 1032 { 1033 content = it.next(); 1034 1035 if (it.hasNext()) 1036 { 1037 content = null; 1038 } 1039 } 1040 1041 return content; 1042 } 1043 1044 /** 1045 * Class representing a reference to a content in an import file. 1046 */ 1047 public class ContentReference 1048 { 1049 1050 /** 1051 * The referenced content doesn't exist in the repository, it's in the import file. 1052 * The reference value is the content ID in the import file. 1053 */ 1054 public static final int TYPE_LOCAL_ID = 1; 1055 1056 /** 1057 * The referenced content exits in the repository and its ID is known. 1058 * The reference value is the content ID in the repository (AmetysObject ID). 1059 */ 1060 public static final int TYPE_CONTENT_ID = 2; 1061 1062 /** 1063 * The referenced content exits in the repository. Its ID is not known, 1064 * but it can be identified by one or several of its metadata. 1065 * The reference value is a Map of metadata name -> value. 1066 */ 1067 public static final int TYPE_CONTENT_VALUES = 3; 1068 1069 /** The reference type. */ 1070 private int _type; 1071 1072 /** The reference value, depends on the reference type. */ 1073 private Object _value; 1074 1075 /** 1076 * Build a content reference. 1077 * @param type the reference type. 1078 * @param value the reference value. 1079 */ 1080 public ContentReference(int type, Object value) 1081 { 1082 this._type = type; 1083 this._value = value; 1084 } 1085 1086 /** 1087 * Get the type. 1088 * @return the type 1089 */ 1090 public int getType() 1091 { 1092 return _type; 1093 } 1094 1095 /** 1096 * Set the type. 1097 * @param type the type to set 1098 */ 1099 public void setType(int type) 1100 { 1101 this._type = type; 1102 } 1103 1104 /** 1105 * Get the value. 1106 * @return the value 1107 */ 1108 public Object getValue() 1109 { 1110 return _value; 1111 } 1112 1113 /** 1114 * Set the value. 1115 * @param value the value to set 1116 */ 1117 public void setValue(Object value) 1118 { 1119 this._value = value; 1120 } 1121 1122 } 1123 1124}