001/*
002 *  Copyright 2022 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.query;
017
018import java.util.List;
019import java.util.Map;
020import java.util.Objects;
021import java.util.Optional;
022
023import org.apache.solr.client.solrj.util.ClientUtils;
024
025/**
026 * Base class for multivalued queries.
027 * @param <T> the value type.
028 */
029public abstract class AbstractMultivaluedQuery<T> extends AbstractOperatorQuery<List<T>>
030{
031    private LogicalOperator _logicalOperator; 
032    
033    /**
034     * Create a new Query
035     * @param fieldName the Solr field name
036     * @param operator the {@link org.ametys.cms.search.query.Query.Operator}
037     * @param logicalOperator the logical operator, default is LogicalOperator.OR
038     * @param values the values
039     */
040    public AbstractMultivaluedQuery(String fieldName, Operator operator, LogicalOperator logicalOperator, List<T> values)
041    {
042        super(fieldName, operator, values);
043        _logicalOperator = Optional.ofNullable(logicalOperator).orElse(LogicalOperator.OR);
044        
045        if (Operator.EQ != operator && Operator.NE != operator && Operator.EXISTS != operator)
046        {
047            throw new IllegalArgumentException("Test operator '" + operator + "' is unknown for test's expression.");
048        }
049    }
050    
051    /**
052     * Computes the value for Solr client
053     * @param value the typed value
054     * @return the value, adapted for Solr
055     */
056    public String singleValueForQuery(T value)
057    {
058        return ClientUtils.escapeQueryChars(value.toString());
059    }
060    
061    @Override
062    public String build() throws QuerySyntaxException
063    {
064        List<T> values = getValue();
065        StringBuilder query = new StringBuilder();
066
067        if (getOperator() == Operator.EXISTS)
068        {
069            query.append(getFieldName()).append(":").append(QueryHelper.EXISTS_VALUE);
070            
071            return query.toString();
072        }
073        
074        if (getOperator() == Operator.NE)
075        {
076            NotQuery.appendNegation(query);
077        }
078        
079        if (values.contains(""))
080        {
081            query.append(getFieldName()).append(":*");
082        }
083        else
084        {
085            int count = values.size();
086            if (count > 1)
087            {
088                query.append('(');
089            }
090
091            boolean first = true;
092            for (T v : values)
093            {
094                if (!first)
095                {
096                    query.append(" " + _logicalOperator + " ");
097                }
098                
099                query.append(getFieldName()).append(':').append(singleValueForQuery(v));
100                first = false;
101            }
102            
103            if (count > 1)
104            {
105                query.append(')');
106            }
107        }
108        
109        return query.toString();
110    }
111
112    public Optional<Object> buildAsJson() throws QuerySyntaxException
113    {
114        if (getOperator() == Operator.EXISTS)
115        {
116            return Optional.of(getFieldName() + ":" + QueryHelper.EXISTS_VALUE);
117        }
118        
119        List<String> clauses = getValue().stream().map(v -> getFieldName() + ":" + singleValueForQuery(v)).toList();
120        
121        if (clauses.isEmpty())
122        {
123            return new MatchAllQuery().buildAsJson();
124        }
125        else if (clauses.size() == 1)
126        {
127            return Optional.of(clauses.get(0));
128        }
129        
130        return Optional.of(Map.of("bool", Map.of(getOperator() == Operator.EQ ? "should" : "must_not", clauses)));
131    }
132    
133    @Override
134    public int hashCode()
135    {
136        return 31 * super.hashCode() + Objects.hash(_logicalOperator);
137    }
138
139    @Override
140    public boolean equals(Object obj)
141    {
142        return super.equals(obj) && Objects.equals(_logicalOperator, ((AbstractMultivaluedQuery) obj)._logicalOperator);
143    }
144}