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.search.advanced;
017
018import java.util.List;
019import java.util.Map;
020import java.util.Objects;
021import java.util.function.BiFunction;
022import java.util.function.Function;
023import java.util.stream.Collectors;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.commons.lang3.StringUtils;
027import org.apache.solr.client.solrj.util.ClientUtils;
028
029import org.ametys.cms.search.QueryBuilder;
030import org.ametys.cms.search.query.NotQuery;
031import org.ametys.cms.search.query.Query;
032import org.ametys.cms.search.query.Query.LogicalOperator;
033import org.ametys.cms.search.query.Query.Operator;
034import org.ametys.runtime.plugin.component.AbstractLogEnabled;
035
036/**
037 * Creates a {@link AbstractTreeNode} from a JSON object, typically got from client-side AdvancedSearchFormPanel.
038 */
039public class TreeMaker extends AbstractLogEnabled implements Component
040{
041    /** The component role. */
042    public static final String ROLE = TreeMaker.class.getName();
043    
044    /**
045     * Creates a {@link AbstractTreeNode} from JSON values
046     * @param <T> the type of the values of the leaves of the tree to create
047     * @param values the JSON values
048     * @param leafValueMaker The function to create the value of a {@link TreeLeaf}
049     * @return the created {@link AbstractTreeNode} from JSON values
050     */
051    public <T> AbstractTreeNode<T> create(Map<String, Object> values, Function<ClientSideCriterionWrapper, T> leafValueMaker)
052    {
053        return _create(values, leafValueMaker);
054    }
055    
056    private <T> AbstractTreeNode<T> _create(Map<String, Object> values, Function<ClientSideCriterionWrapper, T> leafValueMaker)
057    {
058        String type = (String) values.get("type");
059
060        if ("criterion".equals(type))
061        {
062            // Criterion node: create a leaf
063            String id = (String) values.get("id");
064            Object value = values.get("value");
065            String op = (String) values.get("op");
066
067            ClientSideCriterionWrapper clientSideCriterionWrapper = new ClientSideCriterionWrapper(id, value, op, values);
068            return new TreeLeaf<>(leafValueMaker.apply(clientSideCriterionWrapper));
069        }
070        else if ("and".equalsIgnoreCase(type) || "or".equalsIgnoreCase(type))
071        {
072            // AND/OR node: create an internal node
073            @SuppressWarnings("unchecked")
074            List<Map<String, Object>> expressions = (List<Map<String, Object>>) values.get("expressions");
075            List<AbstractTreeNode<T>> children = expressions.stream()
076                .map(exp -> _create(exp, leafValueMaker))
077                .filter(Objects::nonNull)
078                .collect(Collectors.toList());
079            
080            return new TreeInternalNode<>(children, "and".equalsIgnoreCase(type) ? LogicalOperator.AND : LogicalOperator.OR);
081        }
082        else if (values.isEmpty())
083        {
084            return null;
085        }
086        else
087        {
088            throw new IllegalArgumentException("The object 'values' has an incorrect format (" + values + ")");
089        }
090    }
091    
092    /**
093     * Gets the query given the parameters.
094     * <br>If the provided operator as string represents a real server-side {@link Operator}, then it is just the provided {@link BiFunction} which is applied.
095     * <br>Otherwise, the operator is a client-side one, and it will be transformed by modifying the value (by adding wildcards for instance), and deducting the server-side {@link Operator}.
096     * Then, the {@link BiFunction} is applied with the transformed value and the deducted {@link Operator}.
097     * @param value The value
098     * @param stringOperator The operator as string
099     * @param toQuery A function applied to a transformed value 
100     * (not necessarily the one given by the method, as it can be transformed by adding wildcard chars, escaping...) 
101     * and a real {@link Operator} (deducted from the given client-side string operator)
102     * @param language Tha language
103     * @param contextualParameters The contextual parameters
104     * @return The result query
105     */
106    // TODO have those fake client-side operators as real server-side operators
107    public Query toQuery(Object value, String stringOperator, BiFunction<Object, Operator, Query> toQuery, String language, Map<String, Object> contextualParameters)
108    {
109        Query query = null;
110        String wdValue;
111        
112        // Special wildcard searches.
113        switch (stringOperator)
114        {
115            case "contains":
116            case "not-contains":
117                wdValue = "*" + escapeQueryCharsForLikeQuery((String) value, contextualParameters) + "*";
118                query = toQuery.apply(wdValue, Operator.LIKE);
119                if ("not-contains".equals(stringOperator) && query != null)
120                {
121                    query = new NotQuery(query);
122                }
123                break;
124            case "starts-with":
125            case "not-starts-with":
126                wdValue = escapeQueryCharsForLikeQuery((String) value, contextualParameters) + "*";
127                query = toQuery.apply(wdValue, Operator.LIKE);
128                if ("not-starts-with".equals(stringOperator) && query != null)
129                {
130                    query = new NotQuery(query);
131                }
132                break;
133            case "ends-with":
134            case "not-ends-with":
135                wdValue = "*" + escapeQueryCharsForLikeQuery((String) value, contextualParameters);
136                query = toQuery.apply(wdValue, Operator.LIKE);
137                if ("not-ends-with".equals(stringOperator) && query != null)
138                {
139                    query = new NotQuery(query);
140                }
141                break;
142            default:
143                if (StringUtils.isNotBlank(stringOperator))
144                {
145                    Operator operator = Operator.fromName(stringOperator);
146                    query = toQuery.apply(value, operator);
147                }
148        }
149        
150        return query;
151    }
152    
153    /**
154     * Escape special Solr query characters of the user input, before building the query in order to append '*' character(s)
155     * @param string The string to escape.
156     * @param contextualParameters the search contextual parameters.
157     * @return The escaped string.
158     */
159    protected static String escapeQueryCharsForLikeQuery(String string, Map<String, Object> contextualParameters)
160    {
161        contextualParameters.put(QueryBuilder.VALUE_IS_ESCAPED, Boolean.TRUE);
162        return ClientUtils.escapeQueryChars(string);
163    }
164    
165    /**
166     * A wrapper of a client-side criterion
167     */
168    public static class ClientSideCriterionWrapper
169    {
170        private String _id;
171        private Object _value;
172        private String _strOp;
173        private Map<String, Object> _otherProperties;
174
175        ClientSideCriterionWrapper(String id, Object value, String op, Map<String, Object> otherProperties)
176        {
177            _id = id;
178            _value = value;
179            _strOp = op;
180            _otherProperties = otherProperties;
181        }
182
183        /**
184         * Gets the id
185         * @return the id
186         */
187        public String getId()
188        {
189            return _id;
190        }
191
192        /**
193         * Gets the value
194         * @return the value
195         */
196        public Object getValue()
197        {
198            return _value;
199        }
200
201        /**
202         * Gets the string operator
203         * @return the string operator
204         */
205        public String getStringOperator()
206        {
207            return _strOp;
208        }
209
210        /**
211         * Gets the other properties
212         * @return the other properties
213         */
214        public Map<String, Object> getOtherProperties()
215        {
216            return _otherProperties;
217        }
218    }
219}