001/*
002 *  Copyright 2013 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.queriesdirectory;
017
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Optional;
022import java.util.Set;
023import java.util.stream.Collectors;
024
025import javax.jcr.RepositoryException;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.jackrabbit.util.ISO9075;
032
033import org.ametys.cms.repository.Content;
034import org.ametys.cms.search.Sort;
035import org.ametys.cms.search.content.ContentSearcherFactory;
036import org.ametys.cms.search.content.ContentSearcherFactory.SearchModelContentSearcher;
037import org.ametys.cms.search.ui.model.SearchUIModel;
038import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint;
039import org.ametys.core.group.GroupIdentity;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.core.util.JSONUtils;
042import org.ametys.plugins.repository.AmetysObject;
043import org.ametys.plugins.repository.AmetysObjectIterable;
044import org.ametys.plugins.repository.AmetysObjectResolver;
045import org.ametys.plugins.repository.AmetysRepositoryException;
046import org.ametys.runtime.plugin.component.AbstractLogEnabled;
047
048/**
049 * Helper for manipulating {@link Query}
050 *
051 */
052public class QueryHelper extends AbstractLogEnabled implements Serviceable, Component
053{
054    /** Avalon Role */
055    public static final String ROLE = QueryHelper.class.getName();
056    
057    /** The Ametys object resolver */
058    protected AmetysObjectResolver _resolver;
059    
060    /** JSON Utils */
061    protected JSONUtils _jsonUtils;
062    
063    /** SearchUI Model Extension Point */
064    protected SearchUIModelExtensionPoint _searchUiEP;
065    
066    /** Content Searcher Factory */
067    protected ContentSearcherFactory _contentSearcherFactory;
068
069    /** The service manager */
070    protected ServiceManager _manager;
071    
072    public void service(ServiceManager manager) throws ServiceException
073    {
074        _manager = manager;
075        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
076        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
077        _searchUiEP = (SearchUIModelExtensionPoint) manager.lookup(SearchUIModelExtensionPoint.ROLE);
078        _contentSearcherFactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE);
079    }
080    
081    /**
082     * Creates the XPath query to get all query containers
083     * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 
084     * @return The XPath query
085     */
086    static String getXPathForQueryContainers(QueryContainer queryContainer)
087    {
088        return _getXPathQuery(queryContainer, true, ObjectToReturn.QUERY_CONTAINER, Optional.empty(), Optional.empty(), null);
089    }
090    
091    /**
092     * Creates the XPath query to get all queries for administrator
093     * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 
094     * @param onlyDirectChildren <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path
095     * @param type The query type
096     * @return The XPath query
097     */
098    static String getXPathForQueriesForAdministrator(QueryContainer queryContainer, boolean onlyDirectChildren, Optional<String> type)
099    {
100        return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, Optional.empty(), type, null);
101    }
102    
103    /**
104     * Creates the XPath query to get all queries in READ access
105     * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 
106     * @param onlyDirectChildren <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path
107     * @param visibility The user and its groups for checking visibility
108     * @param type The query type
109     * @return The XPath query
110     */
111    static String getXPathForQueriesInReadAccess(QueryContainer queryContainer, boolean onlyDirectChildren, Visibility visibility, Optional<String> type)
112    {
113        return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, Optional.of(visibility), type, true);
114    }
115    
116    /**
117     * Creates the XPath query to get all queries in WRITE access
118     * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 
119     * @param onlyDirectChildren <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path
120     * @param visibility The user and its groups for checking visibility
121     * @param type The query type
122     * @return The XPath query
123     */
124    static String getXPathForQueriesInWriteAccess(QueryContainer queryContainer, boolean onlyDirectChildren, Visibility visibility, Optional<String> type)
125    {
126        return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, Optional.of(visibility), type, false);
127    }
128    
129    private static StringBuilder _getParentPath(QueryContainer queryContainer)
130    {
131        try
132        {
133            StringBuilder parentPath = new StringBuilder("/jcr:root")
134                    .append(ISO9075.encodePath(queryContainer.getNode().getPath()));
135            return parentPath;
136        }
137        catch (RepositoryException e)
138        {
139            throw new AmetysRepositoryException(e);
140        }
141    }
142    
143    private static String _getXPathQuery(QueryContainer queryContainer, boolean onlyDirectChildren, ObjectToReturn objectToReturn, Optional<Visibility> visibility, Optional<String> type, Boolean readAccess)
144    {
145        StringBuilder parentPath = _getParentPath(queryContainer);
146        
147        final String slashOrDoubleSlash = onlyDirectChildren ? "/" : "//";
148        StringBuilder query = parentPath
149                .append(slashOrDoubleSlash)
150                .append("element(*, ")
151                .append(objectToReturn.toNodetype())
152                .append(")");
153        
154        if (visibility.isPresent() || type.isPresent())
155        {
156            query.append('[');
157        }
158        
159        type.ifPresent(t -> query.append("@").append(Query.TYPE).append("='").append(t).append("'"));
160        
161        if (visibility.isPresent())
162        {
163            type.ifPresent(t -> query.append(" and ("));
164            
165            _appendPublicPredicate(query);
166            
167            UserIdentity user = visibility.get().getUser();
168            String login = user.getLogin();
169            String populationId = user.getPopulationId();
170            _appendPrivatePredicate(query, login, populationId);
171            
172            Set<GroupIdentity> groups = visibility.get().getGroups();
173            _appendSharedPredicate(query, login, populationId, groups, readAccess);
174            
175            type.ifPresent(t -> query.append(')'));
176        }
177        
178        
179        if (visibility.isPresent() || type.isPresent())
180        {
181            query.append(']');
182        }
183        
184        return query.toString();
185    }
186    
187    private static void _appendPublicPredicate(StringBuilder query)
188    {
189        query.append("(@").append(Query.VISIBILITY).append("='").append(Query.Visibility.PUBLIC.name())
190             .append("') or ");
191    }
192    
193    private static void _appendPrivatePredicate(StringBuilder query, String login, String populationId)
194    {
195        query.append("(@").append(Query.VISIBILITY).append("='").append(Query.Visibility.PRIVATE.name())
196             .append("' and ");
197        _appendUserCondition(query, login, populationId);
198        query.append(") or ");
199    }
200    
201    private static void _appendSharedPredicate(StringBuilder query, String login, String populationId, Set<GroupIdentity> groups, boolean readAccess)
202    {
203        query.append("(@").append(Query.VISIBILITY).append("='").append(Query.Visibility.SHARED.name())
204             .append("' and (");
205        query.append("(");
206        _appendUserCondition(query, login, populationId);
207        query.append(") or ");
208        
209        if (readAccess)
210        {
211            _appendUserAccessCondition(query, login, populationId, Query.PROFILE_READ_ACCESS);
212            query.append(" or ");
213        }
214        _appendUserAccessCondition(query, login, populationId, Query.PROFILE_WRITE_ACCESS);
215        
216        _appendGroupsCondition(query, groups, readAccess);
217        query.append("))");
218    }
219    
220    private static void _appendUserCondition(StringBuilder query, String login, String populationId)
221    {
222        query.append('@').append(Query.AUTHOR).append("/ametys:login='").append(login)
223             .append("' and @").append(Query.AUTHOR).append("/ametys:population='").append(populationId)
224             .append('\'');
225    }
226    
227    private static void _appendGroupsCondition(StringBuilder query, Set<GroupIdentity> groups, boolean readAccess)
228    {
229        for (GroupIdentity group : groups)
230        {
231            String groupId = group.getId();
232            String groupDirectory = group.getDirectoryId();
233            
234            if (readAccess)
235            {
236                query.append(" or ");
237                _appendGroupAccessCondition(query, groupId, groupDirectory, Query.PROFILE_READ_ACCESS);
238            }
239            query.append(" or ");
240            _appendGroupAccessCondition(query, groupId, groupDirectory, Query.PROFILE_WRITE_ACCESS);
241        }
242    }
243    
244    private static void _appendUserAccessCondition(StringBuilder query, String login, String populationId, String accessNodeName)
245    {
246        query.append("(").append(accessNodeName).append("/").append(Query.USERS).append("/ametys:login='").append(login)
247             .append("' and ").append(accessNodeName).append("/").append(Query.USERS).append("/ametys:population='").append(populationId)
248             .append("')");
249    }
250    
251    private static void _appendGroupAccessCondition(StringBuilder query, String groupId, String groupDirectory, String accessNodeName)
252    {
253        query.append("(").append(accessNodeName).append("/").append(Query.GROUPS).append("/ametys:groupId='").append(groupId)
254             .append("' and ").append(accessNodeName).append("/").append(Query.GROUPS).append("/ametys:groupDirectory='").append(groupDirectory)
255             .append("')");
256    }
257    
258    /**
259     * The user and its groups for checking visibility
260     */
261    public static final class Visibility
262    {
263        private UserIdentity _user;
264        private Set<GroupIdentity> _groups;
265        
266        private Visibility(UserIdentity user, Set<GroupIdentity> groups)
267        {
268            _user = user;
269            _groups = groups;
270        }
271        
272        /**
273         * Creates a new {@link Visibility}
274         * @param user The user
275         * @param groups The user groups
276         * @return the {@link Visibility} wrapper
277         */
278        public static Visibility of(UserIdentity user, Set<GroupIdentity> groups)
279        {
280            return new Visibility(user, groups);
281        }
282        
283        UserIdentity getUser()
284        {
285            return _user;
286        }
287        
288        Set<GroupIdentity> getGroups()
289        {
290            return _groups;
291        }
292    }
293    
294    private static enum ObjectToReturn
295    {
296        QUERY,
297        QUERY_CONTAINER;
298        
299        private String _nodetype;
300        
301        static
302        {
303            QUERY._nodetype = QueryFactory.QUERY_NODETYPE;
304            QUERY_CONTAINER._nodetype = QueryContainerFactory.QUERY_CONTAINER_NODETYPE;
305        }
306        
307        String toNodetype()
308        {
309            return _nodetype;
310        }
311    }
312    
313
314    /**
315     * Execute a query
316     * @param queryId id of the query to execute
317     * @return the results of the query
318     * @throws Exception something went wrong
319     */
320    public AmetysObjectIterable<Content> executeQuery(String queryId) throws Exception
321    {
322        AmetysObject ametysObject = _resolver.resolveById(queryId);
323        if (ametysObject instanceof Query)
324        {
325            return executeQuery((Query) ametysObject);
326        }
327        else
328        {
329            return null;
330        }
331    }
332
333    /**
334     * Execute a query
335     * @param query the query to execute
336     * @return the results of the query, can be null
337     * @throws Exception something went wrong
338     */
339    public AmetysObjectIterable<Content> executeQuery(Query query) throws Exception
340    {
341        Map<String, Object> exportParams = getExportParams(query);
342        
343        int limit = getLimitForQuery(exportParams);
344        List<Sort> sort = getSortForQuery(exportParams);
345        
346        String model = getModelForQuery(exportParams);
347        if ("solr".equals(query.getType()))
348        {
349            String queryStr = getQueryStringForQuery(exportParams);
350            Set<String> contentTypeIds = getContentTypesForQuery(exportParams).stream().collect(Collectors.toSet());
351            AmetysObjectIterable<Content> results = _contentSearcherFactory.create(contentTypeIds)
352                    .withSort(sort)
353                    .withLimits(0, limit)
354                    .search(queryStr);
355                    
356            return results;
357        }
358        else if (Query.Type.SIMPLE.toString().equals(query.getType()) || Query.Type.ADVANCED.toString().equals(query.getType()))
359        {
360            Map<String, Object> values = getValuesForQuery(exportParams);
361            Map<String, Object> contextualParameters = getContextualParametersForQuery(exportParams);
362            String searchMode = getSearchModeForQuery(exportParams);
363            SearchUIModel uiModel = _searchUiEP.getExtension(model);
364            
365            SearchModelContentSearcher searcher = _contentSearcherFactory.create(uiModel);
366            
367            return searcher
368                    .withLimits(0, limit)
369                    .withSort(sort)
370                    .withSearchMode(searchMode)
371                    .search(values, contextualParameters);
372        }
373        else
374        {
375            getLogger().warn("This method should only handle solr, advanced or simole queries. query id '" + query.getId() + "' is type '" + query.getType() + "'");
376            return null;
377        }
378    }
379    
380    /**
381     * Get the limit of results stored in a query, or -1 if none is found
382     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
383     * @return the maximum number of results that this query should return, -1 if no limit
384     */
385    public int getLimitForQuery(Map<String, Object> exportParams)
386    {
387        return Optional.of("limit")
388            .map(exportParams::get)
389            .map(Integer.class::cast)
390            .filter(l -> l >= 0)
391            .orElse(Integer.MAX_VALUE);
392    }
393    
394    /**
395     * Get the model of the query based on exportParams
396     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
397     * @return the model of the query
398     */
399    public String getModelForQuery(Map<String, Object> exportParams)
400    {
401        if (exportParams.containsKey("model"))
402        {
403            return (String) exportParams.get("model");
404        }
405        return null;
406    }
407
408    /**
409     * Get the values of the query based on exportParams
410     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
411     * @return the values of the query
412     */
413    public Map<String, Object> getValuesForQuery(Map<String, Object> exportParams)
414    {
415        if (exportParams.containsKey("values"))
416        {
417            @SuppressWarnings("unchecked")
418            Map<String, Object> values = (Map<String, Object>) exportParams.get("values");
419            return values;
420        }
421        return null;
422    }
423
424    /**
425     * Get the contextual parameters of the query based on exportParams
426     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
427     * @return the contextual parameters of the query
428     */
429    public Map<String, Object> getContextualParametersForQuery(Map<String, Object> exportParams)
430    {
431        if (exportParams.containsKey("contextualParameters"))
432        {
433            @SuppressWarnings("unchecked")
434            Map<String, Object> contextualParameters = (Map<String, Object>) exportParams.get("contextualParameters");
435            return contextualParameters;
436        }
437        return null;
438    }
439
440    /**
441     * Get the sort of the query based on exportParams
442     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
443     * @return the sort of the query
444     */
445    public List<Sort> getSortForQuery(Map<String, Object> exportParams)
446    {
447        if (exportParams.containsKey("sort"))
448        {
449            String sortString = (String) exportParams.get("sort");
450            List<Object> sortlist = _jsonUtils.convertJsonToList(sortString);
451            return sortlist.stream()
452                .map(Map.class::cast)
453                .map(map -> new Sort((String) map.get("property"), Sort.Order.valueOf((String) map.get("direction"))))
454                .collect(Collectors.toList());
455        }
456        return null;
457    }
458    
459    /**
460     * Get the Content Types of the query based on exportParams
461     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
462     * @return the Content Types of the query
463     */
464    public List<String> getContentTypesForQuery(Map<String, Object> exportParams)
465    {
466        Map<String, Object> values = getValuesForQuery(exportParams);
467        if (values.containsKey("contentTypes"))
468        {
469            @SuppressWarnings("unchecked")
470            List<String> contentTypes = (List<String>) values.get("contentTypes");
471            return contentTypes;
472        }
473        return null;
474    }
475    
476    /**
477     * Get the solr query string of the query based on exportParams
478     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
479     * @return the solr query string of the query
480     */
481    public String getQueryStringForQuery(Map<String, Object> exportParams)
482    {
483        Map<String, Object> values = getValuesForQuery(exportParams);
484        if (values.containsKey("query"))
485        {
486            return (String) values.get("query");
487        }
488        return null;
489    }
490    
491    /**
492     * Get the search model of the query based on exportParams
493     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
494     * @return the search model of the query
495     */
496    public String getSearchModeForQuery(Map<String, Object> exportParams)
497    {
498        if (exportParams.containsKey("searchMode"))
499        {
500            return (String) exportParams.get("searchMode");
501        }
502        return "simple";
503    }
504    
505    /**
506     * Get the export params of the query
507     * @param query the query
508     * @return the export params of the query
509     */
510    public Map<String, Object> getExportParams(Query query)
511    {
512        String queryContent = query.getContent();
513        Map<String, Object> jsonMap = _jsonUtils.convertJsonToMap(queryContent);
514        if (jsonMap.containsKey("exportParams"))
515        {
516            Object exportParams = jsonMap.get("exportParams");
517            if (exportParams instanceof Map<?, ?>)
518            {
519                @SuppressWarnings("unchecked")
520                Map<String, Object> exportParamsObject = (Map<String, Object>) exportParams;
521                return exportParamsObject;
522            }
523        }
524        return new HashMap<>();
525    }
526}