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