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}