001/*
002 *  Copyright 2016 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.Arrays;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Objects;
023import java.util.stream.Collectors;
024
025import org.apache.commons.lang3.StringUtils;
026import org.apache.solr.client.solrj.util.ClientUtils;
027
028import org.ametys.cms.search.query.join.JoinKey;
029import org.ametys.cms.search.solr.schema.SchemaHelper;
030import org.ametys.core.util.LambdaUtils;
031
032/**
033 * Represents a {@link Query} on a joined document.
034 */
035public class JoinQuery implements Query
036{
037    /** The field path. */
038    protected Query _subQuery;
039    
040    /** The join keys (paths and optional nested queries) */
041    protected Collection<JoinKey> _joinKeys;
042    
043    /**
044     * Build a join query.
045     * @param subQuery The sub query.
046     * @param joinPaths The field's join paths
047     */
048    public JoinQuery(Query subQuery, String... joinPaths)
049    {
050        this(subQuery, Arrays.asList(joinPaths));
051    }
052    
053    /**
054     * Build a join query.
055     * @param subQuery The sub query.
056     * @param joinPaths The field's join paths
057     */
058    public JoinQuery(Query subQuery, Collection<String> joinPaths)
059    {
060        this(joinPaths.stream().map(jp -> new JoinKey(jp, null)).collect(Collectors.toList()));
061        _subQuery = subQuery;
062    }
063    
064    /**
065     * Build a join query.
066     * @param joinKeys The join paths and optional nested queries
067     */
068    public JoinQuery(JoinKey... joinKeys)
069    {
070        this(Arrays.asList(joinKeys));
071    }
072    
073    /**
074     * Build a join query.
075     * @param joinKeys The join paths and optional nested queries
076     */
077    public JoinQuery(Collection<JoinKey> joinKeys)
078    {
079        _joinKeys = joinKeys;
080        _checkValidPathNames();
081        _checkJoinParams();
082    }
083    
084    private void _checkValidPathNames()
085    {
086        for (JoinKey joinKey : _joinKeys)
087        {
088            String path = joinKey.getKey();
089            if (!SchemaHelper.isNameValid(path))
090            {
091                throw new IllegalArgumentException("Invalid path name '" + path + "' in join.");
092            }
093        }
094    }
095    
096    private void _checkJoinParams()
097    {
098        if (_subQuery == null && _joinKeys.isEmpty())
099        {
100            throw new IllegalArgumentException("The join path is empty and there is no subquery in JoinQuery.");
101        }
102    }
103    
104    @Override
105    public String build() throws QuerySyntaxException
106    {
107        StringBuilder queryString = new StringBuilder();
108        
109        queryString.append("{!ametys join=\"");
110        boolean first = true;
111        for (JoinKey joinKey : _joinKeys)
112        {
113            if (!first)
114            {
115                queryString.append("->");
116            }
117            first = false;
118            String path = joinKey.getKey();
119            queryString.append(path);
120            joinKey.getNestedQuery()
121                   .map(LambdaUtils.wrap(this::_buildQuery))
122                   .filter(StringUtils::isNotBlank)
123                   .ifPresent(nq -> queryString.append('[').append(nq).append(']'));
124        }
125        queryString.append('"');
126        
127        if (_subQuery != null)
128        {
129            String subQuery = _buildQuery(_subQuery);
130            queryString.append(" q=\"")
131                       .append(subQuery)
132                       .append('"');
133        }
134        
135        queryString.append('}');
136        
137        return queryString.toString();
138    }
139    
140    private String _buildQuery(Query query) throws QuerySyntaxException
141    {
142        return ClientUtils.escapeQueryChars(query.build());
143    }
144    
145    @Override
146    public Map<String, Object> buildAsJson() throws QuerySyntaxException
147    {
148        StringBuilder joinString = new StringBuilder();
149        
150        boolean first = true;
151        for (JoinKey joinKey : _joinKeys)
152        {
153            if (!first)
154            {
155                joinString.append("->");
156            }
157            first = false;
158            String path = joinKey.getKey();
159            joinString.append(path);
160            joinKey.getNestedQuery()
161                   .map(LambdaUtils.wrap(q -> q.build()))
162                   .filter(StringUtils::isNotBlank)
163                   .ifPresent(nq -> joinString.append('[').append(nq).append(']'));
164        }
165        
166        Map<String, Object> joinMap = new HashMap<>();
167        
168        joinMap.put("join", joinString.toString());
169        
170        if (_subQuery != null)
171        {
172            joinMap.put("q", _subQuery.buildAsJson());
173        }
174        
175        return Map.of("ametys", joinMap);
176    }
177    
178    @Override
179    public String toString(int indent)
180    {
181        final String joinLineIndent = StringUtils.repeat(' ', indent);
182        final int subIndent = indent + 2;
183        final String subLineIndent = StringUtils.repeat(' ', subIndent);
184        final String join = subLineIndent + "[PATH]" + _joinKeys
185                .stream()
186                .map(JoinKey::toString)
187                .collect(Collectors.joining("->"))
188            + "[/PATH]";
189        final String subq = subLineIndent + "[Q]" + (_subQuery == null ? String.valueOf(_subQuery) : ("\n" + _subQuery.toString(subIndent + 2) + "\n" + subLineIndent)) + "[/Q]";
190        return joinLineIndent + "[JOIN]\n" + join + "\n" + subq + "\n" + joinLineIndent + "[/JOIN]";
191    }
192
193    @Override
194    public int hashCode()
195    {
196        return Objects.hash(_joinKeys, _subQuery);
197    }
198
199    @Override
200    public boolean equals(Object obj)
201    {
202        if (this == obj)
203        {
204            return true;
205        }
206        
207        if (obj == null || getClass() != obj.getClass())
208        {
209            return false;
210        }
211        
212        JoinQuery other = (JoinQuery) obj;
213        return Objects.equals(_joinKeys, other._joinKeys)
214                && Objects.equals(_subQuery, other._subQuery);
215    }
216}