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