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