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