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