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