/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.catalog.source.solr; import static org.apache.commons.lang.Validate.notNull; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; import org.apache.solr.client.solrj.SolrClient; import org.codice.ddf.configuration.PropertyResolver; import org.codice.solr.factory.impl.ConfigurationStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.data.ContentType; import ddf.catalog.filter.FilterAdapter; import ddf.catalog.operation.CreateRequest; import ddf.catalog.operation.CreateResponse; import ddf.catalog.operation.DeleteRequest; import ddf.catalog.operation.DeleteResponse; import ddf.catalog.operation.QueryRequest; import ddf.catalog.operation.SourceResponse; import ddf.catalog.operation.UpdateRequest; import ddf.catalog.operation.UpdateResponse; import ddf.catalog.source.CatalogProvider; import ddf.catalog.source.IngestException; import ddf.catalog.source.SourceMonitor; import ddf.catalog.source.UnsupportedQueryException; import ddf.catalog.util.impl.MaskableImpl; /** * Common base class for all remote Solr Catalog providers. Sub-classes need to implement the * {@link #createClient()} method and return a new {@code SolrClient}. */ public abstract class RemoteSolrCatalogProvider extends MaskableImpl implements CatalogProvider { private static final Logger LOGGER = LoggerFactory.getLogger(RemoteSolrCatalogProvider.class); private static final String PING_ERROR_MESSAGE = "Solr ping failed."; private static final String OK_STATUS = "OK"; private static final String DESCRIBABLE_PROPERTIES_FILE = "/describable.properties"; private static Properties describableProperties = new Properties(); static { try (InputStream inputStream = RemoteSolrCatalogProvider.class.getResourceAsStream( DESCRIBABLE_PROPERTIES_FILE)) { describableProperties.load(inputStream); } catch (IOException e) { LOGGER.info("Did not load properties properly.", e); } } protected static final String SOLR_CATALOG_CORE_NAME = "catalog"; private String url; private CatalogProvider provider = new UnavailableSolrCatalogProvider(); private SolrClient client; private FilterAdapter filterAdapter; private SolrFilterDelegateFactory solrFilterDelegateFactory; private DynamicSchemaResolver resolver; private Future<SolrClient> clientFuture; /** * Constructor. * * @param filterAdapter filter adaptor this provider will use * @param client client this provider will use to connect to Solr. If set to * {@code null}, a new client will be created using * {@link #createClient()} when needed. * @param solrFilterDelegateFactory Solr filter delegate factory this provider will use * @param resolver schema resolver this provider will use. A default schema * resolver will be used if this parameter is {@code null}. */ public RemoteSolrCatalogProvider(FilterAdapter filterAdapter, @Nullable SolrClient client, SolrFilterDelegateFactory solrFilterDelegateFactory, @Nullable DynamicSchemaResolver resolver) { notNull(filterAdapter, "FilterAdapter cannot be null"); notNull(solrFilterDelegateFactory, "SolrFilterDelegateFactory cannot be null"); this.filterAdapter = filterAdapter; this.client = client; this.solrFilterDelegateFactory = solrFilterDelegateFactory; this.resolver = (resolver == null) ? new DynamicSchemaResolver() : resolver; } /** * Constructor. Uses a default {@link DynamicSchemaResolver}. * * @param filterAdapter filter adaptor this provider will use * @param client client this provider will use to connect to Solr. If set to * {@code null}, a new client will be created using * {@link #createClient()} when needed. * @param solrFilterDelegateFactory Solr filter delegate factory this provider will use */ public RemoteSolrCatalogProvider(FilterAdapter filterAdapter, @Nullable SolrClient client, SolrFilterDelegateFactory solrFilterDelegateFactory) { this(filterAdapter, client, solrFilterDelegateFactory, null); } /** * Constructor. Creates and uses a default {@code SolrClient} and {@link DynamicSchemaResolver}. * * @param filterAdapter filter adaptor this provider will use * @param solrFilterDelegateFactory Solr filter delegate factory this provider will use */ public RemoteSolrCatalogProvider(FilterAdapter filterAdapter, SolrFilterDelegateFactory solrFilterDelegateFactory) { this(filterAdapter, null, solrFilterDelegateFactory, null); updateClient(); } @Override public void maskId(String id) { super.maskId(id); provider.maskId(id); } /** * Used to signal to the Solr client to commit on every transaction. Updates * the underlying {@link ConfigurationStore} so that the property is propagated * throughout the Solr Catalog Provider code. * * @param forceAutoCommit {@code true} to force auto-commits */ public void setForceAutoCommit(boolean forceAutoCommit) { ConfigurationStore.getInstance() .setForceAutoCommit(forceAutoCommit); } /** * Disables text path indexing for every subsequent update or insert. * * @param disableTextPath {@code true} to turn off text path indexing */ public void setDisableTextPath(boolean disableTextPath) { ConfigurationStore.getInstance() .setDisableTextPath(disableTextPath); } @Override public Set<ContentType> getContentTypes() { return getProvider().getContentTypes(); } @Override public boolean isAvailable() { return getProvider().isAvailable(); } @Override public boolean isAvailable(SourceMonitor callback) { return getProvider().isAvailable(callback); } @Override public SourceResponse query(QueryRequest queryRequest) throws UnsupportedQueryException { return getProvider().query(queryRequest); } @Override public String getDescription() { return describableProperties.getProperty("description", ""); } @Override public String getOrganization() { return describableProperties.getProperty("organization", ""); } @Override public String getTitle() { return describableProperties.getProperty("name", ""); } @Override public String getVersion() { return describableProperties.getProperty("version", ""); } @Override public CreateResponse create(CreateRequest createRequest) throws IngestException { return getProvider().create(createRequest); } @Override public DeleteResponse delete(DeleteRequest deleteRequest) throws IngestException { return getProvider().delete(deleteRequest); } @Override public UpdateResponse update(UpdateRequest updateRequest) throws IngestException { return getProvider().update(updateRequest); } /** * Shuts down the connection to Solr and releases resources. */ public void shutdown() { closeSolrClient(); } /** * Returns the current Solr server URL. The format of the URL may vary based on the current * Solr configuration (embedded, external or cloud). * * @return current Solr URL */ @Nullable public String getUrl() { return url; } /** * Sets Solr's URL. The format of the URL may vary based on the current Solr configuration, * e.g., embedded, external or cloud. See the {@code system.properties} configuration file * for the different options and formats. * <br/> * Changing the URL will trigger the creation of a new Solr client using {@link #createClient()}. * * @param url new Solr URL */ public void setUrl(@Nullable String url) { updateClient(PropertyResolver.resolveProperties(url)); } /** * Forces an update of the Solr client. */ protected void updateClient() { clientFuture = createClient(); client = null; } /** * Request the creation of a new {@code SolrClient}. * * @return {@code Future} used to retrieve the new {@code SolrClient} created */ protected abstract Future<SolrClient> createClient(); private void updateClient(@Nullable String urlValue) { LOGGER.debug("New url {}", urlValue); if (urlValue != null) { if (!StringUtils.equalsIgnoreCase(urlValue.trim(), url) || getClient() == null) { url = urlValue.trim(); if (getClient() != null) { LOGGER.debug( "Shutting down the connection manager to Solr and releasing allocated resources."); closeSolrClient(); LOGGER.debug("Shutdown complete."); } updateClient(); } } else { url = null; } } private void closeSolrClient() { LOGGER.debug("Closing connection to Solr client."); if (getClient() != null) { try { getClient().close(); } catch (IOException e) { LOGGER.info("Unable to close Solr client", e); } } else if (clientFuture != null && !clientFuture.isDone() && !clientFuture.isCancelled()) { clientFuture.cancel(true); } LOGGER.debug("Finished closing connection to Solr client."); } private CatalogProvider getProvider() { if (!isClientConnected(getClient())) { return new UnavailableSolrCatalogProvider(); } if (provider instanceof UnavailableSolrCatalogProvider) { provider = new SolrCatalogProvider(getClient(), filterAdapter, solrFilterDelegateFactory, resolver); } provider.maskId(getId()); return provider; } private boolean isClientConnected(SolrClient solr) { if (solr == null) { return false; } try { return OK_STATUS.equals(solr.ping() .getResponse() .get("status")); } catch (Exception e) { /* * if we get any type of exception, whether declared by Solr or not, we do not want to * fail, we just want to return false */ LOGGER.info(PING_ERROR_MESSAGE); LOGGER.debug(PING_ERROR_MESSAGE, e); } return false; } private SolrClient getClient() { if (client == null && clientFuture != null) { try { SolrClient solrClient = clientFuture.get(5, TimeUnit.SECONDS); if (solrClient == null) { // If we fail to get a SolrClient after all potential retries have been // exhausted, call updateClient() to keep trying. // See SolrClientFactory.newClient() for details. updateClient(); } return solrClient; } catch (InterruptedException | ExecutionException | TimeoutException e) { LOGGER.debug("Failed to get client from future", e); } } return client; } }