001/*
002 *  Copyright 2018 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016
017package org.ametys.plugins.contenttypeseditor.edition;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.net.MalformedURLException;
023import java.nio.charset.Charset;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Properties;
033import java.util.Set;
034import java.util.stream.Collectors;
035
036import javax.xml.parsers.SAXParserFactory;
037import javax.xml.transform.OutputKeys;
038import javax.xml.transform.TransformerFactory;
039import javax.xml.transform.sax.SAXTransformerFactory;
040import javax.xml.transform.sax.TransformerHandler;
041import javax.xml.transform.stream.StreamResult;
042
043import org.apache.avalon.framework.component.Component;
044import org.apache.avalon.framework.configuration.Configuration;
045import org.apache.avalon.framework.configuration.ConfigurationException;
046import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
047import org.apache.avalon.framework.service.ServiceException;
048import org.apache.avalon.framework.service.ServiceManager;
049import org.apache.avalon.framework.service.Serviceable;
050import org.apache.cocoon.xml.AttributesImpl;
051import org.apache.cocoon.xml.XMLUtils;
052import org.apache.commons.io.IOUtils;
053import org.apache.commons.lang.StringUtils;
054import org.apache.excalibur.source.ModifiableSource;
055import org.apache.excalibur.source.Source;
056import org.apache.excalibur.source.SourceNotFoundException;
057import org.apache.excalibur.source.SourceResolver;
058import org.apache.excalibur.source.SourceUtil;
059import org.apache.xml.serializer.OutputPropertiesFactory;
060import org.xml.sax.SAXException;
061
062import org.ametys.cms.contenttype.AttributeDefinition;
063import org.ametys.cms.contenttype.ContentAttributeDefinition;
064import org.ametys.cms.contenttype.ContentType;
065import org.ametys.cms.contenttype.ContentTypeDefinition;
066import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
067import org.ametys.cms.contenttype.ContentTypesHelper;
068import org.ametys.cms.contenttype.EditContentTypeException;
069import org.ametys.cms.contenttype.EditContentTypeHelper;
070import org.ametys.cms.contenttype.RichTextAttributeDefinition;
071import org.ametys.cms.data.type.ModelItemTypeConstants;
072import org.ametys.cms.model.ContentRestrictedCompositeDefinition;
073import org.ametys.cms.model.ContentRestrictedRepeaterDefinition;
074import org.ametys.cms.repository.Content;
075import org.ametys.cms.repository.ContentAttributeTypeExtensionPoint;
076import org.ametys.core.util.I18nUtils;
077import org.ametys.core.util.I18nizableTextKeyComparator;
078import org.ametys.plugins.repository.model.RepeaterDefinition;
079import org.ametys.runtime.i18n.I18nizableText;
080import org.ametys.runtime.model.ElementDefinition;
081import org.ametys.runtime.model.Enumerator;
082import org.ametys.runtime.model.ModelItem;
083import org.ametys.runtime.model.ModelItemContainer;
084import org.ametys.runtime.model.ModelItemGroup;
085import org.ametys.runtime.model.ModelViewItemGroup;
086import org.ametys.runtime.model.SimpleViewItemGroup;
087import org.ametys.runtime.model.StaticEnumerator;
088import org.ametys.runtime.model.View;
089import org.ametys.runtime.model.ViewElement;
090import org.ametys.runtime.model.ViewItem;
091import org.ametys.runtime.model.ViewItemContainer;
092import org.ametys.runtime.parameter.DefaultValidator;
093import org.ametys.runtime.parameter.Validator;
094import org.ametys.runtime.plugin.component.AbstractLogEnabled;
095
096/**
097 * This component save a content type
098 */
099public class SaveContentTypeComponent extends AbstractLogEnabled implements Component, Serviceable
100{
101    /** The Avalon role name */
102    public static final String ROLE = SaveContentTypeComponent.class.getName();
103
104    /** The directory path of application i18n key */
105    private static final String __I18N_CATALOG_DIR = "context://WEB-INF/i18n/";
106
107    /** The path of rights on content types */
108    private static final String __RIGHTS_FILE = "context://WEB-INF/param/rights.xml";
109
110    /** The right category of a content type */
111    private static final String __RIGHT_CATEGORY = "plugin.cms:PLUGINS_CMS_RIGHTS_CONTENT_CATEGORY";
112
113    /** The url to save the right of content type */
114    private static final String __SAVE_CONTENT_TYPE_RIGHT = "cocoon://_plugins/contenttypes-editor/save/content-type-right";
115
116    /** The name of the default i18n catalogue */
117    private static final String __DEFAULT_CATALOGUE = "application";
118
119    /** The source resolver */
120    protected SourceResolver _sourceResolver;
121
122    /** The content type extension point instance */
123    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
124    /** The content types helper */
125    protected ContentTypesHelper _contentTypesHelper;
126
127    /** The extension point for available attribute types */
128    protected ContentAttributeTypeExtensionPoint _contentAttributeTypeExtensionPoint;
129
130    /** The content type state instance */
131    protected ContentTypeStateComponent _contentTypeStateComponent;
132
133    /** The edit content type helper instance */
134    protected EditContentTypeInformationHelper _editContentTypeInformationHelper;
135
136    /** The edit content type component instance */
137    protected EditContentTypeHelper _editContentTypeHelper;
138    
139    /** I18nUtils instance */
140    protected I18nUtils _i18nUtils;
141    
142    /** Representation of i18n catalog according to the language */
143    private Map<String, I18nCatalog> _i18nCatalogs = new HashMap<>();
144
145    private List<TranslatedValue> _translatedValues;
146
147    private int _fieldsetNumber;
148
149    public void service(ServiceManager manager) throws ServiceException
150    {
151        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
152        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
153        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
154        _contentAttributeTypeExtensionPoint = (ContentAttributeTypeExtensionPoint) manager.lookup(ContentAttributeTypeExtensionPoint.ROLE);
155        _contentTypeStateComponent = (ContentTypeStateComponent) manager.lookup(ContentTypeStateComponent.ROLE);
156        _editContentTypeInformationHelper = (EditContentTypeInformationHelper) manager.lookup(EditContentTypeInformationHelper.ROLE);
157        _editContentTypeHelper = (EditContentTypeHelper) manager.lookup(org.ametys.cms.contenttype.EditContentTypeHelper.ROLE);
158        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
159    }
160
161    /**
162     * Save content type
163     * 
164     * @param contentTypeInfos all information about the content type to save
165     * @return True if the content type was successfully saved
166     */
167    public boolean saveContentType(Map<String, Object> contentTypeInfos)
168    {
169        boolean success = true;
170        this._translatedValues = new ArrayList<>();
171
172        String realContentTypeId = contentTypeInfos.get("id").toString();
173        ContentTypeDefinition contentTypeDefinition = _getContentTypeDefinition(contentTypeInfos);
174
175        // Sax content type file
176        if (_contentTypeStateComponent.getContentTypeMarkedAsNew().contains(realContentTypeId))
177        {
178            try
179            {
180                _editContentTypeHelper.createContentType(contentTypeDefinition);
181
182                // Sax right param
183                if (success)
184                {
185                    try
186                    {
187                        // Adding right in param/rights.xml
188                        if (contentTypeDefinition.getRight() != null)
189                        {
190                            String pluginName = contentTypeInfos.get("pluginName").toString();
191                            String idKey = _generateContentTypeIdKey(realContentTypeId);
192                            String rightLabelKey = _generateI18nKey(pluginName, "right_create", idKey, "label");
193                            String rightDescriptionKey = _generateI18nKey(pluginName, "right_create", idKey, "description");
194                            _saxRightsParam(contentTypeDefinition.getRight(), rightLabelKey, rightDescriptionKey);
195                        }
196                    }
197                    catch (Exception e)
198                    {
199                        getLogger().error("Error when trying to save the right of new content type '" + realContentTypeId + "'", e);
200                        success = false;
201                    }
202                }
203            }
204            catch (EditContentTypeException e)
205            {
206                success = false;
207            }
208        }
209        else
210        {
211            try
212            {
213                _editContentTypeHelper.editContentType(contentTypeDefinition);
214            }
215            catch (EditContentTypeException e)
216            {
217                success = false;
218            }
219        }
220
221        // Sax i18n catalog
222        if (success)
223        {
224            try
225            {
226                // Adding i18n key to catalog
227                Map<String, Map<String, String>> i18nMessageTranslations = _createNewI18nCatalogs();
228                _saxCatalogs(i18nMessageTranslations);
229            }
230            catch (Exception e)
231            {
232                getLogger().error("Error when trying to save i18n key of new content type '" + realContentTypeId + "'", e);
233                success = false;
234            }
235        }
236
237        return success;
238    }
239
240    private ContentTypeDefinition _getContentTypeDefinition(Map<String, Object> contentTypeInfos)
241    {
242        String realContentTypeId = contentTypeInfos.get("id").toString();
243        String contentTypeId = realContentTypeId.replaceFirst("content-type.", "");
244        String pluginName = contentTypeInfos.get("pluginName").toString();
245
246        ContentType existingContentType = _contentTypeExtensionPoint.getExtension(realContentTypeId);
247
248        ContentTypeDefinition contentType = new ContentTypeDefinition(contentTypeInfos.get("id").toString());
249        contentType.setPluginName(pluginName);
250        _addGeneralInformation(contentType, existingContentType, contentTypeInfos, pluginName, contentTypeId);
251        _addModelItems(contentType, existingContentType, contentTypeInfos, pluginName, realContentTypeId);
252        _addViews(contentType, existingContentType, realContentTypeId, contentTypeInfos, pluginName, contentTypeId);
253
254        return contentType;
255    }
256
257    private void _addGeneralInformation(ContentTypeDefinition contentType, ContentType existingContentType, Map<String, Object> contentTypeInfos, String pluginName, String contentTypeId)
258    {
259        I18nizableText label = _getI18nizableText(existingContentType != null ? existingContentType.getLabel() : null, contentTypeInfos.get("label"), pluginName, "contenttype", contentTypeId, "label");
260        contentType.setLabel(label);
261
262        I18nizableText defaultTitle = _getI18nizableText(existingContentType != null ? existingContentType.getDefaultTitle() : null, contentTypeInfos.get("defaultTitle"), pluginName, "contenttype", contentTypeId, "default_title");
263        contentType.setDefaultTitle(defaultTitle);
264
265        I18nizableText description = _getI18nizableText(existingContentType != null ? existingContentType.getDescription() : null, contentTypeInfos.get("description"), pluginName, "contenttype", contentTypeId, "description");
266        contentType.setDescription(description);
267
268        _addCategory(contentType, contentTypeInfos.get("category"), contentTypeInfos.get("newCategory"), pluginName);
269
270        _addIconGlyph(contentType, contentTypeInfos.get("iconGlyph"));
271
272        _addTags(contentType, contentTypeInfos.get("tags"), contentTypeInfos.get("private"), contentTypeInfos.get("referencetable"), contentTypeInfos.get("mixin"));
273
274        _addSupertypeIds(contentType, contentTypeInfos.get("superTypes"));
275
276        _addIsAbstract(contentType, contentTypeInfos.get("abstract"));
277
278        _addRight(contentType, contentTypeId, pluginName);
279    }
280
281    private void _addCategory(ContentTypeDefinition contentTypeDefinition, Object recoveredCategory, Object recoveredNewCategory, String pluginName)
282    {
283        if (recoveredCategory != null)
284        {
285            if (recoveredCategory instanceof Map)
286            {
287                Map categoryMap = (Map) recoveredCategory;
288                if (!categoryMap.isEmpty())
289                {
290                    Object isNewObject = categoryMap.get("isNew");
291                    if (isNewObject != null && isNewObject instanceof Boolean)
292                    {
293                        I18nizableText category = null;
294                        boolean isNew = (Boolean) isNewObject;
295                        if (isNew)
296                        {
297                            category = _getNewCategory(recoveredNewCategory, pluginName);
298                        }
299                        else
300                        {
301                            category = _getExistingCategory(categoryMap);
302                        }
303                        contentTypeDefinition.setCategory(category);
304                    }
305                }
306            }
307        }
308    }
309
310    private I18nizableText _getNewCategory(Object recoveredNewCategory, String pluginName)
311    {
312        I18nizableText category = null;
313
314        if (recoveredNewCategory != null && recoveredNewCategory instanceof Map)
315        {
316            @SuppressWarnings("unchecked")
317            Map<String, Object> newCategory = (Map<String, Object>) recoveredNewCategory;
318
319            if (!newCategory.isEmpty())
320            {
321                Object i18n = newCategory.get("isMultilingual");
322                if (i18n instanceof Boolean)
323                {
324                    boolean isI18n = (boolean) i18n;
325                    if (isI18n)
326                    {
327                        Object recoveredI18nValues = newCategory.get("values");
328                        if (recoveredI18nValues != null && recoveredI18nValues instanceof Map)
329                        {
330                            @SuppressWarnings("unchecked")
331                            Map<String, String> i18nValues = (Map<String, String>) recoveredI18nValues;
332
333                            String categoryKey = _generateCategoryI18nKey(pluginName, newCategory);
334                            category = new I18nizableText("plugin.contenttypes-editor", categoryKey);
335
336                            // Add key and values to the TranslatedValue object
337                            TranslatedValue translatedValue = new TranslatedValue(categoryKey, i18nValues);
338                            _translatedValues.add(translatedValue);
339                        }
340                    }
341                    else
342                    {
343                        Object value = newCategory.get("values");
344                        if (value instanceof String)
345                        {
346                            category = new I18nizableText((String) value);
347                        }
348                    }
349                    _editContentTypeInformationHelper.addNewCategory(newCategory);
350                }
351            }
352        }
353
354        return category;
355    }
356
357    private I18nizableText _getExistingCategory(Map categoryMap)
358    {
359        I18nizableText category = null;
360        Object i18n = categoryMap.get("isMultilingual");
361        if (i18n instanceof Boolean)
362        {
363            boolean isI18n = (boolean) i18n;
364            if (isI18n)
365            {
366                Object recoveredKey = categoryMap.get("key");
367                Object recoveredCatalogue = categoryMap.get("catalogue");
368                if (recoveredKey instanceof String && recoveredCatalogue instanceof String)
369                {
370                    category = new I18nizableText((String) recoveredCatalogue, (String) recoveredKey);
371                }
372            }
373            else
374            {
375                Object value = categoryMap.get("values");
376                if (value instanceof String)
377                {
378                    category = new I18nizableText((String) value);
379                }
380            }
381        }
382        return category;
383    }
384
385    private void _addIconGlyph(ContentTypeDefinition contentTypeDefinition, Object recoverIconGlyph)
386    {
387        if (recoverIconGlyph instanceof String)
388        {
389            String icon = (String) recoverIconGlyph;
390            if (StringUtils.isNotBlank(icon))
391            {
392                contentTypeDefinition.setIconGlyph(icon);
393            }
394        }
395    }
396
397    private void _addTags(ContentTypeDefinition contentTypeDefinition, Object recoverTags, Object recoverIsPrivate, Object recoverIsReferenceTable, Object recoverIsMixin)
398    {
399        Set<String> tags = new HashSet<>();
400        if (recoverTags != null && recoverTags instanceof String)
401        {
402            String[] tagArray = ((String) recoverTags).split(",");
403            for (String tag : tagArray)
404            {
405                if (StringUtils.isNotBlank(tag))
406                {
407                    tags.add(tag);
408                }
409            }
410        }
411        if (recoverIsPrivate != null && recoverIsPrivate instanceof Boolean && (boolean) recoverIsPrivate)
412        {
413            tags.add("private");
414        }
415        if (recoverIsReferenceTable != null && recoverIsReferenceTable instanceof Boolean && (boolean) recoverIsReferenceTable)
416        {
417            tags.add("reference-table");
418        }
419        if (recoverIsMixin != null && recoverIsMixin instanceof Boolean && (boolean) recoverIsMixin)
420        {
421            tags.add("mixin");
422        }
423
424        if (!tags.isEmpty())
425        {
426            contentTypeDefinition.setTags(tags);
427        }
428    }
429
430    private void _addSupertypeIds(ContentTypeDefinition contentTypeDefinition, Object recoverSupertypes)
431    {
432        if (recoverSupertypes != null && recoverSupertypes instanceof List)
433        {
434            @SuppressWarnings("unchecked")
435            List<LinkedHashMap<String, String>> supertypes = (List<LinkedHashMap<String, String>>) recoverSupertypes;
436            List<String> supertypeIds = new ArrayList<>();
437            supertypeIds = supertypes.stream().map(s -> s.get("id")).collect(Collectors.toList());
438            contentTypeDefinition.setSupertypeIds(supertypeIds.toArray(new String[supertypeIds.size()]));
439        }
440    }
441
442    private void _addIsAbstract(ContentTypeDefinition contentTypeDefinition, Object recoverIsAbstract)
443    {
444        boolean isAbstract = recoverIsAbstract != null && recoverIsAbstract instanceof Boolean && (boolean) recoverIsAbstract;
445        contentTypeDefinition.setIsAbstract(isAbstract);
446    }
447
448    private void _addRight(ContentTypeDefinition contentTypeDefinition, String contentTypeId, String pluginName)
449    {
450        String right = pluginName + "_Right_" + _generateContentTypeIdKey(contentTypeId) + "_Create";
451        contentTypeDefinition.setRight(right);
452    }
453
454    private void _addModelItems(ContentTypeDefinition contentType, ContentType existingContentType, Map<String, Object> contentTypeInfos, String pluginName, String realContentTypeId)
455    {
456        Boolean isTitleAttributePresent = false;
457        Object recoverMetadataList = contentTypeInfos.get("attributes");
458        List<ModelItem> modelItems = new ArrayList<>();
459        
460        if (recoverMetadataList != null && recoverMetadataList instanceof List)
461        {
462            List recoverModelItems = (List) recoverMetadataList;
463            
464            for (Object recoverModelItem : recoverModelItems)
465            {
466                if (recoverModelItem instanceof Map)
467                {
468                    @SuppressWarnings("unchecked")
469                    Map<String, Object> modelItemInfo = (Map<String, Object>) recoverModelItem;
470                    isTitleAttributePresent = modelItemInfo.get("name").equals(Content.ATTRIBUTE_TITLE) || isTitleAttributePresent;
471                    Object recoveredReferenceContentTypeId = modelItemInfo.get("referenceContentTypeId");
472                    if (recoveredReferenceContentTypeId != null
473                        && recoveredReferenceContentTypeId instanceof String 
474                        && ((String) recoveredReferenceContentTypeId).equals(realContentTypeId))
475                    {
476                        Object recoverName = modelItemInfo.get("name");
477                        ModelItem oldModelItem = null;
478                        String contentTypeId = realContentTypeId.replaceFirst("content-type.", "");
479                        if (existingContentType != null && recoverName instanceof String && existingContentType.hasModelItem((String) recoverName))
480                        {
481                            oldModelItem = existingContentType.getModelItem((String) recoverName);
482                            ModelItem newModelItem = _getModelItem(modelItemInfo, oldModelItem, pluginName, contentTypeId);
483                            List<String> overriddenAttributes = existingContentType.getOverriddenAttributes();
484
485                            // Check if there is a modification of the attribute to avoid useless overriding
486                            if (!_areModelItemsEqual(oldModelItem, newModelItem, overriddenAttributes))
487                            {
488                                modelItems.add(newModelItem);
489                            }
490                        }
491                        else if (_contentTypeStateComponent.getContentTypeMarkedAsNew().contains(realContentTypeId))
492                        {
493                            modelItems.add(_getModelItem(modelItemInfo, oldModelItem, pluginName, contentTypeId));
494                        }
495                    }
496                }
497            }  
498        }
499        if (!isTitleAttributePresent)
500        {
501            modelItems.add(_createDefaultTitleModelItem());
502        }
503        contentType.setModelItems(modelItems);
504    }
505    
506    private boolean _areModelItemsEqual(ModelItem oldModelItem, ModelItem newModelItem, List<String> overriddenAttributes) 
507    {
508        // If the created MotelItem is a new one. It's not necessary to check the newModelItem nullity because we cannot edit a modelItem removed 
509        if (oldModelItem == null) 
510        {
511            return false;
512        }
513        // If the attribute is already overridden we have to override it again
514        if (overriddenAttributes.contains(oldModelItem.getName()))
515        {
516            return false;
517        }
518        
519        // If the label or the description of the attributes changed we have to override it
520        I18nizableTextKeyComparator i18nKeyComparator = new I18nizableTextKeyComparator();
521        if (i18nKeyComparator.compare(oldModelItem.getLabel(), newModelItem.getLabel()) != 0 
522            || i18nKeyComparator.compare(oldModelItem.getDescription(), newModelItem.getDescription()) != 0)
523        {
524            return false;
525        }
526        
527        // If the edited attribute is a group of modelItems
528        if (oldModelItem instanceof ModelItemGroup)
529        {
530            // Check if there is a modification in the group of ModelItems
531            return _areModelItemGroupsEqual((ModelItemGroup) oldModelItem, (ModelItemGroup) newModelItem, overriddenAttributes);
532        }
533        else
534        {  
535            // Check if there is a modification in the Element
536            return _areElementDefinitionsEqual((ElementDefinition) oldModelItem, (ElementDefinition) newModelItem);
537        }
538    }
539    
540    private boolean _areElementDefinitionsEqual(ElementDefinition oldElem, ElementDefinition newElem)
541    {
542        // If the widget, the validator or the enumerator changed then this element has changed
543        return StringUtils.equals(oldElem.getWidget(), newElem.getWidget())
544            && _areValidatorsEqual(oldElem.getValidator(), newElem.getValidator()) 
545            && _areEnumeratorsEqual(oldElem.getEnumerator(), newElem.getEnumerator())
546            && oldElem.getWidgetParameters().equals(newElem.getWidgetParameters());
547    }
548    
549    private boolean _areModelItemGroupsEqual(ModelItemGroup oldGroup, ModelItemGroup newGroup, List<String> overriddenAttributes)
550    {
551        List<ModelItem> modelItemsTreated = new ArrayList<>();
552        // Compare each attribute in the group
553        for (ModelItem modelItem : oldGroup.getModelItems())
554        {
555            // Check if the modelItem is present in both groups
556            if (newGroup.getModelItem(modelItem.getName()) != null)
557            {
558                modelItemsTreated.add(modelItem);
559                // Recursive call of _areModelItemsEquals function to check each attribute
560                if (!_areModelItemsEqual(modelItem, newGroup.getModelItem(modelItem.getName()), overriddenAttributes))
561                {
562                    return false;
563                }
564            }
565            else
566            {
567                return false;
568            }
569        }
570        // Check if all modelItems of the new groups have been treated
571        for (ModelItem modelItem : newGroup.getModelItems())
572        {
573            if (!modelItemsTreated.contains(modelItem))
574            {
575                return false;
576            }
577        }
578        return true;
579    }
580    private boolean _areEnumeratorsEqual(Enumerator oldEnumerator, Enumerator newEnumerator)
581    {
582        // If the old, the new or if both enumerators are null we can know if they are different
583        if (oldEnumerator == null || newEnumerator == null)
584        {
585            return oldEnumerator == newEnumerator;
586        }
587        // If one enumerator is static and the other custom, we can override them
588        if (oldEnumerator instanceof StaticEnumerator && !(newEnumerator instanceof StaticEnumerator)
589                || oldEnumerator instanceof StaticEnumerator && !(oldEnumerator instanceof StaticEnumerator))
590        {
591            return false;
592        }
593        // If both attribute's enumerator are static we can compare them
594        if (oldEnumerator instanceof StaticEnumerator && newEnumerator instanceof StaticEnumerator)
595        {
596            try
597            {
598                // Compare the values and label of both static enumerators
599                return oldEnumerator.getTypedEntries().equals(newEnumerator.getTypedEntries());
600            }
601            catch (Exception e)
602            {
603                e.printStackTrace();
604            }
605        }
606        // First compare their class names
607        if (oldEnumerator.getClass().getName().equals(newEnumerator.getClass().getName()))
608        {
609            // Compare the configuration in case of they have the same classname
610            return oldEnumerator.getConfiguration().equals(newEnumerator.getConfiguration());
611        }
612        else
613        {
614            return false;
615        }
616    }
617    
618    private boolean _areValidatorsEqual(Validator oldValidator, Validator newValidator) 
619    {
620        // If the old, the new or if both validators are null we can know if they are different
621        if (oldValidator == null || newValidator == null)
622        {
623            return oldValidator == newValidator; 
624        }
625        // Check if the validator's configuration has changed
626        return oldValidator.equals(newValidator);
627    }
628    
629    /**
630     * Compare recovered values stored in a map with existing values stored in a 18nizableText
631     * @param newText values recovered after a user modification of the content
632     * @param existingI18nizableText existing values we need to compare to know if the content has changed
633     * @return the value 0 if there is no difference between old and new values, a value less than 0 if the key is already overridden 
634     * or a value greater than 0 if the new value has changed from the old value. 
635     */
636    private int _compareValuesAnd18nizableText(Map<String, Object> newText, I18nizableText existingI18nizableText) 
637    {
638        @SuppressWarnings("unchecked")
639        Map<String, Object> newValues = (Map<String, Object>) newText.get("values");
640
641        // If the key is already overriden
642        if (existingI18nizableText.toString().matches("application(.*)"))
643        {
644            return -1;
645        }
646        for (Map.Entry<String, Object> entry : newValues.entrySet())
647        {
648            // If the old and the new value are not equals
649            if (!this._i18nUtils.translate(existingI18nizableText, entry.getKey()).equals(entry.getValue()))
650            {
651                return 1;
652            }
653        }
654        return 0;
655    }
656    
657    private ElementDefinition _createDefaultTitleModelItem()
658    {
659        ElementDefinition title = _contentTypesHelper.getTitleAttributeDefinition();
660        title.setValidator(new DefaultValidator(null, true));
661        return title;
662    }
663    
664    @SuppressWarnings("static-access")
665    private ModelItem _getModelItem(Map<String, Object> modelItemInfo, ModelItem existingModelItem, String pluginName, String contentTypeId)
666    {
667        ModelItem modelItem = null;
668
669        Object recoverType = modelItemInfo.get("type");
670        if (recoverType != null && recoverType instanceof String)
671        {
672            String typeId = (String) recoverType;
673            String name = _getString(modelItemInfo.get("name"));
674
675            if (ModelItemTypeConstants.REPEATER_TYPE_ID.equals(typeId))
676            {
677                modelItem = _getRepeater(modelItemInfo, (RepeaterDefinition) existingModelItem, name, pluginName, contentTypeId);
678                _addChildren((ModelItemGroup) modelItem, existingModelItem, modelItemInfo, pluginName, contentTypeId);
679            }
680            else if (ModelItemTypeConstants.COMPOSITE_TYPE_ID.equals(typeId))
681            {
682                modelItem = new ContentRestrictedCompositeDefinition();
683                _addChildren((ModelItemGroup) modelItem, existingModelItem, modelItemInfo, pluginName, contentTypeId);
684            }
685            else
686            {
687                modelItem = _getAttributeDefinition(modelItemInfo, existingModelItem, pluginName, contentTypeId, name, typeId);
688            }
689
690            _addModelItemGeneralInformation(modelItem, existingModelItem, modelItemInfo, name, pluginName, contentTypeId, typeId);
691
692        }
693        return modelItem;
694    }
695
696    private ModelItem _getRepeater(Map<String, Object> repeaterInfo, RepeaterDefinition existingRepeaterDefinition, String name, String pluginName, String contentTypeId)
697    {
698        RepeaterDefinition repeater = new ContentRestrictedRepeaterDefinition();
699        repeater.setInitialSize(_getInteger(repeaterInfo.get("initializeSize")));
700        repeater.setMinSize(_getInteger(repeaterInfo.get("minSize")));
701        repeater.setMaxSize(_getInteger(repeaterInfo.get("maxSize")));
702
703        I18nizableText addLabel = _getI18nizableText(existingRepeaterDefinition != null ? existingRepeaterDefinition.getAddLabel() : null, repeaterInfo.get("addLabel"), pluginName,
704                "contenttype", contentTypeId, "metadata_" + name + "_add_label");
705        repeater.setAddLabel(addLabel);
706
707        I18nizableText deleteLabel = _getI18nizableText(existingRepeaterDefinition != null ? existingRepeaterDefinition.getDeleteLabel() : null, repeaterInfo.get("deleteLabel"),
708                pluginName, "contenttype", contentTypeId, "metadata_" + name + "_delete_label");
709        repeater.setDeleteLabel(deleteLabel);
710
711        return repeater;
712    }
713
714    private ModelItem _getAttributeDefinition(Map<String, Object> attributeInfo, ModelItem existingModelItem, String pluginName, String contentTypeId, String name, String typeId)
715    {
716        AttributeDefinition<? extends Object> attributeDefinition = _getAttributeDefinition(typeId);
717        
718        boolean isMultiple = _getBoolean(attributeInfo.get("multiple"));
719        attributeDefinition.setMultiple(isMultiple);
720        
721        _addWidget(attributeDefinition, attributeInfo, name, pluginName, contentTypeId);
722        _addEnumerator(attributeDefinition, existingModelItem, attributeInfo, name, pluginName, contentTypeId);
723        _addValidator(attributeDefinition, existingModelItem, attributeInfo, name, pluginName, contentTypeId);
724        
725        if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(typeId))
726        {
727            _addSpecificInfoForContentAttribute((ContentAttributeDefinition) attributeDefinition, attributeInfo);
728        }
729        
730        return attributeDefinition;
731    }
732    
733    private AttributeDefinition<? extends Object> _getAttributeDefinition(String typeId)
734    {
735        if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(typeId))
736        {
737            return new ContentAttributeDefinition(_contentTypeExtensionPoint, _contentTypesHelper);
738        }
739        else if (ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(typeId))
740        {
741            return new RichTextAttributeDefinition();
742        }
743        else
744        {
745            return new AttributeDefinition<>();
746        }
747    }
748
749    private void _addWidget(AttributeDefinition<? extends Object> attributeDefinition, Map<String, Object> attributeInfo, String name, String pluginName, String contentTypeId)
750    {
751        String widget = _getString(attributeInfo.get("widget"));
752        attributeDefinition.setWidget(widget);
753
754        Map<String, I18nizableText> widgetParams = null;
755        Object recoverWidgetParams = attributeInfo.get("widgetParams");
756        if (recoverWidgetParams != null && recoverWidgetParams instanceof List)
757        {
758            widgetParams = new HashMap<>();
759            List params = (List) recoverWidgetParams;
760            for (Object recoverWidgetParam : params)
761            {
762                if (recoverWidgetParam != null && recoverWidgetParam instanceof Map)
763                {
764                    Map widgetParam = (Map) recoverWidgetParam;
765                    String recoverLabel = _getString(widgetParam.get("label"));
766                    I18nizableText values = _getI18nizableText(null, widgetParam.get("value"), pluginName, "contentttype", contentTypeId,
767                            "metadata_" + name + "_" + recoverLabel + "_widget_param");
768                    widgetParams.put(recoverLabel, values);
769                }
770            }
771        }
772        attributeDefinition.setWidgetParameters(widgetParams);
773    }
774
775    private void _addEnumerator(AttributeDefinition<? extends Object> attributeDefinition, ModelItem existingModelItem, Map<String, Object> attributeInfo, String name, String pluginName, String contentTypeId)
776    {
777        String enumeratorName = _getString(attributeInfo.get("customEnumeratorClass"));
778        if (StringUtils.isNotBlank(enumeratorName))
779        {
780            _addCustomEnumerator(attributeDefinition, attributeInfo, enumeratorName);
781        }
782        else if (attributeInfo.get("defaultEnumerator") != null)
783        {
784            _addDefaultEnumerator(attributeDefinition, existingModelItem, attributeInfo, name, pluginName, contentTypeId);
785        }
786    }
787
788    private void _addCustomEnumerator(AttributeDefinition<? extends Object> attributeDefinition, Map<String, Object> attributeInfo, String enumeratorName)
789    {
790        attributeDefinition.setCustomEnumerator(enumeratorName);
791
792        String customEnumeratorConfiguration = _getString(attributeInfo.get("customEnumeratorConfiguration"));
793        if (customEnumeratorConfiguration != null)
794        {
795            DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();
796            try
797            {
798                Configuration configuration = builder.build(IOUtils.toInputStream(customEnumeratorConfiguration, Charset.forName("UTF-8")));
799                attributeDefinition.setEnumeratorConfiguration(configuration);
800            }
801            catch (ConfigurationException | SAXException | IOException e)
802            {
803                getLogger().error("The XML configuration of custom enumerator is not valid", e);
804            }
805        }
806    }
807
808    @SuppressWarnings("unchecked")
809    private void _addDefaultEnumerator(AttributeDefinition attributeDefinition, ModelItem existingModelItem, Map<String, Object> attributeInfo, String name, String pluginName, String contentTypeId)
810    {
811        Object recoverEnumerator = attributeInfo.get("defaultEnumerator");
812        if (recoverEnumerator instanceof List)
813        {
814            StaticEnumerator staticEnumerator = new StaticEnumerator<>();
815            
816            List enumerator = (List) recoverEnumerator;
817            for (Object recoverValue : enumerator)
818            {
819                if (recoverValue != null && recoverValue instanceof Map)
820                {
821                    Map value = (Map) recoverValue;
822                    Object recoverEnumLabel = value.get("label");
823                    if (recoverEnumLabel != null && recoverEnumLabel instanceof String)
824                    {
825                        Enumerator existingEnumerator = ((AttributeDefinition) existingModelItem).getEnumerator();
826                        I18nizableText existingEnumValue = null;
827                        if (existingEnumerator != null)
828                        {
829                            try
830                            {
831                                existingEnumValue = existingEnumerator.getEntry(recoverEnumLabel);
832                            }
833                            catch (Exception e)
834                            {
835                                e.printStackTrace();
836                            }
837                        }
838                        String enumLabel = (String) recoverEnumLabel;
839                        Object recoveredEnumValue = value.get("value");
840                        I18nizableText enumValue = _getI18nizableText(existingEnumValue, recoveredEnumValue, pluginName, "contenttype", contentTypeId,
841                                "metadata_" + name + "_" + enumLabel + "_enumeration");
842                        staticEnumerator.add(enumValue, enumLabel);
843                    }
844                }
845            }
846            attributeDefinition.setEnumerator(staticEnumerator);
847        }
848    }
849
850    private void _addValidator(AttributeDefinition<? extends Object> attributeDefinition, ModelItem existingModelItem, Map<String, Object> attributeInfo, String name, String pluginName, String contentTypeId)
851    {
852        String validatorName = _getString(attributeInfo.get("customValidatorClass"));
853        if (StringUtils.isNotBlank(validatorName))
854        {
855            _addCustomValidator(attributeDefinition, attributeInfo, validatorName);
856        }
857        else if (attributeInfo.get("defaultValidator") != null)
858        {
859            _addDefaultValidator(attributeDefinition, existingModelItem, attributeInfo, name, pluginName, contentTypeId);
860        }
861    }
862
863    private void _addCustomValidator(AttributeDefinition<? extends Object> attributeDefinition, Map<String, Object> attributeInfo, String validatorName)
864    {
865        attributeDefinition.setCustomValidator(validatorName);
866
867        String customValidatorConfiguration = _getString(attributeInfo.get("customValidatorConfiguration"));
868        if (customValidatorConfiguration != null)
869        {
870            DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();
871            try
872            {
873                Configuration configuration = builder.build(IOUtils.toInputStream(customValidatorConfiguration, Charset.forName("UTF-8")));
874                attributeDefinition.setValidatorConfiguration(configuration);
875            }
876            catch (ConfigurationException | SAXException | IOException e)
877            {
878                getLogger().error("The XML configuration of custom validator is not valid", e);
879            }
880        }
881    }
882
883    private void _addDefaultValidator(AttributeDefinition<? extends Object> attributeDefinition, ModelItem existingModelItem, Map<String, Object> attributeInfo, String name, String pluginName, String contentTypeId)
884    {
885        Object recoverValidator = attributeInfo.get("defaultValidator");
886        if (recoverValidator instanceof Map)
887        {
888            Map validator = (Map) recoverValidator;
889            boolean isMandatory = _getBoolean(validator.get("mandatory"));
890            String regexp = _getString(validator.get("regexp"));
891
892            I18nizableText invalidText = null;
893            I18nizableText existingInvalidText = null;
894            Object recoverInvalidText = validator.get("invalidText");
895            if (recoverInvalidText != null && recoverInvalidText instanceof Map)
896            {
897                if (existingModelItem != null && existingModelItem instanceof AttributeDefinition)
898                {
899                    Validator existingValidator = ((AttributeDefinition) existingModelItem).getValidator();
900                    if (existingValidator != null)
901                    {
902                        Map<String, Object> existingConfiguration = existingValidator.getConfiguration();
903                        if (existingConfiguration.get("invalidText") != null)
904                        {
905                            existingInvalidText = (I18nizableText) existingConfiguration.get("invalidText");
906                        }
907                    }
908                }
909                invalidText = _getI18nizableText(existingInvalidText, recoverInvalidText, pluginName, "contenttype", contentTypeId, "metadata_" + name + "validator_invalid_text");
910            }
911            DefaultValidator defaultValidator = new DefaultValidator(regexp, invalidText, isMandatory);
912            attributeDefinition.setValidator(defaultValidator);
913        }
914    }
915    
916    private void _addSpecificInfoForContentAttribute(ContentAttributeDefinition attributeDefinition, Map<String, Object> attributeInfo)
917    {
918        Object recoverLinkedContentType = attributeInfo.get("linkedContentType");
919        if (recoverLinkedContentType != null && recoverLinkedContentType instanceof Map)
920        {
921            Map linkedContentType = (Map) recoverLinkedContentType;
922            Object recoverId = linkedContentType.get("id");
923            if (recoverId != null && recoverId instanceof String)
924            {
925                attributeDefinition.setContentTypeId((String) recoverId);
926                
927                String invert = _getString(attributeInfo.get("invertRelationPath"));
928                attributeDefinition.setInvertRelationPath(invert);
929            }
930        }
931    }
932
933    private void _addModelItemGeneralInformation(ModelItem modelItem, ModelItem existingModelItem, Map<String, Object> modelItemInfo, String name, String pluginName, String contentTypeId, String typeId)
934    {
935        if (modelItem != null)
936        {
937            modelItem.setName(name);
938
939            I18nizableText label = _getI18nizableText(existingModelItem != null ? existingModelItem.getLabel() : null, modelItemInfo.get("label"), pluginName,
940                    "contenttype", contentTypeId, "metadata_" + name + "_label");
941            modelItem.setLabel(label);
942
943            I18nizableText description = _getI18nizableText(existingModelItem != null ? existingModelItem.getDescription() : null, modelItemInfo.get("description"),
944                    pluginName, "contenttype", contentTypeId, "metadata_" + name + "_description");
945            modelItem.setDescription(description);
946            modelItem.setType(_contentAttributeTypeExtensionPoint.getExtension(typeId));
947            
948            if (existingModelItem != null)
949            {
950                modelItem.setModel(existingModelItem.getModel());
951            }
952            
953            if (modelItem instanceof ElementDefinition)
954            {
955                ((ElementDefinition) modelItem).setPluginName(pluginName);
956            }
957        }
958    }
959
960    private void _addChildren(ModelItemGroup modelItemGroup, ModelItem existingModelItem, Map<String, Object> modelItemInfo, String pluginName, String contentTypeId)
961    {
962        Object recoverChildren = modelItemInfo.get("children");
963        if (recoverChildren != null && recoverChildren instanceof List)
964        {
965            for (Object recoverChild : (List) recoverChildren)
966            {
967                if (recoverChild != null && recoverChild instanceof Map)
968                {
969                    @SuppressWarnings("unchecked")
970                    Map<String, Object> child = (Map<String, Object>) recoverChild;
971                    
972                    Object recoverName = child.get("name");
973                    ModelItem existingChildModelItem = null;
974                    
975                    if (existingModelItem != null && existingModelItem instanceof ModelItemContainer
976                            && recoverName != null && recoverName instanceof String
977                            && ((ModelItemContainer) existingModelItem).hasModelItem((String) recoverName))
978                    {
979                        existingChildModelItem = ((ModelItemContainer) existingModelItem).getModelItem((String) recoverName);
980                    }
981                    
982                    ModelItem childModelItem = _getModelItem(child, existingChildModelItem, pluginName, contentTypeId);
983                    modelItemGroup.addChild(childModelItem);
984                }
985            }
986        }
987    }
988
989    private void _addViews(ContentTypeDefinition contentTypeDefinition, ContentType existingContentType, String realContentTypeId, Map<String, Object> contentTypeInfos, String pluginName,
990            String contentTypeId)
991    {
992        Object recoverViews = contentTypeInfos.get("views");
993        if (recoverViews != null && recoverViews instanceof List)
994        {
995            List<View> views = new ArrayList<>();
996            
997            _fieldsetNumber = 1;
998            for (Object recoverView : (List) recoverViews)
999            {
1000                if (recoverView != null && recoverView instanceof Map)
1001                {
1002                    @SuppressWarnings("unchecked")
1003                    Map<String, Object> recoverViewInfo = (Map<String, Object>) recoverView;
1004                    Object recoverViewName = recoverViewInfo.get("name");
1005                    View newView = _getView(contentTypeDefinition, recoverViewInfo, (String) recoverViewName, pluginName, contentTypeId);
1006                    if (_contentTypeStateComponent.getContentTypeMarkedAsNew().contains(realContentTypeId))
1007                    {
1008                        views.add(newView);
1009                    }
1010                    else if (existingContentType != null && recoverViewName != null && recoverViewName instanceof String)
1011                    {
1012                        View oldView = existingContentType.getView((String) recoverViewName);
1013                        List<String> overriddenViews = existingContentType.getOverriddenViews();
1014                        if (!oldView.equals(newView, true) || overriddenViews.contains(oldView.getName()))
1015                        {
1016                            views.add(newView);
1017                        }
1018                    }
1019                }
1020            }
1021            contentTypeDefinition.setViews(views);
1022        }
1023    }
1024
1025    private View _getView(ContentTypeDefinition contentTypeDefinition, Map<String, Object> viewInfo, String viewName, String pluginName, String contentTypeId)
1026    {
1027        String dataType = _getString(viewInfo.get("dataType"));
1028        if (StringUtils.isNotBlank(dataType) && dataType.equals("metadata_set"))
1029        {
1030            View view = new View();
1031            view.setName(viewName);
1032
1033            ContentType existingContentType = _contentTypeExtensionPoint.getExtension(contentTypeDefinition.getId());
1034            View existingView = null;
1035            if (existingContentType != null)
1036            {
1037                existingView = existingContentType.getView(viewName);
1038            }
1039
1040            Object recoverLabel = viewInfo.get("label");
1041            I18nizableText label = _getI18nizableText(existingView != null ? existingView.getLabel() : null, recoverLabel, pluginName, "contenttype", contentTypeId,
1042                    "metadatasets_" + viewName + "_view_label");
1043            view.setLabel(label);
1044
1045            Object recoverDescription = viewInfo.get("description");
1046            I18nizableText description = _getI18nizableText(existingView != null ? existingView.getDescription() : null, recoverDescription, pluginName, "contenttype", contentTypeId,
1047                    "metadatasets_" + viewName + "_view_description");
1048            view.setDescription(description);
1049            
1050            Object recoverIconGlyph = viewInfo.get("iconGlyph");
1051            view.setIconGlyph((String) recoverIconGlyph);
1052            
1053            Object recoverIconDecarator = viewInfo.get("iconDecorator");
1054            view.setIconDecorator((String) recoverIconDecarator);
1055            
1056            if (viewInfo.containsKey("isInternal"))
1057            {
1058                Object recoverIsInternal = viewInfo.get("isInternal");
1059                view.setInternal((boolean) recoverIsInternal);
1060            }
1061            
1062            _addChildrenToViewItemContainer(view, contentTypeDefinition, viewInfo, pluginName, contentTypeId);
1063            
1064            return view;
1065        }
1066        else
1067        {
1068            return null;
1069        }
1070    }
1071    
1072    private void _addChildrenToViewItemContainer(ViewItemContainer viewItemContainer, ContentTypeDefinition contentTypeDefinition, Map<String, Object> viewItemContainerInfo, String pluginName, String contentTypeId)
1073    {
1074        Object recoverChildren = viewItemContainerInfo.get("children");
1075        if (recoverChildren != null && recoverChildren instanceof List)
1076        {
1077            for (Object recoverChild : (List) recoverChildren)
1078            {
1079                if (recoverChild != null && recoverChild instanceof Map)
1080                {
1081                    @SuppressWarnings("unchecked")
1082                    Map<String, Object> childInfo = (Map<String, Object>) recoverChild;
1083                    Object recoverChildViewItemName = childInfo.get("name");
1084                    Object recoverChildViewItemPath = childInfo.get("path");
1085                    if (recoverChildViewItemName != null && recoverChildViewItemName instanceof String)
1086                    {
1087                        ViewItem viewItem = _getViewItem(contentTypeDefinition, childInfo, (String) recoverChildViewItemName, (String) recoverChildViewItemPath, pluginName, contentTypeId);
1088                        viewItemContainer.addViewItem(viewItem);
1089                    }
1090                }
1091            }
1092        }
1093    }
1094
1095    private ViewItem _getViewItem(ContentTypeDefinition contentTypeDefinition, Map<String, Object> viewItemInfo, String viewItemName, String viewItemPath, String pluginName, String contentTypeId)
1096    {
1097        ViewItem viewItem = null;
1098
1099        String dataType = _getString(viewItemInfo.get("dataType"));
1100        if (StringUtils.isNotBlank(dataType))
1101        {
1102            if (dataType.equals("fieldset"))
1103            {
1104                viewItem = _getSimpleViewItemGroup(contentTypeDefinition, viewItemInfo, viewItemName, pluginName, contentTypeId);
1105            }
1106            else if (dataType.equals("metadata_ref"))
1107            {
1108                ModelItem modelItem = _getModelItemFromContentTypeDefinition(contentTypeDefinition.getModelItems(), viewItemPath);
1109                if (modelItem == null)
1110                {
1111                    ContentType existingContentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
1112                    modelItem = _getModelItemFromContentTypeDefinition(existingContentType.getModelItems(), viewItemPath);
1113                }
1114                if (modelItem != null)
1115                {
1116                    if (modelItem instanceof ElementDefinition)
1117                    {
1118                        viewItem = new ViewElement();
1119                        ((ViewElement) viewItem).setDefinition((ElementDefinition) modelItem);
1120                    }
1121                    else if (modelItem instanceof ModelItemGroup)
1122                    {
1123                        viewItem = new ModelViewItemGroup();
1124                        ((ModelViewItemGroup) viewItem).setDefinition((ModelItemGroup) modelItem);
1125                        _addChildrenToViewItemContainer((ModelViewItemGroup) viewItem, contentTypeDefinition, viewItemInfo, pluginName, contentTypeId);
1126                    }
1127                }
1128            }
1129
1130        }
1131        return viewItem;
1132    }
1133
1134    private SimpleViewItemGroup _getSimpleViewItemGroup(ContentTypeDefinition contentTypeDefinition, Map<String, Object> viewItemInfo, String viewItemName, String pluginName, String contentTypeId)
1135    {
1136        SimpleViewItemGroup fieldSet = new SimpleViewItemGroup();
1137
1138        Object recoverLabel = viewItemInfo.get("label");
1139        I18nizableText label = _getI18nizableText(null, recoverLabel, pluginName, "contenttype", contentTypeId,
1140                "metadatasets_" + viewItemName + "_view_fieldset_" + _fieldsetNumber + "_label");
1141        fieldSet.setLabel(label);
1142
1143        _addChildrenToViewItemContainer(fieldSet, contentTypeDefinition, viewItemInfo, pluginName, contentTypeId);
1144
1145        return fieldSet;
1146    }
1147    
1148    private ModelItem _getModelItemFromContentTypeDefinition(Collection<? extends ModelItem> modelItems, String modelItemName)
1149    {
1150        for (ModelItem modelItem : modelItems)
1151        {
1152            if (modelItemName.equals(modelItem.getPath()))
1153            {
1154                return modelItem;
1155            }
1156            else if (modelItem instanceof ModelItemContainer)
1157            {
1158                ModelItem child = _getModelItemFromContentTypeDefinition(((ModelItemContainer) modelItem).getModelItems(), modelItemName);
1159                if (child != null)
1160                {
1161                    return child;
1162                }
1163            }
1164        }
1165        
1166        return null;
1167    }
1168
1169    private boolean _getBoolean(Object recoverValue)
1170    {
1171        return recoverValue instanceof Boolean && (boolean) recoverValue;
1172    }
1173
1174    private String _getString(Object recoverValue)
1175    {
1176        String value = null;
1177        if (recoverValue != null && recoverValue instanceof String)
1178        {
1179            value = (String) recoverValue;
1180        }
1181        return value;
1182    }
1183
1184    private int _getInteger(Object recoverValue)
1185    {
1186        int value = 0;
1187        if (recoverValue != null && recoverValue instanceof Integer)
1188        {
1189            value = (int) recoverValue;
1190        }
1191        return value;
1192    }
1193
1194    private I18nizableText _getI18nizableText(I18nizableText i18nizableText, Object recoverValue, String pluginName, String type, String contentTypeId, String label)
1195    {
1196        I18nizableText text = new I18nizableText("");
1197        if (recoverValue != null && recoverValue instanceof LinkedHashMap)
1198        {
1199            String key = _generateI18nKey(pluginName, type, contentTypeId, label);
1200            text = _getI18nizableText(i18nizableText, recoverValue, key);
1201        }
1202        return text;
1203    }
1204
1205    private I18nizableText _getI18nizableText(I18nizableText existingI18nizableText, Object recoverValue, String key)
1206    {
1207        I18nizableText text = new I18nizableText("");
1208
1209        if (recoverValue != null && recoverValue instanceof Map)
1210        {
1211            Map value = (Map) recoverValue;
1212            Object isMultilingual = value.get("isMultilingual");
1213            if (isMultilingual != null && isMultilingual instanceof Boolean)
1214            {
1215                boolean isI18n = (Boolean) isMultilingual;
1216                if (isI18n)
1217                {
1218                    Object recoveredI18nValues = value.get("values");
1219                    if (recoveredI18nValues != null && recoveredI18nValues instanceof Map)
1220                    {
1221                        @SuppressWarnings("unchecked")
1222                        Map<String, String> i18nValues = (Map<String, String>) recoveredI18nValues;
1223                        @SuppressWarnings("unchecked")
1224                        Map<String, Object> newLabel = (Map<String, Object>) recoverValue;
1225                        if (!i18nValues.isEmpty() && existingI18nizableText != null)
1226                        {
1227                            // There is an existing key for the data
1228                            if (existingI18nizableText.isI18n() &&  _compareValuesAnd18nizableText(newLabel, existingI18nizableText) == 0)
1229                            {
1230                                text = existingI18nizableText;
1231                            }
1232                            else
1233                            {
1234                                text = new I18nizableText(__DEFAULT_CATALOGUE, key);
1235                                // Add key and values to the TranslatedValue object
1236                                TranslatedValue translatedValue = new TranslatedValue(key, i18nValues);
1237                                _translatedValues.add(translatedValue);
1238                            } 
1239                        }
1240                        else if (i18nValues.isEmpty())
1241                        {
1242                            text = existingI18nizableText;
1243                        }
1244                    }
1245                }
1246                else
1247                {
1248                    Object recoverLabel = value.get("values");
1249                    if (recoverLabel instanceof String)
1250                    {
1251                        String label = (String) recoverLabel;
1252                        if (StringUtils.isNotBlank(label))
1253                        {
1254                            text = new I18nizableText(label);
1255                        }
1256                    }
1257                }
1258            }
1259        }
1260        return text;
1261    }
1262
1263    private String _generateContentTypeIdKey(String contentTypeId)
1264    {
1265        String key = contentTypeId.replaceFirst("content-type.", "");
1266        key = key.replaceAll("\\s", "_");
1267        key = key.replaceAll("[^\\p{L}&&[^0-9]]", "_");
1268        return key;
1269    }
1270
1271    @SuppressWarnings("unchecked")
1272    private String _generateCategoryI18nKey(String pluginName, Map<String, Object> recoveredCategory)
1273    {
1274        String key = "";
1275        Map<String, String> values = (Map<String, String>) recoveredCategory.get("values");
1276        if (values != null && !values.isEmpty())
1277        {
1278            String categoryLabel = "";
1279            if (values.containsKey("en"))
1280            {
1281                categoryLabel = values.get("en");
1282
1283            }
1284            else
1285            {
1286                List<String> valuesAsList = new ArrayList(values.values());
1287                categoryLabel = valuesAsList.get(0);
1288            }
1289            key = "plugins_" + pluginName + "_" + "content_createcontentmenu_group" + "_" + _generateContentTypeIdKey(categoryLabel);
1290            key = key.toUpperCase();
1291        }
1292        return key;
1293    }
1294
1295    private String _generateI18nKey(String pluginName, String type, String contentTypeId, String label)
1296    {
1297        String id = _generateContentTypeIdKey(contentTypeId);
1298        String key = "plugins_" + pluginName + "_" + type + "_" + id + "_" + label;
1299        key = key.toUpperCase();
1300        return key;
1301    }
1302
1303    private Map<String, Map<String, String>> _createNewI18nCatalogs()
1304    {
1305        Map<String, Map<String, String>> i18nMessageTranslations = new HashMap<>();
1306
1307        for (TranslatedValue translatedValue : _translatedValues)
1308        {
1309            if (translatedValue.getTranslations() != null)
1310            {
1311                for (Entry<String, String> translation : translatedValue.getTranslations().entrySet())
1312                {
1313                    String i18nKey = translatedValue.getKey();
1314                    if (StringUtils.isNotBlank(i18nKey) && StringUtils.isNotBlank(translation.getValue()))
1315                    {
1316                        String language = translation.getKey();
1317                        if (i18nMessageTranslations.containsKey(language))
1318                        {
1319                            Map<String, String> map = i18nMessageTranslations.get(language);
1320                            map.put(i18nKey, translation.getValue());
1321                        }
1322                        else
1323                        {
1324                            Map<String, String> map = new HashMap<>();
1325                            map.put(i18nKey, translation.getValue());
1326                            i18nMessageTranslations.put(language, map);
1327                        }
1328                    }
1329                }
1330            }
1331        }
1332        return i18nMessageTranslations;
1333    }
1334
1335    private void _saxCatalogs(Map<String, Map<String, String>> newI18nMessages) throws Exception
1336    {
1337        // Determine the default catalog language
1338        String defaultLanguage = "";
1339        String defaultI18nCatalogPath = __I18N_CATALOG_DIR + "application.xml";
1340        Source defaultI18nCatalogSource = null;
1341        try
1342        {
1343            defaultI18nCatalogSource = _sourceResolver.resolveURI(defaultI18nCatalogPath);
1344            if (defaultI18nCatalogSource.exists())
1345            {
1346                // Parse default catalog application to know the default
1347                // language
1348                Configuration configuration = new DefaultConfigurationBuilder(true).build(defaultI18nCatalogSource.getInputStream(), "");
1349                defaultLanguage = configuration.getAttribute("xml:lang");
1350            }
1351        }
1352        catch (SourceNotFoundException e)
1353        {
1354            // Silently ignore
1355        }
1356        finally
1357        {
1358            _sourceResolver.release(defaultI18nCatalogSource);
1359        }
1360
1361        for (Entry<String, Map<String, String>> i18nMessageTranslation : newI18nMessages.entrySet())
1362        {
1363            Map<String, String> i18nMessages = new HashMap<>();
1364            String language = i18nMessageTranslation.getKey();
1365            String catalogPath;
1366            if (language.equals(defaultLanguage))
1367            {
1368                catalogPath = __I18N_CATALOG_DIR + "application.xml";
1369                i18nMessages = _readI18nCatalog(catalogPath);
1370            }
1371            else
1372            {
1373                catalogPath = __I18N_CATALOG_DIR + "application_" + language + ".xml";
1374                i18nMessages = _readI18nCatalog(catalogPath);
1375            }
1376            Map<String, String> updatedI18nMessages = _updateI18nMessages(i18nMessages, i18nMessageTranslation.getValue());
1377            _saveI18nCalatog(updatedI18nMessages, catalogPath, language);
1378        }
1379    }
1380
1381    private Map<String, String> _readI18nCatalog(String path) throws Exception
1382    {
1383        Map<String, String> i18nMessages = new LinkedHashMap<>();
1384        Source i18nCatalogSource = null;
1385
1386        try
1387        {
1388            i18nCatalogSource = _sourceResolver.resolveURI(path);
1389            if (i18nCatalogSource.exists())
1390            {
1391                if (_i18nCatalogs.containsKey(path))
1392                {
1393                    I18nCatalog i18nCatalog = _i18nCatalogs.get(path);
1394                    if (i18nCatalog.getLastModified() < i18nCatalogSource.getLastModified())
1395                    {
1396                        SAXParserFactory.newInstance().newSAXParser().parse(i18nCatalogSource.getInputStream(), new I18nMessageHandler(i18nMessages));
1397                        i18nCatalog.setI18nMessages(i18nMessages);
1398                    }
1399                    else
1400                    {
1401                        i18nMessages = i18nCatalog.getI18nMessages();
1402                    }
1403                }
1404                else
1405                {
1406                    SAXParserFactory.newInstance().newSAXParser().parse(i18nCatalogSource.getInputStream(), new I18nMessageHandler(i18nMessages));
1407                    I18nCatalog i18nCatalog = new I18nCatalog(i18nMessages);
1408                    _i18nCatalogs.put(path, i18nCatalog);
1409                }
1410            }
1411        }
1412        catch (SourceNotFoundException e)
1413        {
1414            // Silently ignore
1415        }
1416        finally
1417        {
1418            _sourceResolver.release(i18nCatalogSource);
1419        }
1420
1421        return i18nMessages;
1422    }
1423
1424    private Map<String, String> _updateI18nMessages(Map<String, String> i18nMessages, Map<String, String> newI18nMessages)
1425    {
1426        for (Entry<String, String> newI18nMessage : newI18nMessages.entrySet())
1427        {
1428            String key = newI18nMessage.getKey();
1429            String value = newI18nMessage.getValue();
1430            if (i18nMessages.containsKey(key))
1431            {
1432                i18nMessages.replace(key, value);
1433            }
1434            else
1435            {
1436                i18nMessages.put(key, value);
1437            }
1438        }
1439        return i18nMessages;
1440    }
1441
1442    private void _saveI18nCalatog(Map<String, String> i18nMessages, String catalogPath, String language) throws Exception
1443    {
1444        ModifiableSource defaultI18nCatalogSource = null;
1445        try
1446        {
1447            defaultI18nCatalogSource = (ModifiableSource) _sourceResolver.resolveURI(catalogPath);
1448            try (OutputStream os = defaultI18nCatalogSource.getOutputStream())
1449            {
1450                // create a transformer for saving sax into a file
1451                TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
1452
1453                StreamResult result = new StreamResult(os);
1454                th.setResult(result);
1455
1456                // create the format of result
1457                Properties format = new Properties();
1458                format.put(OutputKeys.METHOD, "xml");
1459                format.put(OutputKeys.INDENT, "yes");
1460                format.put(OutputKeys.ENCODING, "UTF-8");
1461                format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
1462                th.getTransformer().setOutputProperties(format);
1463
1464                // sax the config into the transformer
1465                _saxI18nCatalog(th, i18nMessages, language);
1466
1467            }
1468            catch (Exception e)
1469            {
1470                throw new Exception("An error occured while saving the catalog values.", e);
1471            }
1472            _i18nCatalogs.get(catalogPath).setLastModified(defaultI18nCatalogSource.getLastModified());
1473        }
1474        catch (SourceNotFoundException e)
1475        {
1476            // Silently ignore
1477        }
1478        finally
1479        {
1480            _sourceResolver.release(defaultI18nCatalogSource);
1481        }
1482
1483    }
1484
1485    private void _saxI18nCatalog(TransformerHandler handler, Map<String, String> i18nMessages, String language) throws SAXException
1486    {
1487        handler.startDocument();
1488        AttributesImpl attribute = new AttributesImpl();
1489        attribute.addCDATAAttribute("xml:lang", language);
1490        XMLUtils.startElement(handler, "catalogue", attribute);
1491        for (Entry<String, String> i18Message : i18nMessages.entrySet())
1492        {
1493            attribute = new AttributesImpl();
1494            attribute.addCDATAAttribute("key", i18Message.getKey());
1495            XMLUtils.createElement(handler, "message", attribute, i18Message.getValue());
1496        }
1497        XMLUtils.endElement(handler, "catalogue");
1498        handler.endDocument();
1499    }
1500
1501    private void _saxRightsParam(String id, String label, String description) throws MalformedURLException, IOException
1502    {
1503        // For sax right params, a pipeline is used because it's more simple to copy a part of the existing right file to add or modify a right
1504        String uri = __SAVE_CONTENT_TYPE_RIGHT + "?id=" + id + "&label=" + label + "&description=" + description + "&category=" + __RIGHT_CATEGORY + "&file-path=" + __RIGHTS_FILE;
1505        Source source = null;
1506        ModifiableSource fileSource = null;
1507
1508        try
1509        {
1510            source = _sourceResolver.resolveURI(uri);
1511            fileSource = (ModifiableSource) _sourceResolver.resolveURI(__RIGHTS_FILE);
1512            try (OutputStream resultOs = fileSource.getOutputStream(); InputStream sourceIs = source.getInputStream())
1513            {
1514                SourceUtil.copy(sourceIs, resultOs);
1515            }
1516        }
1517        finally
1518        {
1519            _sourceResolver.release(source);
1520            _sourceResolver.release(fileSource);
1521        }
1522    }
1523}