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