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}