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 <V> The type of {@link WrappedValue} 098 * @param value The value 099 * @param stringOperator The operator as string 100 * @param toQuery A function applied to a transformed value 101 * (not necessarily the one given by the method, as it can be transformed by adding wildcard chars, escaping...) 102 * and a real {@link Operator} (deducted from the given client-side string operator) 103 * @param language The language 104 * @param contextualParameters The contextual parameters 105 * @return The result query 106 */ 107 // TODO have those fake client-side operators as real server-side operators 108 public <V extends WrappedValue> Query toQuery(V value, String stringOperator, BiFunction<V, Operator, Query> toQuery, String language, Map<String, Object> contextualParameters) 109 { 110 Query query = null; 111 String wdValue; 112 113 // Special wildcard searches. 114 switch (stringOperator) 115 { 116 case "is-empty": 117 case "is-not-empty": 118 query = toQuery.apply(value, Operator.EXISTS); 119 if ("is-empty".equals(stringOperator) && query != null) 120 { 121 query = new NotQuery(query); 122 } 123 break; 124 case "contains": 125 case "not-contains": 126 wdValue = "*" + escapeQueryCharsForLikeQuery((String) value.getValue(), contextualParameters) + "*"; 127 value.setValue(wdValue); 128 query = toQuery.apply(value, Operator.LIKE); 129 if ("not-contains".equals(stringOperator) && query != null) 130 { 131 query = new NotQuery(query); 132 } 133 break; 134 case "starts-with": 135 case "not-starts-with": 136 wdValue = escapeQueryCharsForLikeQuery((String) value.getValue(), contextualParameters) + "*"; 137 value.setValue(wdValue); 138 query = toQuery.apply(value, Operator.LIKE); 139 if ("not-starts-with".equals(stringOperator) && query != null) 140 { 141 query = new NotQuery(query); 142 } 143 break; 144 case "ends-with": 145 case "not-ends-with": 146 wdValue = "*" + escapeQueryCharsForLikeQuery((String) value.getValue(), contextualParameters); 147 value.setValue(wdValue); 148 query = toQuery.apply(value, Operator.LIKE); 149 if ("not-ends-with".equals(stringOperator) && query != null) 150 { 151 query = new NotQuery(query); 152 } 153 break; 154 case "search": 155 case "searchStemmed": 156 if (StringUtils.isNotBlank(stringOperator)) 157 { 158 wdValue = "*" + StringUtils.strip(((String) value.getValue()).trim(), "*") + "*"; 159 value.setValue(wdValue); 160 Operator operator = Operator.fromName(stringOperator); 161 query = toQuery.apply(value, operator); 162 } 163 break; 164 default: 165 if (StringUtils.isNotBlank(stringOperator)) 166 { 167 Operator operator = Operator.fromName(stringOperator); 168 query = toQuery.apply(value, operator); 169 } 170 } 171 172 return query; 173 } 174 175 /** 176 * Escape special Solr query characters of the user input, before building the query in order to append '*' character(s) 177 * @param string The string to escape. 178 * @param contextualParameters the search contextual parameters. 179 * @return The escaped string. 180 */ 181 protected static String escapeQueryCharsForLikeQuery(String string, Map<String, Object> contextualParameters) 182 { 183 contextualParameters.put(QueryBuilder.VALUE_IS_ESCAPED, Boolean.TRUE); 184 return ClientUtils.escapeQueryChars(string); 185 } 186 187 /** 188 * A wrapper of a client-side criterion 189 */ 190 public static class ClientSideCriterionWrapper 191 { 192 private String _id; 193 private Object _value; 194 private String _strOp; 195 private Map<String, Object> _otherProperties; 196 197 ClientSideCriterionWrapper(String id, Object value, String op, Map<String, Object> otherProperties) 198 { 199 _id = id; 200 _value = value; 201 _strOp = op; 202 _otherProperties = otherProperties; 203 } 204 205 /** 206 * Gets the id 207 * @return the id 208 */ 209 public String getId() 210 { 211 return _id; 212 } 213 214 /** 215 * Gets the value 216 * @return the value 217 */ 218 public Object getValue() 219 { 220 return _value; 221 } 222 223 /** 224 * Gets the string operator 225 * @return the string operator 226 */ 227 public String getStringOperator() 228 { 229 return _strOp; 230 } 231 232 /** 233 * Gets the other properties 234 * @return the other properties 235 */ 236 public Map<String, Object> getOtherProperties() 237 { 238 return _otherProperties; 239 } 240 } 241}