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.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023import java.util.Set;
024import java.util.stream.Collectors;
025
026import javax.jcr.RepositoryException;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.jackrabbit.util.ISO9075;
033
034import org.ametys.cms.repository.Content;
035import org.ametys.cms.search.Sort;
036import org.ametys.cms.search.content.ContentSearcherFactory;
037import org.ametys.cms.search.content.ContentSearcherFactory.SearchModelContentSearcher;
038import org.ametys.cms.search.ui.model.SearchUIModel;
039import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint;
040import org.ametys.core.util.JSONUtils;
041import org.ametys.plugins.repository.AmetysObject;
042import org.ametys.plugins.repository.AmetysObjectIterable;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.plugins.repository.query.expression.StringExpression;
046import org.ametys.plugins.repository.query.expression.Expression;
047import org.ametys.plugins.repository.query.expression.Expression.Operator;
048import org.ametys.plugins.repository.query.expression.OrExpression;
049import org.ametys.runtime.plugin.component.AbstractLogEnabled;
050
051/**
052 * Helper for manipulating {@link Query}
053 *
054 */
055public class QueryHelper extends AbstractLogEnabled implements Serviceable, Component
056{
057    /** Avalon Role */
058    public static final String ROLE = QueryHelper.class.getName();
059    
060    /** The Ametys object resolver */
061    protected AmetysObjectResolver _resolver;
062    
063    /** JSON Utils */
064    protected JSONUtils _jsonUtils;
065    
066    /** SearchUI Model Extension Point */
067    protected SearchUIModelExtensionPoint _searchUiEP;
068    
069    /** Content Searcher Factory */
070    protected ContentSearcherFactory _contentSearcherFactory;
071
072    /** The service manager */
073    protected ServiceManager _manager;
074
075    public void service(ServiceManager manager) throws ServiceException
076    {
077        _manager = manager;
078        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
079        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
080        _searchUiEP = (SearchUIModelExtensionPoint) manager.lookup(SearchUIModelExtensionPoint.ROLE);
081        _contentSearcherFactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE);
082    }
083    
084    /**
085     * Creates the XPath query to get all query containers
086     * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 
087     * @return The XPath query
088     */
089    static String getXPathForQueryContainers(QueryContainer queryContainer)
090    {
091        return _getXPathQuery(queryContainer, true, ObjectToReturn.QUERY_CONTAINER, List.of());
092    }
093    
094    /**
095     * Creates the XPath query to get all queries for administrator
096     * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 
097     * @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
098     * @param acceptedTypes The type of queries. Can be null or empty to accept all types.
099     * @return The XPath query
100     */
101    static String getXPathForQueriesForAdministrator(QueryContainer queryContainer, boolean onlyDirectChildren, List<String> acceptedTypes)
102    {
103        return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, acceptedTypes);
104    }
105    
106    /**
107     * Creates the XPath query to get all queries in WRITE access
108     * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 
109     * @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
110     * @param acceptedTypes The type of queries. Can be null or empty to accept all types.
111     * @return The XPath query
112     */
113    static String getXPathForQueries(QueryContainer queryContainer, boolean onlyDirectChildren, List<String> acceptedTypes)
114    {
115        return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, acceptedTypes);
116    }
117    
118    private static StringBuilder _getParentPath(QueryContainer queryContainer)
119    {
120        try
121        {
122            StringBuilder parentPath = new StringBuilder("/jcr:root")
123                    .append(ISO9075.encodePath(queryContainer.getNode().getPath()));
124            return parentPath;
125        }
126        catch (RepositoryException e)
127        {
128            throw new AmetysRepositoryException(e);
129        }
130    }
131    
132    private static String _getXPathQuery(QueryContainer queryContainer, boolean onlyDirectChildren, ObjectToReturn objectToReturn, List<String> acceptedTypes)
133    {
134        StringBuilder parentPath = _getParentPath(queryContainer);
135        final String slashOrDoubleSlash = onlyDirectChildren ? "/" : "//";
136        StringBuilder query = parentPath
137                .append(slashOrDoubleSlash)
138                .append("element(*, ")
139                .append(objectToReturn.toNodetype())
140                .append(")");
141        if (acceptedTypes != null && !acceptedTypes.isEmpty())
142        {
143            List<Expression> exprs = new ArrayList<>();
144            for (String type : acceptedTypes)
145            {
146                exprs.add(new StringExpression(Query.TYPE, Operator.EQ, type));
147            }
148            Expression typeExpression = new OrExpression(exprs.toArray(new Expression[exprs.size()]));
149            query.append("[").append(typeExpression.build()).append("]");
150        }
151        
152        return query.toString();
153    }
154    
155    private static enum ObjectToReturn
156    {
157        QUERY,
158        QUERY_CONTAINER;
159        
160        private String _nodetype;
161        
162        static
163        {
164            QUERY._nodetype = QueryFactory.QUERY_NODETYPE;
165            QUERY_CONTAINER._nodetype = QueryContainerFactory.QUERY_CONTAINER_NODETYPE;
166        }
167        
168        String toNodetype()
169        {
170            return _nodetype;
171        }
172    }
173    
174
175    /**
176     * Execute a query
177     * @param queryId id of the query to execute
178     * @return the results of the query
179     * @throws Exception something went wrong
180     */
181    public AmetysObjectIterable<Content> executeQuery(String queryId) throws Exception
182    {
183        AmetysObject ametysObject = _resolver.resolveById(queryId);
184        if (ametysObject instanceof Query)
185        {
186            return executeQuery((Query) ametysObject);
187        }
188        else
189        {
190            return null;
191        }
192    }
193
194    /**
195     * Execute a query
196     * @param query the query to execute
197     * @return the results of the query, can be null
198     * @throws Exception if failed to execute query
199     */
200    public AmetysObjectIterable<Content> executeQuery(Query query) throws Exception
201    {
202        Map<String, Object> exportParams = getExportParams(query);
203        
204        int limit = getLimitForQuery(exportParams);
205        List<Sort> sort = getSortForQuery(exportParams);
206        
207        String model = getModelForQuery(exportParams);
208        if ("solr".equals(query.getType()))
209        {
210            String queryStr = getQueryStringForQuery(exportParams);
211            Set<String> contentTypeIds = getContentTypesForQuery(exportParams).stream().collect(Collectors.toSet());
212            AmetysObjectIterable<Content> results = _contentSearcherFactory.create(contentTypeIds)
213                    .withSort(sort)
214                    .withLimits(0, limit)
215                    .search(queryStr);
216                    
217            return results;
218        }
219        else if (Query.Type.SIMPLE.toString().equals(query.getType()) || Query.Type.ADVANCED.toString().equals(query.getType()))
220        {
221            Map<String, Object> values = getValuesForQuery(exportParams);
222            
223            Map<String, Object> contextualParameters = getContextualParametersForQuery(exportParams);
224            String searchMode = getSearchModeForQuery(exportParams);
225            SearchUIModel uiModel = _searchUiEP.getExtension(model);
226            
227            SearchModelContentSearcher searcher = _contentSearcherFactory.create(uiModel);
228            
229            return searcher
230                    .withLimits(0, limit)
231                    .withSort(sort)
232                    .withSearchMode(searchMode)
233                    .search(values, contextualParameters);
234        }
235        else
236        {
237            getLogger().warn("This method should only handle solr, advanced or simple queries. Query id '{}' is type '{}'", query.getId(), query.getType());
238            return null;
239        }
240    }
241    
242    /**
243     * Get the limit of results stored in a query, or -1 if none is found
244     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
245     * @return the maximum number of results that this query should return, -1 if no limit
246     */
247    public int getLimitForQuery(Map<String, Object> exportParams)
248    {
249        return Optional.of("limit")
250            .map(exportParams::get)
251            .map(Integer.class::cast)
252            .filter(l -> l >= 0)
253            .orElse(Integer.MAX_VALUE);
254    }
255    
256    /**
257     * Get the model of the query based on exportParams
258     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
259     * @return the model of the query
260     */
261    public String getModelForQuery(Map<String, Object> exportParams)
262    {
263        if (exportParams.containsKey("model"))
264        {
265            return (String) exportParams.get("model");
266        }
267        return null;
268    }
269
270    /**
271     * Get the values of the query based on exportParams
272     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
273     * @return the values of the query
274     */
275    public Map<String, Object> getValuesForQuery(Map<String, Object> exportParams)
276    {
277        if (exportParams.containsKey("values"))
278        {
279            @SuppressWarnings("unchecked")
280            Map<String, Object> values = (Map<String, Object>) exportParams.get("values");
281            return values;
282        }
283        return null;
284    }
285
286    /**
287     * Get the contextual parameters of the query based on exportParams
288     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
289     * @return the contextual parameters of the query
290     */
291    public Map<String, Object> getContextualParametersForQuery(Map<String, Object> exportParams)
292    {
293        if (exportParams.containsKey("contextualParameters"))
294        {
295            @SuppressWarnings("unchecked")
296            Map<String, Object> contextualParameters = (Map<String, Object>) exportParams.get("contextualParameters");
297            return contextualParameters;
298        }
299        return null;
300    }
301
302    /**
303     * Get the sort of the query based on exportParams
304     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
305     * @return the sort of the query
306     */
307    public List<Sort> getSortForQuery(Map<String, Object> exportParams)
308    {
309        if (exportParams.containsKey("sort"))
310        {
311            String sortString = (String) exportParams.get("sort");
312            List<Object> sortlist = _jsonUtils.convertJsonToList(sortString);
313            return sortlist.stream()
314                .map(Map.class::cast)
315                .map(map -> new Sort((String) map.get("property"), Sort.Order.valueOf((String) map.get("direction"))))
316                .collect(Collectors.toList());
317        }
318        return null;
319    }
320    
321    /**
322     * Get the Content Types of the query based on exportParams
323     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
324     * @return the Content Types of the query
325     */
326    public List<String> getContentTypesForQuery(Map<String, Object> exportParams)
327    {
328        Map<String, Object> values = getValuesForQuery(exportParams);
329        if (values.containsKey("contentTypes"))
330        {
331            @SuppressWarnings("unchecked")
332            List<String> contentTypes = (List<String>) values.get("contentTypes");
333            return contentTypes;
334        }
335        return null;
336    }
337    
338    /**
339     * Get the solr query string of the query based on exportParams
340     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
341     * @return the solr query string of the query
342     */
343    public String getQueryStringForQuery(Map<String, Object> exportParams)
344    {
345        Map<String, Object> values = getValuesForQuery(exportParams);
346        if (values.containsKey("query"))
347        {
348            return (String) values.get("query");
349        }
350        return null;
351    }
352    
353    /**
354     * Get the search model of the query based on exportParams
355     * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)}
356     * @return the search model of the query
357     */
358    public String getSearchModeForQuery(Map<String, Object> exportParams)
359    {
360        if (exportParams.containsKey("searchMode"))
361        {
362            return (String) exportParams.get("searchMode");
363        }
364        return "simple";
365    }
366    
367    /**
368     * Get the export params of the query
369     * @param query the query
370     * @return the export params of the query
371     */
372    public Map<String, Object> getExportParams(Query query)
373    {
374        String queryContent = query.getContent();
375        Map<String, Object> jsonMap = _jsonUtils.convertJsonToMap(queryContent);
376        if (jsonMap.containsKey("exportParams"))
377        {
378            Object exportParams = jsonMap.get("exportParams");
379            if (exportParams instanceof Map<?, ?>)
380            {
381                @SuppressWarnings("unchecked")
382                Map<String, Object> exportParamsObject = (Map<String, Object>) exportParams;
383                return exportParamsObject;
384            }
385        }
386        return new HashMap<>();
387    }
388}