001/*
002 *  Copyright 2015 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.cms.contenttype;
017
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.avalon.framework.parameters.Parameters;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.cocoon.acting.ServiceableAction;
032import org.apache.cocoon.environment.ObjectModelHelper;
033import org.apache.cocoon.environment.Redirector;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.environment.SourceResolver;
036import org.apache.commons.lang.BooleanUtils;
037import org.apache.commons.lang3.ArrayUtils;
038import org.apache.commons.lang3.ObjectUtils;
039import org.apache.commons.lang3.StringUtils;
040
041import org.ametys.cms.data.type.ModelItemTypeConstants;
042import org.ametys.cms.repository.DefaultContent;
043import org.ametys.core.cocoon.JSonReader;
044import org.ametys.core.util.I18nUtils;
045import org.ametys.runtime.i18n.I18nizableText;
046import org.ametys.runtime.model.ElementDefinition;
047import org.ametys.runtime.model.ModelItem;
048
049/**
050 * Get the common attributes between given content types and/or among given contents
051 */
052public class GetCommonAttributesAction extends ServiceableAction
053{
054    
055    /** Not sortable attribute types */
056    @SuppressWarnings("static-access")
057    protected static final String[] __NOT_SORTABLE_TYPE_IDS =
058    {
059        ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID,
060        ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID,
061        ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID,
062        ModelItemTypeConstants.COMPOSITE_TYPE_ID
063    };
064    
065    /** Allow enumerated attributes */
066    protected static final String __ALLOW_ENUMERATED = "ENUMERATED";
067    
068    /** Allow not enumerated attributes */
069    protected static final String __ALLOW_NOT_ENUMERATED = "NOT-ENUMERATED";
070
071    /** The content type EP */
072    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
073    
074    /** The content types Helper */
075    protected ContentTypesHelper _contentTypesHelper;
076    
077    /** I18n utils */
078    protected I18nUtils _i18nUtils;
079    
080    @Override
081    public void service(ServiceManager smanager) throws ServiceException
082    {
083        super.service(smanager);
084        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
085        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
086        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
087    }
088    
089    @Override
090    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
091    {
092        Request request = ObjectModelHelper.getRequest(objectModel);
093        
094        String viewName = request.getParameter("viewName");
095        List<String> acceptedTypes = Arrays.asList(ObjectUtils.defaultIfNull(request.getParameterValues("acceptedTypes"), ArrayUtils.EMPTY_STRING_ARRAY));
096        boolean onlySortable = BooleanUtils.toBoolean(request.getParameter("onlySortable")); // false by default
097        boolean includeComposite = BooleanUtils.toBoolean(request.getParameter("includeComposite")); // false by default
098        boolean includeSubRepeaters = !"false".equals(request.getParameter("includeSubRepeaters")); // true by default
099        boolean withFullLabel = BooleanUtils.toBoolean(request.getParameter("withFullLabel")); // false by default
100        boolean withLastModified = BooleanUtils.toBoolean(request.getParameter("withLastModified")); // false by default
101        boolean withLastValidation = BooleanUtils.toBoolean(request.getParameter("withLastValidation")); // false by default
102        boolean withCreationDate = BooleanUtils.toBoolean(request.getParameter("withCreationDate")); // false by default
103        boolean withResources = BooleanUtils.toBoolean(request.getParameter("withResources")); // false by default
104        
105        Set<String> contentTypeIds = getContentTypes(request, withResources);
106        boolean hasResources = contentTypeIds.contains("resource");
107        
108        Map<String, Object> result = new HashMap<>();
109        
110        List<Map<String, Object>> commonModelItemsInfo = getCommonModelItemsInfo(contentTypeIds, viewName, acceptedTypes, onlySortable, includeComposite, withFullLabel, hasResources, includeSubRepeaters);
111        handleSpecificInfo(commonModelItemsInfo, withLastModified, withLastValidation, withCreationDate, hasResources, withFullLabel);
112        result.put("attributes", commonModelItemsInfo);
113        
114        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
115        return EMPTY_MAP;
116    }
117    
118    /**
119     * Retrieve model items information for a list of content types
120     * @param contentTypeIds the identifiers of the content types
121     * @param viewName The view name to list model items
122     * @param acceptedTypes The types to accept. All types accepted if null or empty.
123     * @param onlySortable true to only accept sortable model items.
124     * @param includeCompositesAndRepeaters true to include the composites and repeaters as well
125     * @param withFullLabel true to use the full label of the model items, false otherwise
126     * @param hasResources true if there is at least one content type that is a resource, false otherwise.
127     * @param includeSubRepeaters true to include the children of repeaters as well
128     * @return The list of common model items properties (each set of properties is a map representing the model item)
129     */
130    protected List<Map<String, Object>> getCommonModelItemsInfo(Collection<String> contentTypeIds, String viewName, List<String> acceptedTypes, boolean onlySortable, boolean includeCompositesAndRepeaters, boolean withFullLabel, boolean hasResources, boolean includeSubRepeaters)
131    {
132        List<Map<String, Object>> commonModelItemsInfos = new LinkedList<>();
133        
134        if (hasResources)
135        {
136            // no common if resource selected
137            return commonModelItemsInfos;
138        }
139        
140        Map<String, ModelItem> modelItems = _contentTypesHelper.getCommonModelItems(contentTypeIds, viewName);
141
142        for (ModelItem modelItem : modelItems.values())
143        {
144            if (_modelItemPassesConditions(modelItem, acceptedTypes, onlySortable, includeCompositesAndRepeaters, includeSubRepeaters))
145            {
146                Map<String, Object> metadataInfo = getModelItemInfo(modelItem, withFullLabel);
147                commonModelItemsInfos.add(metadataInfo);
148            }
149        }
150        
151        return commonModelItemsInfos;
152    }
153    
154    /**
155     * Indicates if the attribute should be retrieved or filtered out given the options
156     * @param modelItem the attribute definition
157     * @param acceptedTypes The types to accept. All types accepted if null or empty.
158     * @param onlySortable true to only accept sortable attributes.
159     * @param includeCompositeAndRepeaters true to include the composites and repeaters as well
160     * @param includeSubRepeaters true to include the children of repeaters as well
161     * @return true if the model item passes the conditions
162     */
163    @SuppressWarnings("static-access")
164    protected boolean _modelItemPassesConditions(ModelItem modelItem, List<String> acceptedTypes, boolean onlySortable, boolean includeCompositeAndRepeaters, boolean includeSubRepeaters)
165    {
166        String typeId = modelItem.getType().getId();
167        boolean passesCondition = true;
168        
169        if (acceptedTypes != null && !acceptedTypes.isEmpty() && !(acceptedTypes.size() == 1 && StringUtils.isEmpty(acceptedTypes.get(0))))
170        {
171            passesCondition = _passesAcceptedTypesCondition(acceptedTypes, modelItem);
172        }
173        
174        if (passesCondition && onlySortable)
175        {
176            passesCondition = !ArrayUtils.contains(__NOT_SORTABLE_TYPE_IDS, typeId);
177        }
178        
179        if (passesCondition && !includeCompositeAndRepeaters)
180        {
181            passesCondition = !ModelItemTypeConstants.COMPOSITE_TYPE_ID.equals(typeId) && !ModelItemTypeConstants.REPEATER_TYPE_ID.equals(typeId);
182        }
183        
184        if (passesCondition && !includeSubRepeaters)
185        {
186            passesCondition = !_isInARepeater(modelItem);
187        }
188        
189        return passesCondition;
190    }
191    
192    /**
193     * Checks if the model item passes conditions from accepted types
194     * @param acceptedTypes the accepted types
195     * @param modelItem the model item to check
196     * @return true if the model item passes conditions
197     */
198    protected boolean _passesAcceptedTypesCondition(List<String> acceptedTypes, ModelItem modelItem)
199    {
200        boolean passesCondition = false;
201        String typeId = modelItem.getType().getId();
202        
203        Map<String, List<String>> acceptedTypesWithProperties = _getAcceptedTypesWithProperties(acceptedTypes);
204        if (acceptedTypesWithProperties.containsKey(typeId))
205        {
206            List<String> properties = acceptedTypesWithProperties.get(typeId);
207            passesCondition = properties.isEmpty() 
208                || properties.contains(__ALLOW_ENUMERATED) && modelItem instanceof ElementDefinition && ((ElementDefinition) modelItem).getEnumerator() != null
209                || properties.contains(__ALLOW_NOT_ENUMERATED) && (!(modelItem instanceof ElementDefinition) || ((ElementDefinition) modelItem).getEnumerator() == null);
210        }
211        
212        return passesCondition;
213    }
214
215    /**
216     * Get the map of accepted types with its properties
217     * @param acceptedTypes the accepted types configuration
218     * @return the map of accepted type with its properties
219     */
220    protected Map<String, List<String>> _getAcceptedTypesWithProperties(List<String> acceptedTypes)
221    {
222        Map<String, List<String>> acceptedTypesWithProperties = new HashMap<>();
223        for (String acceptedType : acceptedTypes)
224        {
225            String propertiesAsString = StringUtils.substringBetween(acceptedType, "[", "]");
226            if (StringUtils.isNotBlank(propertiesAsString))
227            {
228                List<String> properties = Arrays.asList(StringUtils.split(propertiesAsString, ","));
229                acceptedTypesWithProperties.put(StringUtils.substringBefore(acceptedType, "[").toLowerCase(), properties);
230            }
231            else
232            {
233                acceptedTypesWithProperties.put(acceptedType.toLowerCase(), Collections.EMPTY_LIST);
234            }
235        }
236        return acceptedTypesWithProperties;
237    }
238    
239    /**
240     * Checks if the given model item has a parent that is a repeater
241     * @param modelItem the model item to check
242     * @return <code>true</code> if the given model item has a parent that is a repeater, <code>false</code> otherwise
243     */
244    @SuppressWarnings("static-access")
245    protected boolean _isInARepeater(ModelItem modelItem)
246    {
247        ModelItem parent = modelItem.getParent();
248        if (parent != null)
249        {
250            if (ModelItemTypeConstants.REPEATER_TYPE_ID.equals(parent.getType().getId()))
251            {
252                return true;
253            }
254            else
255            {
256                return _isInARepeater(parent);
257            }
258        }
259        else
260        {
261            return false;
262        }
263    }
264    
265    /**
266     * Get the model item's information
267     * @param modelItem the model item
268     * @param withFullLabel true to use the full label of the model item, false otherwise
269     * @return Model item's information as a map of properties
270     */
271    protected Map<String, Object> getModelItemInfo(ModelItem modelItem, boolean withFullLabel)
272    {
273        Map<String, Object> modelItemInfo = new HashMap<>();
274        
275        modelItemInfo.put("name", modelItem.getPath());
276        modelItemInfo.put("type", modelItem.getType().getId());
277        modelItemInfo.put("label", _i18nUtils.translate(modelItem.getLabel()));
278        
279        if (withFullLabel)
280        {
281            modelItemInfo.put("fullLabel", getModelItemFullLabel(modelItem));
282        }
283        
284        return modelItemInfo;
285    }
286    
287    /**
288     * Get the user friendly full label for the given model item
289     * @param modelItem the model items
290     * @return The full label of the model item
291     */
292    protected String getModelItemFullLabel(ModelItem modelItem)
293    {
294        StringBuilder sb = new StringBuilder();
295        ModelItem parent = modelItem.getParent();
296        if (parent != null)
297        {
298            sb.append(getModelItemFullLabel(parent));
299            sb.append(" > ");
300        }
301        
302        sb.append(_i18nUtils.translate(modelItem.getLabel()));
303        return sb.toString();
304    }
305    
306    /**
307     * Handle specific information, such as adding system properties into the model  list
308     * @param commonModelItemsInfo the mapping of model items' properties
309     * @param withLastModified true to add the last modification date to the model item's info
310     * @param withLastValidation true to add the last validation date to the model item's info
311     * @param withCreationDate true to add the creation date to the model item's info
312     * @param hasResources true if there is at least one content type that is a resource, false otherwise.
313     * @param withFullLabel true to use the full label of the model items, false otherwise
314     */
315    @SuppressWarnings("static-access")
316    protected void handleSpecificInfo(List<Map<String, Object>> commonModelItemsInfo, boolean withLastModified, boolean withLastValidation, boolean withCreationDate, boolean hasResources, boolean withFullLabel)
317    {
318        if (withLastModified)
319        {
320            Map<String, Object> metadataInfo = new HashMap<>();
321            
322            // FIXME web...?
323            metadataInfo.put("name", DefaultContent.METADATA_MODIFIED);
324            metadataInfo.put("type", ModelItemTypeConstants.DATE_TYPE_ID);
325            
326            String label = _i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_LAST_MODIFICATION_DATE"));
327            metadataInfo.put("label", label);
328            
329            if (withFullLabel)
330            {
331                metadataInfo.put("fullLabel", label);
332            }
333            
334            commonModelItemsInfo.add(metadataInfo);
335        }
336        
337        if (!hasResources)
338        {
339            if (withLastValidation)
340            {
341                Map<String, Object> metadataInfo = new HashMap<>();
342                
343                // FIXME web...?
344                metadataInfo.put("name", DefaultContent.METADATA_LAST_VALIDATION);
345                metadataInfo.put("type", ModelItemTypeConstants.DATE_TYPE_ID);
346                
347                String label = _i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_LAST_VALIDATION_DATE"));
348                metadataInfo.put("label", label);
349                
350                if (withFullLabel)
351                {
352                    metadataInfo.put("fullLabel", label);
353                }
354                
355                commonModelItemsInfo.add(metadataInfo);
356            }
357            
358            if (withCreationDate)
359            {
360                Map<String, Object> metadataInfo = new HashMap<>();
361                
362                // FIXME web...?
363                metadataInfo.put("name", DefaultContent.METADATA_CREATION);
364                metadataInfo.put("type", ModelItemTypeConstants.DATE_TYPE_ID);
365                
366                String label = _i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_CREATION_DATE"));
367                metadataInfo.put("label", label);
368                
369                if (withFullLabel)
370                {
371                    metadataInfo.put("fullLabel", label);
372                }
373                
374                commonModelItemsInfo.add(metadataInfo);
375            }
376        }
377        
378    }
379    
380    /**
381     * Get the content types id to search for
382     * @param request the request
383     * @param withResources is there at least one resource ?
384     * @return the content types
385     */
386    protected Set<String> getContentTypes(Request request, boolean withResources)
387    {
388        Set<String> cTypeIds = new HashSet<>();
389        String[] ids = request.getParameterValues("ids");
390        
391        if (ids != null)
392        {
393            for (String id : ids)
394            {
395                if (StringUtils.isNotEmpty(id))
396                {
397                    // id can contains comma (when allOption is selected for example).
398                    for (String idPart : StringUtils.split(id, ','))
399                    {
400                        cTypeIds.add(idPart);
401                    }
402                }
403            }
404        }
405        
406        if (cTypeIds.isEmpty())
407        {
408            cTypeIds.addAll(getAllAvailablesContentTypes(request, true, withResources));
409        }
410        
411        return cTypeIds;
412    }
413    
414    /**
415     * Get all the available content types 
416     * @param request the request
417     * @param publicOnly Only the non private content types will be returned
418     * @param withResources True to add the resources to the list (which is not a content type)
419     * @return all the available content types 
420     */
421    protected Set<String> getAllAvailablesContentTypes (Request request, boolean publicOnly, boolean withResources)
422    {
423        Set<String> types = new HashSet<>();
424        
425        for (String id : _contentTypeExtensionPoint.getExtensionsIds())
426        {
427            if (!publicOnly || !_contentTypeExtensionPoint.getExtension(id).isPrivate())
428            {
429                types.add(id);
430            }
431        }
432        
433        if (withResources)
434        {
435            types.add("resource");
436        }
437        
438        return types;
439    }
440}