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