001/* 002 * Copyright 2015 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.indexing.solr; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.List; 022import java.util.Map; 023 024import org.apache.avalon.framework.context.Context; 025import org.apache.avalon.framework.context.ContextException; 026import org.apache.avalon.framework.context.Contextualizable; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.avalon.framework.service.Serviceable; 030import org.apache.cocoon.components.ContextHelper; 031import org.apache.cocoon.environment.Request; 032import org.apache.solr.client.solrj.SolrClient; 033import org.apache.solr.client.solrj.SolrServerException; 034import org.apache.solr.client.solrj.response.UpdateResponse; 035 036import org.ametys.cms.content.archive.ArchiveConstants; 037import org.ametys.cms.content.indexing.solr.SolrFieldNames; 038import org.ametys.cms.content.indexing.solr.SolrIndexer; 039import org.ametys.cms.indexing.IndexingException; 040import org.ametys.cms.indexing.solr.AdditionalDataIndexer; 041import org.ametys.cms.indexing.solr.AdditionalDataIndexerExtensionPoint; 042import org.ametys.cms.search.query.AndQuery; 043import org.ametys.cms.search.query.OrQuery; 044import org.ametys.cms.search.query.Query; 045import org.ametys.cms.search.solr.SolrClientProvider; 046import org.ametys.core.schedule.progression.ContainerProgressionTracker; 047import org.ametys.core.schedule.progression.ProgressionTrackerFactory; 048import org.ametys.core.schedule.progression.SimpleProgressionTracker; 049import org.ametys.plugins.repository.AmetysObjectIterable; 050import org.ametys.plugins.repository.AmetysRepositoryException; 051import org.ametys.plugins.repository.RepositoryConstants; 052import org.ametys.plugins.repository.UnknownAmetysObjectException; 053import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 054import org.ametys.runtime.i18n.I18nizableText; 055import org.ametys.runtime.plugin.component.AbstractLogEnabled; 056import org.ametys.web.WebConstants; 057import org.ametys.web.indexing.SiteIndexer; 058import org.ametys.web.repository.page.Page; 059import org.ametys.web.repository.site.Site; 060import org.ametys.web.repository.site.SiteManager; 061import org.ametys.web.repository.sitemap.Sitemap; 062import org.ametys.web.search.query.SiteQuery; 063import org.ametys.web.search.query.SitemapQuery; 064 065/** 066 * Solr implementation of {@link SiteIndexer}. 067 */ 068public class SolrSiteIndexer extends AbstractLogEnabled implements SiteIndexer, Serviceable, Contextualizable 069{ 070 /** The site manager. */ 071 protected SiteManager _siteManager; 072 /** The solr indexer. */ 073 protected SolrIndexer _solrIndexer; 074 /** The solr page indexer. */ 075 protected SolrPageIndexer _solrPageIndexer; 076 /** The site document provider handler. */ 077 protected SiteDocumentProviderExtensionPoint _siteDocProviderEP; 078 /** The Solr client provider */ 079 protected SolrClientProvider _solrClientProvider; 080 /** The additional data indexer ep */ 081 protected AdditionalDataIndexerExtensionPoint _additionalDataIndexerExtensionPoint; 082 083 private Context _context; 084 085 @Override 086 public void contextualize(Context context) throws ContextException 087 { 088 _context = context; 089 } 090 091 @Override 092 public void service(ServiceManager manager) throws ServiceException 093 { 094 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 095 _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE); 096 _solrPageIndexer = (SolrPageIndexer) manager.lookup(SolrPageIndexer.ROLE); 097 _siteDocProviderEP = (SiteDocumentProviderExtensionPoint) manager.lookup(SiteDocumentProviderExtensionPoint.ROLE); 098 _solrClientProvider = (SolrClientProvider) manager.lookup(SolrClientProvider.ROLE); 099 _additionalDataIndexerExtensionPoint = (AdditionalDataIndexerExtensionPoint) manager.lookup(AdditionalDataIndexerExtensionPoint.ROLE); 100 } 101 102 @Override 103 public void indexSite(Site site) throws IndexingException 104 { 105 indexSite(site, ProgressionTrackerFactory.createContainerProgressionTracker("Index site '" + site.getName() + "'", getLogger())); 106 } 107 108 @Override 109 public void indexSite(Site site, ContainerProgressionTracker progressionTracker) throws IndexingException 110 { 111 indexSite(site.getName(), progressionTracker); 112 } 113 114 @Override 115 public void indexSite(String siteName) throws IndexingException 116 { 117 indexSite(siteName, ProgressionTrackerFactory.createContainerProgressionTracker("Index site '" + siteName + "'", getLogger())); 118 } 119 120 /** 121 * Index a site in all workspaces 122 * @param siteName the name of the site to index. 123 * @param progressionTracker The progression of the indexation 124 * @throws IndexingException If an error occurs while indexing the site. 125 */ 126 public void indexSite(String siteName, ContainerProgressionTracker progressionTracker) throws IndexingException 127 { 128 progressionTracker.addContainerStep(RepositoryConstants.DEFAULT_WORKSPACE, new I18nizableText("plugin.web", "PLUGINS_WEB_SCHEDULABLE_SITE_INDEXATION_STEPS_LABEL", List.of(RepositoryConstants.DEFAULT_WORKSPACE))); 129 progressionTracker.addContainerStep(WebConstants.LIVE_WORKSPACE, new I18nizableText("plugin.web", "PLUGINS_WEB_SCHEDULABLE_SITE_INDEXATION_STEPS_LABEL", List.of(WebConstants.LIVE_WORKSPACE))); 130 progressionTracker.addContainerStep(ArchiveConstants.ARCHIVE_WORKSPACE, new I18nizableText("plugin.web", "PLUGINS_WEB_SCHEDULABLE_SITE_INDEXATION_STEPS_LABEL", List.of(ArchiveConstants.ARCHIVE_WORKSPACE))); 131 132 indexSite(siteName, RepositoryConstants.DEFAULT_WORKSPACE, (ContainerProgressionTracker) progressionTracker.getStep(RepositoryConstants.DEFAULT_WORKSPACE)); 133 indexSite(siteName, WebConstants.LIVE_WORKSPACE, (ContainerProgressionTracker) progressionTracker.getStep(WebConstants.LIVE_WORKSPACE)); 134 indexSite(siteName, ArchiveConstants.ARCHIVE_WORKSPACE, (ContainerProgressionTracker) progressionTracker.getStep(ArchiveConstants.ARCHIVE_WORKSPACE)); 135 } 136 137 @Override 138 public void indexSite(String siteName, String workspaceName) throws IndexingException 139 { 140 indexSite(siteName, workspaceName, ProgressionTrackerFactory.createContainerProgressionTracker("Index site '" + siteName + "' for workspace " + workspaceName, getLogger())); 141 } 142 143 @Override 144 public void indexSite(String siteName, String workspaceName, ContainerProgressionTracker progressionTracker) throws IndexingException 145 { 146 SolrClient solrClient = _solrClientProvider.getUpdateClient(workspaceName, false); 147 _indexSite(siteName, workspaceName, solrClient, true, progressionTracker); 148 } 149 150 @Override 151 public void indexSite(String siteName, String workspaceName, SolrClient solrClient, ContainerProgressionTracker progressionTracker) throws IndexingException 152 { 153 // Pass false for commit as caller provided a SolrClient and thus will take care of commit operation (if it is a NoAutoCommit Solr client) 154 boolean commit = false; 155 _indexSite(siteName, workspaceName, solrClient, commit, progressionTracker); 156 } 157 158 private void _createProgressionTrackerStepsForSubIndexSite(ContainerProgressionTracker progressionTracker, boolean commit) throws AmetysRepositoryException 159 { 160 progressionTracker.addSimpleStep("unindexing", new I18nizableText("plugin.cms", "PLUGINS_CMS_SCHEDULER_GLOBAL_INDEXATION_UNINDEXING_DOCUMENTS_STEP_LABEL")); 161 162 progressionTracker.addSimpleStep("contents", new I18nizableText("plugin.cms", "PLUGINS_CMS_SCHEDULER_GLOBAL_INDEXATION_CONTENT_STEP_LABEL")); 163 164 progressionTracker.addSimpleStep("sitemaps", new I18nizableText("plugin.web", "PLUGINS_WEB_SCHEDULABLE_SITE_INDEXATION_SITEMAPS_STEP_LABEL")); 165 166 progressionTracker.addSimpleStep("resources", new I18nizableText("plugin.cms", "PLUGINS_CMS_SCHEDULER_GLOBAL_INDEXATION_RESOURCES_STEP_LABEL")); 167 168 for (String docProviderId : _siteDocProviderEP.getExtensionsIds()) 169 { 170 SiteDocumentProvider extension = _siteDocProviderEP.getExtension(docProviderId); 171 progressionTracker.addContainerStep("documents-" + docProviderId, new I18nizableText("plugin.cms", "PLUGINS_CMS_SCHEDULER_GLOBAL_INDEXATION_DOCUMENTS_PROVIDER_STEPS_LABEL", Map.of("0", extension.getLabel()))); 172 } 173 174 if (commit) 175 { 176 progressionTracker.addSimpleStep("commit", new I18nizableText("plugin.cms", "PLUGINS_CMS_SCHEDULER_GLOBAL_INDEXATION_SAVING_STEP_LABEL")); 177 } 178 } 179 180 private void _indexSite(String siteName, String workspaceName, SolrClient solrClient, boolean commit, ContainerProgressionTracker progressionTracker) throws IndexingException 181 { 182 Request request = ContextHelper.getRequest(_context); 183 184 // Retrieve the current workspace. 185 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 186 // Retrieve the current site name. 187 String currentSiteName = (String) request.getAttribute("siteName"); 188 189 try 190 { 191 // Force the workspace. 192 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 193 194 // Get the site in the given workspace. 195 Site site = null; 196 try 197 { 198 site = _siteManager.getSite(siteName); 199 } 200 catch (UnknownAmetysObjectException e) 201 { 202 // Site might not exist in the desired workspace (archive for example) 203 return; 204 } 205 206 _createProgressionTrackerStepsForSubIndexSite(progressionTracker, commit); 207 208 // Set the site name in the request. 209 request.setAttribute("siteName", siteName); 210 211 getLogger().info("Indexing site '{}' in workspace '{}'", siteName, workspaceName); 212 213 // Delete all documents from this site in current workspace 214 _unindexSiteDocuments(siteName, workspaceName, solrClient, progressionTracker.getStep("unindexing")); 215 216 // Index the site's contents in current workspace 217 _indexContents(site, workspaceName, solrClient, progressionTracker.getStep("contents")); 218 219 // Index the site's sitemaps and pages in current workspace 220 _indexSitemaps(site, workspaceName, solrClient, progressionTracker.getStep("sitemaps")); 221 222 // Index the site's resources in current workspace 223 _indexResources(site, workspaceName, solrClient, progressionTracker.getStep("resources")); 224 225 // Add additional site documents 226 for (String docProviderId : _siteDocProviderEP.getExtensionsIds()) 227 { 228 SiteDocumentProvider docProvider = _siteDocProviderEP.getExtension(docProviderId); 229 230 getLogger().debug("Indexing additional documents for site {} with provider {}", siteName, docProvider); 231 232 docProvider.indexSiteDocuments(site, workspaceName, solrClient, progressionTracker.getStep("documents-" + docProviderId)); 233 } 234 235 if (commit) 236 { 237 _solrIndexer.commit(workspaceName, solrClient); 238 ((SimpleProgressionTracker) progressionTracker.getStep("commit")).increment(); 239 } 240 } 241 catch (Exception e) 242 { 243 String error = String.format("Failed to index site %s in workspace %s", siteName, workspaceName); 244 getLogger().error(error, e); 245 throw new IndexingException(error, e); 246 } 247 finally 248 { 249 // Restore the site name. 250 request.setAttribute("siteName", currentSiteName); 251 // Restore context 252 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 253 } 254 } 255 256 @Override 257 public void indexSitemap(Sitemap sitemap) throws IndexingException 258 { 259 indexSitemap(sitemap.getSiteName(), sitemap.getName()); 260 } 261 262 @Override 263 public void indexSitemap(String siteName, String sitemapName) throws IndexingException 264 { 265 indexSitemap(siteName, sitemapName, RepositoryConstants.DEFAULT_WORKSPACE); 266 indexSitemap(siteName, sitemapName, WebConstants.LIVE_WORKSPACE); 267 } 268 269 @Override 270 public void indexSitemap(String siteName, String sitemapName, String workspaceName) throws IndexingException 271 { 272 Request request = ContextHelper.getRequest(_context); 273 274 // Retrieve the current workspace. 275 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 276 277 try 278 { 279 // Force the workspace. 280 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 281 request.setAttribute("siteName", siteName); 282 SolrClient solrClient = _solrClientProvider.getUpdateClient(workspaceName, false); 283 284 getLogger().info("Indexing sitemap '{}' of site '{}' in workspace '{}'", sitemapName, siteName, workspaceName); 285 286 _indexSitemap(siteName, sitemapName, workspaceName, solrClient); 287 288 _solrIndexer.commit(workspaceName, solrClient); 289 } 290 catch (Exception e) 291 { 292 String error = String.format("Failed to index sitemap '%s' for site '%s' and workspace '%s'", sitemapName, siteName, workspaceName); 293 getLogger().error(error, e); 294 throw new IndexingException(error, e); 295 } 296 finally 297 { 298 // Restore context 299 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 300 } 301 } 302 303 @Override 304 public void unindexSite(String siteName) throws IndexingException 305 { 306 unindexSite(siteName, RepositoryConstants.DEFAULT_WORKSPACE); 307 unindexSite(siteName, WebConstants.LIVE_WORKSPACE); 308 unindexSite(siteName, ArchiveConstants.ARCHIVE_WORKSPACE); 309 } 310 311 @Override 312 public void unindexSite(String siteName, String workspaceName) throws IndexingException 313 { 314 Request request = ContextHelper.getRequest(_context); 315 316 // Retrieve the current workspace. 317 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 318 319 try 320 { 321 // Force the workspace. 322 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 323 SolrClient solrClient = _solrClientProvider.getUpdateClient(workspaceName, true); 324 325 getLogger().info("Unindexing site '{}' in workspace {}", siteName, workspaceName); 326 327 _unindexSiteDocuments(siteName, workspaceName, solrClient); 328 } 329 catch (Exception e) 330 { 331 String error = String.format("Failed to unindex the site %s for workspace %s", siteName, workspaceName); 332 getLogger().error(error, e); 333 throw new IndexingException(error, e); 334 } 335 finally 336 { 337 // Restore context 338 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 339 } 340 } 341 342 @Override 343 public void unindexSitemap(String siteName, String sitemapName) throws IndexingException 344 { 345 unindexSitemap(siteName, sitemapName, RepositoryConstants.DEFAULT_WORKSPACE); 346 unindexSitemap(siteName, sitemapName, WebConstants.LIVE_WORKSPACE); 347 } 348 349 @Override 350 public void unindexSitemap(String siteName, String sitemapName, String workspaceName) throws IndexingException 351 { 352 Request request = ContextHelper.getRequest(_context); 353 354 // Retrieve the current workspace. 355 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 356 357 try 358 { 359 // Force the workspace. 360 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 361 SolrClient solrClient = _solrClientProvider.getUpdateClient(workspaceName); 362 363 getLogger().info("Unindexing sitemap '{}' of site '{}' in workspace '{}'", sitemapName, siteName, workspaceName); 364 365 _unindexSitemapDocuments(siteName, sitemapName, workspaceName, solrClient); 366 } 367 catch (Exception e) 368 { 369 String error = String.format("Failed to unindex sitemap '%s' for site '%s' and workspace '%s'", sitemapName, siteName, workspaceName); 370 getLogger().error(error, e); 371 throw new IndexingException(error, e); 372 } 373 finally 374 { 375 // Restore context 376 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 377 } 378 } 379 380 private void _indexSitemaps(Site site, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception 381 { 382 // The sitemap node may not exist if site was created but not yet configured 383 if (site.getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps")) 384 { 385 386 AmetysObjectIterable<Sitemap> sitemaps = site.getSitemaps(); 387 long nbOfSitemaps = sitemaps.getSize(); 388 389 progressionTracker.setSize(nbOfSitemaps); 390 391 for (Sitemap sitemap : sitemaps) 392 { 393 getLogger().info("Indexing sitemap {} started", sitemap.getName()); 394 395 long start = System.currentTimeMillis(); 396 397 _indexSitemap(site.getName(), sitemap.getName(), workspaceName, solrClient); 398 399 long end = System.currentTimeMillis(); 400 401 getLogger().info("Indexing sitemap {} ended in {} ms", sitemap.getName(), end - start); 402 403 progressionTracker.increment(); 404 } 405 } 406 } 407 408 private void _indexSitemap(String siteName, String sitemapName, String workspaceName, SolrClient solrClient) throws Exception 409 { 410 // Get the sitemap in the given workspace. 411 Sitemap sitemap = _siteManager.getSite(siteName).getSitemap(sitemapName); 412 413 // First delete the directory if exists 414 _unindexSitemapDocuments(siteName, sitemapName, workspaceName, solrClient); 415 416 // Index pages of this sitemap 417 AmetysObjectIterable<? extends Page> children = sitemap.getChildrenPages(); 418 for (Page page : children) 419 { 420 // Index page recursively, without committing. 421 _solrPageIndexer.indexPage(page.getId(), workspaceName, true, true, solrClient); 422 } 423 424 // Add additional sitemap documents 425 for (String docProviderId : _siteDocProviderEP.getExtensionsIds()) 426 { 427 SiteDocumentProvider docProvider = _siteDocProviderEP.getExtension(docProviderId); 428 429 getLogger().debug("Indexing additional documents for sitemap {} with provider {}", sitemapName, docProvider); 430 431 docProvider.indexSitemapDocuments(sitemap, workspaceName, solrClient); 432 } 433 } 434 435 private void _unindexSiteDocuments(String siteName, String workspaceName, SolrClient solrClient) throws SolrServerException, IOException, Exception 436 { 437 _unindexSiteDocuments(siteName, workspaceName, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Unindex site document for site '" + siteName + "' for workspace " + workspaceName, getLogger())); 438 } 439 440 private void _unindexSiteDocuments(String siteName, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws SolrServerException, IOException, Exception 441 { 442 List<Query> queries = new ArrayList<>(); 443 444 queries.add(new SiteQuery(siteName)); 445 446 // Calling additional for "content"... 447 // Should also call for "page", but currently not indexing with it 448 Collection<AdditionalDataIndexer> indexers = _additionalDataIndexerExtensionPoint.getIndexers("content"); 449 for (AdditionalDataIndexer indexer : indexers) 450 { 451 if (indexer instanceof AdditionalWebDataIndexer awdIndexer) 452 { 453 queries.add(awdIndexer.getUnindexSiteQuery("content", siteName)); 454 } 455 } 456 457 Query query = new OrQuery(queries); 458 459 // delete 460 String collectionName = _solrClientProvider.getCollectionName(workspaceName); 461 UpdateResponse solrResponse = solrClient.deleteByQuery(collectionName, query.build()); 462 int status = solrResponse.getStatus(); 463 464 if (status != 0) 465 { 466 throw new IOException("Ametys Site de-indexing - Expecting status code of '0' in the Solr response but got : '" + status + "'. Site name : " + siteName); 467 } 468 469 getLogger().debug("Successful site de-indexing. Site identifier : {}", siteName); 470 471 progressionTracker.increment(); 472 } 473 474 private void _unindexSitemapDocuments(String siteName, String sitemapName, String workspace, SolrClient solrClient) throws SolrServerException, IOException, Exception 475 { 476 List<Query> queries = new ArrayList<>(); 477 478 queries.add(new AndQuery(new SiteQuery(siteName), new SitemapQuery(sitemapName))); 479 480 Query query = new OrQuery(queries); 481 482 // delete 483 String collectionName = _solrClientProvider.getCollectionName(workspace); 484 UpdateResponse solrResponse = solrClient.deleteByQuery(collectionName, query.build()); 485 int status = solrResponse.getStatus(); 486 487 if (status != 0) 488 { 489 throw new IOException("Ametys sitemap de-indexing - Expecting status code of '0' in the Solr response but got : '" + status + "'. Sitemap name : " + siteName + "/" + sitemapName); 490 } 491 492 getLogger().debug("Successful sitemap de-indexing. Sitemap name : {}/{}", siteName, sitemapName); 493 } 494 495 /** 496 * Index the contents of a site. 497 * @param site The site to index. 498 * @param workspaceName The workspace name 499 * @param solrClient The solr client to use 500 * @param progressionTracker The progression of the indexation 501 * @throws Exception If an error occurs indexing the contents. 502 */ 503 protected void _indexContents(Site site, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception 504 { 505 _solrIndexer.indexContents(site.getContents(), workspaceName, true, solrClient, progressionTracker); 506 } 507 508 /** 509 * Index the resources of a site. 510 * @param site The site to index. 511 * @param workspaceName The workspace name 512 * @param solrClient The solr client to use 513 * @param progressionTracker The progression of the indexation 514 * @throws Exception If an error occurs indexing the resources. 515 */ 516 protected void _indexResources(Site site, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception 517 { 518 try 519 { 520 _solrIndexer.indexResources(site.getResources(), SolrFieldNames.TYPE_RESOURCE, site.getRootResources(), workspaceName, solrClient, progressionTracker); 521 } 522 catch (UnknownAmetysObjectException e) 523 { 524 // Ignore if the resource root is not present. 525 progressionTracker.increment(); 526 } 527 } 528}