diff --git a/.gitignore b/.gitignore index 317a4d820..4a42a6315 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ atlassian-ide-plugin.xml -.svn/ -data/ +.svn spring-data-elasticsearch.iml spring-data-elasticsearch.ipr spring-data-elasticsearch.iws -target/ \ No newline at end of file +target \ No newline at end of file diff --git a/src/main/java/org/springframework/data/elasticsearch/ElasticsearchException.java b/src/main/java/org/springframework/data/elasticsearch/ElasticsearchException.java new file mode 100644 index 000000000..12b4bf2cf --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/ElasticsearchException.java @@ -0,0 +1,31 @@ +package org.springframework.data.elasticsearch; + + +import java.util.Map; + +public class ElasticsearchException extends RuntimeException{ + + private Map failedDocuments; + + public ElasticsearchException(String message) { + super(message); + } + + public ElasticsearchException(String message, Throwable cause) { + super(message, cause); + } + + public ElasticsearchException(String message, Throwable cause, Map failedDocuments) { + super(message, cause); + this.failedDocuments = failedDocuments; + } + + public ElasticsearchException(String message, Map failedDocuments) { + super(message); + this.failedDocuments = failedDocuments; + } + + public Map getFailedDocuments() { + return failedDocuments; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java new file mode 100644 index 000000000..e12bdc807 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java @@ -0,0 +1,16 @@ +package org.springframework.data.elasticsearch.annotations; + + +import org.springframework.data.annotation.Persistent; + +import java.lang.annotation.*; + +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface Document { + + String indexName() default ""; + String type() default ""; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java new file mode 100644 index 000000000..2f6ba84bf --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java @@ -0,0 +1,25 @@ +package org.springframework.data.elasticsearch.annotations; + + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Query { + + /** + * Elasticsearch query to be used when executing query. May contain placeholders eg. ?0 + * + * @return + */ + String value() default ""; + + /** + * Named Query Named looked up by repository. + * + * @return + */ + String name() default ""; + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/NodeClientFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/client/NodeClientFactoryBean.java new file mode 100644 index 000000000..24dcf6436 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/NodeClientFactoryBean.java @@ -0,0 +1,45 @@ +package org.springframework.data.elasticsearch.client; + +import org.elasticsearch.client.Client; +import org.elasticsearch.client.node.NodeClient; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + +import static org.elasticsearch.node.NodeBuilder.nodeBuilder; + +public class NodeClientFactoryBean implements FactoryBean, InitializingBean{ + + private boolean local; + private NodeClient nodeClient; + + NodeClientFactoryBean() { + } + + public NodeClientFactoryBean(boolean local) { + this.local = local; + } + + @Override + public NodeClient getObject() throws Exception { + return nodeClient; + } + + @Override + public Class getObjectType() { + return NodeClient.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public void afterPropertiesSet() throws Exception { + nodeClient = (NodeClient) nodeBuilder().local(this.local).node().client(); + } + + public void setLocal(boolean local) { + this.local = local; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/TransportClientFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/client/TransportClientFactoryBean.java new file mode 100644 index 000000000..fa15d69b7 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/TransportClientFactoryBean.java @@ -0,0 +1,89 @@ +package org.springframework.data.elasticsearch.client; + +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.InetSocketTransportAddress; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +import java.util.Properties; + +import static org.apache.commons.lang.StringUtils.substringAfter; +import static org.apache.commons.lang.StringUtils.substringBefore; +import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; + + +public class TransportClientFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + private static final Logger logger = LoggerFactory.getLogger(TransportClientFactoryBean.class); + private String[] clusterNodes; + private TransportClient client; + private Properties properties; + static final String COLON = ":"; + + @Override + public void destroy() throws Exception { + try { + logger.info("Closing elasticSearch client"); + if (client != null) { + client.close(); + } + } catch (final Exception e) { + logger.error("Error closing ElasticSearch client: ", e); + } + } + + @Override + public TransportClient getObject() throws Exception { + return client; + } + + @Override + public Class getObjectType() { + return TransportClient.class; + } + + @Override + public boolean isSingleton() { + return false; + } + + @Override + public void afterPropertiesSet() throws Exception { + buildClient(); + } + + protected void buildClient() throws Exception { + client = new TransportClient(settings()); + Assert.notEmpty(clusterNodes,"[Assertion failed] clusterNodes settings missing."); + for (String clusterNode : clusterNodes) { + String hostName = substringBefore(clusterNode, COLON); + String port = substringAfter(clusterNode, COLON); + Assert.hasText(hostName,"[Assertion failed] missing host name in 'clusterNodes'"); + Assert.hasText(port,"[Assertion failed] missing port in 'clusterNodes'"); + logger.info("adding transport node : " + clusterNode); + client.addTransportAddress(new InetSocketTransportAddress(hostName, Integer.valueOf(port))); + } + client.connectedNodes(); + } + + private Settings settings(){ + if(properties != null){ + return settingsBuilder().put(properties).build(); + } + return settingsBuilder() + .put("client.transport.sniff",true).build(); + } + + public void setClusterNodes(String[] clusterNodes) { + this.clusterNodes = clusterNodes; + } + + public void setProperties(Properties properties) { + this.properties = properties; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandler.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandler.java new file mode 100644 index 000000000..c87577c6f --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandler.java @@ -0,0 +1,20 @@ +package org.springframework.data.elasticsearch.config; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; +import org.springframework.data.elasticsearch.repository.config.ElasticsearchRepositoryConfigExtension; +import org.springframework.data.repository.config.RepositoryBeanDefinitionParser; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + + +public class ElasticsearchNamespaceHandler extends NamespaceHandlerSupport{ + + @Override + public void init() { + RepositoryConfigurationExtension extension = new ElasticsearchRepositoryConfigExtension(); + RepositoryBeanDefinitionParser parser = new RepositoryBeanDefinitionParser(extension); + + registerBeanDefinitionParser("repositories", parser); + registerBeanDefinitionParser("node-client", new NodeClientBeanDefinitionParser()); + registerBeanDefinitionParser("transport-client", new TransportClientBeanDefinitionParser()); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/config/NodeClientBeanDefinitionParser.java b/src/main/java/org/springframework/data/elasticsearch/config/NodeClientBeanDefinitionParser.java new file mode 100644 index 000000000..f41ff0142 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/config/NodeClientBeanDefinitionParser.java @@ -0,0 +1,31 @@ +package org.springframework.data.elasticsearch.config; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.data.elasticsearch.client.NodeClientFactoryBean; +import org.w3c.dom.Element; + + +public class NodeClientBeanDefinitionParser extends AbstractBeanDefinitionParser { + + @Override + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(NodeClientFactoryBean.class); + setLocalSettings(element,builder); + return getSourcedBeanDefinition(builder, element, parserContext); + } + + private void setLocalSettings(Element element, BeanDefinitionBuilder builder) { + builder.addPropertyValue("local", Boolean.valueOf(element.getAttribute("local"))); + } + + + private AbstractBeanDefinition getSourcedBeanDefinition(BeanDefinitionBuilder builder, Element source, + ParserContext context) { + AbstractBeanDefinition definition = builder.getBeanDefinition(); + definition.setSource(context.extractSource(source)); + return definition; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/config/TransportClientBeanDefinitionParser.java b/src/main/java/org/springframework/data/elasticsearch/config/TransportClientBeanDefinitionParser.java new file mode 100644 index 000000000..6e78fa052 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/config/TransportClientBeanDefinitionParser.java @@ -0,0 +1,34 @@ +package org.springframework.data.elasticsearch.config; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.data.elasticsearch.client.TransportClientFactoryBean; +import org.w3c.dom.Element; + +import static org.apache.commons.lang.StringUtils.split; + + +public class TransportClientBeanDefinitionParser extends AbstractBeanDefinitionParser { + + private static final String SEPARATOR_CHARS = ","; + + @Override + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(TransportClientFactoryBean.class); + setClusterNodes(element, builder); + return getSourcedBeanDefinition(builder,element, parserContext); + } + + private void setClusterNodes(Element element, BeanDefinitionBuilder builder){ + builder.addPropertyValue("clusterNodes", split(element.getAttribute("cluster-nodes"), SEPARATOR_CHARS)); + } + + private AbstractBeanDefinition getSourcedBeanDefinition(BeanDefinitionBuilder builder, Element source, + ParserContext context) { + AbstractBeanDefinition definition = builder.getBeanDefinition(); + definition.setSource(context.extractSource(source)); + return definition; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java new file mode 100644 index 000000000..716df5af9 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012 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.elasticsearch.core; + +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.BoostableQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.util.Assert; + +import java.util.Iterator; +import java.util.ListIterator; + +import static org.elasticsearch.index.query.QueryBuilders.*; +import static org.springframework.data.elasticsearch.core.query.Criteria.OperationKey; + +class CriteriaQueryProcessor { + + + QueryBuilder createQueryFromCriteria(Criteria criteria) { + BoolQueryBuilder query = boolQuery(); + + ListIterator chainIterator = criteria.getCriteriaChain().listIterator(); + while (chainIterator.hasNext()) { + Criteria chainedCriteria = chainIterator.next(); + if(chainedCriteria.isOr()){ + query.should(createQueryFragmentForCriteria(chainedCriteria)); + }else if(chainedCriteria.isNegating()){ + query.mustNot(createQueryFragmentForCriteria(chainedCriteria)); + }else{ + query.must(createQueryFragmentForCriteria(chainedCriteria)); + } + } + + return query; + } + + + private QueryBuilder createQueryFragmentForCriteria(Criteria chainedCriteria) { + Iterator it = chainedCriteria.getCriteriaEntries().iterator(); + boolean singeEntryCriteria = (chainedCriteria.getCriteriaEntries().size() == 1); + + String fieldName = chainedCriteria.getField().getName(); + Assert.notNull(fieldName,"Unknown field"); + QueryBuilder query = null; + + if(singeEntryCriteria){ + Criteria.CriteriaEntry entry = it.next(); + query = processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName); + }else{ + query = boolQuery(); + while (it.hasNext()){ + Criteria.CriteriaEntry entry = it.next(); + ((BoolQueryBuilder)query).must(processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName)); + } + } + + addBoost(query, chainedCriteria.getBoost()); + return query; + } + + + private QueryBuilder processCriteriaEntry(OperationKey key, Object value, String fieldName) { + if (value == null) { + return null; + } + QueryBuilder query = null; + + switch (key){ + case EQUALS: + query = fieldQuery(fieldName, value); break; + case CONTAINS: + query = fieldQuery(fieldName,"*" + value + "*").analyzeWildcard(true); break; + case STARTS_WITH: + query = fieldQuery(fieldName,value +"*").analyzeWildcard(true); break; + case ENDS_WITH: + query = fieldQuery(fieldName, "*"+value).analyzeWildcard(true); break; + case EXPRESSION: + query = queryString((String)value).field(fieldName); break; + case BETWEEN: + Object[] ranges = (Object[]) value; + query = rangeQuery(fieldName).from(ranges[0]).to(ranges[1]); break; + case FUZZY: + query = fuzzyQuery(fieldName, (String) value); break; + + } + + return query; + } + + private QueryBuilder buildNegationQuery(String fieldName, Iterator it){ + BoolQueryBuilder notQuery = boolQuery(); + while (it.hasNext()){ + notQuery.mustNot(fieldQuery(fieldName, it.next().getValue())); + } + return notQuery; + } + + private void addBoost(QueryBuilder query, float boost){ + if(Float.isNaN(boost)){ + return; + } + if(query instanceof BoostableQueryBuilder){ + ((BoostableQueryBuilder)query).boost(boost); + } + + } + + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java new file mode 100644 index 000000000..6198cf816 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java @@ -0,0 +1,148 @@ +package org.springframework.data.elasticsearch.core; + + +import org.elasticsearch.action.bulk.BulkResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.data.elasticsearch.core.query.*; + +import java.util.List; + +public interface ElasticsearchOperations { + + + /** + * @return Converter in use + */ + ElasticsearchConverter getElasticsearchConverter(); + + /** + * Create an index + * @param clazz + * @param + */ + boolean createIndex(Class clazz); + + /** + * Execute the query against elasticsearch and return the first returned object + * + * @param query + * @param clazz + * @return the first matching object + */ + T queryForObject(GetQuery query, Class clazz); + + /** + * Execute the query against elasticsearch and return the first returned object + * + * @param query + * @param clazz + * @return the first matching object + */ + T queryForObject(CriteriaQuery query, Class clazz); + + + /** + * Execute the query against elasticsearch and return the first returned object + * + * @param query + * @param clazz + * @return the first matching object + */ + T queryForObject(StringQuery query, Class clazz); + + + /** + * Execute the query against elasticsearch and return result as {@link Page} + * + * @param query + * @param clazz + * @return + */ + Page queryForPage(SearchQuery query, Class clazz); + + /** + * Execute the query against elasticsearch and return result as {@link Page} + * + * @param query + * @param clazz + * @return + */ + Page queryForPage(CriteriaQuery query, Class clazz); + + + /** + * Execute the query against elasticsearch and return result as {@link Page} + * + * @param query + * @param clazz + * @return + */ + Page queryForPage(StringQuery query, Class clazz); + + + /** + * return number of elements found by for given query + * + * @param query + * @param clazz + * @return + */ + long count(SearchQuery query, Class clazz); + + /** + * Index an object. Will do save or update + * + * @param query + * @return returns the document id + */ + String index(IndexQuery query); + + /** + * Bulk index all objects. Will do save or update + * + * @param queries + */ + void bulkIndex(List queries); + + /** + * Delete the one object with provided id + * + * @param indexName + * @param type + * @param id + * @return documentId of the document deleted + */ + String delete(String indexName, String type, String id); + + /** + * Delete the one object with provided id + * + * @param clazz + * @param id + * @return documentId of the document deleted + */ + String delete(Class clazz, String id); + + /** + * Delete all records matching the query + * @param clazz + * @param query + */ + void delete(DeleteQuery query, Class clazz); + + /** + * refresh the index + * @param indexName + * @param waitForOperation + */ + void refresh(String indexName,boolean waitForOperation); + + /** + * refresh the index + * @param clazz + * @param waitForOperation + */ + void refresh(Class clazz,boolean waitForOperation); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java new file mode 100644 index 000000000..90595f81f --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -0,0 +1,267 @@ +package org.springframework.data.elasticsearch.core; + +import org.codehaus.jackson.map.DeserializationConfig; +import org.codehaus.jackson.map.ObjectMapper; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequestBuilder; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.count.CountRequestBuilder; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.Requests; +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.sort.SortOrder; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.ElasticsearchException; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.elasticsearch.core.query.*; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.apache.commons.lang.StringUtils.isBlank; +import static org.elasticsearch.action.search.SearchType.DFS_QUERY_THEN_FETCH; +import static org.elasticsearch.client.Requests.indicesExistsRequest; +import static org.elasticsearch.client.Requests.refreshRequest; + + +public class ElasticsearchTemplate implements ElasticsearchOperations { + + private Client client; + private ElasticsearchConverter elasticsearchConverter; + + private ObjectMapper objectMapper = new ObjectMapper(); + + { + objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public ElasticsearchTemplate(Client client) { + this(client, null); + } + + public ElasticsearchTemplate(Client client, ElasticsearchConverter elasticsearchConverter) { + this.client = client; + this.elasticsearchConverter = (elasticsearchConverter == null)? new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext()) : elasticsearchConverter ; + } + + + @Override + public boolean createIndex(Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + return createIndexIfNotCreated(persistentEntity.getIndexName()); + } + + @Override + public ElasticsearchConverter getElasticsearchConverter() { + return elasticsearchConverter; + } + + @Override + public T queryForObject(GetQuery query, Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + GetResponse response = client.prepareGet(persistentEntity.getIndexName(), persistentEntity.getIndexType(), query.getId()) + .execute().actionGet(); + return mapResult(response.getSourceAsString(), clazz); + } + + @Override + public T queryForObject(CriteriaQuery query, Class clazz) { + Page page = queryForPage(query,clazz); + Assert.isTrue(page.getTotalElements() < 2, "Expected 1 but found "+ page.getTotalElements() +" results"); + return page.getTotalElements() > 0? page.getContent().get(0) : null; + } + + @Override + public T queryForObject(StringQuery query, Class clazz) { + Page page = queryForPage(query,clazz); + Assert.isTrue(page.getTotalElements() < 2, "Expected 1 but found "+ page.getTotalElements() +" results"); + return page.getTotalElements() > 0? page.getContent().get(0) : null; + } + + @Override + public Page queryForPage(SearchQuery query, Class clazz) { + SearchRequestBuilder searchRequestBuilder = prepareSearch(query,clazz); + if(query.getElasticsearchFilter() != null){ + searchRequestBuilder.setFilter(query.getElasticsearchFilter()); + } + SearchResponse response = searchRequestBuilder.setQuery(query.getElasticsearchQuery()).execute().actionGet(); + return mapResults(response, clazz, query.getPageable()); + } + + @Override + public Page queryForPage(CriteriaQuery query, Class clazz) { + QueryBuilder elasticsearchQuery = new CriteriaQueryProcessor().createQueryFromCriteria(query.getCriteria()); + SearchResponse response = prepareSearch(query,clazz) + .setQuery(elasticsearchQuery) + .execute().actionGet(); + return mapResults(response, clazz, query.getPageable()); + } + + @Override + public Page queryForPage(StringQuery query, Class clazz) { + SearchResponse response = prepareSearch(query,clazz) + .setQuery(query.getSource()) + .execute().actionGet(); + return mapResults(response, clazz, query.getPageable()); + } + + @Override + public long count(SearchQuery query, Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + CountRequestBuilder countRequestBuilder = client.prepareCount(persistentEntity.getIndexName()) + .setTypes(persistentEntity.getIndexType()); + if(query.getElasticsearchQuery() != null){ + countRequestBuilder.setQuery(query.getElasticsearchQuery()); + } + return countRequestBuilder.execute().actionGet().count(); + } + + @Override + public String index(IndexQuery query) { + return prepareIndex(query) + .execute() + .actionGet().getId(); + } + + @Override + public void bulkIndex(List queries) { + BulkRequestBuilder bulkRequest = client.prepareBulk(); + for(IndexQuery query : queries){ + bulkRequest.add(prepareIndex(query)); + } + BulkResponse bulkResponse = bulkRequest.execute().actionGet(); + if (bulkResponse.hasFailures()) { + Map failedDocuments = new HashMap(); + for (BulkItemResponse item : bulkResponse.items()) { + if (item.failed()) + failedDocuments.put(item.getId(), item.failureMessage()); + } + throw new ElasticsearchException("Bulk indexing has failures. Use ElasticsearchException.getFailedDocuments() for detailed messages [" + failedDocuments+"]", failedDocuments); + } + } + + @Override + public String delete(String indexName, String type, String id) { + return client.prepareDelete(indexName, type, id) + .execute().actionGet().getId(); + } + + @Override + public String delete(Class clazz, String id) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + return delete(persistentEntity.getIndexName(), persistentEntity.getIndexType(), id); + } + + @Override + public void delete(DeleteQuery query, Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + client.prepareDeleteByQuery(persistentEntity.getIndexName()) + .setTypes(persistentEntity.getIndexType()) + .setQuery(query.getElasticsearchQuery()) + .execute().actionGet(); + } + + private boolean createIndexIfNotCreated(String indexName) { + return indexExists(indexName) || createIndex(indexName); + } + + private boolean indexExists(String indexName) { + return client.admin() + .indices() + .exists(indicesExistsRequest(indexName)).actionGet().exists(); + } + + private boolean createIndex(String indexName) { + return client.admin().indices().create(Requests.createIndexRequest(indexName). + settings(new MapBuilder().put("index.refresh_interval", "-1").map())).actionGet().acknowledged(); + } + + private SearchRequestBuilder prepareSearch(Query query, Class clazz){ + int startRecord=0; + if(query.getPageable() != null){ + startRecord = ((query.getPageable().getPageNumber() - 1) * query.getPageable().getPageSize()); + } + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + SearchRequestBuilder searchRequestBuilder = client.prepareSearch(persistentEntity.getIndexName()) + .setSearchType(DFS_QUERY_THEN_FETCH) + .setTypes(persistentEntity.getIndexType()) + .setFrom(startRecord < 0 ? 0 : startRecord) + .setSize(query.getPageable() != null ? query.getPageable().getPageSize() : 10); + + if(query.getSort() != null){ + for(Sort.Order order : query.getSort()){ + searchRequestBuilder.addSort(order.getProperty(), order.getDirection() == Sort.Direction.DESC? SortOrder.DESC : SortOrder.ASC); + } + } + return searchRequestBuilder; + } + + private IndexRequestBuilder prepareIndex(IndexQuery query){ + try { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(query.getObject().getClass()); + return client.prepareIndex(persistentEntity.getIndexName(), persistentEntity.getIndexType(), query.getId()) + .setSource(objectMapper.writeValueAsString(query.getObject())); + } catch (IOException e) { + throw new ElasticsearchException("failed to index the document [id: " + query.getId() +"]",e); + } + } + + public void refresh(String indexName, boolean waitForOperation) { + client.admin().indices() + .refresh(refreshRequest(indexName).waitForOperations(waitForOperation)).actionGet(); + } + + public void refresh(Class clazz, boolean waitForOperation) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + client.admin().indices() + .refresh(refreshRequest(persistentEntity.getIndexName()).waitForOperations(waitForOperation)).actionGet(); + } + + private ElasticsearchPersistentEntity getPersistentEntityFor(Class clazz){ + return elasticsearchConverter.getMappingContext().getPersistentEntity(clazz); + } + + private Page mapResults(SearchResponse response, final Class elementType,final Pageable pageable){ + ResultsMapper resultsMapper = new ResultsMapper(){ + @Override + public Page mapResults(SearchResponse response) { + long totalHits = response.getHits().totalHits(); + List results = new ArrayList(); + for (SearchHit hit : response.getHits()) { + if (hit != null) { + results.add(mapResult(hit.sourceAsString(), elementType)); + } + } + return new PageImpl(results, pageable, totalHits); + } + }; + return resultsMapper.mapResults(response); + } + + private T mapResult(String source, Class clazz){ + if(isBlank(source)){ + return null; + } + try { + return objectMapper.readValue(source, clazz); + } catch (IOException e) { + throw new ElasticsearchException("failed to map source [ " + source + "] to class " + clazz.getSimpleName() , e); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ResultsMapper.java b/src/main/java/org/springframework/data/elasticsearch/core/ResultsMapper.java new file mode 100644 index 000000000..15eadfb7e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/ResultsMapper.java @@ -0,0 +1,11 @@ +package org.springframework.data.elasticsearch.core; + +import org.elasticsearch.action.search.SearchResponse; +import org.springframework.data.domain.Page; + + +public interface ResultsMapper { + + Page mapResults(SearchResponse response); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/DateTimeConverters.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/DateTimeConverters.java new file mode 100644 index 000000000..b3718d812 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/DateTimeConverters.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012 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.elasticsearch.core.convert; + +import org.joda.time.DateTimeZone; +import org.joda.time.LocalDateTime; +import org.joda.time.ReadableInstant; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.springframework.core.convert.converter.Converter; + +import java.util.Date; + +public final class DateTimeConverters { + + private static DateTimeFormatter formatter = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC); + + public enum JodaDateTimeConverter implements Converter { + INSTANCE; + + @Override + public String convert(ReadableInstant source) { + if (source == null) { + return null; + } + return formatter.print(source); + } + + } + + public enum JodaLocalDateTimeConverter implements Converter { + INSTANCE; + + @Override + public String convert(LocalDateTime source) { + if (source == null) { + return null; + } + return formatter.print(source.toDateTime(DateTimeZone.UTC)); + } + + } + + public enum JavaDateConverter implements Converter { + INSTANCE; + + @Override + public String convert(Date source) { + if (source == null) { + return null; + } + + return formatter.print(source.getTime()); + } + + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java new file mode 100644 index 000000000..7c1e34878 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012 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.elasticsearch.core.convert; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.mapping.context.MappingContext; + + +public interface ElasticsearchConverter{ + + /** + * Returns the underlying {@link org.springframework.data.mapping.context.MappingContext} used by the converter. + * + * @return never {@literal null} + */ + MappingContext, ElasticsearchPersistentProperty> getMappingContext(); + + /** + * Returns the underlying {@link org.springframework.core.convert.ConversionService} used by the converter. + * + * @return never {@literal null}. + */ + ConversionService getConversionService(); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java new file mode 100644 index 000000000..938b32fe7 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012 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.elasticsearch.core.convert; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; + + +public class MappingElasticsearchConverter implements ElasticsearchConverter, ApplicationContextAware{ + + private final MappingContext,ElasticsearchPersistentProperty> mappingContext; + private final GenericConversionService conversionService; + + @SuppressWarnings("unused") + private ApplicationContext applicationContext; + + public MappingElasticsearchConverter(MappingContext,ElasticsearchPersistentProperty> mappingContext) { + Assert.notNull(mappingContext); + this.mappingContext = mappingContext; + this.conversionService = new DefaultConversionService(); + } + + @Override + public MappingContext,ElasticsearchPersistentProperty> getMappingContext() { + return mappingContext; + } + + @Override + public ConversionService getConversionService() { + return this.conversionService; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java new file mode 100644 index 000000000..9bac38e2e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012 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.elasticsearch.core.mapping; + +import org.springframework.data.mapping.PersistentEntity; + +public interface ElasticsearchPersistentEntity extends PersistentEntity { + + String getIndexName(); + String getIndexType(); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java new file mode 100644 index 000000000..5656f1e0a --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012 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.elasticsearch.core.mapping; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mapping.PersistentProperty; + +public interface ElasticsearchPersistentProperty extends PersistentProperty{ + + String getFieldName(); + + public enum PropertyToFieldNameConverter implements Converter { + + INSTANCE; + + public String convert(ElasticsearchPersistentProperty source) { + return source.getFieldName(); + } + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchMappingContext.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchMappingContext.java new file mode 100644 index 000000000..727ecb166 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchMappingContext.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012 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.elasticsearch.core.mapping; + +import org.springframework.data.mapping.context.AbstractMappingContext; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.TypeInformation; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; + + +public class SimpleElasticsearchMappingContext extends + AbstractMappingContext, ElasticsearchPersistentProperty> { + + @Override + protected SimpleElasticsearchPersistentEntity createPersistentEntity(TypeInformation typeInformation) { + return new SimpleElasticsearchPersistentEntity(typeInformation); + } + + @Override + protected ElasticsearchPersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor, + SimpleElasticsearchPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + return new SimpleElasticsearchPersistentProperty(field, descriptor, owner, simpleTypeHolder); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java new file mode 100644 index 000000000..7da297396 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012 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.elasticsearch.core.mapping; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.BeanFactoryAccessor; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.util.TypeInformation; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.Assert; + +import java.util.Locale; + +import static org.springframework.util.StringUtils.hasText; + +/** + * Elasticsearch specific {@link org.springframework.data.mapping.PersistentEntity} implementation holding + * + * @param + */ +public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntity implements + ElasticsearchPersistentEntity, ApplicationContextAware { + + private final StandardEvaluationContext context; + private String indexName; + private String indexType; + + public SimpleElasticsearchPersistentEntity(TypeInformation typeInformation) { + super(typeInformation); + this.context = new StandardEvaluationContext(); + Class clazz = typeInformation.getType(); + Assert.isTrue(clazz.isAnnotationPresent(Document.class), + clazz.getSimpleName() + " is not a Document. Make sure the document class is annotated with @Document(indexName=\"foo\")"); + Document document = clazz.getAnnotation(Document.class); + Assert.hasText(document.indexName(), " Unknown indexName. Make sure the indexName is defined. e.g @Document(indexName=\"foo\")"); + this.indexName = typeInformation.getType().getAnnotation(Document.class).indexName(); + this.indexType = hasText(document.type())? document.type() : clazz.getSimpleName().toLowerCase(Locale.ENGLISH); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + context.addPropertyAccessor(new BeanFactoryAccessor()); + context.setBeanResolver(new BeanFactoryResolver(applicationContext)); + context.setRootObject(applicationContext); + } + + @Override + public String getIndexName() { + return indexName; + } + + @Override + public String getIndexType() { + return indexType; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java new file mode 100644 index 000000000..87073ee7a --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012 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.elasticsearch.core.mapping; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.model.SimpleTypeHolder; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; + +/** + * Elasticsearch specific {@link org.springframework.data.mapping.PersistentProperty} implementation processing + */ +public class SimpleElasticsearchPersistentProperty extends AnnotationBasedPersistentProperty implements + ElasticsearchPersistentProperty { + + private static final Set> SUPPORTED_ID_TYPES = new HashSet>(); + private static final Set SUPPORTED_ID_PROPERTY_NAMES = new HashSet(); + + static { + SUPPORTED_ID_TYPES.add(String.class); + SUPPORTED_ID_PROPERTY_NAMES.add("id"); + SUPPORTED_ID_PROPERTY_NAMES.add("documentId"); + } + + public SimpleElasticsearchPersistentProperty(Field field, PropertyDescriptor propertyDescriptor, + PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + super(field, propertyDescriptor, owner, simpleTypeHolder); + } + + @Override + public String getFieldName() { + return field.getName(); + } + + @Override + public boolean isIdProperty() { + return super.isIdProperty() || SUPPORTED_ID_PROPERTY_NAMES.contains(getFieldName()); + } + + @Override + protected Association createAssociation() { + return null; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java new file mode 100644 index 000000000..b6b10bb96 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012 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.elasticsearch.core.query; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.util.Assert; + +/** + * AbstractQuery + * + */ +abstract class AbstractQuery implements Query{ + + private static final Pageable DEFAULT_PAGE = new PageRequest(0, DEFAULT_PAGE_SIZE); + + protected Pageable pageable = DEFAULT_PAGE; + protected Sort sort; + + @Override + public Sort getSort() { + return this.sort; + } + + @Override + public Pageable getPageable() { + return this.pageable; + } + + @Override + public final T setPageable(Pageable pageable) { + Assert.notNull(pageable); + + this.pageable = pageable; + return (T) this.addSort(pageable.getSort()); + } + + @SuppressWarnings("unchecked") + public final T addSort(Sort sort) { + if (sort == null) { + return (T) this; + } + + if (this.sort == null) { + this.sort = sort; + } else { + this.sort = this.sort.and(sort); + } + + return (T) this; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java new file mode 100644 index 000000000..d5ea746d8 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java @@ -0,0 +1,456 @@ +/* + * Copyright 2012 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.elasticsearch.core.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.util.Assert; + +/** + * Criteria is the central class when constructing queries. It follows more or less a fluent API style, which allows to + * easily chain together multiple criteria. + * + */ +public class Criteria { + + public static final String WILDCARD = "*"; + public static final String CRITERIA_VALUE_SEPERATOR = " "; + + private static final String OR_OPERATOR = " OR "; + private static final String AND_OPERATOR = " AND "; + + private Field field; + private float boost = Float.NaN; + private boolean negating = false; + + private List criteriaChain = new ArrayList(1); + + private Set criteria = new LinkedHashSet(); + + public Criteria() { + } + + /** + * Creates a new CriterSimpleFieldia for the Filed with provided name + * + * @param fieldname + */ + public Criteria(String fieldname) { + this(new SimpleField(fieldname)); + } + + /** + * Creates a new Criteria for the given field + * + * @param field + */ + public Criteria(Field field) { + Assert.notNull(field, "Field for criteria must not be null"); + Assert.hasText(field.getName(), "Field.name for criteria must not be null/empty"); + + this.criteriaChain.add(this); + this.field = field; + } + + protected Criteria(List criteriaChain, String fieldname) { + this(criteriaChain, new SimpleField(fieldname)); + } + + protected Criteria(List criteriaChain, Field field) { + Assert.notNull(criteriaChain, "CriteriaChain must not be null"); + Assert.notNull(field, "Field for criteria must not be null"); + Assert.hasText(field.getName(), "Field.name for criteria must not be null/empty"); + + this.criteriaChain.addAll(criteriaChain); + this.criteriaChain.add(this); + this.field = field; + } + + /** + * Static factory method to create a new Criteria for field with given name + * + * @param field + * @return + */ + public static Criteria where(String field) { + return where(new SimpleField(field)); + } + + /** + * Static factory method to create a new Criteria for provided field + * + * @param field + * @return + */ + public static Criteria where(Field field) { + return new Criteria(field); + } + + /** + * Chain using {@code AND} + * + * @param field + * @return + */ + public Criteria and(Field field) { + return new Criteria(this.criteriaChain, field); + } + + /** + * Chain using {@code AND} + * + * @param fieldName + * @return + */ + public Criteria and(String fieldName) { + return new Criteria(this.criteriaChain, fieldName); + } + + /** + * Chain using {@code AND} + * + * @param criteria + * @return + */ + public Criteria and(Criteria criteria) { + this.criteriaChain.add(criteria); + return this; + } + + /** + * Chain using {@code AND} + * + * @param criterias + * @return + */ + public Criteria and(Criteria... criterias) { + this.criteriaChain.addAll(Arrays.asList(criterias)); + return this; + } + + /** + * Chain using {@code OR} + * + * @param field + * @return + */ + public Criteria or(Field field) { + return new OrCriteria(this.criteriaChain, field); + } + + /** + * Chain using {@code OR} + * + * @param criteria + * @return + */ + public Criteria or(Criteria criteria) { + Assert.notNull(criteria, "Cannot chain 'null' criteria."); + + Criteria orConnectedCritiera = new OrCriteria(this.criteriaChain, criteria.getField()); + orConnectedCritiera.criteria.addAll(criteria.criteria); + return orConnectedCritiera; + } + + /** + * Chain using {@code OR} + * + * @param fieldName + * @return + */ + public Criteria or(String fieldName) { + return or(new SimpleField(fieldName)); + } + + /** + * Crates new CriteriaEntry without any wildcards + * + * @param o + * @return + */ + public Criteria is(Object o) { + criteria.add(new CriteriaEntry(OperationKey.EQUALS, o)); + return this; + } + + /** + * Crates new CriteriaEntry with leading and trailing wildcards
+ * NOTE: mind your schema as leading wildcards may not be supported and/or execution might be slow. + * + * @param s + * @return + */ + public Criteria contains(String s) { + assertNoBlankInWildcardedQuery(s, true, true); + criteria.add(new CriteriaEntry(OperationKey.CONTAINS, s)); + return this; + } + + /** + * Crates new CriteriaEntry with trailing wildcard + * + * @param s + * @return + */ + public Criteria startsWith(String s) { + assertNoBlankInWildcardedQuery(s, true, false); + criteria.add(new CriteriaEntry(OperationKey.STARTS_WITH, s)); + return this; + } + + /** + * Crates new CriteriaEntry with leading wildcard
+ * NOTE: mind your schema and execution times as leading wildcards may not be supported. + * + * @param s + * @return + */ + public Criteria endsWith(String s) { + assertNoBlankInWildcardedQuery(s, false, true); + criteria.add(new CriteriaEntry(OperationKey.ENDS_WITH, s)); + return this; + } + + /** + * Crates new CriteriaEntry with trailing - + * + * @return + */ + public Criteria not() { + this.negating = true; + return this; + } + + /** + * Crates new CriteriaEntry with trailing ~ + * + * @param s + * @return + */ + public Criteria fuzzy(String s) { + criteria.add(new CriteriaEntry(OperationKey.FUZZY, s)); + return this; + } + + + /** + * Crates new CriteriaEntry allowing native solr expressions + * + * @param s + * @return + */ + public Criteria expression(String s) { + criteria.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); + return this; + } + + /** + * Boost positive hit with given factor. eg. ^2.3 + * + * @param boost + * @return + */ + public Criteria boost(float boost) { + if (boost < 0) { + throw new InvalidDataAccessApiUsageException("Boost must not be negative."); + } + this.boost = boost; + return this; + } + + /** + * Crates new CriteriaEntry for {@code RANGE [lowerBound TO upperBound]} + * + * @param lowerBound + * @param upperBound + * @return + */ + public Criteria between(Object lowerBound, Object upperBound) { + if (lowerBound == null && upperBound == null) { + throw new InvalidDataAccessApiUsageException("Range [* TO *] is not allowed"); + } + + criteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound })); + return this; + } + + /** + * Crates new CriteriaEntry for {@code RANGE [* TO upperBound]} + * + * @param upperBound + * @return + */ + public Criteria lessThanEqual(Object upperBound) { + between(null, upperBound); + return this; + } + + /** + * Crates new CriteriaEntry for {@code RANGE [lowerBound TO *]} + * + * @param lowerBound + * @return + */ + public Criteria greaterThanEqual(Object lowerBound) { + between(lowerBound, null); + return this; + } + + /** + * Crates new CriteriaEntry for multiple values {@code (arg0 arg1 arg2 ...)} + * + * @param values + * @return + */ + public Criteria in(Object... values) { + if (values.length == 0 || (values.length > 1 && values[1] instanceof Collection)) { + throw new InvalidDataAccessApiUsageException("At least one element " + + (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "") + + " has to be present."); + } + return in(Arrays.asList(values)); + } + + /** + * Crates new CriteriaEntry for multiple values {@code (arg0 arg1 arg2 ...)} + * + * @param values the collection containing the values to match against + * @return + */ + public Criteria in(Iterable values) { + Assert.notNull(values, "Collection of 'in' values must not be null"); + for (Object value : values) { + if (value instanceof Collection) { + in((Collection) value); + } else { + is(value); + } + } + return this; + } + + + private void assertNoBlankInWildcardedQuery(String searchString, boolean leadingWildcard, boolean trailingWildcard) { + if (StringUtils.contains(searchString, CRITERIA_VALUE_SEPERATOR)) { + throw new InvalidDataAccessApiUsageException("Cannot constructQuery '" + (leadingWildcard ? "*" : "") + "\"" + + searchString + "\"" + (trailingWildcard ? "*" : "") + "'. Use epxression or mulitple clauses instead."); + } + } + + /** + * Field targeted by this Criteria + * + * @return + */ + public Field getField() { + return this.field; + } + + public Set getCriteriaEntries() { + return Collections.unmodifiableSet(this.criteria); + } + + /** + * Conjunction to be used with this criteria (AND | OR) + * + * @return + */ + public String getConjunctionOperator() { + return AND_OPERATOR; + } + + public List getCriteriaChain() { + return Collections.unmodifiableList(this.criteriaChain); + } + + public boolean isNegating() { + return this.negating; + } + + public boolean isAnd(){ + return AND_OPERATOR == getConjunctionOperator(); + } + + public boolean isOr(){ + return OR_OPERATOR == getConjunctionOperator(); + } + + public float getBoost() { + return this.boost; + } + + static class OrCriteria extends Criteria { + + public OrCriteria() { + super(); + } + + public OrCriteria(Field field) { + super(field); + } + + public OrCriteria(List criteriaChain, Field field) { + super(criteriaChain, field); + } + + public OrCriteria(List criteriaChain, String fieldname) { + super(criteriaChain, fieldname); + } + + public OrCriteria(String fieldname) { + super(fieldname); + } + + @Override + public String getConjunctionOperator() { + return OR_OPERATOR; + } + + } + + public enum OperationKey { + EQUALS, CONTAINS, STARTS_WITH, ENDS_WITH, EXPRESSION, BETWEEN, FUZZY; + } + + public static class CriteriaEntry { + + private OperationKey key; + private Object value; + + CriteriaEntry(OperationKey key, Object value) { + this.key = key; + this.value = value; + } + + public OperationKey getKey() { + return key; + } + + public Object getValue() { + return value; + } + + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java new file mode 100644 index 000000000..7d0b1f8e0 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012 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.elasticsearch.core.query; + + +import org.springframework.data.domain.Pageable; +import org.springframework.util.Assert; + +public class CriteriaQuery extends AbstractQuery implements Query { + + private Criteria criteria; + private CriteriaQuery() { + } + + public CriteriaQuery(Criteria criteria) { + this(criteria, null); + } + + public CriteriaQuery(Criteria criteria, Pageable pageable) { + this.criteria = criteria; + this.pageable = pageable; + if (pageable != null) { + this.addSort(pageable.getSort()); + } + } + + public static final Query fromQuery(CriteriaQuery source) { + return fromQuery(source, new CriteriaQuery()); + } + + public static T fromQuery(CriteriaQuery source, T destination) { + if (source == null || destination == null) { + return null; + } + + if (source.getCriteria() != null) { + destination.addCriteria(source.getCriteria()); + } + + if (source.getSort() != null) { + destination.addSort(source.getSort()); + } + + return destination; + } + + @SuppressWarnings("unchecked") + public final T addCriteria(Criteria criteria) { + Assert.notNull(criteria, "Cannot add null criteria."); + if (this.criteria == null) { + this.criteria = criteria; + } else { + this.criteria.and(criteria); + } + return (T) this; + } + + public Criteria getCriteria() { + return this.criteria; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/DeleteQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/DeleteQuery.java new file mode 100644 index 000000000..497939613 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/DeleteQuery.java @@ -0,0 +1,17 @@ +package org.springframework.data.elasticsearch.core.query; + + +import org.elasticsearch.index.query.QueryBuilder; + +public class DeleteQuery{ + + private QueryBuilder elasticsearchQuery; + + public QueryBuilder getElasticsearchQuery() { + return elasticsearchQuery; + } + + public void setElasticsearchQuery(QueryBuilder elasticsearchQuery) { + this.elasticsearchQuery = elasticsearchQuery; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Field.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Field.java new file mode 100644 index 000000000..cf8897df3 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Field.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012 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.elasticsearch.core.query; + +/** + * Defines a Field that can be used within a Criteria. + * + */ +public interface Field { + + /** + * Get the name of the field used in schema.xml of solr server + * + * @return + */ + String getName(); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/GetQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/GetQuery.java new file mode 100644 index 000000000..0f9e03ba0 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/GetQuery.java @@ -0,0 +1,15 @@ +package org.springframework.data.elasticsearch.core.query; + + +public class GetQuery{ + + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQuery.java new file mode 100644 index 000000000..e6092366e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQuery.java @@ -0,0 +1,24 @@ +package org.springframework.data.elasticsearch.core.query; + + +public class IndexQuery{ + + private String id; + private Object object; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Object getObject() { + return object; + } + + public void setObject(Object object) { + this.object = object; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java new file mode 100644 index 000000000..c48eebcb7 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java @@ -0,0 +1,51 @@ +package org.springframework.data.elasticsearch.core.query; + + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +public interface Query { + + int DEFAULT_PAGE_SIZE = 10; + + + /** + * restrict result to entries on given page. Corresponds to the 'start' and 'rows' parameter in solr + * + * @param pageable + * @return + */ + T setPageable(Pageable pageable); + + + + /** + * Get filter queries if defined + * + * @return + */ + //List getFilterQueries(); + + /** + * Get page settings if defined + * + * @return + */ + Pageable getPageable(); + + + + /** + * Add {@link org.springframework.data.domain.Sort} to query + * + * @param sort + * @return + */ + T addSort(Sort sort); + + /** + * @return null if not set + */ + Sort getSort(); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java new file mode 100644 index 000000000..3f61075bf --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java @@ -0,0 +1,28 @@ +package org.springframework.data.elasticsearch.core.query; + + +import org.elasticsearch.index.query.FilterBuilder; +import org.elasticsearch.index.query.QueryBuilder; + +public class SearchQuery extends AbstractQuery{ + + private QueryBuilder elasticsearchQuery; + private FilterBuilder elasticsearchFilter; + + public QueryBuilder getElasticsearchQuery() { + return elasticsearchQuery; + } + + public void setElasticsearchQuery(QueryBuilder elasticsearchQuery) { + this.elasticsearchQuery = elasticsearchQuery; + } + + public FilterBuilder getElasticsearchFilter() { + return elasticsearchFilter; + } + + public void setElasticsearchFilter(FilterBuilder elasticsearchFilter) { + this.elasticsearchFilter = elasticsearchFilter; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java new file mode 100644 index 000000000..1fc3dafd6 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012 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.elasticsearch.core.query; + +/** + * The most trivial implementation of a Field + * + */ +public class SimpleField implements Field { + + private final String name; + + public SimpleField(String name) { + this.name = name; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public String toString() { + return this.name; + } + +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java new file mode 100644 index 000000000..e80395fa1 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java @@ -0,0 +1,31 @@ +package org.springframework.data.elasticsearch.core.query; + + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +public class StringQuery extends AbstractQuery{ + + private String source; + + public StringQuery(String source) { + this.source = source; + } + + public StringQuery(String source, Pageable pageable) { + this.source = source; + this.pageable = pageable; + } + + public StringQuery(String source, Pageable pageable, Sort sort) { + this.pageable = pageable; + this.sort = sort; + this.source = source; + } + + + public String getSource() { + return source; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchCrudRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchCrudRepository.java new file mode 100644 index 000000000..d068af9d2 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchCrudRepository.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012 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.elasticsearch.repository; + +import org.springframework.data.repository.PagingAndSortingRepository; + +import java.io.Serializable; +import java.util.List; + +/** + * @param + * @param + */ +public interface ElasticsearchCrudRepository extends PagingAndSortingRepository { + + List save(List entities); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java new file mode 100644 index 000000000..f6cb6f02e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012 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.elasticsearch.repository; + +import org.elasticsearch.index.query.QueryBuilder; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.query.SearchQuery; +import org.springframework.data.repository.NoRepositoryBean; + +import java.io.Serializable; +import java.util.List; + +/** + * @param + * @param + */ +@NoRepositoryBean +public interface ElasticsearchRepository extends ElasticsearchCrudRepository { + + S index(S entity); + + Iterable search(QueryBuilder elasticsearchQuery); + + Page search(QueryBuilder elasticsearchQuery, Pageable pageable); + + Page search(SearchQuery searchQuery); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryBean.java b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryBean.java new file mode 100644 index 000000000..1e8defe22 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryBean.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012 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.elasticsearch.repository.cdi; + +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.repository.support.ElasticsearchRepositoryFactory; +import org.springframework.data.repository.cdi.CdiRepositoryBean; +import org.springframework.util.Assert; + +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import java.lang.annotation.Annotation; +import java.util.Set; + +/** + * Uses CdiRepositoryBean to create ElasticsearchRepository instances. + * + */ +public class ElasticsearchRepositoryBean extends CdiRepositoryBean { + + private final Bean elasticsearchOperationsBean; + + public ElasticsearchRepositoryBean(Bean operations, Set qualifiers, Class repositoryType, + BeanManager beanManager) { + super(qualifiers, repositoryType, beanManager); + + Assert.notNull(operations, "Cannot create repository with 'null' for ElasticsearchOperations."); + this.elasticsearchOperationsBean = operations; + } + + @Override + protected T create(CreationalContext creationalContext, Class repositoryType) { + ElasticsearchOperations elasticsearchOperations = getDependencyInstance(elasticsearchOperationsBean, ElasticsearchOperations.class); + return new ElasticsearchRepositoryFactory(elasticsearchOperations).getRepository(repositoryType); + } + + @Override + public Class getScope() { + return elasticsearchOperationsBean.getScope(); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryExtension.java b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryExtension.java new file mode 100644 index 000000000..278088068 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryExtension.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012 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.elasticsearch.repository.cdi; + +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.repository.cdi.CdiRepositoryExtensionSupport; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.UnsatisfiedResolutionException; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.ProcessBean; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + + +public class ElasticsearchRepositoryExtension extends CdiRepositoryExtensionSupport { + + private final Map> elasticsearchOperationsMap = new HashMap>(); + + @SuppressWarnings("unchecked") + void processBean(@Observes ProcessBean processBean) { + Bean bean = processBean.getBean(); + for (Type type : bean.getTypes()) { + if (type instanceof Class && ElasticsearchOperations.class.isAssignableFrom((Class) type)) { + elasticsearchOperationsMap.put(bean.getQualifiers().toString(), ((Bean) bean)); + } + } + } + + void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager) { + for (Entry, Set> entry : getRepositoryTypes()) { + + Class repositoryType = entry.getKey(); + Set qualifiers = entry.getValue(); + + Bean repositoryBean = createRepositoryBean(repositoryType, qualifiers, beanManager); + afterBeanDiscovery.addBean(repositoryBean); + } + } + + private Bean createRepositoryBean(Class repositoryType, Set qualifiers, BeanManager beanManager) { + Bean elasticsearchOperationsBean = this.elasticsearchOperationsMap.get(qualifiers.toString()); + + if (elasticsearchOperationsBean == null) { + throw new UnsatisfiedResolutionException(String.format("Unable to resolve a bean for '%s' with qualifiers %s.", + ElasticsearchOperations.class.getName(), qualifiers)); + } + return new ElasticsearchRepositoryBean(elasticsearchOperationsBean, qualifiers, repositoryType, beanManager); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoriesRegistrar.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoriesRegistrar.java new file mode 100644 index 000000000..ec1e6f0a4 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoriesRegistrar.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012 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.elasticsearch.repository.config; + +import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +import java.lang.annotation.Annotation; + +/** + * {@link org.springframework.context.annotation.ImportBeanDefinitionRegistrar} implementation to trigger configuration of the {@link EnableElasticsearchRepositories} + * annotation. + * + */ +class ElasticsearchRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableElasticsearchRepositories.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension() + */ + @Override + protected RepositoryConfigurationExtension getExtension() { + return new ElasticsearchRepositoryConfigExtension(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java new file mode 100644 index 000000000..65eb4aac2 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012 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.elasticsearch.repository.config; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.data.elasticsearch.repository.support.ElasticsearchRepositoryFactoryBean; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; +import org.w3c.dom.Element; + + + +/** + * {@link org.springframework.data.repository.config.RepositoryConfigurationExtension} implementation to configure Solr repository configuration support, + * evaluating the {@link EnableElasticsearchRepositories} annotation or the equivalent XML element. + * + */ +public class ElasticsearchRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getRepositoryFactoryClassName() + */ + @Override + public String getRepositoryFactoryClassName() { + return ElasticsearchRepositoryFactoryBean.class.getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModulePrefix() + */ + @Override + protected String getModulePrefix() { + return "elasticsearch"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource) + */ + @Override + public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) { + + AnnotationAttributes attributes = config.getAttributes(); + builder.addPropertyReference("elasticsearchOperations", attributes.getString("elasticsearchTemplateRef")); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.XmlRepositoryConfigurationSource) + */ + @Override + public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource config) { + + Element element = config.getElement(); + builder.addPropertyReference("elasticsearchOperations", element.getAttribute("elasticsearch-template-ref")); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableElasticsearchRepositories.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableElasticsearchRepositories.java new file mode 100644 index 000000000..575c963b7 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableElasticsearchRepositories.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012 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.elasticsearch.repository.config; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; +import org.springframework.data.elasticsearch.repository.support.ElasticsearchRepositoryFactoryBean; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; + +import java.lang.annotation.*; + +/** + * Annotation to enable Elasticsearch repositories. Will scan the package of the annotated configuration class for Spring Data + * repositories by default. + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(ElasticsearchRepositoriesRegistrar.class) +public @interface EnableElasticsearchRepositories { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: + * {@code @EnableElasticsearchRepositories("org.my.pkg")} instead of {@code @EnableElasticsearchRepositories(basePackages="org.my.pkg")}. + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this + * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The + * package of each class specified will be scanned. Consider creating a special no-op marker class or interface in + * each package that serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; + + /** + * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from + * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters. + */ + Filter[] includeFilters() default {}; + + /** + * Specifies which types are not eligible for component scanning. + */ + Filter[] excludeFilters() default {}; + + /** + * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So + * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning + * for {@code PersonRepositoryImpl}. + * + * @return + */ + String repositoryImplementationPostfix() default "Impl"; + + /** + * Configures the location of where to find the Spring Data named queries properties file. Will default to + * {@code META-INFO/elasticsearch-named-queries.properties}. + * + * @return + */ + String namedQueriesLocation() default ""; + + /** + * Returns the key of the {@link org.springframework.data.repository.query.QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to + * {@link org.springframework.data.repository.query.QueryLookupStrategy.Key#CREATE_IF_NOT_FOUND}. + * + * @return + */ + Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; + + /** + * Returns the {@link org.springframework.beans.factory.FactoryBean} class to be used for each repository instance. Defaults to + * {@link ElasticsearchRepositoryFactoryBean}. + * + * @return + */ + Class repositoryFactoryBeanClass() default ElasticsearchRepositoryFactoryBean.class; + + // Solr specific configuration + + /** + * Configures the name of the {@link ElasticsearchTemplate} bean definition to be used to create repositories discovered + * through this annotation. Defaults to {@code elasticsearchTemplate}. + * + * @return + */ + String elasticsearchTemplateRef() default "elasticsearchTemplate"; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java new file mode 100644 index 000000000..ccf736af6 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012 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.elasticsearch.repository.query; + +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; + + +public abstract class AbstractElasticsearchRepositoryQuery implements RepositoryQuery { + + protected ElasticsearchQueryMethod queryMethod; + protected ElasticsearchOperations elasticsearchOperations; + + public AbstractElasticsearchRepositoryQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations) { + this.queryMethod = queryMethod; + this.elasticsearchOperations = elasticsearchOperations; + } + + @Override + public QueryMethod getQueryMethod() { + return queryMethod; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java new file mode 100644 index 000000000..bae324aab --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java @@ -0,0 +1,37 @@ +package org.springframework.data.elasticsearch.repository.query; + + +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.PartTree; + +public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery{ + + private final PartTree tree; + private final MappingContext mappingContext; + + + public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) { + super(method, elasticsearchOperations); + this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); + this.mappingContext = elasticsearchOperations.getElasticsearchConverter().getMappingContext(); + } + + @Override + public Object execute(Object[] parameters) { + ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); + CriteriaQuery query = createQuery(accessor); + if(queryMethod.isPageQuery()){ + return elasticsearchOperations.queryForPage(query, queryMethod.getEntityInformation().getJavaType()); + } + return elasticsearchOperations.queryForObject(query, queryMethod.getEntityInformation().getJavaType()); + } + + public CriteriaQuery createQuery(ParametersParameterAccessor accessor) { + return new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java new file mode 100644 index 000000000..e3772227e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java @@ -0,0 +1,38 @@ +package org.springframework.data.elasticsearch.repository.query; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.elasticsearch.annotations.Query; +import org.springframework.data.elasticsearch.repository.support.ElasticsearchEntityInformation; +import org.springframework.data.elasticsearch.repository.support.ElasticsearchEntityInformationCreator; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Method; + + +public class ElasticsearchQueryMethod extends QueryMethod { + + private final ElasticsearchEntityInformation entityInformation; + private Method method; + + public ElasticsearchQueryMethod(Method method, RepositoryMetadata metadata, ElasticsearchEntityInformationCreator elasticsearchEntityInformationCreator) { + super(method, metadata); + this.entityInformation = elasticsearchEntityInformationCreator.getEntityInformation(metadata.getReturnedDomainClass(method)); + this.method = method; + } + + public boolean hasAnnotatedQuery() { + return getQueryAnnotation() != null; + } + + public String getAnnotatedQuery() { + String query = (String) AnnotationUtils.getValue(getQueryAnnotation(), "value"); + return StringUtils.hasText(query) ? query : null; + } + + private Query getQueryAnnotation() { + return this.method.getAnnotation(Query.class); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java new file mode 100644 index 000000000..288029f57 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java @@ -0,0 +1,77 @@ +package org.springframework.data.elasticsearch.repository.query; + + +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.convert.DateTimeConverters; +import org.springframework.data.elasticsearch.core.query.StringQuery; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.util.Assert; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery{ + + private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)"); + private String query; + + private final GenericConversionService conversionService = new GenericConversionService(); + + { + if (!conversionService.canConvert(java.util.Date.class, String.class)) { + conversionService.addConverter(DateTimeConverters.JavaDateConverter.INSTANCE); + } + if (!conversionService.canConvert(org.joda.time.ReadableInstant.class, String.class)) { + conversionService.addConverter(DateTimeConverters.JodaDateTimeConverter.INSTANCE); + } + if (!conversionService.canConvert(org.joda.time.LocalDateTime.class, String.class)) { + conversionService.addConverter(DateTimeConverters.JodaLocalDateTimeConverter.INSTANCE); + } + + } + + public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations, String query) { + super(queryMethod, elasticsearchOperations); + Assert.notNull(query, "Query cannot be empty"); + this.query = query; + } + + @Override + public Object execute(Object[] parameters) { + ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); + StringQuery stringQuery = createQuery(accessor); + if(queryMethod.isPageQuery()){ + return elasticsearchOperations.queryForPage(stringQuery, queryMethod.getEntityInformation().getJavaType()); + } + return elasticsearchOperations.queryForObject(stringQuery, queryMethod.getEntityInformation().getJavaType()); + } + + + protected StringQuery createQuery(ParametersParameterAccessor parameterAccessor) { + String queryString = replacePlaceholders(this.query, parameterAccessor); + return new StringQuery(queryString); + } + + private String replacePlaceholders(String input, ParametersParameterAccessor accessor) { + Matcher matcher = PARAMETER_PLACEHOLDER.matcher(input); + String result = input; + while (matcher.find()) { + String group = matcher.group(); + int index = Integer.parseInt(matcher.group(1)); + result = result.replace(group, getParameterWithIndex(accessor, index)); + } + return result; + } + + private String getParameterWithIndex(ParametersParameterAccessor accessor, int index) { + Object parameter = accessor.getBindableValue(index); + if (parameter == null) { + return "null"; + } + if (conversionService.canConvert(parameter.getClass(), String.class)) { + return conversionService.convert(parameter, String.class); + } + return parameter.toString(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java new file mode 100644 index 000000000..3a7c7e65a --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java @@ -0,0 +1,115 @@ +package org.springframework.data.elasticsearch.repository.query.parser; + + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.PersistentPropertyPath; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; + +import java.util.Collection; +import java.util.Iterator; + +public class ElasticsearchQueryCreator extends AbstractQueryCreator{ + + private final MappingContext context; + + public ElasticsearchQueryCreator(PartTree tree, ParameterAccessor parameters, MappingContext context) { + super(tree, parameters); + this.context = context; + } + + public ElasticsearchQueryCreator(PartTree tree, MappingContext context) { + super(tree); + this.context = context; + } + + @Override + protected CriteriaQuery create(Part part, Iterator iterator) { + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); + return new CriteriaQuery(from(part.getType(), + new Criteria(path.toDotPath(ElasticsearchPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), iterator)); + } + + @Override + protected CriteriaQuery and(Part part, CriteriaQuery base, Iterator iterator) { + if (base == null) { + return create(part, iterator); + } + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); + return base.addCriteria(from(part.getType(), + new Criteria(path.toDotPath(ElasticsearchPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), iterator)); + } + + @Override + protected CriteriaQuery or(CriteriaQuery base, CriteriaQuery query) { + return new CriteriaQuery(base.getCriteria().or(query.getCriteria())); + } + + @Override + protected CriteriaQuery complete(CriteriaQuery query, Sort sort) { + if (query == null) { + return null; + } + return query.addSort(sort); + } + + + private Criteria from(Part.Type type, Criteria instance, Iterator parameters) { + Criteria criteria = instance; + if (criteria == null) { + criteria = new Criteria(); + } + switch (type) { + case TRUE: + return criteria.is(true); + case FALSE: + return criteria.is(false); + case SIMPLE_PROPERTY: + return criteria.is(parameters.next()); + case NEGATING_SIMPLE_PROPERTY: + return criteria.is(parameters.next()).not(); + case REGEX: + return criteria.expression(parameters.next().toString()); + case LIKE: + case STARTING_WITH: + return criteria.startsWith(parameters.next().toString()); + case ENDING_WITH: + return criteria.endsWith(parameters.next().toString()); + case CONTAINING: + return criteria.contains(parameters.next().toString()); + case AFTER: + case GREATER_THAN: + case GREATER_THAN_EQUAL: + return criteria.greaterThanEqual(parameters.next()); + case BEFORE: + case LESS_THAN: + case LESS_THAN_EQUAL: + return criteria.lessThanEqual(parameters.next()); + case BETWEEN: + return criteria.between(parameters.next(), parameters.next()); + case IN: + return criteria.in(asArray(parameters.next())); + case NOT_IN: + return criteria.in(asArray(parameters.next())).not(); + default: + throw new InvalidDataAccessApiUsageException("Illegal criteria found '" + type + "'."); + } + } + + private Object[] asArray(Object o) { + if (o instanceof Collection) { + return ((Collection) o).toArray(); + } else if (o.getClass().isArray()) { + return (Object[]) o; + } + return new Object[] { o }; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformation.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformation.java new file mode 100644 index 000000000..5f8518b49 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformation.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 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.elasticsearch.repository.support; + +import org.springframework.data.repository.core.EntityInformation; + +import java.io.Serializable; + +/** + * @param + * @param + */ +public interface ElasticsearchEntityInformation extends EntityInformation { + + String getIdAttribute(); + String getIndexName(); + String getType(); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreator.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreator.java new file mode 100644 index 000000000..5b8453049 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreator.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012 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.elasticsearch.repository.support; + +import java.io.Serializable; + +public interface ElasticsearchEntityInformationCreator { + + ElasticsearchEntityInformation getEntityInformation(Class domainClass); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImpl.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImpl.java new file mode 100644 index 000000000..c5006522b --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImpl.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012 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.elasticsearch.repository.support; + +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; + +import java.io.Serializable; + +public class ElasticsearchEntityInformationCreatorImpl implements ElasticsearchEntityInformationCreator { + + private final MappingContext, ElasticsearchPersistentProperty> mappingContext; + + public ElasticsearchEntityInformationCreatorImpl( + MappingContext, ElasticsearchPersistentProperty> mappingContext) { + Assert.notNull(mappingContext); + this.mappingContext = mappingContext; + } + + @Override + @SuppressWarnings("unchecked") + public ElasticsearchEntityInformation getEntityInformation(Class domainClass) { + ElasticsearchPersistentEntity persistentEntity = (ElasticsearchPersistentEntity) mappingContext + .getPersistentEntity(domainClass); + return new MappingElasticsearchEntityInformation(persistentEntity); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java new file mode 100644 index 000000000..53e9b3330 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012 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.elasticsearch.repository.support; + +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery; +import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod; +import org.springframework.data.elasticsearch.repository.query.ElasticsearchStringQuery; +import org.springframework.data.querydsl.QueryDslPredicateExecutor; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import static org.springframework.data.querydsl.QueryDslUtils.QUERY_DSL_PRESENT; + +/** + * Factory to create {@link ElasticsearchRepository} + * + */ +public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport { + + private final ElasticsearchOperations elasticsearchOperations; + private final ElasticsearchEntityInformationCreator entityInformationCreator; + + public ElasticsearchRepositoryFactory(ElasticsearchOperations elasticsearchOperations) { + Assert.notNull(elasticsearchOperations); + this.elasticsearchOperations = elasticsearchOperations; + this.entityInformationCreator = new ElasticsearchEntityInformationCreatorImpl(elasticsearchOperations.getElasticsearchConverter() + .getMappingContext()); + } + + @Override + public ElasticsearchEntityInformation getEntityInformation(Class domainClass) { + return entityInformationCreator.getEntityInformation(domainClass); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected Object getTargetRepository(RepositoryMetadata metadata) { + SimpleElasticsearchRepository repository = new SimpleElasticsearchRepository(getEntityInformation(metadata.getDomainType()), elasticsearchOperations); + repository.setEntityClass(metadata.getDomainType()); + return repository; + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + if (isQueryDslRepository(metadata.getRepositoryInterface())) { + throw new IllegalArgumentException("QueryDsl Support has not been implemented yet."); + } + return SimpleElasticsearchRepository.class; + } + + private static boolean isQueryDslRepository(Class repositoryInterface) { + return QUERY_DSL_PRESENT && QueryDslPredicateExecutor.class.isAssignableFrom(repositoryInterface); + } + + @Override + protected QueryLookupStrategy getQueryLookupStrategy(QueryLookupStrategy.Key key) { + return new ElasticsearchQueryLookupStrategy(); + } + + private class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy { + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, NamedQueries namedQueries) { + + ElasticsearchQueryMethod queryMethod = new ElasticsearchQueryMethod(method, metadata, entityInformationCreator); + String namedQueryName = queryMethod.getNamedQueryName(); + + if (namedQueries.hasQuery(namedQueryName)) { + String namedQuery = namedQueries.getQuery(namedQueryName); + return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery); + } + else if (queryMethod.hasAnnotatedQuery()) { + return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery()); + } + return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations); + } + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryBean.java new file mode 100644 index 000000000..13640c484 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryBean.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012 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.elasticsearch.repository.support; + +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.util.Assert; + +import java.io.Serializable; + +/** + * Spring {@link org.springframework.beans.factory.FactoryBean} implementation to ease container based configuration for XML namespace and JavaConfig. + * + */ +public class ElasticsearchRepositoryFactoryBean, S, ID extends Serializable> extends + RepositoryFactoryBeanSupport { + + private ElasticsearchOperations operations; + + /** + * Configures the {@link ElasticsearchOperations} to be used to create Solr repositories. + * + * @param operations the operations to set + */ + public void setElasticsearchOperations(ElasticsearchOperations operations) { + Assert.notNull(operations); + this.operations = operations; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + Assert.notNull(operations, "ElasticsearchOperations must be configured!"); + } + + + @Override + protected RepositoryFactorySupport createRepositoryFactory() { + return new ElasticsearchRepositoryFactory(operations); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/MappingElasticsearchEntityInformation.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/MappingElasticsearchEntityInformation.java new file mode 100644 index 000000000..2fbaa7561 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/MappingElasticsearchEntityInformation.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012 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.elasticsearch.repository.support; + +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.mapping.model.BeanWrapper; +import org.springframework.data.repository.core.support.AbstractEntityInformation; +import org.springframework.util.Assert; + +import java.io.Serializable; + +/** + * Elasticsearch specific implementation of {@link org.springframework.data.repository.core.support.AbstractEntityInformation} + * + * @param + * @param + */ +public class MappingElasticsearchEntityInformation extends AbstractEntityInformation + implements ElasticsearchEntityInformation { + + private final ElasticsearchPersistentEntity entityMetadata; + private final String indexName; + private final String type; + + public MappingElasticsearchEntityInformation(ElasticsearchPersistentEntity entity) { + this(entity, null, null); + } + + public MappingElasticsearchEntityInformation(ElasticsearchPersistentEntity entity, String indexName, String type) { + super(entity.getType()); + this.entityMetadata = entity; + this.indexName = indexName; + this.type = type; + } + + @SuppressWarnings("unchecked") + @Override + public ID getId(T entity) { + ElasticsearchPersistentProperty id = entityMetadata.getIdProperty(); + try { + return (ID) BeanWrapper.create(entity, null).getProperty(id); + } catch (Exception e) { + throw new IllegalStateException("ID could not be resolved", e); + } + } + + @SuppressWarnings("unchecked") + @Override + public Class getIdType() { + return (Class) String.class; + } + + @Override + public String getIdAttribute() { + Assert.notNull(entityMetadata.getIdProperty(),"Unable to identify 'id' property in class " + entityMetadata.getType().getSimpleName() +". Make sure the 'id' property is annotated with @Id or named as 'id' or 'documentId' "); + return entityMetadata.getIdProperty().getFieldName(); + } + + @Override + public String getIndexName() { + return indexName != null? indexName : entityMetadata.getIndexName(); + } + + @Override + public String getType() { + return type != null? type : entityMetadata.getIndexType(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java new file mode 100644 index 000000000..b3eb2a277 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java @@ -0,0 +1,269 @@ +/* + * Copyright 2012 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.elasticsearch.repository.support; + +import org.elasticsearch.index.query.QueryBuilder; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.*; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.DeleteQuery; +import org.springframework.data.elasticsearch.core.query.GetQuery; +import org.springframework.data.elasticsearch.core.query.IndexQuery; +import org.springframework.data.elasticsearch.core.query.SearchQuery; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.util.Assert; + +import javax.annotation.PostConstruct; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.index.query.QueryBuilders.inQuery; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; + +/** + * Elasticsearch specific repository implementation. Likely to be used as target within {@link ElasticsearchRepositoryFactory} + * + * @param + */ +public class SimpleElasticsearchRepository implements ElasticsearchRepository { + + + private ElasticsearchOperations elasticsearchOperations; + private Class entityClass; + private ElasticsearchEntityInformation entityInformation; + + public SimpleElasticsearchRepository() { + } + + public SimpleElasticsearchRepository(ElasticsearchOperations elasticsearchOperations) { + Assert.notNull(elasticsearchOperations); + this.setElasticsearchOperations(elasticsearchOperations); + } + + public SimpleElasticsearchRepository(ElasticsearchEntityInformation metadata, ElasticsearchOperations elasticsearchOperations) { + this(elasticsearchOperations); + Assert.notNull(metadata); + this.entityInformation = metadata; + setEntityClass(this.entityInformation.getJavaType()); + } + + @PostConstruct + public void createIndex(){ + elasticsearchOperations.createIndex(getEntityClass()); + } + + @Override + public T findOne(String id) { + GetQuery query = new GetQuery(); + query.setId(id); + return elasticsearchOperations.queryForObject(query, getEntityClass()); + } + + @Override + public Iterable findAll() { + int itemCount = (int) this.count(); + if (itemCount == 0) { + return new PageImpl(Collections. emptyList()); + } + return this.findAll(new PageRequest(0, Math.max(1, itemCount))); + } + + @Override + public Page findAll(Pageable pageable) { + SearchQuery query = new SearchQuery(); + query.setElasticsearchQuery(matchAllQuery()); + return elasticsearchOperations.queryForPage(query, getEntityClass()); + } + + @Override + public Iterable findAll(Sort sort) { + SearchQuery query = new SearchQuery(); + query.setElasticsearchQuery(matchAllQuery()); + query.setPageable(new PageRequest(0,Integer.MAX_VALUE, sort)); + return elasticsearchOperations.queryForPage(query, getEntityClass()); + } + + @Override + public Iterable findAll(Iterable ids) { + SearchQuery query = new SearchQuery(); + query.setElasticsearchQuery(inQuery(entityInformation.getIdAttribute(), ids)); + return elasticsearchOperations.queryForPage(query, getEntityClass()); + } + + @Override + public long count() { + SearchQuery query = new SearchQuery(); + return elasticsearchOperations.count(query,getEntityClass()); + } + + @Override + public S save(S entity) { + Assert.notNull(entity, "Cannot save 'null' entity."); + elasticsearchOperations.index(createIndexQuery(entity)); + elasticsearchOperations.refresh(entityInformation.getIndexName(), true); + return entity; + } + + public List save(List entities) { + Assert.notNull(entities, "Cannot insert 'null' as a List."); + Assert.notEmpty(entities,"Cannot insert empty List."); + List queries = new ArrayList(); + for(S s:entities){ + queries.add(createIndexQuery(s)); + } + elasticsearchOperations.bulkIndex(queries); + elasticsearchOperations.refresh(entityInformation.getIndexName(), true); + return entities; + } + + @Override + public S index(S entity) { + return save(entity); + } + + @Override + public Iterable save(Iterable entities) { + Assert.notNull(entities, "Cannot insert 'null' as a List."); + if (!(entities instanceof Collection)) { + throw new InvalidDataAccessApiUsageException("Entities have to be inside a collection"); + } + List queries = new ArrayList(); + for(S s: entities){ + queries.add(createIndexQuery(s)); + } + elasticsearchOperations.bulkIndex(queries); + elasticsearchOperations.refresh(entityInformation.getIndexName(), true); + return entities; + } + + @Override + public boolean exists(String id) { + return findOne(id) != null; + } + + @Override + public Iterable search(QueryBuilder elasticsearchQuery) { + SearchQuery query = new SearchQuery(); + query.setElasticsearchQuery(elasticsearchQuery); + return elasticsearchOperations.queryForPage(query, getEntityClass()); + } + + @Override + public Page search(QueryBuilder elasticsearchQuery, Pageable pageable) { + SearchQuery query = new SearchQuery(); + query.setElasticsearchQuery(elasticsearchQuery); + query.setPageable(pageable); + return elasticsearchOperations.queryForPage(query, getEntityClass()); + } + + @Override + public Page search(SearchQuery query){ + return elasticsearchOperations.queryForPage(query, getEntityClass()); + } + + @Override + public void delete(String id) { + Assert.notNull(id, "Cannot delete entity with id 'null'."); + elasticsearchOperations.delete(entityInformation.getIndexName(), entityInformation.getType(),id); + elasticsearchOperations.refresh(entityInformation.getIndexName(),true); + } + + @Override + public void delete(T entity) { + Assert.notNull(entity, "Cannot delete 'null' entity."); + delete(extractIdFromBean(entity)); + elasticsearchOperations.refresh(entityInformation.getIndexName(), true); + } + + @Override + public void delete(Iterable entities) { + Assert.notNull(entities, "Cannot delete 'null' list."); + for (T entity : entities) { + delete(entity); + } + } + + @Override + public void deleteAll() { + DeleteQuery query = new DeleteQuery(); + query.setElasticsearchQuery(matchAllQuery()); + elasticsearchOperations.delete(query, getEntityClass()); + elasticsearchOperations.refresh(entityInformation.getIndexName(),true); + } + + private IndexQuery createIndexQuery(T entity){ + IndexQuery query = new IndexQuery(); + query.setObject(entity); + query.setId(extractIdFromBean(entity)); + return query; + } + + @SuppressWarnings("unchecked") + private Class resolveReturnedClassFromGenericType() { + ParameterizedType parameterizedType = resolveReturnedClassFromGenericType(getClass()); + return (Class) parameterizedType.getActualTypeArguments()[0]; + } + + private ParameterizedType resolveReturnedClassFromGenericType(Class clazz) { + Object genericSuperclass = clazz.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type rawtype = parameterizedType.getRawType(); + if (SimpleElasticsearchRepository.class.equals(rawtype)) { + return parameterizedType; + } + } + return resolveReturnedClassFromGenericType(clazz.getSuperclass()); + } + + public Class getEntityClass() { + if (!isEntityClassSet()) { + try { + this.entityClass = resolveReturnedClassFromGenericType(); + } catch (Exception e) { + throw new InvalidDataAccessApiUsageException("Unable to resolve EntityClass. Please use according setter!", e); + } + } + return entityClass; + } + + private boolean isEntityClassSet() { + return entityClass != null; + } + + public final void setEntityClass(Class entityClass) { + Assert.notNull(entityClass, "EntityClass must not be null."); + this.entityClass = entityClass; + } + + public final void setElasticsearchOperations(ElasticsearchOperations elasticsearchOperations) { + Assert.notNull(elasticsearchOperations, "ElasticsearchOperations must not be null."); + this.elasticsearchOperations = elasticsearchOperations; + } + + + private String extractIdFromBean(T entity) { + if (entityInformation != null) { + return entityInformation.getId(entity); + } + return null; + } + +} diff --git a/src/main/resources/org/springframework/data/elasticsearch/config/spring-elasticsearch-1.0.xsd b/src/main/resources/org/springframework/data/elasticsearch/config/spring-elasticsearch-1.0.xsd new file mode 100644 index 000000000..f0ddcb71b --- /dev/null +++ b/src/main/resources/org/springframework/data/elasticsearch/config/spring-elasticsearch-1.0.xsd @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/springframework/data/elasticsearch/NonDocumentEntity.java b/src/test/java/org/springframework/data/elasticsearch/NonDocumentEntity.java new file mode 100644 index 000000000..ddcb94cd0 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/NonDocumentEntity.java @@ -0,0 +1,27 @@ +package org.springframework.data.elasticsearch; + + +import org.springframework.data.elasticsearch.annotations.Document; + +@Document(indexName = "Foo") +public class NonDocumentEntity { + + private String someField1; + private String someField2; + + public String getSomeField1() { + return someField1; + } + + public void setSomeField1(String someField1) { + this.someField1 = someField1; + } + + public String getSomeField2() { + return someField2; + } + + public void setSomeField2(String someField2) { + this.someField2 = someField2; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/SampleEntity.java b/src/test/java/org/springframework/data/elasticsearch/SampleEntity.java new file mode 100644 index 000000000..90ba41b05 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/SampleEntity.java @@ -0,0 +1,72 @@ +package org.springframework.data.elasticsearch; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; + +@Document(indexName = "test-index", type = "test-type") +public class SampleEntity { + + @Id + private String id; + private String type; + private String message; + private int rate; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getRate() { + return rate; + } + + public void setRate(int rate) { + this.rate = rate; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SampleEntity)) { + return false; + } + if (this == obj) { + return true; + } + SampleEntity rhs = (SampleEntity) obj; + return new EqualsBuilder().append(this.id, rhs.id) + .append(this.type, rhs.type) + .append(this.message, rhs.message) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder() + .append(id) + .append(type) + .append(message) + .toHashCode(); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandlerTest.java b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandlerTest.java new file mode 100644 index 000000000..79701dc7c --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandlerTest.java @@ -0,0 +1,44 @@ +package org.springframework.data.elasticsearch.config; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.elasticsearch.repositories.SampleElasticsearchRepository; +import org.springframework.data.elasticsearch.client.NodeClientFactoryBean; +import org.springframework.data.elasticsearch.client.TransportClientFactoryBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("namespace.xml") +public class ElasticsearchNamespaceHandlerTest { + + @Autowired + private ApplicationContext context; + + @Test + public void shouldCreatesNodeClient() { + assertThat(context.getBean(NodeClientFactoryBean.class), is(notNullValue())); + assertThat(context.getBean(NodeClientFactoryBean.class), is(instanceOf(NodeClientFactoryBean.class))); + } + + @Test + public void shouldCreateTransportClient() { + assertThat(context.getBean(TransportClientFactoryBean.class), is(notNullValue())); + assertThat(context.getBean(TransportClientFactoryBean.class), is(instanceOf(TransportClientFactoryBean.class))); + } + + @Test + public void shouldCreateRepository(){ + assertThat(context.getBean(TransportClientFactoryBean.class), is(notNullValue())); + assertThat(context.getBean(SampleElasticsearchRepository.class), is(instanceOf(SampleElasticsearchRepository.class))); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/config/EnableElasticsearchRepositoriesTest.java b/src/test/java/org/springframework/data/elasticsearch/config/EnableElasticsearchRepositoriesTest.java new file mode 100644 index 000000000..4ece32ae0 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/config/EnableElasticsearchRepositoriesTest.java @@ -0,0 +1,44 @@ +package org.springframework.data.elasticsearch.config; + + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; +import org.springframework.data.elasticsearch.repositories.SampleElasticsearchRepository; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.elasticsearch.node.NodeBuilder.nodeBuilder; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class EnableElasticsearchRepositoriesTest { + + @Configuration + @EnableElasticsearchRepositories(basePackages = "org.springframework.data.elasticsearch.repositories") + static class Config { + + @Bean + public ElasticsearchOperations elasticsearchTemplate() { + return new ElasticsearchTemplate(nodeBuilder().local(true).node().client()); + } + } + + @Autowired + private SampleElasticsearchRepository repository; + + @Test + public void bootstrapsRepository() { + assertThat(repository, is(notNullValue())); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java new file mode 100644 index 000000000..2032d2a05 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java @@ -0,0 +1,338 @@ +package org.springframework.data.elasticsearch.core; + + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.SampleEntity; +import org.springframework.data.elasticsearch.core.query.*; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.lang.RandomStringUtils.randomNumeric; +import static org.elasticsearch.index.query.FilterBuilders.boolFilter; +import static org.elasticsearch.index.query.FilterBuilders.termFilter; +import static org.elasticsearch.index.query.QueryBuilders.fieldQuery; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:elasticsearch-template-test.xml") +public class ElasticsearchTemplateTest { + + @Autowired + private ElasticsearchTemplate elasticsearchTemplate; + + @Before + public void before(){ + elasticsearchTemplate.createIndex(SampleEntity.class); + DeleteQuery deleteQuery = new DeleteQuery(); + deleteQuery.setElasticsearchQuery(matchAllQuery()); + elasticsearchTemplate.delete(deleteQuery,SampleEntity.class); + elasticsearchTemplate.refresh(SampleEntity.class, true); + } + + @Test + public void shouldReturnCountForGivenSearchQuery(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(matchAllQuery()); + //when + long count = elasticsearchTemplate.count(searchQuery, SampleEntity.class); + //then + assertThat(count, is(equalTo(1L))); + } + + @Test + public void shouldReturnObjectForGivenId(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + //when + GetQuery getQuery = new GetQuery(); + getQuery.setId(documentId); + SampleEntity sampleEntity1 = elasticsearchTemplate.queryForObject(getQuery, SampleEntity.class); + //then + assertNotNull("not null....", sampleEntity1); + assertEquals(sampleEntity, sampleEntity1); + } + + @Test + public void shouldReturnPageForGivenSearchQuery(){ + //given + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(matchAllQuery()); + //when + Page sampleEntities = elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class); + //then + assertThat(sampleEntities, is(notNullValue())); + assertThat(sampleEntities.getTotalElements(), greaterThanOrEqualTo(1L)); + } + + @Test + public void shouldDoBulkIndex(){ + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("some message"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("some message"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + //when + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + //then + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(matchAllQuery()); + Page sampleEntities = elasticsearchTemplate.queryForPage(searchQuery,SampleEntity.class); + assertThat(sampleEntities.getTotalElements(), is(equalTo(2L))); + } + + @Test + public void shouldDeleteDocumentForGivenId(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + //when + elasticsearchTemplate.delete("test-index","test-type",documentId); + elasticsearchTemplate.refresh(SampleEntity.class, true); + //then + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(fieldQuery("id", documentId)); + Page sampleEntities = elasticsearchTemplate.queryForPage(searchQuery,SampleEntity.class); + assertThat(sampleEntities.getTotalElements(), equalTo(0L)); + } + + @Test + public void shouldDeleteEntityForGivenId(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + //when + elasticsearchTemplate.delete(SampleEntity.class,documentId); + elasticsearchTemplate.refresh(SampleEntity.class, true); + //then + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(fieldQuery("id", documentId)); + Page sampleEntities = elasticsearchTemplate.queryForPage(searchQuery,SampleEntity.class); + assertThat(sampleEntities.getTotalElements(), equalTo(0L)); + } + + @Test + public void shouldDeleteDocumentForGivenQuery(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + //when + DeleteQuery deleteQuery = new DeleteQuery(); + deleteQuery.setElasticsearchQuery(fieldQuery("id", documentId)); + elasticsearchTemplate.delete(deleteQuery,SampleEntity.class); + //then + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(fieldQuery("id",documentId)); + Page sampleEntities = elasticsearchTemplate.queryForPage(searchQuery,SampleEntity.class); + assertThat(sampleEntities.getTotalElements(), equalTo(0L)); + } + + @Test + public void shouldTestFilterBuilder(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(matchAllQuery()); + searchQuery.setElasticsearchFilter(boolFilter().must(termFilter("id", documentId))); + //when + Page sampleEntities = elasticsearchTemplate.queryForPage(searchQuery,SampleEntity.class); + //then + assertThat(sampleEntities.getTotalElements(), equalTo(1L)); + } + + @Test + public void testStringQuery(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + StringQuery stringQuery = new StringQuery(matchAllQuery().toString()); + //when + Page sampleEntities = elasticsearchTemplate.queryForPage(stringQuery,SampleEntity.class); + //then + assertThat(sampleEntities.getTotalElements(), equalTo(1L)); + } + + @Test + public void testStringQueryWithPageable(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + StringQuery stringQuery = new StringQuery(matchAllQuery().toString(),new PageRequest(0,10)); + //when + Page sampleEntities = elasticsearchTemplate.queryForPage(stringQuery,SampleEntity.class); + + //then + assertThat(sampleEntities.getTotalElements(), is(greaterThanOrEqualTo(1L))); + } + + @Test + @Ignore("By default, the search request will fail if there is no mapping associated with a field. The ignore_unmapped option allows to ignore fields that have no mapping and not sort by them") + public void testStringQueryWithPageableAndSort(){ + //todo + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + StringQuery stringQuery = new StringQuery(matchAllQuery().toString(),new PageRequest(0,10), new Sort(new Sort.Order(Sort.Direction.ASC,"messsage"))); + + //when + Page sampleEntities = elasticsearchTemplate.queryForPage(stringQuery,SampleEntity.class); + //then + assertThat(sampleEntities.getTotalElements(), is(greaterThanOrEqualTo(1L))); + } + + @Test + public void testStringQueryGetObject(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + StringQuery stringQuery = new StringQuery(fieldQuery("id",documentId).toString()); + + //when + SampleEntity sampleEntity1 = elasticsearchTemplate.queryForObject(stringQuery, SampleEntity.class); + //then + assertThat(sampleEntity1, is(notNullValue())); + assertThat(sampleEntity1.getId(), is(equalTo(documentId))); + } + + @Test + public void testCreateIndex(){ + //when + boolean created = elasticsearchTemplate.createIndex(SampleEntity.class); + //then + assertThat(created, is(true)); + } + + + @Test + public void testCriteria(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some test message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").contains("test")); + //when + SampleEntity sampleEntity1 = elasticsearchTemplate.queryForObject(criteriaQuery,SampleEntity.class); + //then + assertThat(sampleEntity1, is(notNullValue())); + } + + + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/convert/DateTimeConvertersTest.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/DateTimeConvertersTest.java new file mode 100644 index 000000000..82f353b05 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/DateTimeConvertersTest.java @@ -0,0 +1,56 @@ +package org.springframework.data.elasticsearch.core.convert; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.LocalDateTime; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Calendar; +import java.util.TimeZone; + + +public class DateTimeConvertersTest { + @Test + public void testJodaDateTimeConverterWithNullValue() { + Assert.assertNull(DateTimeConverters.JodaDateTimeConverter.INSTANCE.convert(null)); + } + + @Test + public void testJodaDateTimeConverter() { + DateTime dateTime = new DateTime(2013, 1,24 , 6, 35, 0, DateTimeZone.UTC); + Assert.assertEquals("2013-01-24T06:35:00.000Z", + DateTimeConverters.JodaDateTimeConverter.INSTANCE.convert(dateTime)); + } + + @Test + public void testJodaLocalDateTimeConverterWithNullValue() { + Assert.assertNull(DateTimeConverters.JodaLocalDateTimeConverter.INSTANCE.convert(null)); + } + + @Test + public void testJodaLocalDateTimeConverter() { + LocalDateTime dateTime = new LocalDateTime(new DateTime(2013, 1,24, 6, 35, 0, DateTimeZone.UTC).getMillis(), + DateTimeZone.UTC); + Assert.assertEquals("2013-01-24T06:35:00.000Z", + DateTimeConverters.JodaLocalDateTimeConverter.INSTANCE.convert(dateTime)); + } + + @Test + public void testJavaDateConverterWithNullValue() { + Assert.assertNull(DateTimeConverters.JavaDateConverter.INSTANCE.convert(null)); + } + + @Test + public void testJavaDateConverter() { + DateTime dateTime = new DateTime(2013, 1,24, 6, 35, 0, DateTimeZone.UTC); + Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + calendar.setTimeInMillis(dateTime.getMillis()); + + Assert.assertEquals("2013-01-24T06:35:00.000Z", + DateTimeConverters.JavaDateConverter.INSTANCE.convert(calendar.getTime())); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterTest.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterTest.java new file mode 100644 index 000000000..e99f3f1d3 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterTest.java @@ -0,0 +1,40 @@ +package org.springframework.data.elasticsearch.core.convert; + + +import org.junit.Test; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.mapping.context.MappingContext; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +public class MappingElasticsearchConverterTest { + + @Test(expected = IllegalArgumentException.class) + public void shouldFailToInitializeGivenMappingContextIsNull(){ + //given + new MappingElasticsearchConverter(null); + } + + @Test + public void shouldReturnMappingContextWithWhichItWasInitialized(){ + //given + MappingContext mappingContext = new SimpleElasticsearchMappingContext(); + MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext); + //then + assertThat(converter.getMappingContext(), is(notNullValue())); + assertThat(converter.getMappingContext(), is(sameInstance(mappingContext))); + } + + @Test + public void shouldReturnDefaultConversionService(){ + //given + MappingElasticsearchConverter converter = new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext()); + //when + ConversionService conversionService = converter.getConversionService(); + //then + assertThat(conversionService, is(notNullValue())); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryTest.java b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryTest.java new file mode 100644 index 000000000..d5ee3c468 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryTest.java @@ -0,0 +1,627 @@ +package org.springframework.data.elasticsearch.core.query; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.Page; +import org.springframework.data.elasticsearch.SampleEntity; +import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.annotation.Resource; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.lang.RandomStringUtils.randomNumeric; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:elasticsearch-template-test.xml") +public class CriteriaQueryTest { + + @Resource + private ElasticsearchTemplate elasticsearchTemplate; + + @Test + public void shouldTestAndOperation(){ + //given + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some test message"); + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").contains("test").and("message").contains("some")); + //when + SampleEntity sampleEntity1 = elasticsearchTemplate.queryForObject(criteriaQuery,SampleEntity.class); + //then + assertThat(sampleEntity1, is(notNullValue())); + } + + @Test + public void shouldTestOrOperation(){ + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("some message"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("test message"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").contains("some").or("message").contains("test")); + //when + Page page = elasticsearchTemplate.queryForPage(criteriaQuery, SampleEntity.class); + //then + assertThat(page, is(notNullValue())); + assertThat(page.getTotalElements(),is(greaterThanOrEqualTo(1L))); + } + + @Test + public void shouldTestInnerAndCriteria(){ + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("some message"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria().and(new Criteria("message").contains("some"))); + //when + Page page = elasticsearchTemplate.queryForPage(criteriaQuery, SampleEntity.class); + //then + assertThat(page, is(notNullValue())); + assertThat(page.getTotalElements(),is(greaterThanOrEqualTo(1L))); + } + + + @Test + public void shouldTestInnerOrCriteria(){ + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("some message"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria().or(new Criteria("message").contains("some"))); + //when + Page page = elasticsearchTemplate.queryForPage(criteriaQuery, SampleEntity.class); + //then + assertThat(page, is(notNullValue())); + assertThat(page.getTotalElements(),is(greaterThanOrEqualTo(1L))); + } + + @Test + public void testIs() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("some message"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").is("some message")); + //when + Page page= elasticsearchTemplate.queryForPage(criteriaQuery, SampleEntity.class); + //then + assertThat("message", is(criteriaQuery.getCriteria().getField().getName())); + assertThat(page.getTotalElements(),is(greaterThanOrEqualTo(1L))); + } + + @Test + public void testMultipleIs() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("some message"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("test message"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").is("some message").is("test message")); + //when + Page page = elasticsearchTemplate.queryForPage(criteriaQuery,SampleEntity.class); + //then + assertThat("message", is(criteriaQuery.getCriteria().getField().getName())); + assertThat(page.getTotalElements(),is(greaterThanOrEqualTo(2L))); + } + + @Test + public void testEndsWith() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("some message"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("test message end"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + Criteria criteria = new Criteria("message").endsWith("end"); + CriteriaQuery criteriaQuery = new CriteriaQuery(criteria); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery, SampleEntity.class); + //then + assertThat("message", is(criteriaQuery.getCriteria().getField().getName())); + assertThat(sampleEntity, is(notNullValue())); + } + + @Test + public void testStartsWith() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("start some message"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("test message"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + Criteria criteria = new Criteria("message").startsWith("start"); + CriteriaQuery criteriaQuery = new CriteriaQuery(criteria); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery,SampleEntity.class); + //then + assertThat("message", is(criteriaQuery.getCriteria().getField().getName())); + assertThat(sampleEntity,is(notNullValue())); + } + + @Test + public void testContains() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("contains some message"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("test message"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery= new CriteriaQuery(new Criteria("message").contains("contains")); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery,SampleEntity.class); + //then + assertThat("message", is(criteriaQuery.getCriteria().getField().getName())); + assertThat(sampleEntity,is(notNullValue())); + } + + @Test + public void testExpression() { + //given + //todo + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("elasticsearch search"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("test message"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery= new CriteriaQuery(new Criteria("message").expression("+elasticsearch || test")); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery,SampleEntity.class); + //then + assertThat("message", is(criteriaQuery.getCriteria().getField().getName())); + assertThat(sampleEntity,is(notNullValue())); + } + + @Test + public void testCriteriaChain() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("some message search"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("test test message"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").startsWith("some").endsWith("search").contains("message").is("some message search")); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery,SampleEntity.class); + //then + assertThat("message", is(criteriaQuery.getCriteria().getField().getName())); + assertThat(sampleEntity, is(notNullValue())); + } + + @Test + public void testIsNot() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("bar"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("foo"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").is("foo").not()); + //when + Page page = elasticsearchTemplate.queryForPage(criteriaQuery, SampleEntity.class); + //then + assertTrue(criteriaQuery.getCriteria().isNegating()); + assertThat(page,is(notNullValue())); + assertFalse(page.iterator().next().getMessage().contains("foo")); + } + + @Test + public void testBetween() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setRate(100); + sampleEntity1.setMessage("bar"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setRate(200); + sampleEntity2.setMessage("foo"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("rate").between(100,150)); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery, SampleEntity.class); + //then + assertThat(sampleEntity,is(notNullValue())); + } + + @Test + public void testBetweenWithoutUpperBound() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setRate(300); + sampleEntity1.setMessage("bar"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setRate(400); + sampleEntity2.setMessage("foo"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("rate").between(350,null)); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery, SampleEntity.class); + //then + assertThat(sampleEntity,is(notNullValue())); + } + + @Test + public void testBetweenWithoutLowerBound() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setRate(500); + sampleEntity1.setMessage("bar"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setRate(600); + sampleEntity2.setMessage("foo"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("rate").between(null,550)); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery, SampleEntity.class); + //then + assertThat(sampleEntity,is(notNullValue())); + } + + @Test + public void testLessThanEqauls() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setRate(700); + sampleEntity1.setMessage("bar"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setRate(800); + sampleEntity2.setMessage("foo"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("rate").lessThanEqual(750)); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery, SampleEntity.class); + //then + assertThat(sampleEntity,is(notNullValue())); + } + + @Test + public void testGreaterThanEqauls() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setRate(900); + sampleEntity1.setMessage("bar"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setRate(1000); + sampleEntity2.setMessage("foo"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("rate").greaterThanEqual(950)); + //when + SampleEntity sampleEntity = elasticsearchTemplate.queryForObject(criteriaQuery, SampleEntity.class); + //then + assertThat(sampleEntity,is(notNullValue())); + } + + @Test + public void testBoost() { + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setRate(700); + sampleEntity1.setMessage("bar foo"); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(sampleEntity1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setRate(800); + sampleEntity2.setMessage("foo"); + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + indexQueries.add(indexQuery2); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(SampleEntity.class,true); + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").contains("foo").boost(1)); + //when + Page page = elasticsearchTemplate.queryForPage(criteriaQuery, SampleEntity.class); + //then + assertThat(page.getTotalElements(),is(greaterThanOrEqualTo(1L))); + } + + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/CustomMethodRepositoryTest.java b/src/test/java/org/springframework/data/elasticsearch/repositories/CustomMethodRepositoryTest.java new file mode 100644 index 000000000..46073da4a --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/CustomMethodRepositoryTest.java @@ -0,0 +1,59 @@ +package org.springframework.data.elasticsearch.repositories; + + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.elasticsearch.SampleEntity; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.annotation.Resource; + +import static org.apache.commons.lang.RandomStringUtils.randomNumeric; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:custom-method-repository-test.xml") +public class CustomMethodRepositoryTest { + + @Resource + private SampleCustomMethodRepository repository; + + @Test + public void shouldExecuteCustomMethod(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setType("test"); + sampleEntity.setMessage("some message"); + repository.save(sampleEntity); + //when + Page page = repository.findByType("test", new PageRequest(1, 10)); + //then + assertThat(page, is(notNullValue())); + assertThat(page.getTotalElements(), is(greaterThanOrEqualTo(1L))); + } + + @Test + public void shouldExecuteCustomMethodWithQuery(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setType("test"); + sampleEntity.setMessage("customQuery"); + repository.save(sampleEntity); + //when + Page page = repository.findByMessage("customQuery", new PageRequest(1, 10)); + //then + assertThat(page, is(notNullValue())); + assertThat(page.getTotalElements(), is(greaterThanOrEqualTo(1L))); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/RepositoryTest.java b/src/test/java/org/springframework/data/elasticsearch/repositories/RepositoryTest.java new file mode 100644 index 000000000..ecefdaf04 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/RepositoryTest.java @@ -0,0 +1,343 @@ +package org.springframework.data.elasticsearch.repositories; + + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.SampleEntity; +import org.springframework.data.elasticsearch.core.query.SearchQuery; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.annotation.Resource; +import java.util.Arrays; + +import static org.apache.commons.lang.RandomStringUtils.randomNumeric; +import static org.elasticsearch.index.query.QueryBuilders.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:/repository-test.xml") +public class RepositoryTest { + + @Resource + private SampleElasticsearchRepository repository; + + + @Test + public void shouldDoBulkIndexDocument(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId); + sampleEntity1.setMessage("some message"); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId); + sampleEntity2.setMessage("some message"); + + //when + repository.save(Arrays.asList(sampleEntity1, sampleEntity2)); + //then + SampleEntity entityFromElasticSearch = repository.findOne(documentId); + assertThat(entityFromElasticSearch, is(notNullValue())); + } + + @Test + public void shouldSaveDocument(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + //when + repository.save(sampleEntity); + //then + SampleEntity entityFromElasticSearch = repository.findOne(documentId); + assertThat(entityFromElasticSearch, is(notNullValue())); + } + + @Test + public void shouldFindDocumentById(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + repository.save(sampleEntity); + //when + SampleEntity entityFromElasticSearch = repository.findOne(documentId); + //then + assertThat(entityFromElasticSearch, is(notNullValue())); + assertThat(sampleEntity, is((equalTo(sampleEntity)))); + } + + @Test + public void shouldReturnCountOfDocuments(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + repository.save(sampleEntity); + //when + Long count = repository.count(); + //then + assertThat(count, is(greaterThanOrEqualTo(1L))); + } + + @Test + public void shouldFindAllDocuments(){ + //when + Iterable results = repository.findAll(); + //then + assertThat(results, is(notNullValue())); + } + + @Test + public void shouldDeleteDocument(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + repository.save(sampleEntity); + //when + repository.delete(documentId); + //then + SampleEntity entityFromElasticSearch = repository.findOne(documentId); + assertThat(entityFromElasticSearch, is(nullValue())); + } + + @Test + public void shouldSearchDocumentsGivenSearchQuery(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some test message"); + repository.save(sampleEntity); + + SearchQuery query = new SearchQuery(); + query.setElasticsearchQuery(termQuery("message", "test")); + //when + Page page = repository.search(query); + //then + assertThat(page, is(notNullValue())); + assertThat(page.getNumberOfElements(), is(greaterThanOrEqualTo(1))); + } + + @Test + public void shouldSearchDocumentsGivenElasticsearchQuery(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + repository.save(sampleEntity); + //when + Page page = repository.search(termQuery("message", "world"), new PageRequest(0,50)); + //then + assertThat(page, is(notNullValue())); + assertThat(page.getNumberOfElements(), is(greaterThanOrEqualTo(1))); + } + + @Test + @Ignore + public void testFindAllByIdQuery(){ + //todo : find solution for findAll(Iterable ids) + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + repository.save(sampleEntity); + + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + repository.save(sampleEntity2); + + //when + Iterable sampleEntities=repository.findAll(Arrays.asList(documentId,documentId2)); + + //then + assertNotNull("sample entities cant be null..", sampleEntities); + } + + @Test + public void testSaveIterableEntities(){ + //given + + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + + Iterable sampleEntities = Arrays.asList(sampleEntity,sampleEntity2); + + //when + repository.save(sampleEntities); + + //then + Page entities = repository.search(fieldQuery("id", documentId), new PageRequest(0, 50)); + assertNotNull(entities); + } + + @Test + public void testDocumentExistById(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + repository.save(sampleEntity); + + //when + boolean exist = repository.exists(documentId); + + //then + assertEquals(exist, true); + } + + @Test + public void testSearchForGivenSearchQuery(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + repository.save(sampleEntity); + //when + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(fieldQuery("id",documentId)); + Page sampleEntities= repository.search(searchQuery); + //then + assertThat(sampleEntities.getTotalElements(), equalTo(1L)); + } + + @Test + public void testDeleteAll(){ + //when + repository.deleteAll(); + //then + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(matchAllQuery()); + Page sampleEntities= repository.search(searchQuery); + assertThat(sampleEntities.getTotalElements(), equalTo(0L)); + } + + @Test + public void testDeleteByEntity(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + repository.save(sampleEntity); + //when + repository.delete(sampleEntity); + //then + SearchQuery searchQuery = new SearchQuery(); + searchQuery.setElasticsearchQuery(fieldQuery("id", documentId)); + Page sampleEntities= repository.search(searchQuery); + assertThat(sampleEntities.getTotalElements(),equalTo(0L)); + } + + @Test + public void testSearchForReturnIterableEntities(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + repository.save(sampleEntity); + + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + repository.save(sampleEntity2); + + //when + Iterable sampleEntities=repository.search(fieldQuery("id",documentId)); + //then + assertNotNull("sample entities cant be null..", sampleEntities); + } + + @Test + public void testDeleteIterableEntities(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + repository.save(sampleEntity); + + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + repository.save(sampleEntity); + + Iterable sampleEntities = Arrays.asList(sampleEntity,sampleEntity2); + + //when + repository.delete(sampleEntities); + + //then + Page entities = repository.search(fieldQuery("id", documentId), new PageRequest(0,50)); + assertThat(entities.getTotalElements(),equalTo(0L)); + entities = repository.search(fieldQuery("id", documentId2), new PageRequest(0,50)); + assertThat(entities.getTotalElements(), equalTo(0L)); + } + + @Test + public void testIndexEntity(){ + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + //when + repository.index(sampleEntity); + //then + Page entities = repository.search(fieldQuery("id", documentId), new PageRequest(0,50)); + assertThat(entities.getTotalElements(),equalTo(1L)); + } + + @Test + @Ignore("By default, the search request will fail if there is no mapping associated with a field. The ignore_unmapped option allows to ignore fields that have no mapping and not sort by them") + public void testFindBySort(){ + //todo + //given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("A. hello world."); + repository.save(sampleEntity); + + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("B.hello world."); + repository.save(sampleEntity2); + + //when + Iterable sampleEntities=repository.findAll(new Sort(new Sort.Order(Sort.Direction.ASC,"message"))); + + //then + assertNotNull("sample entities cant be null..", sampleEntities); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/SampleCustomMethodRepository.java b/src/test/java/org/springframework/data/elasticsearch/repositories/SampleCustomMethodRepository.java new file mode 100644 index 000000000..f384e2dfd --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/SampleCustomMethodRepository.java @@ -0,0 +1,17 @@ +package org.springframework.data.elasticsearch.repositories; + + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.SampleEntity; +import org.springframework.data.elasticsearch.annotations.Query; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface SampleCustomMethodRepository extends ElasticsearchRepository { + + Page findByType(String type, Pageable pageable); + + @Query("{\"bool\" : {\"must\" : {\"field\" : {\"message\" : \"?0\"}}}}") + Page findByMessage(String message, Pageable pageable); + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/SampleElasticsearchRepository.java b/src/test/java/org/springframework/data/elasticsearch/repositories/SampleElasticsearchRepository.java new file mode 100644 index 000000000..2cedb927a --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/SampleElasticsearchRepository.java @@ -0,0 +1,8 @@ +package org.springframework.data.elasticsearch.repositories; + +import org.springframework.data.elasticsearch.SampleEntity; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface SampleElasticsearchRepository extends ElasticsearchRepository { + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryTest.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryTest.java new file mode 100644 index 000000000..9e4a749c9 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryTest.java @@ -0,0 +1,48 @@ +package org.springframework.data.elasticsearch.repository.support; + + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.querydsl.QueryDslPredicateExecutor; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ElasticsearchRepositoryFactoryTest { + + @Mock + private ElasticsearchOperations operations; + private ElasticsearchConverter converter; + private ElasticsearchRepositoryFactory factory; + MappingContext, ElasticsearchPersistentProperty> mappingContext= new SimpleElasticsearchMappingContext(); + + @Before + public void before(){ + converter = new MappingElasticsearchConverter(mappingContext); + when(operations.getElasticsearchConverter()).thenReturn(converter); + factory = new ElasticsearchRepositoryFactory(operations); + } + + + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionGivenQueryDslRepository(){ + //given + RepositoryMetadata metadata = new DefaultRepositoryMetadata(QueryDslPredicateExecutor.class); + //when + factory.getRepositoryBaseClass(metadata); + } + +} diff --git a/src/test/resources/org/springframework/data/elasticsearch/config/namespace.xml b/src/test/resources/org/springframework/data/elasticsearch/config/namespace.xml new file mode 100644 index 000000000..dc1b7e70c --- /dev/null +++ b/src/test/resources/org/springframework/data/elasticsearch/config/namespace.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file