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.cms.content.referencetable.search;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.LinkedHashMap;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.parameters.Parameters;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.acting.ServiceableAction;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Redirector;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.environment.SourceResolver;
037import org.apache.commons.lang3.ArrayUtils;
038import org.apache.commons.lang3.StringUtils;
039
040import org.ametys.cms.content.ContentHelper;
041import org.ametys.cms.contenttype.ContentType;
042import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
043import org.ametys.cms.contenttype.ContentTypesHelper;
044import org.ametys.cms.data.type.ModelItemTypeConstants;
045import org.ametys.cms.languages.Language;
046import org.ametys.cms.languages.LanguagesManager;
047import org.ametys.cms.repository.Content;
048import org.ametys.cms.repository.WorkflowAwareContent;
049import org.ametys.core.cocoon.JSonReader;
050import org.ametys.core.util.DateUtils;
051import org.ametys.core.util.JSONUtils;
052import org.ametys.core.util.ServerCommHelper;
053import org.ametys.plugins.core.user.UserHelper;
054import org.ametys.plugins.repository.AmetysObjectResolver;
055import org.ametys.plugins.workflow.support.WorkflowProvider;
056import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
057import org.ametys.runtime.i18n.I18nizableText;
058
059import com.opensymphony.workflow.loader.StepDescriptor;
060import com.opensymphony.workflow.loader.WorkflowDescriptor;
061
062/**
063 * Action for getting information about the referencing contents of a content
064 *
065 */
066public class GetReferencingContentsAction extends ServiceableAction
067{
068    /** The extension point for content types */
069    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
070    /** Content types helper */
071    protected ContentTypesHelper _contentTypesHelper;
072    /** The content helper */
073    protected ContentHelper _contentHelper;
074    /** Server comm helper */
075    protected ServerCommHelper _serverCommHelper;
076    /** Language Manager */
077    protected LanguagesManager _languagesManager;
078    /** Workflow provider */
079    protected WorkflowProvider _workflowProvider;
080    /** User helper */
081    protected UserHelper _userHelper;
082    /** The ametys object resolver */
083    protected AmetysObjectResolver _resolver;
084    /** JSON Utils */
085    protected JSONUtils _jsonUtils;
086    
087    @Override
088    public void service(ServiceManager smanager) throws ServiceException
089    {
090        super.service(smanager);
091        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
092        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
093        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
094        _serverCommHelper = (ServerCommHelper) smanager.lookup(ServerCommHelper.ROLE);
095        _contentHelper = (ContentHelper) smanager.lookup(ContentHelper.ROLE);
096        _languagesManager = (LanguagesManager) smanager.lookup(LanguagesManager.ROLE);
097        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
098        _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE);
099        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
100    }
101    
102    @Override
103    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
104    {
105        Map<String, Object> result = new HashMap<>();
106        Request request = ObjectModelHelper.getRequest(objectModel);
107        
108        Map<String, Object> jsParameters = _serverCommHelper.getJsParameters();
109        
110        String contentId = (String) jsParameters.get("contentId");
111
112        int begin = jsParameters.containsKey("start") ? (Integer) jsParameters.get("start") : 0; // Index of search
113        int offset = jsParameters.containsKey("limit") ? (Integer) jsParameters.get("limit") : Integer.MAX_VALUE; // Number of results to SAX
114        
115        Set<String> filteredCTypes = _getFilteredContentTypes(jsParameters);
116                   
117        String lang = (String) jsParameters.get("lang");
118        Locale defaultLocale = StringUtils.isNotEmpty(lang) ? new Locale(lang) : null;
119        
120        Set<Content> referencingContents = new LinkedHashSet<>();
121        List<Map<String, Object>> referencingContentsJson = new ArrayList<>();
122        
123        int index = 0;
124        referencingContents.addAll(getReferencingContents(contentId, true, filteredCTypes));
125        
126        for (Content content : referencingContents)
127        {
128            if (index >= begin && index < begin + offset)
129            {
130                referencingContentsJson.add(getContentData(content, defaultLocale));
131            }
132            
133            index++;
134        }
135            
136        result.put("contents", referencingContentsJson);
137        result.put("total", referencingContents.size());
138        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
139        return EMPTY_MAP;
140    }
141    
142    @SuppressWarnings("unchecked")
143    private Set<String> _getFilteredContentTypes(Map<String, Object> jsParameters)
144    {
145        String jsonFilter = (String) jsParameters.get("filter");
146        if (!StringUtils.isEmpty(jsonFilter))
147        {
148            List<Object> filters = _jsonUtils.convertJsonToList(jsonFilter);
149            
150            return (Set<String>) filters.stream()
151                .filter(Map.class::isInstance)
152                .map(Map.class::cast)
153                .filter(filter -> "contentTypes".equals(filter.get("property")))
154                .map(filter -> filter.get("value"))
155                .filter(List.class::isInstance)
156                .map(List.class::cast)
157                .flatMap(List::stream)
158                .collect(Collectors.toSet());
159        }
160        else
161        {
162            return Set.of();
163        }
164    }
165    
166    /**
167     * Get the contents referencing the entry of a reference table
168     * @param contentId The id of the entry
169     * @param excludeContentOfSameType true to exclude the contents of the same reference table
170     * @param filteredCTypes The filtered content types. If empty no filter on content types should be applied
171     * @return The referencing contents
172     */
173    protected Collection<Content> getReferencingContents (String contentId, boolean excludeContentOfSameType, Set<String> filteredCTypes)
174    {
175        Content content = _resolver.resolveById(contentId);
176        
177        Collection<Content> referencingContents = content.getReferencingContents();
178        
179        if (excludeContentOfSameType || !filteredCTypes.isEmpty())
180        {
181            return referencingContents.stream()
182                .filter(refContent -> !excludeContentOfSameType || _isNotSameType(content, refContent))
183                .filter(refContent -> filteredCTypes.isEmpty() || _filterContentType(refContent, filteredCTypes))
184                .collect(Collectors.toCollection(LinkedHashSet::new));
185        }
186        else
187        {
188            return referencingContents;
189        }
190    }
191    
192    private boolean _filterContentType(Content refContent, Set<String> filteredCTypes)
193    {
194        for (String cType : filteredCTypes)
195        {
196            if (_contentTypesHelper.isInstanceOf(refContent, cType))
197            {
198                return true;
199            }
200        }
201        
202        return false;
203    }
204    
205    private boolean _isNotSameType(Content content, Content refContent)
206    {
207        String[] ctypes = content.getTypes();
208        String[] refCTypes = refContent.getTypes();
209        
210        for (String ctype : ctypes)
211        {
212            if (ArrayUtils.contains(refCTypes, ctype))
213            {
214                return false;
215            }
216        }
217        
218        return true;
219    }
220    
221    /**
222     * Get the JSON representation of content
223     * @param content The content
224     * @param defaultLocale The default locale
225     * @return The content data
226     */
227    protected Map<String, Object> getContentData(Content content, Locale defaultLocale)
228    {
229        Map<String, Object> contentData = new HashMap<>();
230        
231        contentData.put("id", content.getId());
232        contentData.put("name", content.getName());
233        
234        if (_contentHelper.isMultilingual(content) && ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(content.getType(Content.ATTRIBUTE_TITLE).getId()))
235        {
236            contentData.put("title", _contentHelper.getTitleVariants(content));
237        }
238        else
239        {
240            contentData.put("title", _contentHelper.getTitle(content));
241        }
242        
243        contentData.put("language", _language2json(content));
244        contentData.put("contentTypes", _contentTypes2json(content));
245        contentData.put("iconGlyph", _contentTypesHelper.getIconGlyph(content));
246        contentData.put("iconDecorator", _contentTypesHelper.getIconDecorator(content));
247        contentData.put("smallIcon", _contentTypesHelper.getSmallIcon(content));
248        contentData.put("mediumIcon", _contentTypesHelper.getMediumIcon(content));
249        contentData.put("largeIcon", _contentTypesHelper.getLargeIcon(content));
250        contentData.put("isSimple", _contentHelper.isSimple(content));
251        contentData.put("lastModified", DateUtils.zonedDateTimeToString(content.getLastModified())); 
252        contentData.put("creationDate", DateUtils.zonedDateTimeToString(content.getCreationDate())); 
253        contentData.put("creator", _userHelper.user2json(content.getCreator(), true));
254        contentData.put("contributor", _userHelper.user2json(content.getLastContributor(), true));
255        contentData.put("workflowStep", _workflowStep2json(content));
256        
257        return contentData;
258    }
259    
260    private List<I18nizableText> _contentTypes2json(Content content)
261    {
262        List<I18nizableText> cTypes = new ArrayList<>();
263        for (String cTypeId : content.getTypes())
264        {
265            ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
266            if (cType != null)
267            {
268                cTypes.add(cType.getLabel());
269            }
270            else if (getLogger().isWarnEnabled())
271            {
272                getLogger().warn(String.format("Trying to get the label for an unknown content type : '%s'.", cTypeId));
273            }
274        }
275        
276        return cTypes;
277    }
278    
279    private Map<String, Object> _language2json(Content content)
280    {
281        Map<String, Object> infos = new LinkedHashMap<>();
282        
283        String languageCode = content.getLanguage();
284        infos.put("code", languageCode);
285        
286        if (languageCode != null)
287        {
288            Language language = _languagesManager.getLanguage(languageCode);
289            if (language != null)
290            {
291                infos.put("icon", language.getSmallIcon());
292                infos.put("label", language.getLabel());
293            }
294        }
295        
296        return infos;
297    }
298    
299    private Map<String, Object> _workflowStep2json(Content content)
300    {
301        Map<String, Object> workflowInfos = new LinkedHashMap<>();
302        
303        if (content instanceof WorkflowAwareContent)
304        {
305            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
306            int currentStepId = Math.toIntExact(waContent.getCurrentStepId());
307            
308            StepDescriptor stepDescriptor = _getStepDescriptor(waContent, currentStepId);
309            
310            if (stepDescriptor != null)
311            {
312                I18nizableText workflowStepName = new I18nizableText("application", stepDescriptor.getName());
313                
314                workflowInfos.put("stepId", currentStepId);
315                workflowInfos.put("name", workflowStepName);
316                
317                String[] icons = new String[] {"small", "medium", "large"};
318                for (String icon : icons)
319                {
320                    if ("application".equals(workflowStepName.getCatalogue()))
321                    {
322                        workflowInfos.put(icon + "Icon", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-" + icon + ".png");
323                    }
324                    else
325                    {
326                        String pluginName = workflowStepName.getCatalogue().substring("plugin.".length());
327                        workflowInfos.put(icon + "Icon", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-" + icon + ".png");
328                    }
329                }
330            }
331        }
332        
333        return workflowInfos;
334    }
335    
336    private StepDescriptor _getStepDescriptor(WorkflowAwareContent content, int stepId)
337    {
338        long workflowId = content.getWorkflowId();
339        
340        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
341        
342        String workflowName = workflow.getWorkflowName(workflowId);
343        WorkflowDescriptor workflowDescriptor = workflow.getWorkflowDescriptor(workflowName);
344        
345        if (workflowDescriptor != null)
346        {
347            StepDescriptor stepDescriptor = workflowDescriptor.getStep(stepId);
348            if (stepDescriptor != null)
349            {
350                return stepDescriptor;
351            }
352            else if (getLogger().isWarnEnabled())
353            {
354                getLogger().warn("Unknown step id '" + stepId + "' for workflow for name : " + workflowName);
355            }
356        }
357        else if (getLogger().isWarnEnabled())
358        {
359            getLogger().warn("Unknown workflow for name : " + workflowName);
360        }
361        
362        return null;
363    }
364}