001/* 002 * Copyright 2018 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.web.frontoffice.search.instance.model; 017 018import java.lang.reflect.Constructor; 019import java.util.List; 020import java.util.stream.Collectors; 021import java.util.stream.Stream; 022 023import org.apache.commons.lang3.NotImplementedException; 024 025import org.ametys.cms.search.query.AndQuery; 026import org.ametys.cms.search.query.MatchAllQuery; 027import org.ametys.cms.search.query.MatchNoneQuery; 028import org.ametys.cms.search.query.NotQuery; 029import org.ametys.cms.search.query.OrQuery; 030import org.ametys.cms.search.query.Query; 031import org.ametys.cms.search.query.Query.LogicalOperator; 032import org.ametys.cms.search.query.Query.Operator; 033import org.ametys.cms.search.query.TagQuery; 034import org.ametys.web.frontoffice.search.metamodel.Returnable; 035import org.ametys.web.frontoffice.search.metamodel.context.ContextQueriesWrapper; 036import org.ametys.web.repository.page.Page; 037import org.ametys.web.repository.site.Site; 038import org.ametys.web.search.query.ChildPageQuery; 039import org.ametys.web.search.query.DescendantPageQuery; 040import org.ametys.web.search.query.SiteQuery; 041 042/** 043 * A search context 044 */ 045public class SearchContext 046{ 047 private SiteContext _sites; 048 private SitemapContext _sitemap; 049 private ContextLang _langs; 050 // keep id instead of real tag objects and then re-resolve them to avoid RepositoryException about session closed 051 private List<String> _tagIds; 052 private boolean _tagAutoposting; 053 054 /** 055 * Creates a SearchContext 056 * @param sites The site context 057 * @param sitemap The sitemap context 058 * @param langs The lang of the context 059 * @param tags The tags of the context 060 * @param tagAutoposting <code>true</code> if search on tags should be with autoposting 061 */ 062 public SearchContext(SiteContext sites, 063 SitemapContext sitemap, 064 ContextLang langs, 065 List<String> tags, 066 boolean tagAutoposting) 067 { 068 _sites = sites; 069 _sitemap = sitemap; 070 _langs = langs; 071 _tagIds = tags; 072 _tagAutoposting = tagAutoposting; 073 } 074 075 /** 076 * Gets the query corresponding to the site part of the context 077 * @param currentSite the current site 078 * @return the query corresponding to the site part of the context 079 */ 080 public Query getSiteQuery(Site currentSite) 081 { 082 String currentSiteName = currentSite.getName(); 083 switch (_sites.getType()) 084 { 085 case CURRENT: 086 // test is current 087 return new SiteQuery(Operator.EQ, currentSiteName); 088 089 case AMONG: 090 // test is one of sites of SiteContext 091 List<String> siteNames = _sites.getSites() 092 .get() 093 .stream() 094 .map(Site::getName) 095 .collect(Collectors.toList()); 096 return new SiteQuery(Operator.EQ, siteNames); 097 098 case ALL: 099 // test existence 100 return new SiteQuery(); 101 102 case OTHERS: 103 // test existence and is not current 104 return new AndQuery( 105 new SiteQuery(), 106 new SiteQuery(Operator.NE, currentSiteName)); 107 108 default: 109 throw new NotImplementedException("This SiteContextType is not handled: " + _sites.getType()); 110 } 111 } 112 113 /** 114 * Gets the query corresponding to the sitemap part of the context 115 * @param currentPage the current page 116 * @return the query corresponding to the sitemap part of the context 117 */ 118 public Query getSitemapQuery(Page currentPage) 119 { 120 Stream<Query> childPageQueriesStream; 121 switch (_sitemap.getType()) 122 { 123 case CURRENT_SITE: 124 // Do nothing more 125 return null; 126 127 case CHILD_PAGES: 128 return new DescendantPageQuery(currentPage.getId()); 129 case CHILD_PAGES_OF: 130 childPageQueriesStream = _sitemap.getPages().get() 131 .stream() 132 .map(Page::getId) 133 .map(ancestorPageId -> new DescendantPageQuery(ancestorPageId)); 134 break; 135 case DIRECT_CHILD_PAGES: 136 return new ChildPageQuery(currentPage.getId()); 137 case DIRECT_CHILD_PAGES_OF: 138 childPageQueriesStream = _sitemap.getPages().get() 139 .stream() 140 .map(Page::getId) 141 .map(parentPageId -> new ChildPageQuery(parentPageId)); 142 break; 143 default: 144 throw new NotImplementedException("This SitemapContextType is not handled: " + _sitemap.getType()); 145 } 146 147 // If no selected page, we do not want any result => in this special case, return a MatchNoneQuery 148 List<Query> childPageQueries = childPageQueriesStream.collect(Collectors.toList()); 149 return childPageQueries.isEmpty() ? new MatchNoneQuery() : new OrQuery(childPageQueries); 150 } 151 152 /** 153 * Gets the {@link ContextLang} and the current lang 154 * @param currentLang The current lang 155 * @return the wrapper of the {@link ContextLang} and the current lang 156 */ 157 public ContextLangAndCurrentLang getContextLang(String currentLang) 158 { 159 return new ContextLangAndCurrentLang(_langs, currentLang); 160 } 161 162 /** 163 * Class wrapping a {@link ContextLang} and the current lang. 164 * <br>Needed because at the time of the {@link ContextLangAndCurrentLang} constructor call, the Query cannot be created yet, even though the current lang is known (the {@link Returnable}s are responsible for creating the query via {@link LangQueryProducer}) 165 * @see LangQueryProducer 166 * @see Returnable#filterReturnedDocumentQuery 167 * @see ContextQueriesWrapper#getQuery 168 */ 169 public static final class ContextLangAndCurrentLang 170 { 171 ContextLang _contextLangs; 172 String _currentLang; 173 174 ContextLangAndCurrentLang(ContextLang contextLangs, String currentLang) 175 { 176 _contextLangs = contextLangs; 177 _currentLang = currentLang; 178 } 179 } 180 181 /** 182 * Class wrapping an implementation of {@link Query} in order to {@link #produce} the final query executed for limiting the context language for a given {@link Returnable}. 183 * <br>Instances of this class should be created only in {@link Returnable#filterReturnedDocumentQuery} methods, and then passed to {@link ContextQueriesWrapper#getQuery} 184 * @see Returnable#filterReturnedDocumentQuery 185 * @see ContextQueriesWrapper#getQuery 186 */ 187 public static final class LangQueryProducer 188 { 189 private Constructor< ? extends Query> _noArgConstructor; 190 private Constructor< ? extends Query> _opAndStrConstructor; 191 private boolean _acceptDocWithNolang; 192 193 /** 194 * Constructs a {@link LangQueryProducer} 195 * <br>The given class must have at least two constructors: 196 * <ul> 197 * <li>one with no arguments in order to test {@link Operator#EXISTS existence};</li> 198 * <li>one with an {@link Operator} and an array of {@link String} values in order to test {@link Operator#EQ equality} of the current language.</li> 199 * </ul> 200 * @param languageQueryClass The implementation of {@link Query} for testing the lang 201 * @param acceptDocWithNolang <code>true</code> to accept documents with no lang 202 */ 203 public LangQueryProducer(Class<? extends Query> languageQueryClass, boolean acceptDocWithNolang) 204 { 205 try 206 { 207 _noArgConstructor = languageQueryClass.getConstructor(); 208 _opAndStrConstructor = languageQueryClass.getConstructor(Operator.class, String[].class); 209 } 210 catch (NoSuchMethodException | SecurityException e) 211 { 212 throw new IllegalArgumentException("The provided class is not valid. It should have a no arg constructor, and a constructor with an Operator and String...", e); 213 } 214 _acceptDocWithNolang = acceptDocWithNolang; 215 } 216 217 /** 218 * Produces the Query for testing the context language 219 * @param contextLangAndCurrentLang The wrapper of the {@link ContextLang} and the real current language 220 * @return a {@link Query} 221 * @throws Exception if an error occurs 222 */ 223 public final Query produce(ContextLangAndCurrentLang contextLangAndCurrentLang) throws Exception 224 { 225 String currentLang = contextLangAndCurrentLang._currentLang; 226 ContextLang contextLang = contextLangAndCurrentLang._contextLangs; 227 228 Query equalityQuery = _opAndStrConstructor.newInstance(Operator.EQ, new String[] {currentLang}); 229 Query nonEqualityQuery = _opAndStrConstructor.newInstance(Operator.NE, new String[] {currentLang}); 230 Query existenceQuery = _noArgConstructor.newInstance(); 231 Query nonExistenceQuery = new NotQuery(existenceQuery); 232 233 switch (contextLang) 234 { 235 case CURRENT: 236 if (_acceptDocWithNolang) 237 { 238 // test is current, or not present 239 return new OrQuery(equalityQuery, nonExistenceQuery); 240 } 241 else 242 { 243 // test is current 244 return equalityQuery; 245 } 246 247 case OTHERS: 248 if (_acceptDocWithNolang) 249 { 250 // is not current (we do not care about existence) 251 return nonEqualityQuery; 252 } 253 else 254 { 255 // test existence and is not current 256 return new AndQuery(nonEqualityQuery, existenceQuery); 257 } 258 259 case ALL: 260 if (_acceptDocWithNolang) 261 { 262 // nothing to test, whether the doc has or not the lang field, it is accepted 263 return new MatchAllQuery(); 264 } 265 else 266 { 267 // test existence 268 return existenceQuery; 269 } 270 271 default: 272 throw new NotImplementedException("This ContextLang is not handled: " + contextLang); 273 } 274 } 275 } 276 277 /** 278 * Gets the query corresponding to the tag part of the context 279 * @return the query corresponding to the tag part of the context 280 */ 281 public Query getTagQuery() 282 { 283 return _tagIds.isEmpty() ? null : new TagQuery(Operator.EQ, _tagAutoposting, LogicalOperator.AND, _tagIds.toArray(new String[_tagIds.size()])); 284 } 285 286 @Override 287 public int hashCode() 288 { 289 final int prime = 31; 290 int result = 1; 291 result = prime * result + ((_langs == null) ? 0 : _langs.hashCode()); 292 result = prime * result + ((_sitemap == null) ? 0 : _sitemap.hashCode()); 293 result = prime * result + ((_sites == null) ? 0 : _sites.hashCode()); 294 result = prime * result + (_tagAutoposting ? 1231 : 1237); 295 result = prime * result + ((_tagIds == null) ? 0 : _tagIds.hashCode()); 296 return result; 297 } 298 299 @Override 300 public boolean equals(Object obj) 301 { 302 if (this == obj) 303 { 304 return true; 305 } 306 if (obj == null) 307 { 308 return false; 309 } 310 if (!(obj instanceof SearchContext)) 311 { 312 return false; 313 } 314 SearchContext other = (SearchContext) obj; 315 if (_langs != other._langs) 316 { 317 return false; 318 } 319 if (_sitemap == null) 320 { 321 if (other._sitemap != null) 322 { 323 return false; 324 } 325 } 326 else if (!_sitemap.equals(other._sitemap)) 327 { 328 return false; 329 } 330 if (_sites == null) 331 { 332 if (other._sites != null) 333 { 334 return false; 335 } 336 } 337 else if (!_sites.equals(other._sites)) 338 { 339 return false; 340 } 341 if (_tagAutoposting != other._tagAutoposting) 342 { 343 return false; 344 } 345 if (_tagIds == null) 346 { 347 if (other._tagIds != null) 348 { 349 return false; 350 } 351 } 352 else if (!_tagIds.equals(other._tagIds)) 353 { 354 return false; 355 } 356 return true; 357 } 358}