/*
* Copyright 2012 - 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.solr.core;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.SolrPingResponse;
import org.apache.solr.client.solrj.response.UpdateResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.solr.SolrRealtimeGetRequest;
import org.springframework.data.solr.UncategorizedSolrException;
import org.springframework.data.solr.VersionUtil;
import org.springframework.data.solr.core.QueryParserBase.NamedObjectsFacetQuery;
import org.springframework.data.solr.core.QueryParserBase.NamedObjectsHighlightQuery;
import org.springframework.data.solr.core.QueryParserBase.NamedObjectsQuery;
import org.springframework.data.solr.core.convert.MappingSolrConverter;
import org.springframework.data.solr.core.convert.SolrConverter;
import org.springframework.data.solr.core.mapping.SimpleSolrMappingContext;
import org.springframework.data.solr.core.mapping.SolrPersistentEntity;
import org.springframework.data.solr.core.mapping.SolrPersistentProperty;
import org.springframework.data.solr.core.query.FacetQuery;
import org.springframework.data.solr.core.query.HighlightQuery;
import org.springframework.data.solr.core.query.Query;
import org.springframework.data.solr.core.query.SolrDataQuery;
import org.springframework.data.solr.core.query.TermsQuery;
import org.springframework.data.solr.core.query.result.Cursor;
import org.springframework.data.solr.core.query.result.DelegatingCursor;
import org.springframework.data.solr.core.query.result.FacetPage;
import org.springframework.data.solr.core.query.result.GroupPage;
import org.springframework.data.solr.core.query.result.HighlightPage;
import org.springframework.data.solr.core.query.result.ScoredPage;
import org.springframework.data.solr.core.query.result.SolrResultPage;
import org.springframework.data.solr.core.query.result.StatsPage;
import org.springframework.data.solr.core.query.result.TermsPage;
import org.springframework.data.solr.core.query.result.TermsResultPage;
import org.springframework.data.solr.core.schema.SolrJsonResponse;
import org.springframework.data.solr.core.schema.SolrPersistentEntitySchemaCreator;
import org.springframework.data.solr.core.schema.SolrPersistentEntitySchemaCreator.Feature;
import org.springframework.data.solr.core.schema.SolrSchemaRequest;
import org.springframework.data.solr.server.SolrServerFactory;
import org.springframework.data.solr.server.support.HttpSolrServerFactory;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Implementation of {@link SolrOperations}
*
* @author Christoph Strobl
* @author Joachim Uhrlass
* @author Francisco Spaeth
*/
public class SolrTemplate implements SolrOperations, InitializingBean, ApplicationContextAware {
private static final Logger LOGGER = LoggerFactory.getLogger(SolrTemplate.class);
private static final PersistenceExceptionTranslator EXCEPTION_TRANSLATOR = new SolrExceptionTranslator();
private final QueryParsers queryParsers = new QueryParsers();
private MappingContext<? extends SolrPersistentEntity<?>, SolrPersistentProperty> mappingContext;
private ApplicationContext applicationContext;
private String solrCore;
@SuppressWarnings("serial") private static final List<String> ITERABLE_CLASSES = new ArrayList<String>() {
{
add(List.class.getName());
add(Collection.class.getName());
add(Iterator.class.getName());
}
};
private SolrServerFactory solrServerFactory;
private SolrConverter solrConverter;
private Set<Feature> schemaCreationFeatures;
public SolrTemplate(SolrServer solrServer) {
this(solrServer, null);
}
public SolrTemplate(SolrServer solrServer, String core) {
this(new HttpSolrServerFactory(solrServer, core));
this.solrCore = core;
}
public SolrTemplate(SolrServerFactory solrServerFactory) {
this(solrServerFactory, null);
}
public SolrTemplate(SolrServerFactory solrServerFactory, SolrConverter solrConverter) {
Assert.notNull(solrServerFactory, "SolrServerFactory must not be 'null'.");
Assert.notNull(solrServerFactory.getSolrServer(), "SolrServerFactory has to return a SolrServer.");
this.solrServerFactory = solrServerFactory;
}
@Override
public <T> T execute(SolrCallback<T> action) {
Assert.notNull(action);
try {
SolrServer solrServer = this.getSolrServer();
return action.doInSolr(solrServer);
} catch (Exception e) {
DataAccessException resolved = getExceptionTranslator().translateExceptionIfPossible(
new RuntimeException(e.getMessage(), e));
throw resolved == null ? new UncategorizedSolrException(e.getMessage(), e) : resolved;
}
}
@Override
public SolrPingResponse ping() {
return execute(new SolrCallback<SolrPingResponse>() {
@Override
public SolrPingResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.ping();
}
});
}
@Override
public long count(final SolrDataQuery query) {
Assert.notNull(query, "Query must not be 'null'.");
return execute(new SolrCallback<Long>() {
@Override
public Long doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
SolrQuery solrQuery = queryParsers.getForClass(query.getClass()).constructSolrQuery(query);
solrQuery.setStart(0);
solrQuery.setRows(0);
return solrServer.query(solrQuery).getResults().getNumFound();
}
});
}
@Override
public UpdateResponse saveBean(Object obj) {
return saveBean(obj, -1);
}
@Override
public UpdateResponse saveBean(final Object objectToAdd, final int commitWithinMs) {
assertNoCollection(objectToAdd);
return execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.add(convertBeanToSolrInputDocument(objectToAdd), commitWithinMs);
}
});
}
@Override
public UpdateResponse saveBeans(Collection<?> beans) {
return saveBeans(beans, -1);
}
@Override
public UpdateResponse saveBeans(final Collection<?> beansToAdd, final int commitWithinMs) {
return execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.add(convertBeansToSolrInputDocuments(beansToAdd), commitWithinMs);
}
});
}
@Override
public UpdateResponse saveDocument(SolrInputDocument document) {
return saveDocument(document, -1);
}
@Override
public UpdateResponse saveDocument(final SolrInputDocument documentToAdd, final int commitWithinMs) {
return execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.add(documentToAdd, commitWithinMs);
}
});
}
@Override
public UpdateResponse saveDocuments(Collection<SolrInputDocument> documents) {
return saveDocuments(documents, -1);
}
@Override
public UpdateResponse saveDocuments(final Collection<SolrInputDocument> documentsToAdd, final int commitWithinMs) {
return execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.add(documentsToAdd, commitWithinMs);
}
});
}
@Override
public UpdateResponse delete(SolrDataQuery query) {
Assert.notNull(query, "Query must not be 'null'.");
final String queryString = this.queryParsers.getForClass(query.getClass()).getQueryString(query);
return execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.deleteByQuery(queryString);
}
});
}
@Override
public UpdateResponse deleteById(final String id) {
Assert.notNull(id, "Cannot delete 'null' id.");
return execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.deleteById(id);
}
});
}
@Override
public UpdateResponse deleteById(Collection<String> ids) {
Assert.notNull(ids, "Cannot delete 'null' collection.");
final List<String> toBeDeleted = new ArrayList<String>(ids);
return execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.deleteById(toBeDeleted);
}
});
}
@Override
public <T> T queryForObject(Query query, Class<T> clazz) {
Assert.notNull(query, "Query must not be 'null'.");
Assert.notNull(clazz, "Target class must not be 'null'.");
query.setPageRequest(new PageRequest(0, 1));
QueryResponse response = query(query);
if (response.getResults().size() > 0) {
if (response.getResults().size() > 1) {
LOGGER.warn("More than 1 result found for singe result query ('{}'), returning first entry in list");
}
return (T) convertSolrDocumentListToBeans(response.getResults(), clazz).get(0);
}
return null;
}
private <T> SolrResultPage<T> doQueryForPage(Query query, Class<T> clazz) {
NamedObjectsQuery namedObjectsQuery = new NamedObjectsQuery(query);
QueryResponse response = query(namedObjectsQuery);
Map<String, Object> objectsName = namedObjectsQuery.getNamesAssociation();
return createSolrResultPage(query, clazz, response, objectsName);
}
@Override
public <T> ScoredPage<T> queryForPage(Query query, Class<T> clazz) {
Assert.notNull(query, "Query must not be 'null'.");
Assert.notNull(clazz, "Target class must not be 'null'.");
return doQueryForPage(query, clazz);
}
@Override
public <T> GroupPage<T> queryForGroupPage(Query query, Class<T> clazz) {
Assert.notNull(query, "Query must not be 'null'.");
Assert.notNull(clazz, "Target class must not be 'null'.");
return doQueryForPage(query, clazz);
}
/*
* (non-Javadoc)
* @see org.springframework.data.solr.core.SolrOperations#queryForStatsPage(org.springframework.data.solr.core.query.Query, java.lang.Class)
*/
@Override
public <T> StatsPage<T> queryForStatsPage(Query query, Class<T> clazz) {
Assert.notNull(query, "Query must not be 'null'.");
Assert.notNull(clazz, "Target class must not be 'null'.");
return doQueryForPage(query, clazz);
}
@Override
public <T> FacetPage<T> queryForFacetPage(FacetQuery query, Class<T> clazz) {
Assert.notNull(query, "Query must not be 'null'.");
Assert.notNull(clazz, "Target class must not be 'null'.");
NamedObjectsFacetQuery namedObjectsQuery = new NamedObjectsFacetQuery(query);
QueryResponse response = query(namedObjectsQuery);
Map<String, Object> objectsName = namedObjectsQuery.getNamesAssociation();
SolrResultPage<T> page = createSolrResultPage(query, clazz, response, objectsName);
page.addAllFacetFieldResultPages(ResultHelper.convertFacetQueryResponseToFacetPageMap(query, response));
page.addAllFacetPivotFieldResult(ResultHelper.convertFacetQueryResponseToFacetPivotMap(query, response));
page.setFacetQueryResultPage(ResultHelper.convertFacetQueryResponseToFacetQueryResult(query, response));
return page;
}
@Override
public <T> HighlightPage<T> queryForHighlightPage(HighlightQuery query, Class<T> clazz) {
Assert.notNull(query, "Query must not be 'null'.");
Assert.notNull(clazz, "Target class must not be 'null'.");
NamedObjectsHighlightQuery namedObjectsQuery = new NamedObjectsHighlightQuery(query);
QueryResponse response = query(namedObjectsQuery);
Map<String, Object> objectsName = namedObjectsQuery.getNamesAssociation();
SolrResultPage<T> page = createSolrResultPage(query, clazz, response, objectsName);
ResultHelper.convertAndAddHighlightQueryResponseToResultPage(response, page);
return page;
}
private <T> SolrResultPage<T> createSolrResultPage(Query query, Class<T> clazz, QueryResponse response,
Map<String, Object> objectsName) {
List<T> beans = convertQueryResponseToBeans(response, clazz);
SolrDocumentList results = response.getResults();
long numFound = results == null ? 0 : results.getNumFound();
Float maxScore = results == null ? null : results.getMaxScore();
Pageable pageRequest = query.getPageRequest();
SolrResultPage<T> page = new SolrResultPage<T>(beans, pageRequest, numFound, maxScore);
page.setFieldStatsResults(ResultHelper.convertFieldStatsInfoToFieldStatsResultMap(response.getFieldStatsInfo()));
page.setGroupResults(ResultHelper.convertGroupQueryResponseToGroupResultMap(query, objectsName, response, this,
clazz));
return page;
}
@Override
public TermsPage queryForTermsPage(TermsQuery query) {
Assert.notNull(query, "Query must not be 'null'.");
QueryResponse response = query(query);
TermsResultPage page = new TermsResultPage();
page.addAllTerms(ResultHelper.convertTermsQueryResponseToTermsMap(response));
return page;
}
final QueryResponse query(SolrDataQuery query) {
Assert.notNull(query, "Query must not be 'null'");
SolrQuery solrQuery = queryParsers.getForClass(query.getClass()).constructSolrQuery(query);
LOGGER.debug("Executing query '" + solrQuery + "' against solr.");
return executeSolrQuery(solrQuery);
}
final QueryResponse executeSolrQuery(final SolrQuery solrQuery) {
return execute(new SolrCallback<QueryResponse>() {
@Override
public QueryResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.query(solrQuery);
}
});
}
@Override
public void commit() {
execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.commit();
}
});
}
@Override
public void softCommit() {
if (VersionUtil.isSolr3XAvailable()) {
throw new UnsupportedOperationException(
"Soft commit is not available for solr version lower than 4.x - Please check your depdendencies.");
}
execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.commit(true, true, true);
}
});
}
@Override
public void rollback() {
execute(new SolrCallback<UpdateResponse>() {
@Override
public UpdateResponse doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
return solrServer.rollback();
}
});
}
@Override
public SolrInputDocument convertBeanToSolrInputDocument(Object bean) {
if (bean instanceof SolrInputDocument) {
return (SolrInputDocument) bean;
}
SolrInputDocument document = new SolrInputDocument();
getConverter().write(bean, document);
return document;
}
/**
* @param collectionName
* @return
* @since 1.3
*/
public String getSchemaName(String collectionName) {
return execute(new SolrCallback<String>() {
@Override
public String doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
SolrJsonResponse response = SolrSchemaRequest.name().process(solrServer);
if (response != null) {
return response.getNode("name").asText();
}
return null;
}
});
}
/*
* (non-Javadoc)
* @see org.springframework.data.solr.core.SolrOperations#queryForCursor(org.springframework.data.solr.core.query.Query, java.lang.Class)
*/
public <T> Cursor<T> queryForCursor(Query query, final Class<T> clazz) {
return new DelegatingCursor<T>(queryParsers.getForClass(query.getClass()).constructSolrQuery(query)) {
@Override
protected org.springframework.data.solr.core.query.result.DelegatingCursor.PartialResult<T> doLoad(
SolrQuery nativeQuery) {
QueryResponse response = executeSolrQuery(nativeQuery);
if (response == null) {
return new PartialResult<T>("", Collections.<T> emptyList());
}
return new PartialResult<T>(response.getNextCursorMark(), convertQueryResponseToBeans(response, clazz));
}
}.open();
}
@Override
public <T> Collection<T> getById(final Collection<? extends Serializable> ids, final Class<T> clazz) {
if (CollectionUtils.isEmpty(ids)) {
return Collections.emptyList();
}
return execute(new SolrCallback<Collection<T>>() {
@Override
public Collection<T> doInSolr(SolrServer solrServer) throws SolrServerException, IOException {
QueryResponse response = new SolrRealtimeGetRequest(ids).process(solrServer);
return convertSolrDocumentListToBeans(response.getResults(), clazz);
}
});
}
@Override
public <T> T getById(Serializable id, Class<T> clazz) {
Assert.notNull(id, "Id must not be 'null'.");
Collection<T> result = getById(Collections.singletonList(id), clazz);
if (result.isEmpty()) {
return null;
}
return result.iterator().next();
}
private Collection<SolrInputDocument> convertBeansToSolrInputDocuments(Iterable<?> beans) {
if (beans == null) {
return Collections.emptyList();
}
List<SolrInputDocument> resultList = new ArrayList<SolrInputDocument>();
for (Object bean : beans) {
resultList.add(convertBeanToSolrInputDocument(bean));
}
return resultList;
}
public <T> List<T> convertQueryResponseToBeans(QueryResponse response, Class<T> targetClass) {
return response != null ? convertSolrDocumentListToBeans(response.getResults(), targetClass) : Collections
.<T> emptyList();
}
public <T> List<T> convertSolrDocumentListToBeans(SolrDocumentList documents, Class<T> targetClass) {
if (documents == null) {
return Collections.<T> emptyList();
}
return getConverter().read(documents, targetClass);
}
public <T> T convertSolrDocumentToBean(SolrDocument document, Class<T> targetClass) {
return getConverter().read(targetClass, document);
}
protected void assertNoCollection(Object o) {
if (null != o && (o.getClass().isArray() || ITERABLE_CLASSES.contains(o.getClass().getName()))) {
throw new IllegalArgumentException("Collections are not supported for this operation");
}
}
private final SolrConverter getDefaultSolrConverter() {
MappingSolrConverter converter = new MappingSolrConverter(this.mappingContext);
converter.afterPropertiesSet(); // have to call this one to initialize default converters
return converter;
}
@Override
public final SolrServer getSolrServer() {
return solrServerFactory.getSolrServer(this.solrCore);
}
@Override
public SolrConverter getConverter() {
return this.solrConverter;
}
public static PersistenceExceptionTranslator getExceptionTranslator() {
return EXCEPTION_TRANSLATOR;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public void registerQueryParser(Class<? extends SolrDataQuery> clazz, QueryParser queryParser) {
this.queryParsers.registerParser(clazz, queryParser);
}
public void setSolrConverter(SolrConverter solrConverter) {
this.solrConverter = solrConverter;
}
public String getSolrCore() {
return solrCore;
}
public void setSolrCore(String solrCore) {
this.solrCore = solrCore;
}
@Override
public void afterPropertiesSet() {
if (this.mappingContext == null) {
this.mappingContext = new SimpleSolrMappingContext(
new SolrPersistentEntitySchemaCreator(this.solrServerFactory).enable(this.schemaCreationFeatures));
}
if (this.solrConverter == null) {
this.solrConverter = getDefaultSolrConverter();
}
registerPersistenceExceptionTranslator();
}
private void registerPersistenceExceptionTranslator() {
if (this.applicationContext != null
&& this.applicationContext.getBeansOfType(PersistenceExceptionTranslator.class).isEmpty()) {
if (this.applicationContext instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) this.applicationContext).getBeanFactory().registerSingleton(
"solrExceptionTranslator", EXCEPTION_TRANSLATOR);
}
}
}
/**
* @since 1.3
* @param mappingContext
*/
public void setMappingContext(MappingContext<? extends SolrPersistentEntity<?>, SolrPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
}
/**
* @since 1.3
* @param schemaCreationFeatures
*/
public void setSchemaCreationFeatures(Collection<Feature> schemaCreationFeatures) {
this.schemaCreationFeatures = new HashSet<Feature>(schemaCreationFeatures);
}
/**
* @since 1.3
* @return
*/
public Set<Feature> getSchemaCreationFeatures() {
if (CollectionUtils.isEmpty(this.schemaCreationFeatures)) {
return Collections.emptySet();
}
return Collections.unmodifiableSet(this.schemaCreationFeatures);
}
}