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 */
016package org.ametys.plugins.contenttypeseditor.edition;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.Iterator;
023import java.util.LinkedHashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.commons.lang.StringUtils;
035import org.apache.commons.lang.WordUtils;
036
037import org.ametys.cms.contenttype.AutomaticContentType;
038import org.ametys.cms.contenttype.ContentAttributeDefinition;
039import org.ametys.cms.contenttype.ContentType;
040import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
041import org.ametys.cms.contenttype.ContentTypesHelper;
042import org.ametys.cms.data.type.ModelItemTypeConstants;
043import org.ametys.cms.repository.ContentAttributeTypeExtensionPoint;
044import org.ametys.core.right.RightsExtensionPoint;
045import org.ametys.core.ui.Callable;
046import org.ametys.core.ui.widgets.WidgetsManager;
047import org.ametys.core.util.I18nUtils;
048import org.ametys.plugins.contenttypeseditor.ContentTypeInformationsHelper;
049import org.ametys.plugins.contenttypeseditor.ContentTypeInformationsHelper.ContentTypeAttributeDataType;
050import org.ametys.plugins.contenttypeseditor.edition.ContentTypeStateComponent.ContentTypeState;
051import org.ametys.plugins.repository.AmetysObject;
052import org.ametys.plugins.repository.AmetysObjectIterable;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.repository.model.CompositeDefinition;
055import org.ametys.plugins.repository.model.RepeaterDefinition;
056import org.ametys.runtime.i18n.I18nizableText;
057import org.ametys.runtime.model.ModelItem;
058import org.ametys.runtime.model.ModelItemGroup;
059import org.ametys.runtime.plugin.PluginsManager;
060import org.ametys.runtime.plugin.component.AbstractLogEnabled;
061
062/**
063 * This component propose method for helping to create and edit a content type
064 */
065public class EditContentTypeInformationHelper extends AbstractLogEnabled implements Component, Serviceable
066{
067    /** The Avalon role name */
068    public static final String ROLE = EditContentTypeInformationHelper.class.getName();
069
070    /** The content type helper */
071    protected ContentTypesHelper _contentTypesHelper;
072
073    /** The content type extension point instance */
074    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
075
076    /** The extension point for available attribute types */
077    protected ContentAttributeTypeExtensionPoint _contentAttributeTypeExtensionPoint;
078
079    /** The content type extension point instance */
080    protected ContentTypeInformationsHelper _contentTypeInformationHelper;
081
082    /** The content type state component instance */
083    protected ContentTypeStateComponent _contentTypeStateComponent;
084
085    /** The right extension point instance */
086    protected RightsExtensionPoint _rightsExtensionPoint;
087
088    /** The i18nUtils instance */
089    protected I18nUtils _i18nUtils;
090    
091    /** The ametys object resolver instance */
092    protected AmetysObjectResolver _ametysObjectResolver;
093    
094    /** The widgets manager */
095    protected WidgetsManager _widgetsManager;
096
097    private Collection<Map<String, Object>> _newCategories;
098
099    @Override
100    public void service(ServiceManager manager) throws ServiceException
101    {
102        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
103        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
104        _contentAttributeTypeExtensionPoint = (ContentAttributeTypeExtensionPoint) manager.lookup(ContentAttributeTypeExtensionPoint.ROLE);
105        _contentTypeInformationHelper = (ContentTypeInformationsHelper) manager.lookup(ContentTypeInformationsHelper.ROLE);
106        _contentTypeStateComponent = (ContentTypeStateComponent) manager.lookup(ContentTypeStateComponent.ROLE);
107        _rightsExtensionPoint = (RightsExtensionPoint) manager.lookup(RightsExtensionPoint.ROLE);
108        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
109        _newCategories = new HashSet<>();
110        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
111        _widgetsManager = (WidgetsManager) manager.lookup(WidgetsManager.ROLE);
112    }
113
114    /**
115     * Determine if content types are compatible
116     * 
117     * @param contentTypeIds Ids of content types
118     * @return True if content types are compatible
119     */
120    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
121    public boolean areContentTypesCompatible(List<String> contentTypeIds)
122    {
123        try
124        {
125            _contentTypesHelper.getModelItems(contentTypeIds.toArray(new String[contentTypeIds.size()]));
126            return true;
127        }
128        catch (ConfigurationException e)
129        {
130            getLogger().error("Content types are not compatibles", e);
131            return false;
132        }
133    }
134
135    /**
136     * Get names of active plugins
137     * 
138     * @return Names of active plugins
139     */
140    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
141    public Collection<Map<String, String>> getPluginNames()
142    {
143        return PluginsManager.getInstance().getPluginNames().stream().map(name -> _toMap(name)).collect(Collectors.toList());
144    }
145
146    private Map<String, String> _toMap(String name)
147    {
148        Map<String, String> map = new HashMap<>();
149        map.put("id", name);
150        map.put("label", name);
151        return map;
152    }
153
154    /**
155     * Retrieve content type informations
156     * 
157     * @param superTypesIds Ids of super content types
158     * @return Content type informations : attributes of super content types and
159     *         default views
160     */
161    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
162    public Map<String, Object> getContentTypeInfos(List<Map<String, String>> superTypesIds)
163    {
164        Map<String, Object> contentTypeInfos = new HashMap<>();
165
166        List<Map<String, Object>> attributes = new ArrayList<>();
167        // Adding attributes of superType
168        if (superTypesIds != null && !superTypesIds.isEmpty())
169        {
170            for (Map<String, String> superTypeId : superTypesIds)
171            {
172                ContentType cType = _contentTypeExtensionPoint.getExtension(superTypeId.get("id"));
173                attributes.addAll(_contentTypeInformationHelper.getModelItemsInformation(cType, true, false));
174            }
175            contentTypeInfos.put("attributes", attributes);
176        }
177        // Adding default views
178        List<Map<String, Object>> views = new ArrayList<>();
179        views.add(getView("main", "ametysicon-document112"));
180        views.add(getView("abstract", "ametysicon-document77"));
181        views.add(getView("link", "ametysicon-internet58"));
182        views.add(getView("details", "ametysicon-column3"));
183        views.add(getView("index", "ametysicon-column3"));
184        contentTypeInfos.put("views", views);
185
186        return contentTypeInfos;
187    }
188
189    /**
190     * Get all categories and categories created with the content type editor
191     * 
192     * @return All categories
193     */
194    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
195    public Collection<Map<String, Object>> getCategories()
196    {
197        Collection<Map<String, Object>> categories = new LinkedHashSet<>();
198        Set<I18nizableText> categoriesSet = new HashSet<>();
199        Set<String> contentTypeIds = _contentTypeExtensionPoint.getExtensionsIds();
200        for (String contentTypeId : contentTypeIds)
201        {
202            ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
203            I18nizableText category = contentType.getCategory();
204            if (category.isI18n() && category.getKey().isEmpty()
205                || !category.isI18n() && category.getLabel().isEmpty())
206            {
207                category = new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_CREATECONTENTMENU_GROUP_10_CONTENT");
208            }
209            categoriesSet.add(category);
210        }
211        for (I18nizableText category : categoriesSet)
212        {
213            @SuppressWarnings("unchecked")
214            Map<String, Object> enhancedCategory = (Map<String, Object>) _contentTypeInformationHelper.getEnhancedMultilingualString(category, false);
215            if (category.isI18n())
216            {
217                enhancedCategory.put("key", category.getKey());
218                enhancedCategory.put("catalogue", category.getCatalogue());
219            }
220            enhancedCategory.put("isNew", false);
221            categories.add(enhancedCategory);
222        }
223        // Course of new categories
224        for (Map<String, Object> newCategory : _newCategories)
225        {
226            categories.add(newCategory);
227        }
228        Map<String, Object> newCategory = new HashMap<>();
229        newCategory.put("isMultilingual", true);
230        I18nizableText newCategoryKey = new I18nizableText("plugin.contenttypes-editor",
231                "PLUGINS_CONTENTTYPESEDITOR_ADD_CONTENT_TYPE_DIALOG_CONTENT_TYPE_NEW_CATEGORY_INPUT_LABEL");
232        Map<String, String> categoryTranslation = new HashMap<>();
233        categoryTranslation.put("fr", _i18nUtils.translate(newCategoryKey, "fr"));
234        categoryTranslation.put("en", _i18nUtils.translate(newCategoryKey, "en"));
235        newCategory.put("values", categoryTranslation);
236        newCategory.put("isNew", true);
237        newCategory.put("key", newCategoryKey.getKey());
238        newCategory.put("catalogue", newCategoryKey.getCatalogue());
239        categories.add(newCategory);
240        return categories;
241    }
242
243    /**
244     * Retrieves attribute types
245     * @return Existing attribute types
246     */
247    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
248    public Collection<Map<String, String>> getAttributeType()
249    {
250        Collection<Map<String, String>> attributeTypes = new HashSet<>();
251
252        for (String typeId : _contentAttributeTypeExtensionPoint.getExtensionsIds())
253        {
254            Map<String, String> type = new HashMap<>();
255            
256            type.put("id", typeId);
257            type.put("label", WordUtils.capitalize(typeId));
258            
259            attributeTypes.add(type);
260        }
261
262        return attributeTypes;
263    }
264
265    /**
266     * Get model items paths of the given content type
267     * @param contentTypeId the content type's identifier
268     * @return The model items paths of the given content type
269     */
270    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
271    public Collection<Map<String, String>> getAttributePaths(String contentTypeId)
272    {
273        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
274        return _getModelItemsPaths(contentType.getModelItems());
275    }
276
277    private Collection<Map<String, String>> _getModelItemsPaths(Collection<? extends ModelItem> modelItems)
278    {
279        Collection<Map<String, String>> modelItemPaths = new HashSet<>();
280        
281        for (ModelItem modelItem : modelItems)
282        {
283            Map<String, String> modelItemPath = new HashMap<>();
284            modelItemPath.put("name", modelItem.getPath());
285            modelItemPaths.add(modelItemPath);
286            
287            if (modelItem instanceof ModelItemGroup)
288            {
289                modelItemPaths.addAll(_getModelItemsPaths(((ModelItemGroup) modelItem).getModelItems()));
290            }
291        }
292        
293        return modelItemPaths;
294    }
295    
296    /**
297     * Retrieves model item names of a content type
298     * @param contentTypeId The id of content type
299     * @return Model item names of content type
300     */
301    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
302    public Collection<Map<String, Object>> getAttributeNames(String contentTypeId)
303    {
304        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
305        return _getModelItemsNames(contentType.getModelItems());
306    }
307
308    private Collection<Map<String, Object>> _getModelItemsNames(Collection<? extends ModelItem> modelItems)
309    {
310        Collection<Map<String, Object>> modelItemNames = new HashSet<>();
311        
312        for (ModelItem modelItem : modelItems)
313        {
314            Map<String, Object> modelItemName = new HashMap<>();
315            
316            modelItemName.put("name", modelItem.getName());
317            modelItemName.put("path", modelItem.getPath());
318            modelItemName.put("isRepeaterOrComposite", modelItem instanceof RepeaterDefinition || modelItem instanceof CompositeDefinition);
319            
320            modelItemNames.add(modelItemName);
321            
322            if (modelItem instanceof ModelItemGroup)
323            {
324                modelItemNames.addAll(_getModelItemsNames(((ModelItemGroup) modelItem).getModelItems()));
325            }
326        }
327        
328        return modelItemNames;
329    }
330
331    /**
332     * Add a new category to list of new category
333     * 
334     * @param newCategory The new category
335     */
336    public void addNewCategory(Map<String, Object> newCategory)
337    {
338        if (newCategory != null && newCategory.size() > 0)
339        {
340            _newCategories.add(newCategory);
341        }
342    }
343
344    private Map<String, Object> getView(String name, String iconGlyph)
345    {
346        Map<String, Object> view = new HashMap<>();
347        view.put("dataType", ContentTypeAttributeDataType.METADATA_SET.name().toLowerCase());
348        view.put("name", name);
349        Map<String, Object> label = new HashMap<>();
350        label.put("isMultilingual", false);
351        label.put("values", name);
352        view.put("label", label);
353        view.put("isEdition", false);
354        view.put("iconGlyph", iconGlyph);
355        view.put("iconCls", iconGlyph);
356        view.put("leaf", true);
357        view.put("isInternal", false);
358        return view;
359    }
360
361    /**
362     * Check if a content type is editable
363     * 
364     * @param contentTypeId Id of content type
365     * @return True if the content type is editable
366     */
367    public boolean isEditableContentType(String contentTypeId)
368    {
369        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
370        return contentType instanceof AutomaticContentType;
371    }
372    
373    /**
374     * Get all attributes which can be parent reference
375     * 
376     * @param contentTypeId The content type id
377     * @param superTypesIds Supertype of content type
378     * @param recoverAttributeList Updated attributes of content type
379     * @return all attributes which can be parent reference
380     */
381    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
382    public Collection<Map<String, String>> getParentReferenceAttributes(String contentTypeId, List<String> superTypesIds, Object recoverAttributeList)
383    {
384        Collection<Map<String, String>> parentReferenceModelItems = new HashSet<>();
385        String stateContentType = _contentTypeStateComponent.getStateContentType(contentTypeId);
386
387        Set<String> newContentTypes = _contentTypeStateComponent.getContentTypeMarkedAsNew();
388
389        if (newContentTypes.contains(contentTypeId) || stateContentType.equals(ContentTypeState.EDIT.name().toLowerCase()))
390        {
391            List attributeList = (List) recoverAttributeList;
392            for (Object recoverAttribute : attributeList)
393            {
394                parentReferenceModelItems.addAll(_getParentReferenceRecoverModelItems(recoverAttribute));
395            }
396        }
397        else if (stateContentType.equals(ContentTypeState.RESTART.name().toLowerCase()))
398        {
399            ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
400            parentReferenceModelItems.addAll(_getParentReferenceModelItems(contentType.getModelItems()));
401        }
402        
403        if (superTypesIds != null && !superTypesIds.isEmpty())
404        {
405            for (String newContentTypeId : newContentTypes)
406            {
407                ContentType newContentType = _contentTypeExtensionPoint.getExtension(newContentTypeId);
408                if (newContentType != null)
409                {
410                    parentReferenceModelItems.addAll(_getParentReferenceModelItems(newContentType.getModelItems()));
411                }
412            }
413        }
414
415        return parentReferenceModelItems;
416    }
417    
418    private Collection<Map<String, String>> _getParentReferenceRecoverModelItems(Object recoverModelItem)
419    {
420        Collection<Map<String, String>> parentReferenceModelItems = new HashSet<>();
421        
422        if (recoverModelItem instanceof Map)
423        {
424            Map modelItemInfo = (Map) recoverModelItem;
425            
426            Object recoveredModelItemName = modelItemInfo.get("name");
427            if (recoveredModelItemName != null && recoveredModelItemName instanceof String)
428            {
429                String modelItemName = (String) recoveredModelItemName;
430                if (StringUtils.isNotBlank(modelItemName))
431                {
432                    Object recoveredModelItemDataTypeId = modelItemInfo.get("type");
433                    if (recoveredModelItemDataTypeId != null && recoveredModelItemDataTypeId instanceof String)
434                    {
435                        String dataTypeId = (String) recoveredModelItemDataTypeId;
436                        if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(dataTypeId))
437                        {
438                            Object recoverInvertRelationPath = modelItemInfo.get("invertRelationPath");
439                            if (recoverInvertRelationPath != null)
440                            {
441                                String invertRelationPath = (String) recoverInvertRelationPath;
442                                if (StringUtils.isNotBlank(invertRelationPath))
443                                {
444                                    Map<String, String> parentReferenceAttribute = new HashMap<>();
445                                    parentReferenceAttribute.put("name", modelItemName);
446                                    parentReferenceModelItems.add(parentReferenceAttribute);
447                                }
448                            }
449                        }
450                    }
451                    Object recoveredAttributeChildren = modelItemInfo.get("children");
452                    if (recoveredAttributeChildren != null && recoveredAttributeChildren instanceof List)
453                    {
454                        List attributeChildrens =  (List) recoveredAttributeChildren;
455                        for (Object recoverAttributeChildren : attributeChildrens)
456                        {
457                            parentReferenceModelItems.addAll(_getParentReferenceRecoverModelItems(recoverAttributeChildren));
458                        }
459                    }
460                }
461            }
462        }
463        
464        return parentReferenceModelItems;
465    }
466    
467    private Collection<Map<String, String>> _getParentReferenceModelItems(Collection<? extends ModelItem> modelItems)
468    {
469        Collection<Map<String, String>> parentReferenceModelItems = new HashSet<>();
470        
471        for (ModelItem modelItem : modelItems)
472        {
473            if (modelItem instanceof ContentAttributeDefinition && StringUtils.isNotBlank(((ContentAttributeDefinition) modelItem).getInvertRelationPath()))
474            {
475                Map<String, String> data = new HashMap<>();
476                data.put("name", modelItem.getName());
477                parentReferenceModelItems.add(data);
478            }
479            
480            if (modelItem instanceof ModelItemGroup)
481            {
482                parentReferenceModelItems.addAll(_getParentReferenceModelItems(((ModelItemGroup) modelItem).getModelItems()));
483            }
484        }
485        
486        return parentReferenceModelItems;
487    }
488    
489    /**
490     * Get invalid content if the attribute argument becomes mandatory
491     * 
492     * @param contentTypeId The id of content type
493     * @param mandatoryAttributeName The name of the attribute
494     * @return a list with invalid content names
495     */
496    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
497    public List<String> getInvalidContent(String contentTypeId, String mandatoryAttributeName)
498    {
499        List<String> invalidContents = new ArrayList<>();
500        AmetysObjectIterable<AmetysObject> query = _ametysObjectResolver.query(
501                "//element(*,ametys:content)[@ametys-internal:contentType='" + contentTypeId + "' and not(@ametys:" + mandatoryAttributeName + ")]");
502        Iterator<AmetysObject> it = query.iterator();
503        while (it.hasNext())
504        {
505            AmetysObject ametysObject = it.next();
506            String contentName = ametysObject.getName();
507            invalidContents.add(contentName);
508        }
509        return invalidContents;
510    }
511    
512    /**
513     * Get all default widget
514     * 
515     * @return All Default widget
516     */
517    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
518    public Collection<Map<String, String>> getWidgets()
519    {
520        Collection<Map<String, String>> widgets = new HashSet<>();
521        for (String id : _widgetsManager.getExtensionsIds())
522        {
523            Map<String, String> widgetInfo = new HashMap<>();
524            widgetInfo.put("id", id);
525            widgetInfo.put("label", id);
526            widgets.add(widgetInfo);
527        }
528        return widgets;
529    }
530
531    /**
532     * Get if a content type exists
533     * @param contentTypeId The id of content type
534     * @return True if the content type id exists
535     */
536    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
537    public boolean isExistingContentType(String contentTypeId)
538    {
539        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
540        return contentType != null ? true : false;
541    }
542}