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()) || (!category.isI18n() && category.getLabel().isEmpty()))
205            {
206                category = new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_CREATECONTENTMENU_GROUP_10_CONTENT");
207            }
208            categoriesSet.add(category);
209        }
210        for (I18nizableText category : categoriesSet)
211        {
212            @SuppressWarnings("unchecked")
213            Map<String, Object> enhancedCategory = (Map<String, Object>) _contentTypeInformationHelper.getEnhancedMultilingualString(category, false);
214            if (category.isI18n())
215            {
216                enhancedCategory.put("key", category.getKey());
217                enhancedCategory.put("catalogue", category.getCatalogue());
218            }
219            enhancedCategory.put("isNew", false);
220            categories.add(enhancedCategory);
221        }
222        // Course of new categories
223        for (Map<String, Object> newCategory : _newCategories)
224        {
225            categories.add(newCategory);
226        }
227        Map<String, Object> newCategory = new HashMap<>();
228        newCategory.put("isMultilingual", true);
229        I18nizableText newCategoryKey = new I18nizableText("plugin.contenttypes-editor",
230                "PLUGINS_CONTENTTYPESEDITOR_ADD_CONTENT_TYPE_DIALOG_CONTENT_TYPE_NEW_CATEGORY_INPUT_LABEL");
231        Map<String, String> categoryTranslation = new HashMap<>();
232        categoryTranslation.put("fr", _i18nUtils.translate(newCategoryKey, "fr"));
233        categoryTranslation.put("en", _i18nUtils.translate(newCategoryKey, "en"));
234        newCategory.put("values", categoryTranslation);
235        newCategory.put("isNew", true);
236        newCategory.put("key", newCategoryKey.getKey());
237        newCategory.put("catalogue", newCategoryKey.getCatalogue());
238        categories.add(newCategory);
239        return categories;
240    }
241
242    /**
243     * Retrieves attribute types
244     * @return Existing attribute types
245     */
246    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
247    public Collection<Map<String, String>> getAttributeType()
248    {
249        Collection<Map<String, String>> attributeTypes = new HashSet<>();
250
251        for (String typeId : _contentAttributeTypeExtensionPoint.getExtensionsIds())
252        {
253            Map<String, String> type = new HashMap<>();
254            
255            type.put("id", typeId);
256            type.put("label", WordUtils.capitalize(typeId));
257            
258            attributeTypes.add(type);
259        }
260
261        return attributeTypes;
262    }
263
264    /**
265     * Get model items paths of the given content type
266     * @param contentTypeId the content type's identifier
267     * @return The model items paths of the given content type
268     */
269    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
270    public Collection<Map<String, String>> getAttributePaths(String contentTypeId)
271    {
272        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
273        return _getModelItemsPaths(contentType.getModelItems());
274    }
275
276    private Collection<Map<String, String>> _getModelItemsPaths(Collection<? extends ModelItem> modelItems)
277    {
278        Collection<Map<String, String>> modelItemPaths = new HashSet<>();
279        
280        for (ModelItem modelItem : modelItems)
281        {
282            Map<String, String> modelItemPath = new HashMap<>();
283            modelItemPath.put("name", modelItem.getPath());
284            modelItemPaths.add(modelItemPath);
285            
286            if (modelItem instanceof ModelItemGroup)
287            {
288                modelItemPaths.addAll(_getModelItemsPaths(((ModelItemGroup) modelItem).getModelItems()));
289            }
290        }
291        
292        return modelItemPaths;
293    }
294    
295    /**
296     * Retrieves model item names of a content type
297     * @param contentTypeId The id of content type
298     * @return Model item names of content type
299     */
300    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
301    public Collection<Map<String, Object>> getAttributeNames(String contentTypeId)
302    {
303        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
304        return _getModelItemsNames(contentType.getModelItems());
305    }
306
307    private Collection<Map<String, Object>> _getModelItemsNames(Collection<? extends ModelItem> modelItems)
308    {
309        Collection<Map<String, Object>> modelItemNames = new HashSet<>();
310        
311        for (ModelItem modelItem : modelItems)
312        {
313            Map<String, Object> modelItemName = new HashMap<>();
314            
315            modelItemName.put("name", modelItem.getName());
316            modelItemName.put("path", modelItem.getPath());
317            modelItemName.put("isRepeaterOrComposite", modelItem instanceof RepeaterDefinition || modelItem instanceof CompositeDefinition);
318            
319            modelItemNames.add(modelItemName);
320            
321            if (modelItem instanceof ModelItemGroup)
322            {
323                modelItemNames.addAll(_getModelItemsNames(((ModelItemGroup) modelItem).getModelItems()));
324            }
325        }
326        
327        return modelItemNames;
328    }
329
330    /**
331     * Add a new category to list of new category
332     * 
333     * @param newCategory The new category
334     */
335    public void addNewCategory(Map<String, Object> newCategory)
336    {
337        if (newCategory != null && newCategory.size() > 0)
338        {
339            _newCategories.add(newCategory);
340        }
341    }
342
343    private Map<String, Object> getView(String name, String iconGlyph)
344    {
345        Map<String, Object> view = new HashMap<>();
346        view.put("dataType", ContentTypeAttributeDataType.METADATA_SET.name().toLowerCase());
347        view.put("name", name);
348        Map<String, Object> label = new HashMap<>();
349        label.put("isMultilingual", false);
350        label.put("values", name);
351        view.put("label", label);
352        view.put("isEdition", false);
353        view.put("iconGlyph", iconGlyph);
354        view.put("iconCls", iconGlyph);
355        view.put("leaf", true);
356        view.put("isInternal", false);
357        return view;
358    }
359
360    /**
361     * Check if a content type is editable
362     * 
363     * @param contentTypeId Id of content type
364     * @return True if the content type is editable
365     */
366    public boolean isEditableContentType(String contentTypeId)
367    {
368        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
369        return contentType instanceof AutomaticContentType;
370    }
371    
372    /**
373     * Get all attributes which can be parent reference
374     * 
375     * @param contentTypeId The content type id
376     * @param superTypesIds Supertype of content type
377     * @param recoverAttributeList Updated attributes of content type
378     * @return all attributes which can be parent reference
379     */
380    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
381    public Collection<Map<String, String>> getParentReferenceAttributes(String contentTypeId, List<String> superTypesIds, Object recoverAttributeList)
382    {
383        Collection<Map<String, String>> parentReferenceModelItems = new HashSet<>();
384        String stateContentType = _contentTypeStateComponent.getStateContentType(contentTypeId);
385
386        Set<String> newContentTypes = _contentTypeStateComponent.getContentTypeMarkedAsNew();
387
388        if (newContentTypes.contains(contentTypeId) || stateContentType.equals(ContentTypeState.EDIT.name().toLowerCase()))
389        {
390            List attributeList = (List) recoverAttributeList;
391            for (Object recoverAttribute : attributeList)
392            {
393                parentReferenceModelItems.addAll(_getParentReferenceRecoverModelItems(recoverAttribute));
394            }
395        }
396        else if (stateContentType.equals(ContentTypeState.RESTART.name().toLowerCase()))
397        {
398            ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
399            parentReferenceModelItems.addAll(_getParentReferenceModelItems(contentType.getModelItems()));
400        }
401        
402        if (superTypesIds != null && !superTypesIds.isEmpty())
403        {
404            for (String newContentTypeId : newContentTypes)
405            {
406                ContentType newContentType = _contentTypeExtensionPoint.getExtension(newContentTypeId);
407                if (newContentType != null)
408                {
409                    parentReferenceModelItems.addAll(_getParentReferenceModelItems(newContentType.getModelItems()));
410                }
411            }
412        }
413
414        return parentReferenceModelItems;
415    }
416    
417    private Collection<Map<String, String>> _getParentReferenceRecoverModelItems(Object recoverModelItem)
418    {
419        Collection<Map<String, String>> parentReferenceModelItems = new HashSet<>();
420        
421        if (recoverModelItem instanceof Map)
422        {
423            Map modelItemInfo = (Map) recoverModelItem;
424            
425            Object recoveredModelItemName = modelItemInfo.get("name");
426            if (recoveredModelItemName != null && recoveredModelItemName instanceof String)
427            {
428                String modelItemName = (String) recoveredModelItemName;
429                if (StringUtils.isNotBlank(modelItemName))
430                {
431                    Object recoveredModelItemDataTypeId = modelItemInfo.get("type");
432                    if (recoveredModelItemDataTypeId != null && recoveredModelItemDataTypeId instanceof String)
433                    {
434                        String dataTypeId = (String) recoveredModelItemDataTypeId;
435                        if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(dataTypeId))
436                        {
437                            Object recoverInvertRelationPath = modelItemInfo.get("invertRelationPath");
438                            if (recoverInvertRelationPath != null)
439                            {
440                                String invertRelationPath = (String) recoverInvertRelationPath;
441                                if (StringUtils.isNotBlank(invertRelationPath))
442                                {
443                                    Map<String, String> parentReferenceAttribute = new HashMap<>();
444                                    parentReferenceAttribute.put("name", modelItemName);
445                                    parentReferenceModelItems.add(parentReferenceAttribute);
446                                }
447                            }
448                        }
449                    }
450                    Object recoveredAttributeChildren = modelItemInfo.get("children");
451                    if (recoveredAttributeChildren != null && recoveredAttributeChildren instanceof List)
452                    {
453                        List attributeChildrens =  (List) recoveredAttributeChildren;
454                        for (Object recoverAttributeChildren : attributeChildrens)
455                        {
456                            parentReferenceModelItems.addAll(_getParentReferenceRecoverModelItems(recoverAttributeChildren));
457                        }
458                    }
459                }
460            }
461        }
462        
463        return parentReferenceModelItems;
464    }
465    
466    private Collection<Map<String, String>> _getParentReferenceModelItems(Collection<? extends ModelItem> modelItems)
467    {
468        Collection<Map<String, String>> parentReferenceModelItems = new HashSet<>();
469        
470        for (ModelItem modelItem : modelItems)
471        {
472            if (modelItem instanceof ContentAttributeDefinition && StringUtils.isNotBlank(((ContentAttributeDefinition) modelItem).getInvertRelationPath()))
473            {
474                Map<String, String> data = new HashMap<>();
475                data.put("name", modelItem.getName());
476                parentReferenceModelItems.add(data);
477            }
478            
479            if (modelItem instanceof ModelItemGroup)
480            {
481                parentReferenceModelItems.addAll(_getParentReferenceModelItems(((ModelItemGroup) modelItem).getModelItems()));
482            }
483        }
484        
485        return parentReferenceModelItems;
486    }
487    
488    /**
489     * Get invalid content if the attribute argument becomes mandatory
490     * 
491     * @param contentTypeId The id of content type
492     * @param mandatoryAttributeName The name of the attribute
493     * @return a list with invalid content names
494     */
495    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
496    public List<String> getInvalidContent(String contentTypeId, String mandatoryAttributeName)
497    {
498        List<String> invalidContents = new ArrayList<>();
499        AmetysObjectIterable<AmetysObject> query = _ametysObjectResolver.query(
500                "//element(*,ametys:content)[@ametys-internal:contentType='" + contentTypeId + "' and not(@ametys:" + mandatoryAttributeName + ")]");
501        Iterator<AmetysObject> it = query.iterator();
502        while (it.hasNext())
503        {
504            AmetysObject ametysObject = it.next();
505            String contentName = ametysObject.getName();
506            invalidContents.add(contentName);
507        }
508        return invalidContents;
509    }
510    
511    /**
512     * Get all default widget
513     * 
514     * @return All Default widget
515     */
516    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
517    public Collection<Map<String, String>> getWidgets()
518    {
519        Collection<Map<String, String>> widgets = new HashSet<>();
520        for (String id : _widgetsManager.getExtensionsIds())
521        {
522            Map<String, String> widgetInfo = new HashMap<>();
523            widgetInfo.put("id", id);
524            widgetInfo.put("label", id);
525            widgets.add(widgetInfo);
526        }
527        return widgets;
528    }
529
530    /**
531     * Get if a content type exists
532     * @param contentTypeId The id of content type
533     * @return True if the content type id exists
534     */
535    @Callable(right = "CMS_Rights_EditContentType", context = "/cms")
536    public boolean isExistingContentType(String contentTypeId)
537    {
538        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
539        return contentType != null ? true : false;
540    }
541}