001/*
002 *  Copyright 2017 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.cms.content.compare;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Date;
022import java.util.List;
023import java.util.Objects;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.commons.io.IOUtils;
030import org.apache.commons.lang3.ArrayUtils;
031
032import org.ametys.cms.contenttype.AbstractMetadataSetElement;
033import org.ametys.cms.contenttype.ContentConstants;
034import org.ametys.cms.contenttype.ContentType;
035import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
036import org.ametys.cms.contenttype.ContentTypesHelper;
037import org.ametys.cms.contenttype.Fieldset;
038import org.ametys.cms.contenttype.MetadataDefinition;
039import org.ametys.cms.contenttype.MetadataDefinitionHolder;
040import org.ametys.cms.contenttype.MetadataDefinitionReference;
041import org.ametys.cms.contenttype.MetadataSet;
042import org.ametys.cms.contenttype.MetadataType;
043import org.ametys.cms.contenttype.RepeaterDefinition;
044import org.ametys.cms.repository.Content;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.plugins.repository.AmetysRepositoryException;
047import org.ametys.plugins.repository.metadata.BinaryMetadata;
048import org.ametys.plugins.repository.metadata.CompositeMetadata;
049import org.ametys.plugins.repository.metadata.RichText;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.runtime.plugin.component.AbstractLogEnabled;
052
053/**
054 * Object used to compare two contents
055 *
056 */
057public class ContentComparator extends AbstractLogEnabled implements Component, Serviceable
058{
059    /** The Avalon role */
060    public static final String ROLE = ContentComparator.class.getName();
061    
062    private ContentTypesHelper _contentTypesHelper;
063    private ContentTypeExtensionPoint _contentTypeExtensionPoint;
064    
065    public void service(ServiceManager manager) throws ServiceException
066    {
067        this._contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
068        this._contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
069    }
070    
071    /**
072     * Compare 2 contents, for all their common metadatas (contents with different contentTypes are rejected)
073     * @param content1 1st content
074     * @param content2 new content
075     * @return {@link Result}
076     * @throws AmetysRepositoryException repository exception
077     * @throws IOException IO exception
078     */
079    public Result compare(Content content1, Content content2) throws AmetysRepositoryException, IOException
080    {
081        return compare(content1, content2, null);
082    }
083    
084    /**
085     * Compare 2 contents, for all their common metadatas
086     * @param content1 1st content
087     * @param content2 new content
088     * @param strictCompare true to reject contents with different content types
089     * @return {@link Result}
090     * @throws AmetysRepositoryException repository exception
091     * @throws IOException IO exception
092     */
093    public Result compare(Content content1, Content content2, boolean strictCompare) throws AmetysRepositoryException, IOException
094    {
095        return compare(content1, content2, null, strictCompare);
096    }
097    
098    /**
099     * Compare 2 contents, filtering with a metadataSet (contents with different contentTypes are rejected)
100     * @param content1 1st content
101     * @param content2 new content
102     * @param metadataSetName name of the metadataSet
103     * @return {@link Result}
104     * @throws AmetysRepositoryException repository exception
105     * @throws IOException IO exception
106     */
107    public Result compare(Content content1, Content content2, String metadataSetName) throws AmetysRepositoryException, IOException
108    {
109        return compare(content1, content2, metadataSetName, true);
110    }
111    
112    /**
113     * Compare 2 contents, filtering with a metadataSet
114     * @param content1 1st content
115     * @param content2 new content
116     * @param metadataSetName name of the metadataSet
117     * @param strictCompare true to reject contents with different content types
118     * @return {@link Result}
119     * @throws AmetysRepositoryException repository exception
120     * @throws IOException IO exception
121     */
122    public Result compare(Content content1, Content content2, String metadataSetName, boolean strictCompare) throws AmetysRepositoryException, IOException
123    {
124        return _compare(content1, content2, metadataSetName, strictCompare);
125    }
126    
127    private Result _compare(Content content1, Content content2, String metadataSetName, boolean strictCompare) throws AmetysRepositoryException, IOException
128    {
129        //if strictCompare, we check if the types are strictly equals
130        List<String> types1 = Arrays.asList(content1.getTypes());
131        List<String> types2 = Arrays.asList(content2.getTypes());
132        if (strictCompare && (types1.size() != types2.size() || !types1.containsAll(types2) || !types2.containsAll(types1)))
133        {
134            Result result = new Result(content1, content2);
135            result.setNotEquals();
136            return result;
137        }
138        else
139        {
140            MetadataSet metadataSet = _generateMetadataSet(metadataSetName, content1);
141            Result result = new Result(content1, content2, metadataSetName, metadataSet);
142            if (metadataSet != null)
143            {
144                for (AbstractMetadataSetElement abstractMetadataSetElement : metadataSet.getElements())
145                {
146                    _compareMetadatas(result, content1, content1.getMetadataHolder(), content2, content2.getMetadataHolder(), "", abstractMetadataSetElement, "");
147                }
148            }
149            else
150            {
151                result.setNotEquals();
152            }
153            
154            return result;
155        }
156    }
157    
158    /**
159     * Generate a metadataSet using a name (can be null) and a content
160     * @param metadataSetName name of the metadataSet. If null, a __generated__ metadataSet will be created using all metadatas
161     * @param content content where the metadataSet should be retreived
162     * @return MetadataSet from the name, or generated
163     */
164    private MetadataSet _generateMetadataSet(String metadataSetName, Content content)
165    {
166        if (metadataSetName != null)
167        {
168            return _contentTypesHelper.getMetadataSetForView(metadataSetName, content.getTypes(), content.getMixinTypes());
169        }
170        else
171        {
172            MetadataSet metadataSet;
173            metadataSet = new MetadataSet();
174            metadataSet.setName("__generated__");
175            metadataSet.setLabel(new I18nizableText("Live edition metadataset"));
176            metadataSet.setDescription(new I18nizableText("Live edition metadataset"));
177            metadataSet.setSmallIcon(null);
178            metadataSet.setMediumIcon(null);
179            metadataSet.setLargeIcon(null);
180            metadataSet.setEdition(true);
181            metadataSet.setInternal(true);
182            
183            String[] types = ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
184            for (String contentTypeId1 : types)
185            {
186                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId1);
187                _fillMetadataSet(metadataSet, contentType);
188            }
189            
190            return metadataSet;
191        }
192    }
193    
194    /**
195     * Recursive method to generate a metadataSet
196     * @param metadataSet MetadataSet where definitions will be added
197     * @param rootDefinition holder of the definitions to look for
198     */
199    private void _fillMetadataSet(AbstractMetadataSetElement metadataSet, MetadataDefinitionHolder rootDefinition)
200    {
201        for (String metadataName : rootDefinition.getMetadataNames())
202        {
203            MetadataDefinitionReference metaDefRef;
204            //if the metadataSet already contains the definition, no need to create a new one
205            if (metadataSet.hasMetadataDefinitionReference(metadataName))
206            {
207                metaDefRef = metadataSet.getMetadataDefinitionReference(metadataName);
208            }
209            else
210            {
211                metaDefRef = new MetadataDefinitionReference(metadataName, "__generated__");
212                metadataSet.addElement(metaDefRef);
213            }
214            
215            MetadataDefinition metadataDefinition = rootDefinition.getMetadataDefinition(metadataName);
216            //if the definition is a composite, we need to go deeper
217            if (metadataDefinition.getType() == MetadataType.COMPOSITE)
218            {
219                _fillMetadataSet(metaDefRef, metadataDefinition);
220            }
221        }
222        
223    }
224    
225    /**
226     * Compare 2 metadata and add the differences in result
227     * @param result global result where differences will be stocked
228     * @param content1 1st content
229     * @param content1CompositeMetadata compositeMetadata fot content1
230     * @param content2 2nd content
231     * @param content2CompositeMetadata compositeMetadata fot content2
232     * @param contentPath path of it's content (inside of composite for example)
233     * @param abstractMetadataSetElement element that will be compared
234     * @param metadataDefinitionPath path in the metadataDefinition (different from contentPath because of repeaters)
235     * @throws AmetysRepositoryException repository exception
236     * @throws IOException IO Exception (comparison of InputStream mainly)
237     */
238    private void _compareMetadatas(Result result, Content content1, CompositeMetadata content1CompositeMetadata, Content content2, CompositeMetadata content2CompositeMetadata, String contentPath, AbstractMetadataSetElement abstractMetadataSetElement, String metadataDefinitionPath) throws AmetysRepositoryException, IOException
239    {
240        //Si définition, on compare, si fieldset, on re-boucle
241        if (abstractMetadataSetElement instanceof MetadataDefinitionReference)
242        {
243            MetadataDefinitionReference metadataDefinitionReference = (MetadataDefinitionReference) abstractMetadataSetElement;
244            String metadataName = metadataDefinitionReference.getMetadataName();
245            MetadataDefinition metadataDefinition1 = _getMetadataDefinition(metadataDefinitionPath + metadataName, content1);
246            MetadataDefinition metadataDefinition2 = _getMetadataDefinition(metadataDefinitionPath + metadataName, content2);
247            if (metadataDefinition1 != null && metadataDefinition2 != null && metadataDefinition1.getReferenceContentType().equals(metadataDefinition2.getReferenceContentType()))
248            {
249                if (_checkMetadataPresence(result, content1CompositeMetadata, content2CompositeMetadata, contentPath, metadataName, true))
250                {
251                    //Si la définition dit que c'est du composite ou du repeater, la il faut aller voir les enfants, et donc faire avancer le path
252                    //Sinon, on compare
253                    if (metadataDefinition1.getType() == MetadataType.COMPOSITE) 
254                    {
255                        _compareCompositeMetadata(result, metadataDefinition1, metadataDefinitionReference, content1, content1CompositeMetadata, content2, content2CompositeMetadata, contentPath, metadataDefinitionPath);
256                    }
257                    else
258                    {
259                        _compareMetadataContent(result, content1CompositeMetadata, content2CompositeMetadata, metadataDefinition1, metadataName, contentPath);
260                    }
261                }
262            }
263            else
264            {
265                //TODO voir si on ignore, ou si on dit que c'est différent
266                //TODO voir ce qu'il se passe si d'un côté il y a la définition d'un côté et pas de l'autre
267            }
268            
269        }
270        else if (abstractMetadataSetElement instanceof Fieldset)
271        {
272            for (AbstractMetadataSetElement metadata : abstractMetadataSetElement.getElements())
273            {
274                _compareMetadatas(result, content1, content1CompositeMetadata, content2, content2CompositeMetadata, contentPath, metadata, metadataDefinitionPath);
275            }
276        }
277    }
278
279    /**
280     * Compare 2 composite metadatas (repeater or composite)
281     * @param result global result where differences will be stocked
282     * @param metadataDefinition1 definition of the metadata (from content1)
283     * @param metadataDefinitionReference metadata definition reference
284     * @param content1 1st content
285     * @param content1CompositeMetadata 1st composite metadata
286     * @param content2 2nd content
287     * @param content2CompositeMetadata 2nd composite metadata
288     * @param contentPath path of it's content (inside of composite for example)
289     * @param metadataDefinitionPath path in the metadataDefinition (different from contentPath because of repeaters)
290     * @throws AmetysRepositoryException repository exception
291     * @throws IOException IO Exception
292     */
293    private void _compareCompositeMetadata(Result result, MetadataDefinition metadataDefinition1, MetadataDefinitionReference metadataDefinitionReference, Content content1, CompositeMetadata content1CompositeMetadata, Content content2, CompositeMetadata content2CompositeMetadata, String contentPath, String metadataDefinitionPath) throws AmetysRepositoryException, IOException
294    {
295        String metadataName = metadataDefinitionReference.getMetadataName();
296        if (metadataDefinition1 instanceof RepeaterDefinition)
297        {
298            CompositeMetadata compositeMetadata1 = content1CompositeMetadata.getCompositeMetadata(metadataName);
299            CompositeMetadata compositeMetadata2 = content2CompositeMetadata.getCompositeMetadata(metadataName);
300            String[] metadataNames1 = compositeMetadata1.getMetadataNames();
301            String[] metadataNames2 = compositeMetadata2.getMetadataNames();
302            if (metadataNames1.length > metadataNames2.length)
303            {
304                for (int i = metadataNames2.length + 1; i <= metadataNames1.length; i++)
305                {
306                    result.setNotEquals();
307                    Change change = new Change(contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR + Integer.toString(i), ChangeType.REMOVED);
308                    result.addChange(change);
309                }
310            }
311            else if (metadataNames1.length < metadataNames2.length)
312            {
313                for (int i = metadataNames1.length + 1; i <= metadataNames2.length; i++)
314                {
315                    result.setNotEquals();
316                    Change change = new Change(contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR + Integer.toString(i), ChangeType.ADDED);
317                    result.addChange(change);
318                }
319            }
320            //Si il y en a plus d'un côté ou de l'autre, on compare quand même ceux qui sont en commun
321            for (int i = 1; i <= Math.min(metadataNames1.length, metadataNames2.length); i++)
322            {
323                String nodeName = Integer.toString(i);
324                if (_checkMetadataPresence(result, compositeMetadata1, compositeMetadata2, contentPath + metadataName, nodeName, true))
325                {
326                    CompositeMetadata subCompositeMetadata1 = compositeMetadata1.getCompositeMetadata(nodeName);
327                    CompositeMetadata subCompositeMetadata2 = compositeMetadata2.getCompositeMetadata(nodeName);
328                    //Maintenant on boucle sur les métadonnées du noeud
329                    for (AbstractMetadataSetElement metadata : metadataDefinitionReference.getElements())
330                    {
331                        _compareMetadatas(result, content1, subCompositeMetadata1, content2, subCompositeMetadata2,
332                                contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR + nodeName + ContentConstants.METADATA_PATH_SEPARATOR,
333                                metadata, metadataDefinitionPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR);
334                    }
335                }
336            }
337        }
338        else
339        {
340            //Composite
341            for (AbstractMetadataSetElement metadata : metadataDefinitionReference.getElements())
342            {
343                _compareMetadatas(result, content1, content1CompositeMetadata.getCompositeMetadata(metadataName),
344                        content2, content2CompositeMetadata.getCompositeMetadata(metadataName),
345                        contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR,
346                        metadata,
347                        metadataDefinitionPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR);
348            }
349        }
350    }
351
352    /**
353     * Get the definition of a metadata in a content
354     * @param metadataPath path of the metadata
355     * @param content content
356     * @return MetadataDefinition
357     */
358    private MetadataDefinition _getMetadataDefinition(String metadataPath, Content content)
359    {
360        return _contentTypesHelper.getMetadataDefinition(metadataPath, content);
361    }
362    
363    /**
364     * Compare 2 non-composite metadata
365     * @param result global result where differences will be stocked
366     * @param content1CompositeMetadata compositeMetadata fot content1
367     * @param content2CompositeMetadata compositeMetadata fot content2
368     * @param metadataDefinition metadata definition
369     * @param metadataName Metadata Name
370     * @param contentPath path of it's content (inside of composite for example)
371     * @throws AmetysRepositoryException repository exception
372     * @throws IOException IO Exception
373     */
374    private void _compareMetadataContent(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, MetadataDefinition metadataDefinition, String metadataName, String contentPath) throws AmetysRepositoryException, IOException
375    {
376        if (metadataDefinition.isMultiple())
377        {
378            _compareMultipleMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataDefinition, metadataName, contentPath);
379        }
380        else
381        {
382            _compareSingleMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataDefinition, metadataName, contentPath);
383        }
384    }
385    
386    /**
387     * Compare 2 non-composite non-multivalued metadata
388     * @param result global result where differences will be stocked
389     * @param content1CompositeMetadata compositeMetadata fot content1
390     * @param content2CompositeMetadata compositeMetadata fot content2
391     * @param metadataDefinition path in the metadataDefinition (different from contentPath because of repeaters)
392     * @param metadataName Metadata Name
393     * @param contentPath path of it's content (inside of composite for example)
394     * @throws AmetysRepositoryException repository exception
395     * @throws IOException IO Exception
396     */
397    private void _compareSingleMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, MetadataDefinition metadataDefinition, String metadataName, String contentPath) throws AmetysRepositoryException, IOException
398    {
399        switch (metadataDefinition.getType())
400        {
401            case SUB_CONTENT:
402                //On ne compare pas
403                break;
404            case COMPOSITE:
405                //Impossible
406                break;
407            case FILE:
408                _compareFileMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
409                break;
410            case BINARY:
411                _compareBinaryMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
412                break;
413            case BOOLEAN:
414                _compareBooleanMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
415                break;
416            case CONTENT:
417                _compareContentMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
418                break;
419            case REFERENCE:
420                _compareReferenceMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
421                break;
422            case USER:
423                _compareUserMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
424                break;
425            case DATE:
426            case DATETIME:
427                _compareDateMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
428                break;
429            case DOUBLE:
430                _compareDoubleMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
431                break;
432            case LONG:
433                _compareLongMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
434                break;
435            case GEOCODE:
436                _compareGeoCodeMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
437                break;
438            case RICH_TEXT:
439                _compareRichTextMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
440                break;
441            case STRING:
442                _compareStringMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
443                break;
444            default:
445                getLogger().warn("Metadata type '" + metadataDefinition.getType() + " is not supported for content comparison");
446                break;
447            
448        }
449    }
450    
451    /**
452     * Compare 2 non-composite multivalued metadata
453     * @param result global result where differences will be stocked
454     * @param content1CompositeMetadata compositeMetadata fot content1
455     * @param content2CompositeMetadata compositeMetadata fot content2
456     * @param metadataDefinition path in the metadataDefinition (different from contentPath because of repeaters)
457     * @param metadataName Metadata Name
458     * @param contentPath path of it's content (inside of composite for example)
459     * @throws AmetysRepositoryException repository exception
460     */
461    private void _compareMultipleMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, MetadataDefinition metadataDefinition, String metadataName, String contentPath)
462    {
463        switch (metadataDefinition.getType())
464        {
465            case SUB_CONTENT:
466                //On ne compare pas
467                break;
468            case COMPOSITE:
469                //Impossible
470                break;
471            case FILE:
472                // Pas de liste
473                break;
474            case BINARY:
475            case RICH_TEXT:
476                // Pas de liste
477                break;
478            case BOOLEAN:
479                _compareList(result, content1CompositeMetadata.getBooleanArray(metadataName), content2CompositeMetadata.getBooleanArray(metadataName), metadataName, contentPath);
480                break;
481            case CONTENT:
482            case STRING:
483                _compareList(result, content1CompositeMetadata.getStringArray(metadataName), content2CompositeMetadata.getStringArray(metadataName), metadataName, contentPath);
484                break;
485            case REFERENCE:
486                //Pas de liste
487                break;
488            case USER:
489                _compareList(result, content1CompositeMetadata.getUserArray(metadataName), content2CompositeMetadata.getUserArray(metadataName), metadataName, contentPath);
490                break;
491            case DATE:
492            case DATETIME:
493                _compareList(result, content1CompositeMetadata.getDateArray(metadataName), content2CompositeMetadata.getDateArray(metadataName), metadataName, contentPath);
494                break;
495            case DOUBLE:
496                _compareList(result, content1CompositeMetadata.getDoubleArray(metadataName), content2CompositeMetadata.getDoubleArray(metadataName), metadataName, contentPath);
497                break;
498            case LONG:
499                _compareList(result, content1CompositeMetadata.getLongArray(metadataName), content2CompositeMetadata.getLongArray(metadataName), metadataName, contentPath);
500                break;
501            case GEOCODE:
502                // Pas de liste
503                break;
504            default:
505                getLogger().warn("Metadata type '" + metadataDefinition.getType() + " list is not supported for content comparison");
506                break;
507            
508        }
509    }
510    
511    /**
512     * see {@link ContentComparator#_compareList(Result, Object[], Object[], String, String)}
513     * 
514     * @param result Result where diff will be added
515     * @param list1 1st list
516     * @param list2 2nd list
517     * @param metadataName Metadata Name
518     * @param contentPath Content Path
519     */
520    private void _compareList(Result result, boolean[] list1, boolean[] list2, String metadataName, String contentPath)
521    {
522        Object[] newList1 = {};
523        Object[] newList2 = {};
524        for (boolean b : list1)
525        {
526            newList1 = ArrayUtils.add(newList1, new Boolean(b));
527        }
528        for (boolean b : list2)
529        {
530            newList2 = ArrayUtils.add(newList2, new Boolean(b));
531        }
532        _compareList(result, newList1, newList2, metadataName, contentPath);
533    }
534
535    /**
536     * see {@link ContentComparator#_compareList(Result, Object[], Object[], String, String)}
537     * 
538     * @param result Result where diff will be added
539     * @param list1 1st list
540     * @param list2 2nd list
541     * @param metadataName Metadata Name
542     * @param contentPath Content Path
543     */
544    private void _compareList(Result result, long[] list1, long[] list2, String metadataName, String contentPath)
545    {
546        Object[] newList1 = {};
547        Object[] newList2 = {};
548        for (long l : list1)
549        {
550            newList1 = ArrayUtils.add(newList1, new Long(l));
551        }
552        for (long l : list2)
553        {
554            newList2 = ArrayUtils.add(newList2, new Long(l));
555        }
556        _compareList(result, newList1, newList2, metadataName, contentPath);
557    }
558
559    /**
560     * see {@link ContentComparator#_compareList(Result, Object[], Object[], String, String)}
561     * 
562     * @param result Result where diff will be added
563     * @param list1 1st list
564     * @param list2 2nd list
565     * @param metadataName Metadata Name
566     * @param contentPath Content Path
567     */
568    private void _compareList(Result result, double[] list1, double[] list2, String metadataName, String contentPath)
569    {
570        Object[] newList1 = {};
571        Object[] newList2 = {};
572        for (double d : list1)
573        {
574            newList1 = ArrayUtils.add(newList1, new Double(d));
575        }
576        for (double d : list2)
577        {
578            newList2 = ArrayUtils.add(newList2, new Double(d));
579        }
580        _compareList(result, newList1, newList2, metadataName, contentPath);
581    }
582    
583    /**
584     * Compare 2 lists of any Object
585     * @param <T> Any Object
586     * @param result result in which the differences will be added
587     * @param list1 1st list
588     * @param list2 2nd list
589     * @param metadataName Used only for the difference definition
590     * @param contentPath Used only for the difference definition
591     */
592    private <T> void _compareList(Result result, T[] list1, T[] list2, String metadataName, String contentPath)
593    {
594        if (!Objects.deepEquals(list1, list2))
595        {
596            ChangeType type = ChangeType.MODIFIED;
597            ChangeTypeDetail detail = ChangeTypeDetail.NONE;
598            if (list1.length == list2.length)
599            {
600                T[] diff = removeAll(list1, list2);
601                if (diff.length == 0)
602                {
603                    detail = ChangeTypeDetail.ORDER;
604                }
605            }
606            else
607            {
608                if (containsAllOnce(list2, list1))
609                {
610                    detail = ChangeTypeDetail.MORE;
611                }
612                else if (containsAllOnce(list1, list2))
613                {
614                    detail = ChangeTypeDetail.LESS;
615                }
616            }
617            result.setNotEquals();
618            Change change = new Change(contentPath + metadataName, type, detail);
619            result.addChange(change);
620        }
621    }
622    
623    /**
624     * Remove ONCE all elements from list2 in list1
625     * [A, A, B, C], [A, B] will result [A, C]
626     * @param <T> Any Object
627     * @param list1 origin list
628     * @param list2 list of objects to remove
629     * @return a new list with removed objects
630     */
631    private static <T> T[] removeAll(T[] list1, T[] list2)
632    {
633        T[] result = list1.clone();
634        for (T element : list2)
635        {
636            result = ArrayUtils.removeElement(result, element);
637        }
638        return result;
639    }
640    
641    /**
642     * Check if all elements in list2 are present only 1 time in list1
643     * [A, B, C], [A, A, B, C] will return false
644     * [A, A, B, C], [A, A, A, B, C] will return false
645     * [A, A, B, C], [A, B, C] will return true
646     * @param <T> Any Object
647     * @param list1 complete list
648     * @param list2 elements to check
649     * @return true if all elements in list2 are once in list1
650     */
651    private static <T> boolean containsAllOnce(T[] list1, T[] list2)
652    {
653        boolean result = true;
654        T[] listClone = list1.clone();
655        for (T element : list2)
656        {
657            if (!ArrayUtils.contains(listClone, element))
658            {
659                result = false;
660                break;
661            }
662            else
663            {
664                listClone = ArrayUtils.removeElement(listClone, element);
665            }
666        }
667        return result;
668    }
669    
670    /**
671     * Compare 2 string metadata <br>
672     * <p>{@link ChangeType} can be <br>
673     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
674     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
675     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
676     * <p>{@link ChangeTypeDetail} can be <br>
677     * {@link ChangeTypeDetail#NONE} no particular change (ABCD - OHSGDS)<br> 
678     * {@link ChangeTypeDetail#LESS_CONTENT_END} (ABCD - ABC)<br>
679     * {@link ChangeTypeDetail#MORE_CONTENT_END} (ABCD - ABCDZZ)<br> 
680     * {@link ChangeTypeDetail#LESS_CONTENT_START} (ABCD - BCD)<br>
681     * {@link ChangeTypeDetail#MORE_CONTENT_START} (ABCD - ZZABCD)<br>
682     * {@link ChangeTypeDetail#LESS_CONTENT} (ABCD - BC)<br>
683     * {@link ChangeTypeDetail#MORE_CONTENT} (ABCD - ZZABCDZZ)</p>
684     * 
685     * @param result Result where diff will be added
686     * @param content1CompositeMetadata 1st content
687     * @param content2CompositeMetadata 2nd content
688     * @param metadataName metadata Name
689     * @param contentPath Content Path
690     */
691    private void _compareStringMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath)
692    {
693        String str1 = content1CompositeMetadata.getString(metadataName);
694        String str2 = content2CompositeMetadata.getString(metadataName);
695        if (!str1.equals(str2))
696        {
697            ChangeType type = ChangeType.MODIFIED;
698            ChangeTypeDetail detail = ChangeTypeDetail.NONE;
699            if (str1.isEmpty() && !str2.isEmpty())
700            {
701                type = ChangeType.ADDED;
702            }
703            else if (!str1.isEmpty() && str2.isEmpty())
704            {
705                type = ChangeType.REMOVED;
706            }
707            else if (str1.startsWith(str2))
708            {
709                detail = ChangeTypeDetail.LESS_CONTENT_END;
710            }
711            else if (str2.startsWith(str1))
712            {
713                detail = ChangeTypeDetail.MORE_CONTENT_END;
714            }
715            else if (str1.endsWith(str2))
716            {
717                detail = ChangeTypeDetail.LESS_CONTENT_START;
718            }
719            else if (str2.endsWith(str1))
720            {
721                detail = ChangeTypeDetail.MORE_CONTENT_START;
722            }
723            else if (str1.contains(str2))
724            {
725                detail = ChangeTypeDetail.LESS_CONTENT;
726            }
727            else if (str2.contains(str1))
728            {
729                detail = ChangeTypeDetail.MORE_CONTENT;
730            }
731            result.setNotEquals();
732            Change change = new Change(contentPath + metadataName, type, detail);
733            result.addChange(change);
734        }
735    }
736
737    /**
738     * Compare 2 RichText metadata <br>
739     * <p>{@link ChangeType} can be <br>
740     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
741     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
742     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
743     * <p>sub-paths will be added for RichText : encoding, lastModified, mimeType and inputStream</p>
744     * 
745     * @param result Result where diff will be added
746     * @param content1CompositeMetadata 1st content
747     * @param content2CompositeMetadata 2nd content
748     * @param metadataName metadata Name
749     * @param contentPath Content Path
750     * @throws AmetysRepositoryException repository exception
751     * @throws IOException IO Exception
752     */
753    private void _compareRichTextMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath) throws AmetysRepositoryException, IOException
754    {
755        RichText object1 = content1CompositeMetadata.getRichText(metadataName);
756        RichText object2 = content2CompositeMetadata.getRichText(metadataName);
757        
758        if (object1 != null && object2 != null)
759        {
760            _objectCompare(result, object1.getEncoding(), object2.getEncoding(), contentPath, metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "encoding");
761            _objectCompare(result, object1.getLastModified(), object2.getLastModified(), contentPath, metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "lastModified");
762            _objectCompare(result, object1.getMimeType(), object2.getMimeType(), contentPath, metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "mimeType");
763            
764            if (!IOUtils.contentEquals(object1.getInputStream(), object2.getInputStream()))
765            {
766                result.setNotEquals();
767                Change change = new Change(contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "inputStream", ChangeType.MODIFIED);
768                result.addChange(change);
769            }
770            //Note : folders are not compared, they are used for pictures and id of the pictures are in the inputstream, so if something changes, the inputstream changes.
771        }
772        else if (object1 != null && object2 == null)
773        {
774            result.setNotEquals();
775            Change change = new Change(contentPath + metadataName, ChangeType.REMOVED);
776            result.addChange(change);
777        }
778        else if (object1 == null && object2 != null)
779        {
780            result.setNotEquals();
781            Change change = new Change(contentPath + metadataName, ChangeType.ADDED);
782            result.addChange(change);
783        }
784    }
785    
786    /**
787     * Compare 2 GeoCode metadata <br>
788     * <p>{@link ChangeType} can be <br>
789     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
790     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
791     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
792     * <p>sub-paths will be added for GeoCode : latitude and longitude</p>
793     * <p>For each sub-path, {@link ChangeTypeDetail} can be <br>
794     * {@link ChangeTypeDetail#MORE}<br> 
795     * {@link ChangeTypeDetail#LESS}</p>
796     * 
797     * @param result Result where diff will be added
798     * @param content1CompositeMetadata 1st content
799     * @param content2CompositeMetadata 2nd content
800     * @param metadataName metadata Name
801     * @param contentPath Content Path
802     */
803    private void _compareGeoCodeMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath)
804    {
805        CompositeMetadata compositeMetadata1 = content1CompositeMetadata.getCompositeMetadata(metadataName);
806        CompositeMetadata compositeMetadata2 = content2CompositeMetadata.getCompositeMetadata(metadataName);
807
808        if (_checkMetadataPresence(result, compositeMetadata1, compositeMetadata2, contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR, "latitude", true))
809        {
810            double latitude1 = compositeMetadata1.getDouble("latitude");
811            double latitude2 = compositeMetadata2.getDouble("latitude");
812            if (latitude1 != latitude2)
813            {
814                ChangeTypeDetail detail = ChangeTypeDetail.NONE;
815                if (latitude2 > latitude1)
816                {
817                    detail = ChangeTypeDetail.MORE;
818                }
819                else
820                {
821                    detail = ChangeTypeDetail.LESS;
822                }
823                result.setNotEquals();
824                Change change = new Change(contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "latitude", ChangeType.MODIFIED, detail);
825                result.addChange(change);
826            }
827        }
828        if (_checkMetadataPresence(result, compositeMetadata1, compositeMetadata2, contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR, "longitude", true))
829        {
830            double longitude1 = compositeMetadata1.getDouble("longitude");
831            double longitude2 = compositeMetadata2.getDouble("longitude");
832            if (longitude1 != longitude2)
833            {
834                ChangeTypeDetail detail = ChangeTypeDetail.NONE;
835                if (longitude2 > longitude1)
836                {
837                    detail = ChangeTypeDetail.MORE;
838                }
839                else
840                {
841                    detail = ChangeTypeDetail.LESS;
842                }
843                result.setNotEquals();
844                Change change = new Change(contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "longitude", ChangeType.MODIFIED, detail);
845                result.addChange(change);
846            }
847        }
848    }
849    
850    /**
851     * Compare 2 Long metadata <br>
852     * <p>{@link ChangeType} can be <br>
853     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
854     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
855     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
856     * <p>{@link ChangeTypeDetail} can be <br>
857     * {@link ChangeTypeDetail#MORE}<br> 
858     * {@link ChangeTypeDetail#LESS}</p>
859     * 
860     * @param result Result where diff will be added
861     * @param content1CompositeMetadata 1st content
862     * @param content2CompositeMetadata 2nd content
863     * @param metadataName metadata Name
864     * @param contentPath Content Path
865     */
866    private void _compareLongMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath)
867    {
868        long long1 = content1CompositeMetadata.getLong(metadataName);
869        long long2 = content2CompositeMetadata.getLong(metadataName);
870        if (long1 != long2)
871        {
872            ChangeTypeDetail detail = ChangeTypeDetail.NONE;
873            if (long2 > long1)
874            {
875                detail = ChangeTypeDetail.MORE;
876            }
877            else
878            {
879                detail = ChangeTypeDetail.LESS;
880            }
881            result.setNotEquals();
882            Change change = new Change(contentPath + metadataName, ChangeType.MODIFIED, detail);
883            result.addChange(change);
884        }
885    }
886    
887    /**
888     * Compare 2 Double metadata <br>
889     * <p>{@link ChangeType} can be <br>
890     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
891     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
892     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
893     * <p>{@link ChangeTypeDetail} can be <br>
894     * {@link ChangeTypeDetail#MORE}<br> 
895     * {@link ChangeTypeDetail#LESS}</p>
896     * 
897     * @param result Result where diff will be added
898     * @param content1CompositeMetadata 1st content
899     * @param content2CompositeMetadata 2nd content
900     * @param metadataName metadata Name
901     * @param contentPath Content Path
902     */
903    private void _compareDoubleMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath)
904    {
905        double double1 = content1CompositeMetadata.getDouble(metadataName);
906        double double2 = content2CompositeMetadata.getDouble(metadataName);
907        if (double1 != double2)
908        {
909            ChangeTypeDetail detail = ChangeTypeDetail.NONE;
910            if (double2 > double1)
911            {
912                detail = ChangeTypeDetail.MORE;
913            }
914            else
915            {
916                detail = ChangeTypeDetail.LESS;
917            }
918            result.setNotEquals();
919            Change change = new Change(contentPath + metadataName, ChangeType.MODIFIED, detail);
920            result.addChange(change);
921        }
922    }
923    
924    /**
925     * Compare 2 Date metadata <br>
926     * <p>{@link ChangeType} can be <br>
927     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
928     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
929     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
930     * <p>{@link ChangeTypeDetail} can be <br>
931     * {@link ChangeTypeDetail#AFTER}<br> 
932     * {@link ChangeTypeDetail#BEFORE}</p>
933     * 
934     * @param result Result where diff will be added
935     * @param content1CompositeMetadata 1st content
936     * @param content2CompositeMetadata 2nd content
937     * @param metadataName metadata Name
938     * @param contentPath Content Path
939     */
940    private void _compareDateMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath)
941    {
942        Date date1 = content1CompositeMetadata.getDate(metadataName);
943        Date date2 = content2CompositeMetadata.getDate(metadataName);
944        if (!date1.equals(date2))
945        {
946            ChangeTypeDetail detail = ChangeTypeDetail.NONE;
947            if (date2.before(date1))
948            {
949                detail = ChangeTypeDetail.BEFORE;
950            }
951            else
952            {
953                detail = ChangeTypeDetail.AFTER;
954            }
955            result.setNotEquals();
956            Change change = new Change(contentPath + metadataName, ChangeType.MODIFIED, detail);
957            result.addChange(change);
958        }
959    }
960    
961    /**
962     * Compare 2 Content metadata <br>
963     * <p>{@link ChangeType} can be <br>
964     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
965     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
966     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
967     * 
968     * @param result Result where diff will be added
969     * @param content1CompositeMetadata 1st content
970     * @param content2CompositeMetadata 2nd content
971     * @param metadataName metadata Name
972     * @param contentPath Content Path
973     */
974    private void _compareContentMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath)
975    {
976        String obj1 = content1CompositeMetadata.getString(metadataName);
977        String obj2 = content2CompositeMetadata.getString(metadataName);
978        _objectCompare(result, obj1, obj2, contentPath, metadataName);
979    }
980
981    /**
982     * Compare 2 Reference metadata <br>
983     * <p>{@link ChangeType} can be <br>
984     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
985     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
986     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
987     * <p>sub-paths will be added for Reference : type and value</p>
988     * 
989     * @param result Result where diff will be added
990     * @param content1CompositeMetadata 1st content
991     * @param content2CompositeMetadata 2nd content
992     * @param metadataName metadata Name
993     * @param contentPath Content Path
994     */
995    private void _compareReferenceMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath)
996    {
997        CompositeMetadata compositeMetadata1 = content1CompositeMetadata.getCompositeMetadata(metadataName);
998        CompositeMetadata compositeMetadata2 = content2CompositeMetadata.getCompositeMetadata(metadataName);
999
1000        if (_checkMetadataPresence(result, compositeMetadata1, compositeMetadata2, contentPath + ContentConstants.METADATA_PATH_SEPARATOR + metadataName, "type", true))
1001        {
1002            String type1 = compositeMetadata1.getString("type");
1003            String type2 = compositeMetadata2.getString("type");
1004            _objectCompare(result, type1, type2, contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR, "type");
1005        }
1006        
1007        if (_checkMetadataPresence(result, compositeMetadata1, compositeMetadata2, contentPath + ContentConstants.METADATA_PATH_SEPARATOR + metadataName, "value", true))
1008        {
1009            String value1 = compositeMetadata1.getString("value");
1010            String value2 = compositeMetadata2.getString("value");
1011            _objectCompare(result, value1, value2, contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR, "value");
1012        }
1013    }
1014
1015    /**
1016     * Compare 2 User metadata <br>
1017     * <p>{@link ChangeType} can be <br>
1018     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
1019     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
1020     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
1021     * 
1022     * @param result Result where diff will be added
1023     * @param content1CompositeMetadata 1st content
1024     * @param content2CompositeMetadata 2nd content
1025     * @param metadataName metadata Name
1026     * @param contentPath Content Path
1027     */
1028    private void _compareUserMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath)
1029    {
1030        UserIdentity obj1 = content1CompositeMetadata.getUser(metadataName);
1031        UserIdentity obj2 = content2CompositeMetadata.getUser(metadataName);
1032        _objectCompare(result, obj1, obj2, contentPath, metadataName);
1033    }
1034
1035    /**
1036     * Compare 2 File metadata <br>
1037     * If both files do not have the same type, a {@link ChangeTypeDetail#TYPE} will be added to result<br>
1038     * see {@link ContentComparator#_compareBinaryMetadata(Result, CompositeMetadata, CompositeMetadata, String, String)} if objects are binary<br>
1039     * see {@link ContentComparator#_objectCompare(Result, Object, Object, String, String)} if objects are references<br>
1040     * 
1041     * <p>{@link ChangeType} can be <br>
1042     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
1043     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
1044     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
1045     * 
1046     * @param result Result where diff will be added
1047     * @param content1CompositeMetadata 1st content
1048     * @param content2CompositeMetadata 2nd content
1049     * @param metadataName metadata Name
1050     * @param contentPath Content Path
1051     * @throws AmetysRepositoryException repository exception
1052     * @throws IOException IO Exception
1053     */
1054    private void _compareFileMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath) throws AmetysRepositoryException, IOException
1055    {
1056        if (content1CompositeMetadata.getType(metadataName).equals(content2CompositeMetadata.getType(metadataName)))
1057        {
1058            if (CompositeMetadata.MetadataType.BINARY.equals(content1CompositeMetadata.getType(metadataName)))
1059            {
1060                _compareBinaryMetadata(result, content1CompositeMetadata, content2CompositeMetadata, metadataName, contentPath);
1061            }
1062            else
1063            {
1064                _objectCompare(result, content1CompositeMetadata.getString(metadataName), content2CompositeMetadata.getString(metadataName), contentPath, metadataName);
1065            }
1066        }
1067        else
1068        {
1069            Change change = new Change(contentPath + metadataName, ChangeType.MODIFIED, ChangeTypeDetail.TYPE);
1070            result.setNotEquals();
1071            result.addChange(change);
1072        }
1073    }
1074    
1075    /**
1076     * Compare 2 Binary metadata <br>
1077     * <p>{@link ChangeType} can be <br>
1078     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
1079     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
1080     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
1081     * <p>sub-paths will be added for Binary : encoding, fileName, lastModified, mimeType and inputStream</p>
1082     * 
1083     * @param result Result where diff will be added
1084     * @param content1CompositeMetadata 1st content
1085     * @param content2CompositeMetadata 2nd content
1086     * @param metadataName metadata Name
1087     * @param contentPath Content Path
1088     * @throws AmetysRepositoryException repository exception
1089     * @throws IOException IO Exception
1090     */
1091    private void _compareBinaryMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath) throws AmetysRepositoryException, IOException
1092    {
1093        BinaryMetadata object1 = content1CompositeMetadata.getBinaryMetadata(metadataName);
1094        BinaryMetadata object2 = content2CompositeMetadata.getBinaryMetadata(metadataName);
1095        
1096        if (object1 != null && object2 != null)
1097        {
1098            _objectCompare(result, object1.getEncoding(), object2.getEncoding(), contentPath, metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "encoding");
1099            _objectCompare(result, object1.getFilename(), object2.getFilename(), contentPath, metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "fileName");
1100            _objectCompare(result, object1.getLastModified(), object2.getLastModified(), contentPath, metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "lastModified");
1101            _objectCompare(result, object1.getMimeType(), object2.getMimeType(), contentPath, metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "mimeType");
1102            
1103            if (!IOUtils.contentEquals(object1.getInputStream(), object2.getInputStream()))
1104            {
1105                result.setNotEquals();
1106                Change change = new Change(contentPath + metadataName + ContentConstants.METADATA_PATH_SEPARATOR + "inputStream", ChangeType.MODIFIED);
1107                result.addChange(change);
1108            }
1109        }
1110        else if (object1 != null && object2 == null)
1111        {
1112            result.setNotEquals();
1113            Change change = new Change(contentPath + metadataName, ChangeType.REMOVED);
1114            result.addChange(change);
1115        }
1116        else if (object1 == null && object2 != null)
1117        {
1118            result.setNotEquals();
1119            Change change = new Change(contentPath + metadataName, ChangeType.ADDED);
1120            result.addChange(change);
1121        }
1122        
1123        
1124    }
1125    
1126    /**
1127     * Compare 2 Boolean metadata <br>
1128     * <p>{@link ChangeType} can be <br>
1129     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
1130     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
1131     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
1132     * 
1133     * @param result Result where diff will be added
1134     * @param content1CompositeMetadata 1st content
1135     * @param content2CompositeMetadata 2nd content
1136     * @param metadataName metadata Name
1137     * @param contentPath Content Path
1138     */
1139    private void _compareBooleanMetadata(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String metadataName, String contentPath)
1140    {
1141        Boolean obj1 = content1CompositeMetadata.getBoolean(metadataName);
1142        Boolean obj2 = content2CompositeMetadata.getBoolean(metadataName);
1143        _objectCompare(result, obj1, obj2, contentPath, metadataName);
1144    }
1145    
1146    /**
1147     * Compare 2 Objects<br>
1148     * <p>{@link ChangeType} can be <br>
1149     * {@link ChangeType#ADDED} if metadata is present in content2CompositeMetadata and not in content1CompositeMetadata<br>
1150     * {@link ChangeType#MODIFIED} if metadata is present in content2CompositeMetadata and in content1CompositeMetadata<br>
1151     * {@link ChangeType#REMOVED}if metadata is not present in content2CompositeMetadata and present in content1CompositeMetadata</p>
1152     * 
1153     * @param result Result where diff will be added
1154     * @param object1 1st object
1155     * @param object2 2nd object
1156     * @param contentPath path (used to log)
1157     * @param metadataName metadata Name
1158     */
1159    private void _objectCompare(Result result, Object object1, Object object2, String contentPath, String metadataName)
1160    {
1161        //normalement pas besoin de tester la nullité, ça a du être vérifié plus tôt déjà (par checkMetadataPresence)
1162        if (object1 != null && object2 != null)
1163        {
1164            if (!object1.equals(object2))
1165            {
1166                result.setNotEquals();
1167                Change change = new Change(contentPath + metadataName, ChangeType.MODIFIED);
1168                result.addChange(change);
1169            }
1170        }
1171        else if (object1 != null && object2 == null)
1172        {
1173            result.setNotEquals();
1174            Change change = new Change(contentPath + metadataName, ChangeType.REMOVED);
1175            result.addChange(change);
1176        }
1177        else if (object1 == null && object2 != null)
1178        {
1179            result.setNotEquals();
1180            Change change = new Change(contentPath + metadataName, ChangeType.ADDED);
1181            result.addChange(change);
1182        }
1183    }
1184
1185    /**
1186     * Check if a metadata is present in both contents and add a change if requested
1187     * @param result The result
1188     * @param content1CompositeMetadata 1st composite
1189     * @param content2CompositeMetadata 2nd composite
1190     * @param contentPath path (used to log)
1191     * @param metadataName name of the metadata
1192     * @param logChange true to add a change in the list (if one have it and the other don't)
1193     * @return true if both contents have the node
1194     */
1195    private boolean _checkMetadataPresence(Result result, CompositeMetadata content1CompositeMetadata, CompositeMetadata content2CompositeMetadata, String contentPath, String metadataName, boolean logChange)
1196    {
1197        if (content1CompositeMetadata.hasMetadata(metadataName) && content2CompositeMetadata.hasMetadata(metadataName))
1198        {
1199            return true;
1200        }
1201        else if (logChange && content1CompositeMetadata.hasMetadata(metadataName) && !content2CompositeMetadata.hasMetadata(metadataName))
1202        {
1203            result.setNotEquals();
1204            Change change = new Change(contentPath + metadataName, ChangeType.REMOVED);
1205            result.addChange(change);
1206        }
1207        else if (logChange && !content1CompositeMetadata.hasMetadata(metadataName) && content2CompositeMetadata.hasMetadata(metadataName))
1208        {
1209            result.setNotEquals();
1210            Change change = new Change(contentPath + metadataName, ChangeType.ADDED);
1211            result.addChange(change);
1212        }
1213        return false;
1214    }
1215    
1216    /**
1217     * Result of a comparison between 2 contents
1218     */
1219    public class Result
1220    {
1221        private Content _content1;
1222        private Content _content2;
1223        private String _metadataSetName;
1224        private MetadataSet _metadataSet;
1225        
1226        private boolean _areEquals;
1227        private List<Change> _changes;
1228        
1229        /**
1230         * Create a new result from 2 contents
1231         * @param content1 1st content
1232         * @param content2 2nd content
1233         */
1234        public Result(Content content1, Content content2)
1235        {
1236            this(content1, content2, null, null);
1237        }
1238        
1239        /**
1240         * Create a new result from 2 contents and the name of a metadataSet
1241         * @param content1 1st content
1242         * @param content2 2nd content
1243         * @param metadataSetName name of the MetadataSet
1244         * @param metadataSet MetadataSet
1245         */
1246        public Result(Content content1, Content content2, String metadataSetName, MetadataSet metadataSet)
1247        {
1248            this._content1 = content1;
1249            this._content2 = content2;
1250            this._metadataSetName = metadataSetName;
1251            this._metadataSet = metadataSet;
1252            if (content1 != null && content2 != null)
1253            {
1254                this._areEquals = true;
1255            }
1256            else
1257            {
1258                this._areEquals = false;
1259            }
1260            this._changes = new ArrayList<>();
1261        }
1262        /**
1263         * are the contents equals ?
1264         * @return true if equals
1265         */
1266        public boolean areEquals()
1267        {
1268            return this._areEquals;
1269        }
1270        /**
1271         * If contents are not equals, a list of {@link Change} is generated
1272         * @return list of {@link Change}
1273         */
1274        public List<Change> getChanges()
1275        {
1276            return this._changes;
1277        }
1278        
1279        /**
1280         * get the 1st content of the comparison
1281         * @return 1st content
1282         */
1283        public Content getContent1()
1284        {
1285            return this._content1;
1286        }
1287        
1288        /**
1289         * get the 1st content of the comparison
1290         * @return 2nd content
1291         */
1292        public Content getContent2()
1293        {
1294            return this._content2;
1295        }
1296        
1297        /**
1298         * get the name of the MetadataSet
1299         * @return MetadataSet Name (can be null)
1300         */
1301        public String getMetadataSetName()
1302        {
1303            return this._metadataSetName;
1304        }
1305        
1306        /**
1307         * get the metadataSet (generated from the metadataSet name, of from all metadatas)
1308         * @return MetadataSet
1309         */
1310        public MetadataSet getMetadataSet()
1311        {
1312            return this._metadataSet;
1313        }
1314        
1315        /**
1316         * add a change to the list
1317         * @param change a new change
1318         */
1319        protected void addChange(Change change)
1320        {
1321            this._changes.add(change);
1322        }
1323        
1324        /**
1325         * declare that the 2 contents are not equals
1326         */
1327        protected void setNotEquals()
1328        {
1329            this._areEquals = false;
1330        }
1331    }
1332    
1333    /**
1334     * One of the changes between 2 contents
1335     *
1336     */
1337    public class Change
1338    {
1339        private String _path;
1340        private ChangeType _changeType;
1341        private ChangeTypeDetail _changeTypeDetail;
1342        /**
1343         * Constructor with no details for this change
1344         * @param path path of the metadata
1345         * @param type type of change
1346         */
1347        public Change (String path, ChangeType type)
1348        {
1349            this(path, type, ChangeTypeDetail.NONE);
1350        }
1351        /**
1352         * Constructor with change detail
1353         * @param path path of the metadata
1354         * @param type type of change
1355         * @param detail detail of the change
1356         */
1357        public Change (String path, ChangeType type, ChangeTypeDetail detail)
1358        {
1359            this._path = path;
1360            this._changeType = type;
1361            this._changeTypeDetail = detail;
1362        }
1363        /**
1364         * path of the change
1365         * @return path of the change
1366         */
1367        public String getPath()
1368        {
1369            return _path;
1370        }
1371        /**
1372         * Type of change
1373         * @return Type of change
1374         */
1375        public ChangeType getChangeType()
1376        {
1377            return _changeType;
1378        }
1379        /**
1380         * Detail of change
1381         * @return Detail of change
1382         */
1383        public ChangeTypeDetail getChangeTypeDetail()
1384        {
1385            return _changeTypeDetail;
1386        }
1387        
1388        
1389    }
1390    /**
1391     * General type of change
1392     */
1393    public enum ChangeType
1394    {
1395        /**
1396         * The metadata is added in the new content
1397         */
1398        ADDED,
1399        /**
1400         * The metadata is removed in the new content
1401         */
1402        REMOVED,
1403        /**
1404         * The metadata is modified in the new content
1405         */
1406        MODIFIED
1407    }
1408    /**
1409     * More precise change info
1410     */
1411    public enum ChangeTypeDetail
1412    {
1413        /**
1414         * No particular detail
1415         */
1416        NONE,
1417        /**
1418         * New (int/bool/long) is bigger than old
1419         */
1420        MORE,
1421        /**
1422         * New (int/bool/long) is smaller than old
1423         */
1424        LESS,
1425        /**
1426         * Same data, but order changed
1427         */
1428        ORDER,
1429        /**
1430         * New (date) is before old
1431         */
1432        BEFORE,
1433        /**
1434         * New (date) is after old
1435         */
1436        AFTER,
1437        /**
1438         * More content in new field
1439         */
1440        MORE_CONTENT,
1441        /**
1442         * More content at the start of the new field
1443         */
1444        MORE_CONTENT_START,
1445        /**
1446         * More content at the end of the new field
1447         */
1448        MORE_CONTENT_END,
1449        /**
1450         * Less content in the new field
1451         */
1452        LESS_CONTENT,
1453        /**
1454         * Less content at the start of the new field
1455         */
1456        LESS_CONTENT_START,
1457        /**
1458         * Less content at the end of the new field
1459         */
1460        LESS_CONTENT_END,
1461        /**
1462         * MetadataType changed (reference file to binary file for example)
1463         */
1464        TYPE
1465    }
1466}