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}