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