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