001/*
002 * (C) Copyright 2012-2018 Nuxeo (http://nuxeo.com/) and others.
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 *
016 * Contributors:
017 *     Antoine Taillefer
018 */
019package org.nuxeo.ecm.diff.service.impl;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Calendar;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import org.apache.commons.collections.CollectionUtils;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.NuxeoException;
037import org.nuxeo.ecm.core.schema.SchemaManager;
038import org.nuxeo.ecm.core.schema.types.Field;
039import org.nuxeo.ecm.core.schema.types.FieldImpl;
040import org.nuxeo.ecm.core.schema.types.QName;
041import org.nuxeo.ecm.core.schema.types.Type;
042import org.nuxeo.ecm.diff.content.ContentDiffHelper;
043import org.nuxeo.ecm.diff.model.DiffBlockDefinition;
044import org.nuxeo.ecm.diff.model.DiffComplexFieldDefinition;
045import org.nuxeo.ecm.diff.model.DiffDisplayBlock;
046import org.nuxeo.ecm.diff.model.DiffFieldDefinition;
047import org.nuxeo.ecm.diff.model.DiffFieldItemDefinition;
048import org.nuxeo.ecm.diff.model.DifferenceType;
049import org.nuxeo.ecm.diff.model.DocumentDiff;
050import org.nuxeo.ecm.diff.model.PropertyDiff;
051import org.nuxeo.ecm.diff.model.PropertyDiffDisplay;
052import org.nuxeo.ecm.diff.model.PropertyType;
053import org.nuxeo.ecm.diff.model.SchemaDiff;
054import org.nuxeo.ecm.diff.model.impl.ComplexPropertyDiff;
055import org.nuxeo.ecm.diff.model.impl.ContentDiffDisplayImpl;
056import org.nuxeo.ecm.diff.model.impl.ContentProperty;
057import org.nuxeo.ecm.diff.model.impl.ContentPropertyDiff;
058import org.nuxeo.ecm.diff.model.impl.DiffBlockDefinitionImpl;
059import org.nuxeo.ecm.diff.model.impl.DiffDisplayBlockImpl;
060import org.nuxeo.ecm.diff.model.impl.DiffFieldDefinitionImpl;
061import org.nuxeo.ecm.diff.model.impl.DiffFieldItemDefinitionImpl;
062import org.nuxeo.ecm.diff.model.impl.ListPropertyDiff;
063import org.nuxeo.ecm.diff.model.impl.PropertyDiffDisplayImpl;
064import org.nuxeo.ecm.diff.model.impl.SimplePropertyDiff;
065import org.nuxeo.ecm.diff.service.ComplexPropertyHelper;
066import org.nuxeo.ecm.diff.service.DiffDisplayService;
067import org.nuxeo.ecm.platform.forms.layout.api.BuiltinModes;
068import org.nuxeo.ecm.platform.forms.layout.api.FieldDefinition;
069import org.nuxeo.ecm.platform.forms.layout.api.LayoutDefinition;
070import org.nuxeo.ecm.platform.forms.layout.api.LayoutRowDefinition;
071import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition;
072import org.nuxeo.ecm.platform.forms.layout.api.WidgetReference;
073import org.nuxeo.ecm.platform.forms.layout.api.impl.FieldDefinitionImpl;
074import org.nuxeo.ecm.platform.forms.layout.api.impl.LayoutDefinitionImpl;
075import org.nuxeo.ecm.platform.forms.layout.api.impl.LayoutRowDefinitionImpl;
076import org.nuxeo.ecm.platform.forms.layout.api.impl.WidgetDefinitionImpl;
077import org.nuxeo.ecm.platform.forms.layout.api.impl.WidgetReferenceImpl;
078import org.nuxeo.ecm.platform.forms.layout.api.service.LayoutStore;
079import org.nuxeo.runtime.api.Framework;
080import org.nuxeo.runtime.model.ComponentInstance;
081import org.nuxeo.runtime.model.DefaultComponent;
082
083/**
084 * Default implementation of the {@link DiffDisplayService}.
085 *
086 * @author Antoine Taillefer (ataillefer@nuxeo.com)
087 * @since 5.6
088 */
089public class DiffDisplayServiceImpl extends DefaultComponent implements DiffDisplayService {
090
091    private static final long serialVersionUID = 6608445970773402827L;
092
093    private static final Log LOGGER = LogFactory.getLog(DiffDisplayServiceImpl.class);
094
095    protected static final String DIFF_DISPLAY_EXTENSION_POINT = "diffDisplay";
096
097    protected static final String DIFF_DEFAULT_DISPLAY_EXTENSION_POINT = "diffDefaultDisplay";
098
099    protected static final String DIFF_BLOCK_EXTENSION_POINT = "diffBlock";
100
101    protected static final String DIFF_WIDGET_CATEGORY = "diff";
102
103    protected static final String DIFF_BLOCK_DEFAULT_TEMPLATE_PATH = "/layouts/layout_diff_template.xhtml";
104
105    protected static final String DIFF_BLOCK_LABEL_PROPERTY_NAME = "label";
106
107    protected static final String DIFF_BLOCK_DEFAULT_LABEL_PREFIX = "label.diffBlock.";
108
109    protected static final String DIFF_WIDGET_LABEL_PREFIX = "label.";
110
111    protected static final String CONTENT_DIFF_LINKS_WIDGET_NAME = "contentDiffLinks";
112
113    protected static final String CONTENT_DIFF_LINKS_WIDGET_NAME_SUFFIX = "_contentDiffLinks";
114
115    protected static final String DIFF_WIDGET_FIELD_DEFINITION_VALUE = "value";
116
117    protected static final String DIFF_WIDGET_FIELD_DEFINITION_DIFFERENCE_TYPE = "differenceType";
118
119    protected static final String DIFF_WIDGET_FIELD_DEFINITION_STYLE_CLASS = "styleClass";
120
121    protected static final String DIFF_WIDGET_FIELD_DEFINITION_FILENAME = "filename";
122
123    protected static final String DIFF_WIDGET_FIELD_DEFINITION_DISPLAY_HTML_CONVERSION = "displayHtmlConversion";
124
125    protected static final String DIFF_WIDGET_FIELD_DEFINITION_DISPLAY_TEXT_CONVERSION = "displayTextConversion";
126
127    protected static final String DIFF_WIDGET_PROPERTY_DISPLAY_ALL_ITEMS = "displayAllItems";
128
129    protected static final String DIFF_WIDGET_PROPERTY_DISPLAY_ITEM_INDEXES = "displayItemIndexes";
130
131    // TODO: refactor name (not related to widget)
132    protected static final String DIFF_LIST_WIDGET_INDEX_SUBWIDGET_FIELD = "index";
133
134    protected static final String DIFF_LIST_WIDGET_INDEX_SUBWIDGET_TYPE = "int";
135
136    protected static final String DIFF_LIST_WIDGET_INDEX_SUBWIDGET_LABEL = "label.list.index";
137
138    protected static final String DIFF_LIST_WIDGET_VALUE_SUBWIDGET_FIELD = "value";
139
140    /** Diff excluded fields contributions. */
141    protected Map<String, List<String>> diffExcludedFieldsContribs = new HashMap<>();
142
143    /** Diff complex fields contributions. */
144    protected Map<String, Map<String, DiffComplexFieldDefinition>> diffComplexFieldsContribs = new HashMap<>();
145
146    /** Diff display contributions. */
147    protected Map<String, List<String>> diffDisplayContribs = new HashMap<>();
148
149    /** Diff block contributions. */
150    protected Map<String, DiffBlockDefinition> diffBlockContribs = new HashMap<>();
151
152    @Override
153    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
154
155        if (DIFF_DEFAULT_DISPLAY_EXTENSION_POINT.equals(extensionPoint)) {
156            if (contribution instanceof DiffExcludedFieldsDescriptor) {
157                registerDiffExcludedFields((DiffExcludedFieldsDescriptor) contribution);
158            } else if (contribution instanceof DiffComplexFieldDescriptor) {
159                registerDiffComplexField((DiffComplexFieldDescriptor) contribution);
160            }
161        } else if (DIFF_DISPLAY_EXTENSION_POINT.equals(extensionPoint)) {
162            if (contribution instanceof DiffDisplayDescriptor) {
163                registerDiffDisplay((DiffDisplayDescriptor) contribution);
164            }
165        } else if (DIFF_BLOCK_EXTENSION_POINT.equals(extensionPoint)) {
166            if (contribution instanceof DiffBlockDescriptor) {
167                registerDiffBlock((DiffBlockDescriptor) contribution);
168            }
169        }
170        super.registerContribution(contribution, extensionPoint, contributor);
171    }
172
173    @Override
174    public Map<String, List<String>> getDiffExcludedSchemas() {
175        return diffExcludedFieldsContribs;
176    }
177
178    @Override
179    public List<String> getDiffExcludedFields(String schemaName) {
180        return diffExcludedFieldsContribs.get(schemaName);
181    }
182
183    @Override
184    public List<DiffComplexFieldDefinition> getDiffComplexFields() {
185        List<DiffComplexFieldDefinition> diffComplexFields = new ArrayList<>();
186        for (Map<String, DiffComplexFieldDefinition> diffComplexFieldsBySchema : diffComplexFieldsContribs.values()) {
187            for (DiffComplexFieldDefinition diffComplexField : diffComplexFieldsBySchema.values()) {
188                diffComplexFields.add(diffComplexField);
189            }
190        }
191        return diffComplexFields;
192    }
193
194    @Override
195    public DiffComplexFieldDefinition getDiffComplexField(String schemaName, String fieldName) {
196        Map<String, DiffComplexFieldDefinition> diffComplexFieldsBySchema = diffComplexFieldsContribs.get(schemaName);
197        if (diffComplexFieldsBySchema != null) {
198            return diffComplexFieldsBySchema.get(fieldName);
199        }
200        return null;
201    }
202
203    @Override
204    public Map<String, List<String>> getDiffDisplays() {
205        return diffDisplayContribs;
206    }
207
208    @Override
209    public List<String> getDiffDisplay(String docType) {
210        return diffDisplayContribs.get(docType);
211
212    }
213
214    @Override
215    public Map<String, DiffBlockDefinition> getDiffBlockDefinitions() {
216        return diffBlockContribs;
217    }
218
219    @Override
220    public DiffBlockDefinition getDiffBlockDefinition(String name) {
221        return diffBlockContribs.get(name);
222    }
223
224    @Override
225    public List<DiffDisplayBlock> getDiffDisplayBlocks(DocumentDiff docDiff, DocumentModel leftDoc,
226            DocumentModel rightDoc) {
227
228        String leftDocType = leftDoc.getType();
229        String rightDocType = rightDoc.getType();
230        if (leftDocType.equals(rightDocType)) {
231            LOGGER.info(String.format(
232                    "The 2 documents have the same type '%s' => looking for a diffDisplay contribution defined for this type or the nearest super type.",
233                    leftDocType));
234            List<String> diffBlockRefs = getNearestSuperTypeDiffDisplay(leftDocType);
235            if (diffBlockRefs != null) {
236                LOGGER.info(String.format(
237                        "Found a diffDisplay contribution defined for the type '%s' or one of its super type => using it to display the diff.",
238                        leftDocType));
239                return getDiffDisplayBlocks(getDiffBlockDefinitions(diffBlockRefs), docDiff, leftDoc, rightDoc);
240            } else {
241                LOGGER.info(String.format(
242                        "No diffDisplay contribution was defined for the type '%s' or one of its super type => using default diff display.",
243                        leftDocType));
244            }
245        } else {
246            LOGGER.info(String.format(
247                    "The 2 documents don't have the same type: '%s'/'%s' => looking for a diffDisplay contribution defined for the nearest common super type.",
248                    leftDocType, rightDocType));
249            List<String> diffBlockRefs = getNearestSuperTypeDiffDisplay(leftDocType, rightDocType);
250            if (diffBlockRefs != null) {
251                LOGGER.info(String.format(
252                        "Found a diffDisplay contribution defined for a common super type of the types '%s'/'%s' => using it to display the diff.",
253                        leftDocType, rightDocType));
254                return getDiffDisplayBlocks(getDiffBlockDefinitions(diffBlockRefs), docDiff, leftDoc, rightDoc);
255            } else {
256                LOGGER.info(String.format(
257                        "No diffDisplay contribution was defined for any of the common super types of the types '%s'/'%s' => using default diff display.",
258                        leftDocType, rightDocType));
259            }
260        }
261        return getDefaultDiffDisplayBlocks(docDiff, leftDoc, rightDoc);
262    }
263
264    public List<DiffDisplayBlock> getDefaultDiffDisplayBlocks(DocumentDiff docDiff, DocumentModel leftDoc,
265            DocumentModel rightDoc) {
266
267        return getDiffDisplayBlocks(getDefaultDiffBlockDefinitions(docDiff), docDiff, leftDoc, rightDoc);
268    }
269
270    protected List<String> getNearestSuperTypeDiffDisplay(String docTypeName) {
271
272        List<String> diffDisplay = getDiffDisplay(docTypeName);
273        Type docType = getSchemaManager().getDocumentType(docTypeName);
274        while (diffDisplay == null && docType != null) {
275            Type superType = docType.getSuperType();
276            if (superType != null) {
277                diffDisplay = getDiffDisplay(superType.getName());
278            }
279            docType = superType;
280        }
281        return diffDisplay;
282    }
283
284    protected List<String> getNearestSuperTypeDiffDisplay(String leftDocTypeName, String rightDocTypeName) {
285        List<String> leftDocSuperTypeNames = new ArrayList<>();
286        List<String> rightDocSuperTypeNames = new ArrayList<>();
287        leftDocSuperTypeNames.add(leftDocTypeName);
288        rightDocSuperTypeNames.add(rightDocTypeName);
289
290        Type leftDocType = getSchemaManager().getDocumentType(leftDocTypeName);
291        Type rightDocType = getSchemaManager().getDocumentType(rightDocTypeName);
292        if (leftDocType != null && rightDocType != null) {
293            Type[] leftDocTypeHierarchy = leftDocType.getTypeHierarchy();
294            for (Type type : leftDocTypeHierarchy) {
295                leftDocSuperTypeNames.add(type.getName());
296            }
297            Type[] rightDocTypeHierarchy = rightDocType.getTypeHierarchy();
298            for (Type type : rightDocTypeHierarchy) {
299                rightDocSuperTypeNames.add(type.getName());
300            }
301        }
302
303        for (String superTypeName : leftDocSuperTypeNames) {
304            if (rightDocSuperTypeNames.contains(superTypeName)) {
305                return getNearestSuperTypeDiffDisplay(superTypeName);
306            }
307        }
308
309        return null;
310    }
311
312    /**
313     * Registers a diff excluded fields contrib.
314     */
315    protected final void registerDiffExcludedFields(DiffExcludedFieldsDescriptor descriptor) {
316
317        String schemaName = descriptor.getSchema();
318        if (!StringUtils.isEmpty(schemaName)) {
319            boolean enabled = descriptor.isEnabled();
320            // Check existing diffExcludedFields contrib for this schema
321            List<String> diffExcludedFields = diffExcludedFieldsContribs.get(schemaName);
322            if (diffExcludedFields != null) {
323                // If !enabled remove contrib
324                if (!enabled) {
325                    diffExcludedFieldsContribs.remove(schemaName);
326                }
327                // Else override contrib (no merge)
328                // TODO: implement merge
329                else {
330                    diffExcludedFieldsContribs.put(schemaName, getDiffExcludedFieldRefs(descriptor.getFields()));
331                }
332            }
333            // No existing diffExcludedFields contrib for this
334            // schema and enabled => add contrib
335            else if (enabled) {
336                diffExcludedFieldsContribs.put(schemaName, getDiffExcludedFieldRefs(descriptor.getFields()));
337            }
338        }
339    }
340
341    /**
342     * Registers a diff complex field contrib.
343     */
344    protected final void registerDiffComplexField(DiffComplexFieldDescriptor descriptor) {
345
346        String schemaName = descriptor.getSchema();
347        String fieldName = descriptor.getName();
348        if (!StringUtils.isEmpty(schemaName) && !StringUtils.isEmpty(fieldName)) {
349            // Check existing diffComplexField contrib for this schema/field
350            DiffComplexFieldDefinition diffComplexField = getDiffComplexField(schemaName, fieldName);
351            if (diffComplexField != null) {
352                // Override contrib (no merge)
353                // TODO: implement merge
354                diffComplexFieldsContribs.get(schemaName).put(fieldName, descriptor.getDiffComplexFieldDefinition());
355            }
356            // No existing diffComplexField contrib for this
357            // schema/field => add contrib
358            else {
359                diffComplexFieldsContribs.computeIfAbsent(schemaName, k -> new HashMap<>()) //
360                                         .put(fieldName, descriptor.getDiffComplexFieldDefinition());
361            }
362        }
363    }
364
365    /**
366     * Registers a diff display contrib.
367     *
368     * @param descriptor the contribution
369     */
370    protected final void registerDiffDisplay(DiffDisplayDescriptor descriptor) {
371
372        String docType = descriptor.getType();
373        if (!StringUtils.isEmpty(docType)) {
374            boolean enabled = descriptor.isEnabled();
375            // Check existing diffDisplay contrib for this type
376            List<String> diffDisplay = diffDisplayContribs.get(docType);
377            if (diffDisplay != null) {
378                // If !enabled remove contrib
379                if (!enabled) {
380                    diffDisplayContribs.remove(docType);
381                }
382                // Else override contrib (no merge)
383                // TODO: implement merge
384                else {
385                    diffDisplayContribs.put(docType, getDiffBlockRefs(descriptor.getDiffBlocks()));
386                }
387            }
388            // No existing diffDisplay contrib for this
389            // type and enabled => add contrib
390            else if (enabled) {
391                diffDisplayContribs.put(docType, getDiffBlockRefs(descriptor.getDiffBlocks()));
392            }
393        }
394    }
395
396    protected final List<String> getDiffBlockRefs(List<DiffBlockReferenceDescriptor> diffBlocks) {
397
398        List<String> diffBlockRefs = new ArrayList<>();
399        for (DiffBlockReferenceDescriptor diffBlockRef : diffBlocks) {
400            diffBlockRefs.add(diffBlockRef.getName());
401        }
402        return diffBlockRefs;
403    }
404
405    protected final List<String> getDiffExcludedFieldRefs(List<DiffFieldDescriptor> diffExcludedFields) {
406
407        List<String> diffExcludedFieldRefs = new ArrayList<>();
408        for (DiffFieldDescriptor diffExcludedFieldRef : diffExcludedFields) {
409            diffExcludedFieldRefs.add(diffExcludedFieldRef.getName());
410        }
411        return diffExcludedFieldRefs;
412    }
413
414    protected final void registerDiffBlock(DiffBlockDescriptor descriptor) {
415
416        String diffBlockName = descriptor.getName();
417        if (!StringUtils.isEmpty(diffBlockName)) {
418            List<DiffFieldDescriptor> fieldDescriptors = descriptor.getFields();
419            // No field descriptors => don't take diff block into account.
420            if (fieldDescriptors == null || fieldDescriptors.isEmpty()) {
421                LOGGER.warn(String.format(
422                        "The diffBlock contribution named '%s' has no fields, it won't be taken into account.",
423                        diffBlockName));
424            } else {
425                List<DiffFieldDefinition> fields = new ArrayList<>();
426                // Some field descriptors were found => use them to add the
427                // described fields, taking their order into account.
428                for (DiffFieldDescriptor fieldDescriptor : fieldDescriptors) {
429                    String category = fieldDescriptor.getCategory();
430                    String schema = fieldDescriptor.getSchema();
431                    String name = fieldDescriptor.getName();
432                    boolean displayContentDiffLinks = fieldDescriptor.isDisplayContentDiffLinks();
433                    List<DiffFieldItemDescriptor> fieldItemDescriptors = fieldDescriptor.getItems();
434                    if (!StringUtils.isEmpty(schema) && !StringUtils.isEmpty(name)) {
435                        List<DiffFieldItemDefinition> items = new ArrayList<>();
436                        for (DiffFieldItemDescriptor fieldItemDescriptor : fieldItemDescriptors) {
437                            items.add(new DiffFieldItemDefinitionImpl(fieldItemDescriptor.getName(),
438                                    fieldItemDescriptor.isDisplayContentDiffLinks()));
439                        }
440                        fields.add(new DiffFieldDefinitionImpl(category, schema, name, displayContentDiffLinks, items));
441                    }
442                }
443                // TODO: implement merge
444                diffBlockContribs.put(diffBlockName, new DiffBlockDefinitionImpl(diffBlockName,
445                        descriptor.getTemplates(), fields, descriptor.getProperties()));
446            }
447        }
448    }
449
450    protected final List<DiffBlockDefinition> getDefaultDiffBlockDefinitions(DocumentDiff docDiff) {
451
452        List<DiffBlockDefinition> diffBlockDefs = new ArrayList<>();
453
454        for (String schemaName : docDiff.getSchemaNames()) {
455            List<String> diffExcludedFields = getDiffExcludedFields(schemaName);
456            // Only add the schema fields if the whole schema is not excluded
457            if (diffExcludedFields == null || diffExcludedFields.size() > 0) {
458                SchemaDiff schemaDiff = docDiff.getSchemaDiff(schemaName);
459                List<DiffFieldDefinition> fieldDefs = new ArrayList<>();
460                for (String fieldName : schemaDiff.getFieldNames()) {
461                    // Only add the field if it is not excluded
462                    if (diffExcludedFields == null || !diffExcludedFields.contains(fieldName)) {
463                        List<DiffFieldItemDefinition> fieldItems = new ArrayList<>();
464                        DiffComplexFieldDefinition complexFieldDef = getDiffComplexField(schemaName, fieldName);
465                        if (complexFieldDef != null) {
466                            List<DiffFieldItemDefinition> includedItems = complexFieldDef.getIncludedItems();
467                            List<DiffFieldItemDefinition> excludedItems = complexFieldDef.getExcludedItems();
468                            // Check included field items
469                            if (!CollectionUtils.isEmpty(includedItems)) {
470                                fieldItems.addAll(includedItems);
471                            }
472                            // Check excluded field items
473                            else if (!CollectionUtils.isEmpty(excludedItems)) {
474                                Field complexField = ComplexPropertyHelper.getField(schemaName, fieldName);
475                                if (complexField.getType().isListType()) {
476                                    complexField = ComplexPropertyHelper.getListFieldItem(complexField);
477                                }
478                                if (!complexField.getType().isComplexType()) {
479                                    throw new NuxeoException(String.format(
480                                            "Cannot compute field items for [%s:%s] since it is not a complex nor a complex list property.",
481                                            schemaName, fieldName));
482                                }
483                                List<Field> complexFieldItems = ComplexPropertyHelper.getComplexFieldItems(
484                                        complexField);
485                                for (Field complexFieldItem : complexFieldItems) {
486                                    String complexFieldItemName = complexFieldItem.getName().getLocalName();
487                                    boolean isFieldItem = true;
488                                    for (DiffFieldItemDefinition fieldItemDef : excludedItems) {
489                                        if (fieldItemDef.getName().equals(complexFieldItemName)) {
490                                            isFieldItem = false;
491                                            break;
492                                        }
493                                    }
494                                    if (isFieldItem) {
495                                        fieldItems.add(new DiffFieldItemDefinitionImpl(complexFieldItemName));
496                                    }
497                                }
498                            }
499                        }
500                        fieldDefs.add(
501                                new DiffFieldDefinitionImpl(DIFF_WIDGET_CATEGORY, schemaName, fieldName, fieldItems));
502                    }
503                }
504
505                Map<String, String> defaultDiffBlockTemplates = new HashMap<>();
506                defaultDiffBlockTemplates.put(BuiltinModes.ANY, DIFF_BLOCK_DEFAULT_TEMPLATE_PATH);
507
508                Map<String, Map<String, Serializable>> defaultDiffBlockProperties = new HashMap<>();
509                Map<String, Serializable> labelProperty = new HashMap<>();
510                labelProperty.put(DIFF_BLOCK_LABEL_PROPERTY_NAME, DIFF_BLOCK_DEFAULT_LABEL_PREFIX + schemaName);
511                defaultDiffBlockProperties.put(BuiltinModes.ANY, labelProperty);
512
513                diffBlockDefs.add(new DiffBlockDefinitionImpl(schemaName, defaultDiffBlockTemplates, fieldDefs,
514                        defaultDiffBlockProperties));
515            }
516        }
517
518        return diffBlockDefs;
519    }
520
521    protected final List<DiffBlockDefinition> getDiffBlockDefinitions(List<String> diffBlockRefs) {
522
523        List<DiffBlockDefinition> diffBlockDefinitions = new ArrayList<>();
524        for (String diffBlockRef : diffBlockRefs) {
525            diffBlockDefinitions.add(getDiffBlockDefinition(diffBlockRef));
526        }
527        return diffBlockDefinitions;
528    }
529
530    protected final List<DiffDisplayBlock> getDiffDisplayBlocks(List<DiffBlockDefinition> diffBlockDefinitions,
531            DocumentDiff docDiff, DocumentModel leftDoc, DocumentModel rightDoc) {
532
533        List<DiffDisplayBlock> diffDisplayBlocks = new ArrayList<>();
534
535        for (DiffBlockDefinition diffBlockDef : diffBlockDefinitions) {
536            if (diffBlockDef != null) {
537                DiffDisplayBlock diffDisplayBlock = getDiffDisplayBlock(diffBlockDef, docDiff, leftDoc, rightDoc);
538                if (!diffDisplayBlock.isEmpty()) {
539                    diffDisplayBlocks.add(diffDisplayBlock);
540                }
541            }
542        }
543
544        return diffDisplayBlocks;
545    }
546
547    protected final DiffDisplayBlock getDiffDisplayBlock(DiffBlockDefinition diffBlockDefinition, DocumentDiff docDiff,
548            DocumentModel leftDoc, DocumentModel rightDoc) {
549
550        Map<String, Map<String, PropertyDiffDisplay>> leftValue = new HashMap<>();
551        Map<String, Map<String, PropertyDiffDisplay>> rightValue = new HashMap<>();
552        Map<String, Map<String, PropertyDiffDisplay>> contentDiffValue = new HashMap<>();
553
554        List<LayoutRowDefinition> layoutRowDefinitions = new ArrayList<>();
555        List<WidgetDefinition> widgetDefinitions = new ArrayList<>();
556
557        List<DiffFieldDefinition> fieldDefinitions = diffBlockDefinition.getFields();
558        for (DiffFieldDefinition fieldDefinition : fieldDefinitions) {
559
560            String category = fieldDefinition.getCategory();
561            if (StringUtils.isEmpty(category)) {
562                category = DIFF_WIDGET_CATEGORY;
563            }
564            String schemaName = fieldDefinition.getSchema();
565            String fieldName = fieldDefinition.getName();
566            boolean displayContentDiffLinks = fieldDefinition.isDisplayContentDiffLinks();
567            List<DiffFieldItemDefinition> fieldItemDefs = fieldDefinition.getItems();
568
569            SchemaDiff schemaDiff = docDiff.getSchemaDiff(schemaName);
570            if (schemaDiff != null) {
571                PropertyDiff fieldDiff = schemaDiff.getFieldDiff(fieldName);
572                if (fieldDiff != null) {
573
574                    Serializable leftProperty = (Serializable) leftDoc.getProperty(schemaName, fieldName);
575                    Serializable rightProperty = (Serializable) rightDoc.getProperty(schemaName, fieldName);
576
577                    // Only include field diff if it is significant
578                    if (isFieldDiffSignificant(leftProperty, rightProperty)) {
579
580                        String propertyName = getPropertyName(schemaName, fieldName);
581                        List<WidgetReference> widgetReferences = new ArrayList<>();
582
583                        // Set property widget definition
584                        WidgetDefinition propertyWidgetDefinition = getWidgetDefinition(category, propertyName,
585                                fieldDiff.getPropertyType(), null, fieldItemDefs, false);
586                        widgetDefinitions.add(propertyWidgetDefinition);
587                        // Set property widget ref
588                        WidgetReferenceImpl propertyWidgetRef = new WidgetReferenceImpl(category, propertyName);
589                        widgetReferences.add(propertyWidgetRef);
590
591                        // Check if must display the content diff links widget
592                        if (!displayContentDiffLinks) {
593                            for (DiffFieldItemDefinition fieldItemDef : fieldItemDefs) {
594                                if (fieldItemDef.isDisplayContentDiffLinks()) {
595                                    displayContentDiffLinks = true;
596                                    break;
597                                }
598                            }
599                        }
600                        // Set content diff links widget definition and ref if
601                        // needed
602                        if (displayContentDiffLinks) {
603                            WidgetDefinition contentDiffLinksWidgetDefinition = getWidgetDefinition(category,
604                                    propertyName, fieldDiff.getPropertyType(), null, fieldItemDefs, true);
605                            widgetDefinitions.add(contentDiffLinksWidgetDefinition);
606                            WidgetReferenceImpl contentDiffLinksWidgetRef = new WidgetReferenceImpl(category,
607                                    propertyName + CONTENT_DIFF_LINKS_WIDGET_NAME_SUFFIX);
608                            widgetReferences.add(contentDiffLinksWidgetRef);
609                        }
610
611                        // Set layout row definition
612                        LayoutRowDefinition layoutRowDefinition = new LayoutRowDefinitionImpl(propertyName, null,
613                                widgetReferences, false, true);
614                        layoutRowDefinitions.add(layoutRowDefinition);
615
616                        // Set diff display field value
617                        boolean isDisplayAllItems = isDisplayAllItems(propertyWidgetDefinition);
618                        boolean isDisplayItemIndexes = isDisplayItemIndexes(propertyWidgetDefinition);
619
620                        // Left diff display
621                        setFieldDiffDisplay(leftProperty, fieldDiff, isDisplayAllItems, isDisplayItemIndexes, leftValue,
622                                schemaName, fieldName, leftDoc, PropertyDiffDisplay.RED_BACKGROUND_STYLE_CLASS);
623
624                        // Right diff display
625                        setFieldDiffDisplay(rightProperty, fieldDiff, isDisplayAllItems, isDisplayItemIndexes,
626                                rightValue, schemaName, fieldName, rightDoc,
627                                PropertyDiffDisplay.GREEN_BACKGROUND_STYLE_CLASS);
628
629                        // Content diff display
630                        if (displayContentDiffLinks) {
631                            PropertyDiffDisplay contentDiffDisplay = getFieldXPaths(propertyName, fieldDiff,
632                                    leftProperty, rightProperty, isDisplayAllItems, isDisplayItemIndexes,
633                                    fieldItemDefs);
634                            contentDiffValue.computeIfAbsent(schemaName, k -> new HashMap<>()) //
635                                            .put(fieldName, contentDiffDisplay);
636                        }
637                    }
638                }
639            }
640        }
641
642        // Build layout definition
643        LayoutDefinition layoutDefinition = new LayoutDefinitionImpl(diffBlockDefinition.getName(),
644                diffBlockDefinition.getProperties(), diffBlockDefinition.getTemplates(), layoutRowDefinitions,
645                widgetDefinitions);
646
647        // Build diff display block
648        Map<String, Serializable> diffBlockProperties = diffBlockDefinition.getProperties(BuiltinModes.ANY);
649        DiffDisplayBlock diffDisplayBlock = new DiffDisplayBlockImpl(
650                (String) diffBlockProperties.get(DIFF_BLOCK_LABEL_PROPERTY_NAME), leftValue, rightValue,
651                contentDiffValue, layoutDefinition);
652
653        return diffDisplayBlock;
654    }
655
656    /**
657     * Checks if the difference between the two specified properties is significant.
658     * <p>
659     * For example in the case of a date property, checks if the difference is greater than 1 minute since we don't
660     * display seconds in the default date widget.
661     */
662    protected boolean isFieldDiffSignificant(Serializable leftProperty, Serializable rightProperty) {
663
664        if (leftProperty instanceof Calendar && rightProperty instanceof Calendar) {
665            Calendar leftDate = (Calendar) leftProperty;
666            Calendar rightDate = (Calendar) rightProperty;
667            if (Math.abs(leftDate.getTimeInMillis() - rightDate.getTimeInMillis()) <= 60000) {
668                return false;
669            }
670        }
671        return true;
672    }
673
674    protected final boolean isDisplayAllItems(WidgetDefinition wDef) {
675
676        // Check 'displayAllItems' widget property
677        return getBooleanProperty(wDef, BuiltinModes.ANY, DIFF_WIDGET_PROPERTY_DISPLAY_ALL_ITEMS);
678    }
679
680    protected final boolean isDisplayItemIndexes(WidgetDefinition wDef) {
681
682        // Check 'displayItemIndexes' widget property
683        return getBooleanProperty(wDef, BuiltinModes.ANY, DIFF_WIDGET_PROPERTY_DISPLAY_ITEM_INDEXES);
684    }
685
686    protected final boolean getBooleanProperty(WidgetDefinition wDef, String mode, String property) {
687
688        Map<String, Map<String, Serializable>> props = wDef.getProperties();
689        if (props != null) {
690            Map<String, Serializable> modeProps = props.get(mode);
691            if (modeProps != null) {
692                Serializable propertyValue = modeProps.get(property);
693                if (propertyValue instanceof String) {
694                    return Boolean.parseBoolean((String) propertyValue);
695                }
696            }
697        }
698        return false;
699    }
700
701    /**
702     * Sets the field diff display.
703     *
704     * @param property the property
705     * @param fieldDiff the field diff
706     * @param isDisplayAllItems the is display all items
707     * @param isDisplayItemIndexes the is display item indexes
708     * @param value the value
709     * @param schemaName the schema name
710     * @param fieldName the field name
711     * @param doc the doc
712     * @param styleClass the style class
713     */
714    protected void setFieldDiffDisplay(Serializable property, PropertyDiff fieldDiff, boolean isDisplayAllItems,
715            boolean isDisplayItemIndexes, Map<String, Map<String, PropertyDiffDisplay>> value, String schemaName,
716            String fieldName, DocumentModel doc, String styleClass) {
717
718        PropertyDiffDisplay fieldDiffDisplay = getFieldDiffDisplay(property, fieldDiff, isDisplayAllItems,
719                isDisplayItemIndexes, false, styleClass);
720        Map<String, PropertyDiffDisplay> schemaMap = value.computeIfAbsent(schemaName, k -> new HashMap<>());
721        schemaMap.put(fieldName, fieldDiffDisplay);
722        // Handle mime type for the note:note property
723        putMimeTypeDiffDisplay(schemaName, fieldName, schemaMap, doc);
724    }
725
726    protected final void putMimeTypeDiffDisplay(String schemaName, String fieldName,
727            Map<String, PropertyDiffDisplay> schemaMap, DocumentModel doc) {
728
729        if ("note".equals(schemaName) && "note".equals(fieldName) && !schemaMap.containsKey("mime_type")) {
730            schemaMap.put("mime_type",
731                    new PropertyDiffDisplayImpl((Serializable) doc.getProperty("note", "mime_type")));
732        }
733    }
734
735    protected final PropertyDiffDisplay getFieldDiffDisplay(Serializable property, PropertyDiff propertyDiff,
736            boolean isDisplayAllItems, boolean isDisplayItemIndexes, boolean mustApplyStyleClass, String styleClass) {
737
738        if (property == null) {
739            String fieldDiffDisplayStyleClass = PropertyDiffDisplay.DEFAULT_STYLE_CLASS;
740            if (mustApplyStyleClass && propertyDiff != null) {
741                fieldDiffDisplayStyleClass = styleClass;
742            }
743            // TODO: add DifferenceType.removed?
744            return new PropertyDiffDisplayImpl(null, fieldDiffDisplayStyleClass);
745        }
746
747        // List type
748        if (isListType(property)) {
749            List<Serializable> listProperty = getListProperty(property);
750            return getListFieldDiffDisplay(listProperty, (ListPropertyDiff) propertyDiff, isDisplayAllItems,
751                    isDisplayItemIndexes, styleClass);
752        }
753        // Other types (scalar, complex, content)
754        else {
755            return getFinalFieldDiffDisplay(property, propertyDiff, mustApplyStyleClass, styleClass);
756        }
757    }
758
759    protected final PropertyDiffDisplay getFinalFieldDiffDisplay(Serializable fieldDiffDisplay,
760            PropertyDiff propertyDiff, boolean mustApplyStyleClass, String styleClass) {
761
762        String finalFieldDiffDisplayStyleClass = PropertyDiffDisplay.DEFAULT_STYLE_CLASS;
763        if (mustApplyStyleClass && propertyDiff != null) {
764            finalFieldDiffDisplayStyleClass = styleClass;
765        }
766        PropertyDiffDisplay finalFieldDiffDisplay;
767        if (isComplexType(fieldDiffDisplay)) {
768            ComplexPropertyDiff complexPropertyDiff = null;
769            if (propertyDiff != null) {
770                if (!propertyDiff.isComplexType()) {
771                    throw new NuxeoException(
772                            "'fieldDiffDisplay' is of complex type whereas 'propertyDiff' is not, this is inconsistent");
773                }
774                complexPropertyDiff = (ComplexPropertyDiff) propertyDiff;
775            }
776            Map<String, Serializable> complexFieldDiffDisplay = getComplexProperty(fieldDiffDisplay);
777            for (String complexItemName : complexFieldDiffDisplay.keySet()) {
778                PropertyDiff complexItemPropertyDiff = null;
779                if (complexPropertyDiff != null) {
780                    complexItemPropertyDiff = complexPropertyDiff.getDiff(complexItemName);
781                }
782                complexFieldDiffDisplay.put(complexItemName,
783                        // TODO: shouldn't we call getFieldDiffDisplay in case
784                        // of an embedded list?
785                        getFinalFieldDiffDisplay(complexFieldDiffDisplay.get(complexItemName), complexItemPropertyDiff,
786                                true, styleClass));
787            }
788            finalFieldDiffDisplay = new PropertyDiffDisplayImpl((Serializable) complexFieldDiffDisplay);
789        } else if (fieldDiffDisplay instanceof Calendar) {
790            // TODO: add propertyDiff.getDifferenceType()?
791            finalFieldDiffDisplay = new PropertyDiffDisplayImpl(((Calendar) fieldDiffDisplay).getTime(),
792                    finalFieldDiffDisplayStyleClass);
793        } else {
794            // TODO: add propertyDiff.getDifferenceType()?
795            finalFieldDiffDisplay = new PropertyDiffDisplayImpl(fieldDiffDisplay, finalFieldDiffDisplayStyleClass);
796        }
797        return finalFieldDiffDisplay;
798    }
799
800    /**
801     * Gets the list field diff display.
802     *
803     * @param listProperty the list property
804     * @param listPropertyDiff the list property diff
805     * @param isDisplayAllItems the is display all items
806     * @param isDisplayItemIndexes the is display item indexes
807     * @param styleClass the style class
808     * @return the list field diff display
809     */
810    protected final PropertyDiffDisplay getListFieldDiffDisplay(List<Serializable> listProperty,
811            ListPropertyDiff listPropertyDiff, boolean isDisplayAllItems, boolean isDisplayItemIndexes,
812            String styleClass) {
813
814        // Get list property indexes
815        // By default: only items that are different (ie. held by the
816        // propertyDiff)
817        List<Integer> listPropertyIndexes = new ArrayList<>();
818        if (isDisplayAllItems) {
819            // All items
820            for (int index = 0; index < listProperty.size(); index++) {
821                listPropertyIndexes.add(index);
822            }
823        } else {
824            if (listPropertyDiff != null) {
825                listPropertyIndexes = listPropertyDiff.getDiffIndexes();
826            }
827        }
828
829        return getComplexListFieldDiffDisplay(listProperty, listPropertyIndexes, listPropertyDiff, isDisplayAllItems,
830                isDisplayItemIndexes, styleClass);
831    }
832
833    protected final PropertyDiffDisplay getComplexListFieldDiffDisplay(List<Serializable> listProperty,
834            List<Integer> listPropertyIndexes, ListPropertyDiff listPropertyDiff, boolean isDisplayAllItems,
835            boolean isDisplayItemIndexes, String styleClass) {
836
837        if (listPropertyIndexes.isEmpty()) {
838            // TODO: add differenceType?
839            return new PropertyDiffDisplayImpl(new ArrayList<Serializable>());
840        }
841        boolean isComplexListWidget = isDisplayItemIndexes
842                || (listPropertyDiff != null && listPropertyDiff.isComplexListType());
843
844        if (isComplexListWidget) {
845            List<Map<String, Serializable>> listFieldDiffDisplay = new ArrayList<>();
846            Set<String> complexPropertyItemNames = null;
847            for (int index : listPropertyIndexes) {
848
849                Map<String, Serializable> listItemDiffDisplay = new HashMap<>();
850                // Put item index if wanted
851                if (isDisplayItemIndexes) {
852                    listItemDiffDisplay.put(DIFF_LIST_WIDGET_INDEX_SUBWIDGET_FIELD, index + 1);
853                }
854                // Only put value if index is in list range
855                if (index < listProperty.size()) {
856                    Serializable listPropertyItem = listProperty.get(index);
857                    PropertyDiff listItemPropertyDiff = null;
858                    if (listPropertyDiff != null) {
859                        listItemPropertyDiff = listPropertyDiff.getDiff(index);
860                    }
861                    if (isComplexType(listPropertyItem)) { // Complex
862                                                           // list
863                        ComplexPropertyDiff complexPropertyDiff = null;
864                        if (listItemPropertyDiff != null && listItemPropertyDiff.isComplexType()) {
865                            complexPropertyDiff = (ComplexPropertyDiff) listItemPropertyDiff;
866                        }
867                        Map<String, Serializable> complexProperty = getComplexProperty(listPropertyItem);
868                        complexPropertyItemNames = complexProperty.keySet();
869                        for (String complexPropertyItemName : complexPropertyItemNames) {
870                            Serializable complexPropertyItem = complexProperty.get(complexPropertyItemName);
871                            // TODO: take into account subwidget properties
872                            // 'displayAllItems' and 'displayItemIndexes'
873                            // instead of inheriting them from the parent
874                            // widget.
875                            PropertyDiff complexItemPropertyDiff = null;
876                            if (complexPropertyDiff != null) {
877                                complexItemPropertyDiff = complexPropertyDiff.getDiff(complexPropertyItemName);
878                            }
879                            listItemDiffDisplay.put(complexPropertyItemName,
880                                    getFieldDiffDisplay(complexPropertyItem, complexItemPropertyDiff, isDisplayAllItems,
881                                            isDisplayItemIndexes, true, styleClass));
882                        }
883                    } else { // Scalar or content list
884                        listItemDiffDisplay.put(DIFF_LIST_WIDGET_VALUE_SUBWIDGET_FIELD, getFinalFieldDiffDisplay(
885                                listPropertyItem, listItemPropertyDiff, isDisplayAllItems, styleClass));
886                    }
887                } else {// Index not in list range => put null value
888                    if (complexPropertyItemNames != null) {
889                        for (String complexPropertyItemName : complexPropertyItemNames) {
890                            // TODO: add DifferenceType.removed?
891                            listItemDiffDisplay.put(complexPropertyItemName,
892                                    new PropertyDiffDisplayImpl(null, styleClass));
893                        }
894                    } else {
895                        // TODO: add DifferenceType.removed?
896                        listItemDiffDisplay.put(DIFF_LIST_WIDGET_VALUE_SUBWIDGET_FIELD, new PropertyDiffDisplayImpl(
897                                null, isDisplayAllItems ? styleClass : PropertyDiffDisplay.DEFAULT_STYLE_CLASS));
898                    }
899                }
900                listFieldDiffDisplay.add(listItemDiffDisplay);
901            }
902            return new PropertyDiffDisplayImpl((Serializable) listFieldDiffDisplay);
903        } else {
904            List<Serializable> listFieldDiffDisplay = new ArrayList<>();
905            for (int index : listPropertyIndexes) {
906                // Only put value if index is in list range
907                if (index < listProperty.size()) {
908                    PropertyDiff listItemPropertyDiff = null;
909                    if (listPropertyDiff != null) {
910                        listItemPropertyDiff = listPropertyDiff.getDiff(index);
911                    }
912                    listFieldDiffDisplay.add(getFinalFieldDiffDisplay(listProperty.get(index), listItemPropertyDiff,
913                            isDisplayAllItems, styleClass));
914                } else {// Index not in list range => put null value
915                    // TODO: add DifferenceType.removed?
916                    listFieldDiffDisplay.add(new PropertyDiffDisplayImpl(null,
917                            isDisplayAllItems ? styleClass : PropertyDiffDisplay.DEFAULT_STYLE_CLASS));
918                }
919            }
920            return new PropertyDiffDisplayImpl((Serializable) listFieldDiffDisplay);
921        }
922    }
923
924    protected final PropertyDiffDisplay getFieldXPaths(String propertyName, PropertyDiff propertyDiff,
925            Serializable leftProperty, Serializable rightProperty, boolean isDisplayAllItems,
926            boolean isDisplayItemIndexes, List<DiffFieldItemDefinition> complexFieldItemDefs) {
927
928        PropertyDiffDisplay fieldXPaths = null;
929        if (propertyDiff == null) {
930            throw new NuxeoException("The 'propertyDiff' parameter cannot be null.");
931        }
932
933        boolean isDisplayHtmlConversion = ContentDiffHelper.isDisplayHtmlConversion(leftProperty)
934                && ContentDiffHelper.isDisplayHtmlConversion(rightProperty);
935        boolean isDisplayTextConversion = ContentDiffHelper.isDisplayTextConversion(leftProperty)
936                && ContentDiffHelper.isDisplayTextConversion(rightProperty);
937
938        // Simple type
939        if (propertyDiff.isSimpleType()) {
940            SimplePropertyDiff simplePropertyDiff = (SimplePropertyDiff) propertyDiff;
941            // Keep fieldXPaths null if one of the left or right properties is
942            // empty
943            if (!StringUtils.isEmpty(simplePropertyDiff.getLeftValue())
944                    && !StringUtils.isEmpty(simplePropertyDiff.getRightValue())) {
945                fieldXPaths = new ContentDiffDisplayImpl(propertyName, simplePropertyDiff.getDifferenceType(),
946                        isDisplayHtmlConversion, isDisplayTextConversion);
947            }
948        }
949        // Content type
950        else if (propertyDiff.isContentType()) {
951            ContentPropertyDiff contentPropertyDiff = (ContentPropertyDiff) propertyDiff;
952            ContentProperty leftContent = contentPropertyDiff.getLeftContent();
953            ContentProperty rightContent = contentPropertyDiff.getRightContent();
954            // Keep fieldXPaths null if one of the left or right properties is
955            // empty
956            if (leftContent != null && rightContent != null
957                    && ((StringUtils.isNotEmpty(leftContent.getFilename())
958                            && StringUtils.isNotEmpty(rightContent.getFilename()))
959                            || (StringUtils.isNotEmpty(leftContent.getDigest())
960                                    && StringUtils.isNotEmpty(rightContent.getDigest())))) {
961                fieldXPaths = new ContentDiffDisplayImpl(propertyName, contentPropertyDiff.getDifferenceType(),
962                        isDisplayHtmlConversion, isDisplayTextConversion);
963            }
964        }
965        // Complex type
966        else if (propertyDiff.isComplexType()) {
967
968            Map<String, Serializable> leftComplexProperty = getComplexPropertyIfNotNull(leftProperty);
969            Map<String, Serializable> rightComplexProperty = getComplexPropertyIfNotNull(rightProperty);
970
971            // TODO (maybe): take into account subwidget properties
972            // 'displayAllItems' and 'displayItemIndexes'
973            // instead of inheriting them from the parent
974            // widget.
975            Map<String, PropertyDiff> complexPropertyDiffMap = ((ComplexPropertyDiff) propertyDiff).getDiffMap();
976            Map<String, Serializable> complexPropertyXPaths = new HashMap<>();
977            if (CollectionUtils.isEmpty(complexFieldItemDefs)) {
978                Iterator<String> complexFieldItemNamesIt = complexPropertyDiffMap.keySet().iterator();
979                while (complexFieldItemNamesIt.hasNext()) {
980                    String complexFieldItemName = complexFieldItemNamesIt.next();
981                    setComplexPropertyXPaths(complexPropertyXPaths, complexFieldItemName,
982                            getSubPropertyFullName(propertyName, complexFieldItemName), complexPropertyDiffMap,
983                            leftComplexProperty, rightComplexProperty, isDisplayAllItems, isDisplayItemIndexes);
984                }
985            } else {
986                for (DiffFieldItemDefinition complexFieldItemDef : complexFieldItemDefs) {
987                    if (complexFieldItemDef.isDisplayContentDiffLinks()) {
988                        String complexFieldItemName = complexFieldItemDef.getName();
989                        if (complexPropertyDiffMap.containsKey(complexFieldItemName)) {
990                            setComplexPropertyXPaths(complexPropertyXPaths, complexFieldItemName,
991                                    getSubPropertyFullName(propertyName, complexFieldItemName), complexPropertyDiffMap,
992                                    leftComplexProperty, rightComplexProperty, isDisplayAllItems, isDisplayItemIndexes);
993                        }
994                    }
995                }
996            }
997            fieldXPaths = new ContentDiffDisplayImpl((Serializable) complexPropertyXPaths);
998        }
999        // List type
1000        else {
1001            List<Serializable> leftListProperty = getListPropertyIfNotNull(leftProperty);
1002            List<Serializable> rightListProperty = getListPropertyIfNotNull(rightProperty);
1003
1004            ListPropertyDiff listPropertyDiff = (ListPropertyDiff) propertyDiff;
1005
1006            // Get list property indexes
1007            // By default: only items that are different (ie. held by the
1008            // propertyDiff)
1009            List<Integer> listPropertyIndexes = new ArrayList<>();
1010            if (isDisplayAllItems) {
1011                // All items
1012                int listPropertySize = Math.min(leftListProperty.size(), rightListProperty.size());
1013
1014                for (int index = 0; index < listPropertySize; index++) {
1015                    listPropertyIndexes.add(index);
1016                }
1017            } else {
1018                listPropertyIndexes = listPropertyDiff.getDiffIndexes();
1019            }
1020            fieldXPaths = getComplexListXPaths(propertyName, listPropertyIndexes, listPropertyDiff, leftListProperty,
1021                    rightListProperty, isDisplayAllItems, isDisplayItemIndexes);
1022        }
1023        return fieldXPaths;
1024    }
1025
1026    protected final PropertyDiffDisplay getComplexListXPaths(String propertyName, List<Integer> listPropertyIndexes,
1027            ListPropertyDiff listPropertyDiff, List<Serializable> leftListProperty,
1028            List<Serializable> rightListProperty, boolean isDisplayAllItems, boolean isDisplayItemIndexes) {
1029
1030        if (listPropertyIndexes.isEmpty()) {
1031            // TODO: add differenceType?
1032            return new ContentDiffDisplayImpl(new ArrayList<Serializable>());
1033        }
1034        boolean isComplexListWidget = isDisplayItemIndexes
1035                || (listPropertyDiff != null && listPropertyDiff.isComplexListType());
1036
1037        if (isComplexListWidget) {
1038            List<Map<String, Serializable>> listFieldXPaths = new ArrayList<>();
1039            for (int index : listPropertyIndexes) {
1040
1041                Map<String, Serializable> listItemXPaths = new HashMap<>();
1042                // Put item index if wanted
1043                if (isDisplayItemIndexes) {
1044                    listItemXPaths.put(DIFF_LIST_WIDGET_INDEX_SUBWIDGET_FIELD, index + 1);
1045                }
1046                PropertyDiff listItemPropertyDiff = listPropertyDiff.getDiff(index);
1047                if (listItemPropertyDiff != null) {
1048
1049                    Serializable leftListPropertyItem = null;
1050                    Serializable rightListPropertyItem = null;
1051                    if (index < leftListProperty.size()) {
1052                        leftListPropertyItem = leftListProperty.get(index);
1053                    }
1054                    if (index < rightListProperty.size()) {
1055                        rightListPropertyItem = rightListProperty.get(index);
1056                    }
1057                    Map<String, Serializable> leftComplexProperty = null;
1058                    Map<String, Serializable> rightComplexProperty = null;
1059                    if (isComplexType(leftListPropertyItem)) {
1060                        leftComplexProperty = getComplexProperty(leftListPropertyItem);
1061                    }
1062                    if (isComplexType(rightListPropertyItem)) {
1063                        rightComplexProperty = getComplexProperty(rightListPropertyItem);
1064                    }
1065
1066                    // Complex list
1067                    if (listItemPropertyDiff.isComplexType()) {
1068                        Map<String, PropertyDiff> complexPropertyDiffMap = ((ComplexPropertyDiff) listItemPropertyDiff).getDiffMap();
1069                        Iterator<String> complexPropertyItemNamesIt = complexPropertyDiffMap.keySet().iterator();
1070                        while (complexPropertyItemNamesIt.hasNext()) {
1071                            String complexPropertyItemName = complexPropertyItemNamesIt.next();
1072                            // TODO: take into account subwidget properties
1073                            // 'displayAllItems' and 'displayItemIndexes'
1074                            // instead of inheriting them from the parent
1075                            // widget.
1076                            setComplexPropertyXPaths(listItemXPaths, complexPropertyItemName,
1077                                    getSubPropertyFullName(propertyName,
1078                                            getSubPropertyFullName(String.valueOf(index), complexPropertyItemName)),
1079                                    complexPropertyDiffMap, leftComplexProperty, rightComplexProperty,
1080                                    isDisplayAllItems, isDisplayItemIndexes);
1081                        }
1082                    }
1083                    // Scalar or content list
1084                    else {
1085                        String listItemXPath = null;
1086                        // Keep listItemXPath null if one of the left or right
1087                        // properties is empty
1088                        if (leftListPropertyItem != null && rightListPropertyItem != null) {
1089                            listItemXPath = getSubPropertyFullName(propertyName, String.valueOf(index));
1090                        }
1091                        boolean isDisplayHtmlConversion = ContentDiffHelper.isDisplayHtmlConversion(
1092                                leftListPropertyItem)
1093                                && ContentDiffHelper.isDisplayHtmlConversion(rightListPropertyItem);
1094                        boolean isDisplayTextConversion = ContentDiffHelper.isDisplayTextConversion(
1095                                leftListPropertyItem)
1096                                && ContentDiffHelper.isDisplayTextConversion(rightListPropertyItem);
1097                        listItemXPaths.put(DIFF_LIST_WIDGET_VALUE_SUBWIDGET_FIELD,
1098                                new ContentDiffDisplayImpl(listItemXPath, listItemPropertyDiff.getDifferenceType(),
1099                                        isDisplayHtmlConversion, isDisplayTextConversion));
1100                    }
1101                }
1102                listFieldXPaths.add(listItemXPaths);
1103            }
1104            return new ContentDiffDisplayImpl((Serializable) listFieldXPaths);
1105        } else {
1106            List<PropertyDiffDisplay> listFieldXPaths = new ArrayList<>();
1107            for (int index : listPropertyIndexes) {
1108                PropertyDiffDisplay listItemXPath = null;
1109                // Keep listItemXPath null if one of the left or right
1110                // properties is empty
1111                if (index < leftListProperty.size() && index < rightListProperty.size()) {
1112                    PropertyDiff listItemPropertyDiff = null;
1113                    if (listPropertyDiff != null) {
1114                        listItemPropertyDiff = listPropertyDiff.getDiff(index);
1115                    }
1116                    DifferenceType differenceType = DifferenceType.different;
1117                    if (listItemPropertyDiff != null) {
1118                        differenceType = listItemPropertyDiff.getDifferenceType();
1119                    }
1120                    Serializable leftListPropertyItem = leftListProperty.get(index);
1121                    Serializable rightListPropertyItem = rightListProperty.get(index);
1122                    boolean isDisplayHtmlConversion = ContentDiffHelper.isDisplayHtmlConversion(leftListPropertyItem)
1123                            && ContentDiffHelper.isDisplayHtmlConversion(rightListPropertyItem);
1124                    boolean isDisplayTextConversion = ContentDiffHelper.isDisplayTextConversion(leftListPropertyItem)
1125                            && ContentDiffHelper.isDisplayTextConversion(rightListPropertyItem);
1126                    listItemXPath = new ContentDiffDisplayImpl(
1127                            getSubPropertyFullName(propertyName, String.valueOf(index)), differenceType,
1128                            isDisplayHtmlConversion, isDisplayTextConversion);
1129                }
1130                listFieldXPaths.add(listItemXPath);
1131            }
1132            return new ContentDiffDisplayImpl((Serializable) listFieldXPaths);
1133        }
1134    }
1135
1136    /**
1137     * Sets the complex property xpaths.
1138     *
1139     * @param complexPropertyXPaths the complex property xpaths
1140     * @param complexFieldItemName the complex field item name
1141     * @param subPropertyFullName the sub property full name
1142     * @param complexPropertyDiffMap the complex property diff map
1143     * @param leftComplexProperty the left complex property
1144     * @param rightComplexProperty the right complex property
1145     * @param isDisplayAllItems the is display all items
1146     * @param isDisplayItemIndexes the is display item indexes
1147     */
1148    protected void setComplexPropertyXPaths(Map<String, Serializable> complexPropertyXPaths,
1149            String complexFieldItemName, String subPropertyFullName, Map<String, PropertyDiff> complexPropertyDiffMap,
1150            Map<String, Serializable> leftComplexProperty, Map<String, Serializable> rightComplexProperty,
1151            boolean isDisplayAllItems, boolean isDisplayItemIndexes) {
1152
1153        Serializable leftComplexPropertyItemValue = null;
1154        Serializable rightComplexPropertyItemValue = null;
1155        if (leftComplexProperty != null) {
1156            leftComplexPropertyItemValue = leftComplexProperty.get(complexFieldItemName);
1157        }
1158        if (rightComplexProperty != null) {
1159            rightComplexPropertyItemValue = rightComplexProperty.get(complexFieldItemName);
1160        }
1161        complexPropertyXPaths.put(complexFieldItemName,
1162                getFieldXPaths(subPropertyFullName, complexPropertyDiffMap.get(complexFieldItemName),
1163                        leftComplexPropertyItemValue, rightComplexPropertyItemValue, isDisplayAllItems,
1164                        isDisplayItemIndexes, null));
1165    }
1166
1167    protected boolean isListType(Serializable property) {
1168
1169        return property instanceof List<?> || property instanceof Serializable[];
1170    }
1171
1172    protected boolean isComplexType(Serializable property) {
1173
1174        return property instanceof Map<?, ?>;
1175    }
1176
1177    /**
1178     * Casts or convert a {@link Serializable} property to {@link List <Serializable>}.
1179     *
1180     * @param property the property
1181     * @return the list property
1182     * @throws ClassCastException if the {@code property} is not a {@link List <Serializable>} nor an array.
1183     */
1184    @SuppressWarnings("unchecked")
1185    protected List<Serializable> getListProperty(Serializable property) {
1186        List<Serializable> listProperty;
1187        if (property instanceof List<?>) { // List
1188            listProperty = (List<Serializable>) property;
1189        } else { // Array
1190            listProperty = Arrays.asList((Serializable[]) property);
1191        }
1192        return listProperty;
1193    }
1194
1195    /**
1196     * Gets the list property if the {@code property} is not null.
1197     *
1198     * @param property the property
1199     * @return the list property if the {@code property} is not null, null otherwise
1200     * @throws NuxeoException if the {@code property} is not a list.
1201     */
1202    protected List<Serializable> getListPropertyIfNotNull(Serializable property) {
1203
1204        if (property != null) {
1205            if (!isListType(property)) {
1206                throw new NuxeoException(
1207                        "Tryed to get a list property from a Serializable property that is not a list, this is inconsistent.");
1208            }
1209            return getListProperty(property);
1210        }
1211        return null;
1212    }
1213
1214    /**
1215     * Casts a {@link Serializable} property to {@link Map<String, Serializable>}.
1216     *
1217     * @param property the property
1218     * @return the complex property
1219     * @throws ClassCastException if the {@code property} is not a {@link Map <String, Serializable>}.
1220     */
1221    @SuppressWarnings("unchecked")
1222    protected Map<String, Serializable> getComplexProperty(Serializable property) {
1223        return (Map<String, Serializable>) property;
1224    }
1225
1226    /**
1227     * Gets the complex property if the {@code property} is not null.
1228     *
1229     * @param property the property
1230     * @return the complex property if the {@code property} is not null, null otherwise
1231     * @throws NuxeoException if the {@code property} is not a list.
1232     */
1233    protected Map<String, Serializable> getComplexPropertyIfNotNull(Serializable property) {
1234
1235        if (property != null) {
1236            if (!isComplexType(property)) {
1237                throw new NuxeoException(
1238                        "Tryed to get a complex property from a Serializable property that is not a map, this is inconsistent.");
1239            }
1240            return getComplexProperty(property);
1241        }
1242        return null;
1243    }
1244
1245    // TODO: better separate regular and contentDiffLinksWidget cases (call // submethods)
1246    protected final WidgetDefinition getWidgetDefinition(String category, String propertyName, String propertyType,
1247            Field field, List<DiffFieldItemDefinition> complexFieldItemDefs, boolean isContentDiffLinksWidget) {
1248
1249        boolean isGeneric = false;
1250        boolean isCloned = false;
1251
1252        WidgetDefinition wDef;
1253        if (!isContentDiffLinksWidget) {
1254            // Look for a specific widget in the "diff" category named with the
1255            // property name
1256            wDef = getLayoutStore().getWidgetDefinition(category, propertyName);
1257            if (wDef == null) {
1258                isGeneric = true;
1259                // Fallback on a generic widget in the "diff" category named
1260                // with the property type
1261                wDef = getLayoutStore().getWidgetDefinition(category, propertyType);
1262                if (wDef == null) {
1263                    throw new NuxeoException(String.format(
1264                            "Could not find any specific widget named '%s', nor any generic widget named '%s'. Please make sure at least a generic widget is defined for this type.",
1265                            propertyName, propertyType));
1266                }
1267            }
1268        } else {
1269            isGeneric = true;
1270            if (PropertyType.isSimpleType(propertyType) || PropertyType.isContentType(propertyType)) {
1271                wDef = getLayoutStore().getWidgetDefinition(category, CONTENT_DIFF_LINKS_WIDGET_NAME);
1272                if (wDef == null) {
1273                    throw new NuxeoException(String.format(
1274                            "Could not find any generic widget named '%s'. Please make sure a generic widget is defined with this name.",
1275                            CONTENT_DIFF_LINKS_WIDGET_NAME));
1276                }
1277            } else {
1278                // Get the generic widget in the "diff" category named with
1279                // the property type
1280                wDef = getLayoutStore().getWidgetDefinition(category, propertyType);
1281                if (wDef == null) {
1282                    throw new NuxeoException(String.format(
1283                            "Could not find any generic widget named '%s'. Please make sure a generic widget is defined for this type.",
1284                            propertyType));
1285                }
1286            }
1287        }
1288
1289        if (isGeneric) {
1290            // Clone widget definition
1291            wDef = wDef.clone();
1292            isCloned = true;
1293            // Set widget name
1294            String widgetName = propertyName;
1295            if (isContentDiffLinksWidget) {
1296                widgetName += CONTENT_DIFF_LINKS_WIDGET_NAME_SUFFIX;
1297            }
1298            wDef.setName(widgetName);
1299
1300            // Set labels
1301            Map<String, String> labels = new HashMap<>();
1302            labels.put(BuiltinModes.ANY, DIFF_WIDGET_LABEL_PREFIX + getPropertyLabel(propertyName));
1303            wDef.setLabels(labels);
1304
1305            // Set translated
1306            wDef.setTranslated(true);
1307        }
1308
1309        // Set field definitions if generic or specific and not already set in
1310        // widget definition
1311        if (isGeneric || !isFieldDefinitions(wDef)) {
1312
1313            String fieldName = propertyName;
1314            if (field != null) {
1315                fieldName = field.getName().getLocalName();
1316            }
1317
1318            FieldDefinition[] fieldDefinitions;
1319            if (isContentDiffLinksWidget) {
1320                fieldDefinitions = new FieldDefinition[4];
1321                fieldDefinitions[0] = new FieldDefinitionImpl(null,
1322                        getFieldDefinition(fieldName, DIFF_WIDGET_FIELD_DEFINITION_VALUE));
1323                fieldDefinitions[1] = new FieldDefinitionImpl(null,
1324                        getFieldDefinition(fieldName, DIFF_WIDGET_FIELD_DEFINITION_DIFFERENCE_TYPE));
1325                fieldDefinitions[2] = new FieldDefinitionImpl(null,
1326                        getFieldDefinition(fieldName, DIFF_WIDGET_FIELD_DEFINITION_DISPLAY_HTML_CONVERSION));
1327                fieldDefinitions[3] = new FieldDefinitionImpl(null,
1328                        getFieldDefinition(fieldName, DIFF_WIDGET_FIELD_DEFINITION_DISPLAY_TEXT_CONVERSION));
1329            } else {
1330                int fieldCount = 2;
1331                if (PropertyType.isContentType(propertyType) || ("note:note".equals(propertyName))) {
1332                    fieldCount = 3;
1333                }
1334                fieldDefinitions = new FieldDefinition[fieldCount];
1335                fieldDefinitions[0] = new FieldDefinitionImpl(null,
1336                        getFieldDefinition(fieldName, DIFF_WIDGET_FIELD_DEFINITION_VALUE));
1337
1338                FieldDefinition styleClassFieldDef = new FieldDefinitionImpl(null,
1339                        getFieldDefinition(fieldName, DIFF_WIDGET_FIELD_DEFINITION_STYLE_CLASS));
1340                if (PropertyType.isContentType(propertyType)) {
1341                    fieldDefinitions[1] = new FieldDefinitionImpl(null,
1342                            getFieldDefinition(getFieldDefinition(fieldName, DIFF_WIDGET_FIELD_DEFINITION_VALUE),
1343                                    DIFF_WIDGET_FIELD_DEFINITION_FILENAME));
1344                    fieldDefinitions[2] = styleClassFieldDef;
1345                } else if ("note:note".equals(propertyName)) {
1346                    fieldDefinitions[1] = new FieldDefinitionImpl(null,
1347                            getFieldDefinition("note:mime_type", DIFF_WIDGET_FIELD_DEFINITION_VALUE));
1348                    fieldDefinitions[2] = styleClassFieldDef;
1349                } else {
1350                    fieldDefinitions[1] = styleClassFieldDef;
1351                }
1352            }
1353
1354            // Clone if needed
1355            if (!isCloned) {
1356                wDef = wDef.clone();
1357                isCloned = true;
1358            }
1359            wDef.setFieldDefinitions(fieldDefinitions);
1360        }
1361
1362        // Set subwidgets if not already set
1363        if (!isSubWidgets(wDef)) {
1364            if (PropertyType.isListType(propertyType) || PropertyType.isComplexType(propertyType)) {
1365
1366                Field declaringField = field;
1367                if (declaringField == null) {
1368                    declaringField = ComplexPropertyHelper.getField(getPropertySchema(propertyName),
1369                            getPropertyField(propertyName));
1370                }
1371                // Clone if needed
1372                if (!isCloned) {
1373                    wDef = wDef.clone();
1374                    isCloned = true;
1375                }
1376                wDef.setSubWidgetDefinitions(getSubWidgetDefinitions(category, propertyName, propertyType,
1377                        declaringField, complexFieldItemDefs, isDisplayItemIndexes(wDef), isContentDiffLinksWidget));
1378            }
1379        }
1380
1381        return wDef;
1382    }
1383
1384    protected final boolean isSubWidgets(WidgetDefinition wDef) {
1385
1386        WidgetDefinition[] subWidgetDefs = wDef.getSubWidgetDefinitions();
1387        return subWidgetDefs != null && subWidgetDefs.length > 0;
1388    }
1389
1390    protected final boolean isFieldDefinitions(WidgetDefinition wDef) {
1391
1392        FieldDefinition[] fieldDefs = wDef.getFieldDefinitions();
1393        return fieldDefs != null && fieldDefs.length > 0;
1394    }
1395
1396    protected final String getFieldDefinition(String fieldName, String subPropertyName) {
1397
1398        return fieldName + "/" + subPropertyName;
1399    }
1400
1401    protected final WidgetDefinition[] getSubWidgetDefinitions(String category, String propertyName,
1402            String propertyType, Field field, List<DiffFieldItemDefinition> complexFieldItemDefs,
1403            boolean isDisplayItemIndexes, boolean isContentDiffLinks) {
1404
1405        WidgetDefinition[] subWidgetDefs = null;
1406        // Complex
1407        if (PropertyType.isComplexType(propertyType)) {
1408            subWidgetDefs = getComplexSubWidgetDefinitions(category, propertyName, field, complexFieldItemDefs, false,
1409                    isContentDiffLinks);
1410        }
1411        // Scalar or content list
1412        else if (PropertyType.isScalarListType(propertyType) || PropertyType.isContentListType(propertyType)) {
1413            Field listFieldItem = ComplexPropertyHelper.getListFieldItem(field);
1414            subWidgetDefs = initSubWidgetDefinitions(isDisplayItemIndexes, 1);
1415            subWidgetDefs[subWidgetDefs.length - 1] = getWidgetDefinition(category,
1416                    getSubPropertyFullName(propertyName, listFieldItem.getName().getLocalName()),
1417                    ComplexPropertyHelper.getFieldType(listFieldItem),
1418                    new FieldImpl(new QName(DIFF_LIST_WIDGET_VALUE_SUBWIDGET_FIELD), field.getType(),
1419                            listFieldItem.getType()),
1420                    null, isContentDiffLinks);
1421        }
1422        // Complex list
1423        else if (PropertyType.isComplexListType(propertyType)) {
1424            Field listFieldItem = ComplexPropertyHelper.getListFieldItem(field);
1425            subWidgetDefs = getComplexSubWidgetDefinitions(category, propertyName, listFieldItem, complexFieldItemDefs,
1426                    isDisplayItemIndexes, isContentDiffLinks);
1427        }
1428        return subWidgetDefs;
1429    }
1430
1431    protected final WidgetDefinition[] getComplexSubWidgetDefinitions(String category, String propertyName, Field field,
1432            List<DiffFieldItemDefinition> complexFieldItemDefs, boolean isDisplayItemIndexes,
1433            boolean isContentDiffLinks) {
1434
1435        WidgetDefinition[] subWidgetDefs;
1436        int subWidgetIndex = isDisplayItemIndexes ? 1 : 0;
1437
1438        if (CollectionUtils.isEmpty(complexFieldItemDefs)) {
1439            List<Field> complexFieldItems = ComplexPropertyHelper.getComplexFieldItems(field);
1440            subWidgetDefs = initSubWidgetDefinitions(isDisplayItemIndexes, complexFieldItems.size());
1441
1442            for (Field complexFieldItem : complexFieldItems) {
1443                subWidgetDefs[subWidgetIndex] = getWidgetDefinition(category,
1444                        getSubPropertyFullName(propertyName, complexFieldItem.getName().getLocalName()),
1445                        ComplexPropertyHelper.getFieldType(complexFieldItem), complexFieldItem, null,
1446                        isContentDiffLinks);
1447                subWidgetIndex++;
1448            }
1449        } else {
1450            int subWidgetCount = complexFieldItemDefs.size();
1451            // Only add a subwidget for the items marked to display the content
1452            // diff links
1453            if (isContentDiffLinks) {
1454                subWidgetCount = 0;
1455                for (DiffFieldItemDefinition complexFieldItemDef : complexFieldItemDefs) {
1456                    if (complexFieldItemDef.isDisplayContentDiffLinks()) {
1457                        subWidgetCount++;
1458                    }
1459                }
1460            }
1461            subWidgetDefs = initSubWidgetDefinitions(isDisplayItemIndexes, subWidgetCount);
1462
1463            for (DiffFieldItemDefinition complexFieldItemDef : complexFieldItemDefs) {
1464                if (!isContentDiffLinks || complexFieldItemDef.isDisplayContentDiffLinks()) {
1465                    String complexFieldItemName = complexFieldItemDef.getName();
1466                    Field complexFieldItem = ComplexPropertyHelper.getComplexFieldItem(field, complexFieldItemName);
1467                    if (complexFieldItem != null) {
1468                        subWidgetDefs[subWidgetIndex] = getWidgetDefinition(category,
1469                                getSubPropertyFullName(propertyName, complexFieldItemName),
1470                                ComplexPropertyHelper.getFieldType(complexFieldItem), complexFieldItem, null,
1471                                isContentDiffLinks);
1472                        subWidgetIndex++;
1473                    }
1474                }
1475            }
1476        }
1477        return subWidgetDefs;
1478    }
1479
1480    protected final WidgetDefinition[] initSubWidgetDefinitions(boolean isDisplayItemIndexes, int subWidgetCount) {
1481
1482        WidgetDefinition[] subWidgetDefs;
1483        if (isDisplayItemIndexes) {
1484            subWidgetDefs = new WidgetDefinition[subWidgetCount + 1];
1485            subWidgetDefs[0] = getIndexSubwidgetDefinition();
1486        } else {
1487            subWidgetDefs = new WidgetDefinition[subWidgetCount];
1488        }
1489
1490        return subWidgetDefs;
1491    }
1492
1493    protected final WidgetDefinition getIndexSubwidgetDefinition() {
1494
1495        FieldDefinition[] fieldDefinitions = { new FieldDefinitionImpl(null, DIFF_LIST_WIDGET_INDEX_SUBWIDGET_FIELD) };
1496
1497        return new WidgetDefinitionImpl(DIFF_LIST_WIDGET_INDEX_SUBWIDGET_FIELD, DIFF_LIST_WIDGET_INDEX_SUBWIDGET_TYPE,
1498                DIFF_LIST_WIDGET_INDEX_SUBWIDGET_LABEL, null, true, null, Arrays.asList(fieldDefinitions), null, null);
1499    }
1500
1501    /**
1502     * Gets the property name.
1503     *
1504     * @param schema the schema
1505     * @param field the field
1506     * @return the property name
1507     */
1508    protected final String getPropertyName(String schema, String field) {
1509
1510        StringBuilder sb = new StringBuilder();
1511        if (!StringUtils.isEmpty(schema)) {
1512            sb.append(schema);
1513            sb.append(":");
1514        }
1515        sb.append(field);
1516        return sb.toString();
1517    }
1518
1519    protected final String getSubPropertyFullName(String basePropertyName, String subPropertyName) {
1520
1521        if (StringUtils.isEmpty(subPropertyName)) {
1522            return basePropertyName;
1523        }
1524        return basePropertyName + '/' + subPropertyName;
1525    }
1526
1527    protected final String getPropertySchema(String propertyName) {
1528
1529        int indexOfColon = propertyName.indexOf(':');
1530        if (indexOfColon > -1) {
1531            return propertyName.substring(0, indexOfColon);
1532        }
1533        return null;
1534    }
1535
1536    protected final String getPropertyField(String propertyName) {
1537
1538        int indexOfColon = propertyName.indexOf(':');
1539        if (indexOfColon > -1 && indexOfColon < propertyName.length() - 1) {
1540            return propertyName.substring(indexOfColon + 1);
1541        }
1542        return propertyName;
1543    }
1544
1545    protected final String getPropertyLabel(String propertyName) {
1546
1547        return propertyName.replaceAll(":", ".").replaceAll("/", ".");
1548    }
1549
1550    /**
1551     * Gets the schema manager.
1552     *
1553     * @return the schema manager
1554     */
1555    protected final SchemaManager getSchemaManager() {
1556        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
1557        if (schemaManager == null) {
1558            throw new NuxeoException("SchemaManager service is null.");
1559        }
1560        return schemaManager;
1561    }
1562
1563    /**
1564     * Gets the layout store service.
1565     *
1566     * @return the layout store service
1567     */
1568    protected final LayoutStore getLayoutStore() {
1569        LayoutStore layoutStore = Framework.getService(LayoutStore.class);
1570        if (layoutStore == null) {
1571            throw new NuxeoException("LayoutStore service is null.");
1572        }
1573        return layoutStore;
1574    }
1575}