001/* 002 * Copyright 2018 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.synchronize; 017 018import java.io.ByteArrayInputStream; 019import java.io.IOException; 020import java.io.StringReader; 021import java.lang.reflect.Array; 022import java.nio.charset.StandardCharsets; 023import java.time.LocalDate; 024import java.time.ZonedDateTime; 025import java.util.ArrayList; 026import java.util.Date; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032 033import javax.jcr.RepositoryException; 034import javax.jcr.Value; 035 036import org.apache.avalon.framework.component.Component; 037import org.apache.avalon.framework.service.ServiceException; 038import org.apache.avalon.framework.service.ServiceManager; 039import org.apache.avalon.framework.service.Serviceable; 040import org.apache.commons.codec.digest.DigestUtils; 041import org.apache.commons.io.IOUtils; 042import org.apache.commons.lang.StringUtils; 043import org.apache.commons.lang3.ArrayUtils; 044import org.apache.tika.Tika; 045import org.apache.tika.mime.MimeType; 046import org.apache.tika.mime.MimeTypeException; 047import org.apache.tika.mime.MimeTypes; 048import org.slf4j.Logger; 049 050import org.ametys.cms.FilterNameHelper; 051import org.ametys.cms.content.external.ExternalizableMetadataHelper; 052import org.ametys.cms.content.external.ExternalizableMetadataProvider.ExternalizableMetadataStatus; 053import org.ametys.cms.contenttype.ContentType; 054import org.ametys.cms.contenttype.MetadataDefinition; 055import org.ametys.cms.contenttype.MetadataType; 056import org.ametys.cms.repository.Content; 057import org.ametys.cms.repository.DefaultContent; 058import org.ametys.cms.repository.ModifiableDefaultContent; 059import org.ametys.cms.repository.WorkflowAwareContent; 060import org.ametys.cms.workflow.ContentWorkflowHelper; 061import org.ametys.cms.workflow.CreateContentFunction; 062import org.ametys.core.observation.Event; 063import org.ametys.core.observation.ObservationManager; 064import org.ametys.core.user.CurrentUserProvider; 065import org.ametys.core.util.DateUtils; 066import org.ametys.plugins.contentio.ContentImporterHelper; 067import org.ametys.plugins.repository.AmetysObjectResolver; 068import org.ametys.plugins.repository.AmetysRepositoryException; 069import org.ametys.plugins.repository.lock.LockHelper; 070import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata; 071import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 072import org.ametys.plugins.repository.metadata.ModifiableRichText; 073import org.ametys.plugins.workflow.AbstractWorkflowComponent; 074import org.ametys.plugins.workflow.component.CheckRightsCondition; 075 076import com.google.common.base.CharMatcher; 077import com.google.common.collect.ImmutableMap; 078import com.opensymphony.workflow.InvalidActionException; 079import com.opensymphony.workflow.WorkflowException; 080 081/** 082 * Class for basics operations on SCC. 083 */ 084public class BaseSynchroComponent implements Serviceable, Component 085{ 086 /** Avalon Role */ 087 public static final String ROLE = BaseSynchroComponent.class.getName(); 088 089 /** The content workflow helper */ 090 protected ContentWorkflowHelper _workflowHelper; 091 092 /** The observation manager */ 093 protected ObservationManager _observationManager; 094 095 /** The current user provider */ 096 protected CurrentUserProvider _currentUserProvider; 097 098 /** The ametys object resolver */ 099 protected AmetysObjectResolver _resolver; 100 101 @Override 102 public void service(ServiceManager manager) throws ServiceException 103 { 104 _workflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 105 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 106 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 107 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 108 } 109 110 /** 111 * Remove the metadata if exists 112 * @param metadataHolder The metadata holder 113 * @param metadataName The name of the metadata 114 * @param synchronize <code>true</code> if the data is synchronize 115 * @return <code>true</code> if the metadata have been removed 116 */ 117 public boolean removeMetadataIfExists(ModifiableCompositeMetadata metadataHolder, String metadataName, boolean synchronize) 118 { 119 if (synchronize) 120 { 121 return ExternalizableMetadataHelper.removeExternalMetadataIfExists(metadataHolder, metadataName); 122 } 123 else 124 { 125 boolean hasMetadata = metadataHolder.hasMetadata(metadataName); 126 if (hasMetadata) 127 { 128 metadataHolder.removeMetadata(metadataName); 129 } 130 return hasMetadata; 131 } 132 } 133 134 /** 135 * Validates a content after import 136 * @param content The content to validate 137 * @param validationActionId Validation action ID to use for this content 138 * @param logger The logger 139 */ 140 public void validateContent(WorkflowAwareContent content, int validationActionId, Logger logger) 141 { 142 validateContent(content, validationActionId, false, logger); 143 } 144 145 /** 146 * Validates a content after import 147 * @param content The content to validate 148 * @param validationActionId Validation action ID to use for this content 149 * @param ignoreRights true if checking rights needs to be ignored 150 * @param logger The logger 151 */ 152 public void validateContent(WorkflowAwareContent content, int validationActionId, boolean ignoreRights, Logger logger) 153 { 154 try 155 { 156 Map<String, Object> inputs = new HashMap<>(); 157 if (ignoreRights) 158 { 159 inputs.put(CheckRightsCondition.FORCE, true); 160 } 161 162 _workflowHelper.doAction(content, validationActionId, inputs); 163 logger.info("The content '{}' ({}) has been validated after import", content.getTitle(), content.getId()); 164 } 165 catch (InvalidActionException e) 166 { 167 logger.error("The content '{}' ({}) cannot be validated after import: may miss mandatory metadata ?", content.getTitle(), content.getId(), e); 168 } 169 catch (WorkflowException e) 170 { 171 logger.error("The content '{}' ({}) cannot be validated after import", content.getTitle(), content.getId(), e); 172 } 173 } 174 175 /** 176 * Does workflow action 177 * @param content The synchronized content 178 * @param actionId Workflow action 179 * @param event Type of event 180 * @param logger The logger 181 * @return A {@link Map} with one or two {@link Boolean}, "success" tells if the operation have been done successfully, "error" tells if an error occurs during the content saving. The save can be successful but an error can occurs during the workflow update. 182 * @throws RepositoryException if an error occurs when trying to rollback pending changes in the repository. 183 */ 184 public Map<String, Boolean> applyChanges(ModifiableDefaultContent content, Integer actionId, String event, Logger logger) throws RepositoryException 185 { 186 return applyChanges(content, actionId, event, false, logger); 187 } 188 189 /** 190 * Does workflow action 191 * @param content The synchronized content 192 * @param actionId Workflow action 193 * @param event Type of event 194 * @param ignoreRights true if checking rights needs to be ignored 195 * @param logger The logger 196 * @return A {@link Map} with one or two {@link Boolean}, "success" tells if the operation have been done successfully, "error" tells if an error occurs during the content saving. The save can be successful but an error can occurs during the workflow update. 197 * @throws RepositoryException if an error occurs when trying to rollback pending changes in the repository. 198 */ 199 public Map<String, Boolean> applyChanges(ModifiableDefaultContent content, Integer actionId, String event, boolean ignoreRights, Logger logger) throws RepositoryException 200 { 201 Map<String, Boolean> resultMap = new HashMap<>(); 202 203 try 204 { 205 content.setLastModified(new Date()); 206 try 207 { 208 content.saveChanges(); 209 } 210 catch (AmetysRepositoryException e) 211 { 212 resultMap.put("error", Boolean.TRUE); 213 logger.error("An error occurred while saving changes on content '{}'.", content.getId(), e); 214 215 // Rollback pending changes 216 content.getNode().getSession().refresh(false); 217 resultMap.put("success", Boolean.FALSE); 218 return resultMap; 219 } 220 221 if (content.isLocked() && !LockHelper.isLockOwner(content, _currentUserProvider.getUser())) 222 { 223 logger.warn("Cannot apply changes because content {} is currently locked by {}", content.getTitle(), _currentUserProvider.getUser()); 224 resultMap.put("success", Boolean.TRUE); 225 return resultMap; 226 } 227 228 // Create new version 229 content.checkpoint(); 230 231 // Notify observers that the content has been modified 232 Map<String, Object> eventParams = new HashMap<>(); 233 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT, content); 234 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT_ID, content.getId()); 235 _observationManager.notify(new Event(event, _currentUserProvider.getUser(), eventParams)); 236 237 Map<String, Object> inputs = new HashMap<>(); 238 if (ignoreRights) 239 { 240 inputs.put(CheckRightsCondition.FORCE, true); 241 } 242 243 _workflowHelper.doAction(content, actionId, inputs); 244 } 245 catch (WorkflowException | InvalidActionException e) 246 { 247 resultMap.put("error", Boolean.TRUE); 248 logger.error("Unable to update workflow of content '{}' ({})", content.getTitle(), content.getId(), e); 249 } 250 251 resultMap.put("success", Boolean.TRUE); 252 return resultMap; 253 } 254 255 /** 256 * Update the invert relation by adding/removing the content to/from the old values. 257 * @param metadataToEdit Metadata holder to edit 258 * @param metadataName Metadata name to set 259 * @param content The content to add or remove 260 * @param remove <code>true</code> if we wan't to remove the content from the relation 261 * @return <code>true</code> if there are changes 262 */ 263 public boolean updateRelation(ModifiableCompositeMetadata metadataToEdit, String metadataName, Content content, boolean remove) 264 { 265 return updateRelation(metadataToEdit, metadataName, content.getId(), remove); 266 } 267 268 /** 269 * Update the invert relation by adding/removing the content to/from the old values. 270 * @param metadataToEdit Metadata holder to edit 271 * @param metadataName Metadata name to set 272 * @param contentId The content to add or remove 273 * @param remove <code>true</code> if we wan't to remove the content from the relation 274 * @return <code>true</code> if there are changes 275 */ 276 public boolean updateRelation(ModifiableCompositeMetadata metadataToEdit, String metadataName, String contentId, boolean remove) 277 { 278 String[] oldValues = metadataToEdit.getStringArray(metadataName, new String[0]); 279 String[] newValues = null; 280 281 // If we need to remove the value 282 if (remove && ArrayUtils.contains(oldValues, contentId)) 283 { 284 newValues = ArrayUtils.removeElement(oldValues, contentId); 285 } 286 // If we need to add the value 287 else if (!remove && !ArrayUtils.contains(oldValues, contentId)) 288 { 289 newValues = ArrayUtils.add(oldValues, contentId); 290 } 291 292 // If there is a change to apply 293 if (newValues != null) 294 { 295 List<Content> contents = new ArrayList<>(); 296 for (String value : newValues) 297 { 298 contents.add(_resolver.resolveById(value)); 299 } 300 301 return ExternalizableMetadataHelper.setMetadata(metadataToEdit, metadataName, contents.toArray(new Content[contents.size()])); 302 } 303 304 return false; 305 } 306 307 /** 308 * Add the current synchronizable collection as property 309 * @param content The synchronized content 310 * @param collectionId The ID of the collection 311 * @throws RepositoryException if an error occurred 312 */ 313 public void updateSCCProperty(DefaultContent content, String collectionId) throws RepositoryException 314 { 315 if (content.getNode().hasProperty(SynchronizableContentsCollection.COLLECTION_ID_PROPERTY)) 316 { 317 Value[] values = content.getNode().getProperty(SynchronizableContentsCollection.COLLECTION_ID_PROPERTY).getValues(); 318 Set<String> collectionIds = new HashSet<>(); 319 for (Value value : values) 320 { 321 collectionIds.add(value.getString()); 322 } 323 collectionIds.add(collectionId); 324 325 content.getNode().setProperty(SynchronizableContentsCollection.COLLECTION_ID_PROPERTY, collectionIds.toArray(new String[] {})); 326 } 327 else 328 { 329 content.getNode().setProperty(SynchronizableContentsCollection.COLLECTION_ID_PROPERTY, new String[] {collectionId}); 330 } 331 } 332 333 /** 334 * Creates content action with result from request 335 * @param contentType Type of the content to create 336 * @param workflowName Workflow to use for this content 337 * @param initialActionId Action ID for initialization 338 * @param lang The language 339 * @param contentTitle The content title 340 * @param contentPrefix The content prefix for the node creation 341 * @param logger The logger 342 * @return A {@link Map} with the created content in "content", and a {@link Boolean} in "error" if an error occurs. 343 */ 344 public Map<String, Object> createContentAction(String contentType, String workflowName, int initialActionId, String lang, String contentTitle, String contentPrefix, Logger logger) 345 { 346 logger.info("Creating content '{}' with the content type '{}' for language {}", contentTitle, contentType, lang); 347 348 Map<String, Object> resultMap = new HashMap<>(); 349 350 String contentName = _getContentName(contentTitle, lang, contentPrefix); 351 352 Map<String, Object> inputs = new HashMap<>(); 353 354 inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, lang); 355 inputs.put(CreateContentFunction.CONTENT_NAME_KEY, contentName); 356 inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, contentTitle); 357 inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {contentType}); 358 359 Map<String, Object> results = new HashMap<>(); 360 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results); 361 362 try 363 { 364 Map<String, Object> workflowResult = _workflowHelper.createContent(workflowName, initialActionId, contentName, contentTitle, new String[] {contentType}, null, lang); 365 resultMap.put("content", _resolver.resolveById((String) workflowResult.get("contentId"))); 366 } 367 catch (WorkflowException e) 368 { 369 resultMap.put("error", Boolean.TRUE); 370 logger.error("Failed to initialize workflow for content {} and language {}", contentTitle, lang, e); 371 } 372 373 return resultMap; 374 } 375 376 /** 377 * Gets the content name 378 * @param title The name 379 * @param lang The lang of the content 380 * @param prefix The prefix to put before the title 381 * @return The content name 382 */ 383 private String _getContentName(String title, String lang, String prefix) 384 { 385 return FilterNameHelper.filterName(prefix + "-" + title + "-" + lang); 386 } 387 388 /** 389 * Fill the metadata with remove value. 390 * @param content The content to synchronize 391 * @param contentType The content type 392 * @param logicalMetadataPath The logical metadata path without the entries 393 * @param completeMetadataPath The complete metadata path from the root of the content 394 * @param remoteValue The remote value 395 * @param synchronize <code>true</code> if synchronizable 396 * @param create <code>true</code> if content is creating, false if it is updated 397 * @param logger The logger 398 * @return A {@link Map} with a {@link Boolean} in "hasChanges" value if changes has been made, and a {@link Boolean} in "error" value if an error occurs. 399 */ 400 public Map<String, Boolean> synchronizeMetadata(ModifiableDefaultContent content, ContentType contentType, String logicalMetadataPath, String completeMetadataPath, List<Object> remoteValue, boolean synchronize, boolean create, Logger logger) 401 { 402 ModifiableCompositeMetadata metadataHolder = getMetadataHolder(content.getMetadataHolder(), completeMetadataPath); 403 String[] arrayPath = completeMetadataPath.split("/"); 404 String metadataName = arrayPath[arrayPath.length - 1]; 405 406 return synchronizeMetadata(content, contentType, logicalMetadataPath, metadataHolder, metadataName, remoteValue, synchronize, create, logger); 407 } 408 409 /** 410 * Fill the metadata with remote value. 411 * @param content The content to synchronize 412 * @param contentType The content type 413 * @param logicalMetadataPath The logical metadata path without the entries 414 * @param metadataHolder the metadata holder of the content to synchronize 415 * @param metadataName the name of the metadata 416 * @param remoteValue The remote value 417 * @param synchronize <code>true</code> if synchronizable 418 * @param create <code>true</code> if content is creating, false if it is updated 419 * @param logger The logger 420 * @return A {@link Map} with a {@link Boolean} in "hasChanges" value if changes has been made, and a {@link Boolean} in "error" value if an error occurs. 421 */ 422 public Map<String, Boolean> synchronizeMetadata(ModifiableDefaultContent content, ContentType contentType, String logicalMetadataPath, ModifiableCompositeMetadata metadataHolder, String metadataName, List<Object> remoteValue, boolean synchronize, boolean create, Logger logger) 423 { 424 MetadataDefinition metadataDef = contentType.getMetadataDefinitionByPath(logicalMetadataPath); 425 426 if (metadataDef != null) 427 { 428 if (remoteValue != null && !remoteValue.isEmpty()) 429 { 430 Object valueToSet; 431 if (metadataDef.getType().equals(MetadataType.RICH_TEXT)) 432 { 433 valueToSet = remoteValue.get(0); // remoteValue is not empty at this stage 434 return ImmutableMap.of("hasChanges", _setRichTextMetadata(metadataHolder, metadataName, valueToSet, synchronize, content.getTitle(), logger)); 435 } 436 else if (metadataDef.getType().equals(MetadataType.BINARY) || metadataDef.getType().equals(MetadataType.FILE)) 437 { 438 valueToSet = remoteValue.get(0); // remoteValue is not empty at this stage 439 return ImmutableMap.of("hasChanges", _setBinaryMetadata(metadataHolder, metadataName, valueToSet, synchronize, content.getTitle(), logger)); 440 } 441 else if (metadataDef.getType().equals(MetadataType.DATE)) 442 { 443 valueToSet = remoteValue.get(0); // remoteValue is not empty at this stage 444 return _setMetadata(metadataHolder, metadataName, _getDateValueToSet(valueToSet), synchronize, create, content.getTitle(), contentType.getId(), logger); 445 } 446 else if (metadataDef.getType().equals(MetadataType.DATETIME)) 447 { 448 valueToSet = remoteValue.get(0); // remoteValue is not empty at this stage 449 return _setMetadata(metadataHolder, metadataName, _getDateTimeValueToSet(valueToSet), synchronize, create, content.getTitle(), contentType.getId(), logger); 450 } 451 else if (metadataDef.isMultiple()) 452 { 453 valueToSet = _toTypedArray(remoteValue.get(0).getClass(), remoteValue); 454 return _setMetadata(metadataHolder, metadataName, valueToSet, synchronize, create, content.getTitle(), contentType.getId(), logger); 455 } 456 else 457 { 458 valueToSet = remoteValue.get(0); // remoteValue is not empty at this stage 459 return _setMetadata(metadataHolder, metadataName, valueToSet, synchronize, create, content.getTitle(), contentType.getId(), logger); 460 } 461 } 462 else if (metadataDef.getDefaultValue() != null) 463 { 464 return _setMetadata(metadataHolder, metadataName, metadataDef.getDefaultValue(), synchronize, create, content.getTitle(), contentType.getId(), logger); 465 } 466 } 467 468 return ImmutableMap.of("hasChanges", Content.METADATA_TITLE.equals(metadataName) ? false : removeMetadataIfExists(metadataHolder, metadataName, synchronize)); 469 } 470 471 private Object _getDateValueToSet(Object remoteValue) 472 { 473 Object valueToSet = remoteValue; 474 if (remoteValue instanceof java.sql.Date) 475 { 476 valueToSet = ((java.sql.Date) remoteValue).toLocalDate(); 477 } 478 else if (remoteValue instanceof java.sql.Timestamp) 479 { 480 valueToSet = ((java.sql.Timestamp) remoteValue).toLocalDateTime().toLocalDate(); 481 } 482 else if (remoteValue instanceof Date) 483 { 484 valueToSet = DateUtils.asLocalDate((Date) remoteValue); 485 } 486 487 if (valueToSet instanceof LocalDate) 488 { 489 return DateUtils.asDate((LocalDate) valueToSet); 490 } 491 492 return remoteValue; 493 } 494 495 private Object _getDateTimeValueToSet(Object remoteValue) 496 { 497 Object valueToSet = remoteValue; 498 if (remoteValue instanceof java.sql.Date) 499 { 500 valueToSet = DateUtils.asZonedDateTime(((java.sql.Date) remoteValue).toInstant().toEpochMilli()); 501 } 502 else if (remoteValue instanceof java.sql.Timestamp) 503 { 504 valueToSet = DateUtils.asZonedDateTime(((java.sql.Timestamp) remoteValue).toInstant().toEpochMilli()); 505 } 506 else if (remoteValue instanceof Date) 507 { 508 valueToSet = DateUtils.asZonedDateTime((Date) remoteValue); 509 } 510 511 if (valueToSet instanceof ZonedDateTime) 512 { 513 return DateUtils.asDate((ZonedDateTime) valueToSet); 514 } 515 516 return remoteValue; 517 } 518 519 /** 520 * Set the richtext metadata 521 * @param metadataHolder the metadata holder 522 * @param metadataName the metadata name 523 * @param valueToSet the value to set 524 * @param synchronize true if the metadata is synchronize 525 * @param title the content title 526 * @param logger the logger 527 * @return <code>true</code> if changes were made 528 */ 529 private boolean _setRichTextMetadata(ModifiableCompositeMetadata metadataHolder, String metadataName, Object valueToSet, boolean synchronize, String title, Logger logger) 530 { 531 try 532 { 533 String docbook = ContentImporterHelper.textToDocbook(_getLinesFromValue(valueToSet)); 534 535 ByteArrayInputStream is = new ByteArrayInputStream(docbook.getBytes(StandardCharsets.UTF_8)); 536 ModifiableRichText richText = ExternalizableMetadataHelper.getRichText(metadataHolder, metadataName, synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL, true); 537 richText.setInputStream(is); 538 richText.setMimeType("text/xml"); 539 richText.setLastModified(new Date()); 540 541 return true; 542 } 543 catch (IOException e) 544 { 545 logger.error("An error occured while setting the rich text value for metadata '{}' of the content '{}'", metadataName, title, e); 546 return false; 547 } 548 } 549 550 /** 551 * Set the binary metadata 552 * @param metadataHolder the metadata holder 553 * @param metadataName the metadata name 554 * @param valueToSet the value to set 555 * @param synchronize true if the metadata is synchronize 556 * @param title the content title 557 * @param logger the logger 558 * @return <code>true</code> if changes were made 559 */ 560 protected boolean _setBinaryMetadata(ModifiableCompositeMetadata metadataHolder, String metadataName, Object valueToSet, boolean synchronize, String title, Logger logger) 561 { 562 ModifiableBinaryMetadata binary = ExternalizableMetadataHelper.getBinaryMetadata(metadataHolder, metadataName, synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL, true); 563 String hash = binary.getHash(); 564 try 565 { 566 byte[] bytes = _getBytesFromValue(valueToSet); 567 568 try 569 { 570 ByteArrayInputStream byteArrayInputStreamForTika = new ByteArrayInputStream(bytes); 571 Tika tika = new Tika(); 572 String mediaTypeAsString = tika.detect(byteArrayInputStreamForTika); 573 MimeType mimeType = MimeTypes.getDefaultMimeTypes().forName(mediaTypeAsString); 574 575 ByteArrayInputStream byteArrayInputStreamForHash = new ByteArrayInputStream(bytes); 576 if (StringUtils.isBlank(hash) || !hash.equals(_getSHA1Hash(byteArrayInputStreamForHash, logger))) 577 { 578 binary.setInputStream(new ByteArrayInputStream(bytes)); 579 binary.setMimeType(mediaTypeAsString); 580 binary.setLastModified(new Date()); 581 binary.setFilename(FilterNameHelper.filterName(title) + "-" + metadataName + mimeType.getExtension()); 582 583 return true; 584 } 585 } 586 catch (MimeTypeException | IOException e) 587 { 588 logger.warn("An error occurred, can't set the binary to metadata " + metadataName, e); 589 } 590 591 return false; 592 } 593 catch (IOException e) 594 { 595 logger.error("An error occured while setting the binary value for metadata '{}' of the content '{}'", metadataName, title, e); 596 return false; 597 } 598 } 599 600 /** 601 * Get the bytes array from the value to set 602 * @param valueToSet the value to set 603 * @return the bytes array 604 * @throws IOException if an error occurred 605 */ 606 private byte[] _getBytesFromValue(Object valueToSet) throws IOException 607 { 608 if (valueToSet instanceof byte[]) 609 { 610 return (byte[]) valueToSet; 611 } 612 613 return IOUtils.toByteArray(new StringReader(valueToSet.toString()), StandardCharsets.UTF_8); 614 } 615 616 /** 617 * Get the lines array from valueToSet for the richText 618 * @param valueToSet the value to set 619 * @return the lines array 620 * @throws IOException if an error occurred 621 */ 622 private String[] _getLinesFromValue(Object valueToSet) throws IOException 623 { 624 if (valueToSet instanceof byte[]) 625 { 626 String strValue = IOUtils.toString((byte[]) valueToSet, "UTF-8"); 627 return new String[] {CharMatcher.javaIsoControl().and(CharMatcher.anyOf("\r\n\t").negate()).removeFrom(strValue)}; 628 } 629 630 return new String[] {valueToSet.toString()}; 631 } 632 633 private Map<String, Boolean> _setMetadata(ModifiableCompositeMetadata metadataHolder, String metadataName, Object valueToSet, boolean synchronize, boolean forceExternalStatus, String contentTitle, String contentType, Logger logger) 634 { 635 Map<String, Boolean> resultMap = new HashMap<>(); 636 637 try 638 { 639 boolean hasChanges; 640 if (synchronize) 641 { 642 hasChanges = ExternalizableMetadataHelper.setExternalMetadata(metadataHolder, metadataName, valueToSet, forceExternalStatus); 643 } 644 else 645 { 646 hasChanges = ExternalizableMetadataHelper.setMetadata(metadataHolder, metadataName, valueToSet); 647 } 648 resultMap.put("hasChanges", hasChanges); 649 } 650 catch (UnsupportedOperationException e) 651 { 652 logger.error("An error occured during the synchronization of the field '{}' with the value '{}' in the content '{}' type of '{}'", metadataName, valueToSet.toString(), contentTitle, contentType); 653 resultMap.put("error", Boolean.TRUE); 654 } 655 return resultMap; 656 } 657 658 /** 659 * Get the metadata holder for the requested metadata path. 660 * @param parentMetadata Initial metadata 661 * @param metadataPath Metadata path from the parent 662 * @return A metadata holder 663 */ 664 public ModifiableCompositeMetadata getMetadataHolder(ModifiableCompositeMetadata parentMetadata, String metadataPath) 665 { 666 int pos = metadataPath.indexOf("/"); 667 if (pos == -1) 668 { 669 return parentMetadata; 670 } 671 else 672 { 673 return getMetadataHolder(parentMetadata.getCompositeMetadata(metadataPath.substring(0, pos), true), metadataPath.substring(pos + 1)); 674 } 675 } 676 677 private String _getSHA1Hash(ByteArrayInputStream stream, Logger logger) 678 { 679 try 680 { 681 return DigestUtils.sha1Hex(stream); 682 } 683 catch (IOException e) 684 { 685 logger.warn("Unable to get hash from stream", e); 686 return null; 687 } 688 } 689 690 // CONTENTIO-95 Multiple values are always converted as Object[] and cannot be set 691 /** 692 * This method have been build to go through the CONTENTIO-95 problem. The toArray() method returns an Object[] and it's not supported by {@link ExternalizableMetadataHelper} methods. 693 * @param <T> The type of the array 694 * @param classToCastTo The class to cast to 695 * @param list The list to transform to an array 696 * @return A typed array. 697 */ 698 @SuppressWarnings({"cast", "unchecked"}) 699 private static <T> T[] _toTypedArray(Class<?> classToCastTo, List<Object> list) 700 { 701 // Cast to Content if instance of Content instead of another type (for structures containing both Container and SubProgram) 702 if (Content.class.isAssignableFrom(classToCastTo)) 703 { 704 return (T[]) list.toArray((Content[]) Array.newInstance(Content.class, list.size())); 705 } 706 return (T[]) list.toArray((T[]) Array.newInstance(classToCastTo, list.size())); 707 } 708}