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