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 The 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}