diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQuery.java index 835bd5cfd..2cc60d4fb 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQuery.java @@ -23,15 +23,13 @@ import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.support.Neo4jTemplate; import org.springframework.data.neo4j.support.typerepresentation.LabelBasedNodeTypeRepresentationStrategy; -import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentationStrategyFactory; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.parser.Part; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import static org.springframework.util.StringUtils.*; +import static org.springframework.util.StringUtils.hasText; public class CypherQuery implements CypherQueryDefinition { private final VariableContext variableContext = new VariableContext(); @@ -79,15 +77,8 @@ public class CypherQuery implements CypherQueryDefinition { if (!addedStartClause(partInfo)) { whereClauses.add(new WhereClause(partInfo,template)); } - } else if (leafProperty.isRelationship()) { - startClauses.add(new NodeEntityMatchingStartClause(partInfo)); - if (useLabels) { - whereClauses.add(new LabelBasedTypeRestrictingWhereClause(new PartInfo(path, variableContext.getVariableFor(entity), part, -1), entity, template)); - } else { - whereClauses.add(new IndexBasedTypeRestrictingWhereClause(new PartInfo(path, variableContext.getVariableFor(entity), part, -1), entity, template)); - } - } else if (leafProperty.isIdProperty()) { - startClauses.add(new NodeEntityMatchingStartClause(partInfo)); + } else if (leafProperty.isRelationship() || leafProperty.isIdProperty()) { + startClauses.add(new GraphIdStartClause(partInfo)); if (useLabels) { whereClauses.add(new LabelBasedTypeRestrictingWhereClause(new PartInfo(path, variableContext.getVariableFor(entity), part, -1), entity, template)); } else { @@ -128,11 +119,19 @@ public class CypherQuery implements CypherQueryDefinition { private boolean addedStartClause(PartInfo partInfo) { if (!partInfo.isIndexed()) return false; - for (StartClause startClause : startClauses) { - // only merge when same index and same variable + + ListIterator it = startClauses.listIterator(); + while (it.hasNext()) { + StartClause startClause = it.next(); if (startClause.sameIdentifier(partInfo)) { if (startClause.sameIndex(partInfo)) { startClause.merge(partInfo); + if (startClause.hasMultipleParts()) { + // Replace it as we could not detect this initially ... + List newList = new ArrayList(); + newList.addAll(startClause.getPartInfos()); + it.set(StartClauseFactory.create(newList)); + } return true; } else { // must stop b/c of invalid combination (same identifier but different index) @@ -140,7 +139,7 @@ public class CypherQuery implements CypherQueryDefinition { } } } - startClauses.add(new StartClause(partInfo)); + startClauses.add(StartClauseFactory.create(partInfo)); return true; } @@ -174,52 +173,82 @@ public class CypherQuery implements CypherQueryDefinition { } private String render() { + String startClauses = collectionToDelimitedString(this.startClauses, ", "); String matchClauses = toQueryString(this.matchClauses); String whereClauses = collectionToDelimitedString(this.whereClauses, " AND "); StringBuilder builder = new StringBuilder(""); + boolean startClauseInUse = renderStartClauses(builder, startClauses); + renderMatchClauses(builder, matchClauses, startClauseInUse); + renderWhereClauses(builder, whereClauses); + renderReturnClauses(builder); - boolean matchKeyWordUsed = false; - boolean startClauseUsed = buildStartClauseIfRequired(builder, startClauses); - if (!startClauseUsed && useLabels) { - matchKeyWordUsed = true; - builder.append(" MATCH ").append(defaultMatchBasedStartClause(entity)); - } - if (hasText(matchClauses)) { - builder.append(matchKeyWordUsed ? " , " : " MATCH "); - builder.append(matchClauses); - } - - if (hasText(whereClauses)) { - builder.append(" WHERE ").append(whereClauses); - } + return builder.toString(); + } + private void renderReturnClauses(StringBuilder builder) { String returnEntity = String.format(QueryTemplates.VARIABLE,getEntityName(entity)); if (isCountQuery) { builder.append(" RETURN ").append("count(").append(returnEntity).append(")"); } else { builder.append(" RETURN ").append(returnEntity); } - return builder.toString(); + } + + private void renderWhereClauses(StringBuilder builder, String whereClauses) { + if (hasText(whereClauses)) { + builder.append(" WHERE ").append(whereClauses); + } + } + + private void renderMatchClauses(StringBuilder builder, String matchClauses, boolean startClauseInUse) { + if (hasText(matchClauses)) { + builder.append(" MATCH ").append(matchClauses); + return; + } + if (useLabelBasedTRS() && !startClauseInUse) { + builder.append(" MATCH ").append(defaultMatchBasedStartClause(entity)); + return; + } } /** - * Note: This will change to get rid of the start clauses completely but - * for now we just get it to work! + * From Neo4j 2.0 onwards the start clause is optional (although still needs to be + * used for certain cases where index lookups are required etc. This method takes + * the currently built up query in builder (which should be empty at this point) + * and begins to add in any currently defined start clauses. In the absence of + * any start clauses, it will add in the default start clause if using an indexing + * strtegy. + * + * @param builder current query which has been built up + * @param startClauses List of current start clauses + * @return true if this method resulted in the query having a START clause + * applied to it otherwise false */ - private boolean buildStartClauseIfRequired(StringBuilder builder, String startClauses) { + private boolean renderStartClauses(StringBuilder builder, String startClauses) { if (hasText(startClauses)) { builder.append("START ").append(startClauses); return true; - } else if (!useLabels) { - // TODO: Need to change index based stuff to also not use START + } + if (useIndexBasedTRS()) { builder.append("START ").append(defaultIndexBasedStartClause(entity)); return true; } + + // For a label based strategy, we do not use a start clause + // but rather begin with a MATCH clause return false; } + private boolean useIndexBasedTRS() { + return !useLabels; + } + + private boolean useLabelBasedTRS() { + return useLabels; + } + private String addSorts(Sort sort) { final List sorts = formatSorts(sort); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExactIndexBasedStartClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExactIndexBasedStartClause.java new file mode 100644 index 000000000..d6079bfe5 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExactIndexBasedStartClause.java @@ -0,0 +1,60 @@ +/** + * Copyright 2013 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.neo4j.repository.query; + +import org.neo4j.helpers.collection.IteratorUtil; +import org.springframework.data.neo4j.support.Neo4jTemplate; +import org.springframework.data.repository.query.Parameter; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Represents a start clause which makes use of an exact index + * match. + * + * @author Nicki Watt + */ +public class ExactIndexBasedStartClause extends IndexBasedStartClause { + + public ExactIndexBasedStartClause(PartInfo partInfo) { + super(partInfo); + } + + @Override + public String toString() { + final PartInfo partInfo = getPartInfo(); + final String identifier = partInfo.getIdentifier(); + final String indexName = partInfo.getIndexName(); + final int parameterIndex = partInfo.getParameterIndex(); + return String.format(QueryTemplates.START_CLAUSE_INDEX_LOOKUP, identifier, indexName, partInfo.getNeo4jPropertyName(), parameterIndex); + } + + @Override + public Map resolveParameters(Map parameters, Neo4jTemplate template) { + Map myParameters = findMyParameters(parameters.keySet()); + + Map result = new LinkedHashMap(parameters); + result.keySet().removeAll(myParameters.keySet()); + + final Map values = matchToPartsAndConvert(myParameters, parameters,template); + + Parameter firstParam = IteratorUtil.first(myParameters.keySet()); + Object value=IteratorUtil.first(values.values()); + result.put(firstParam, value); + return result; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/FullTextIndexBasedStartClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/FullTextIndexBasedStartClause.java new file mode 100644 index 000000000..e6dd972de --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/FullTextIndexBasedStartClause.java @@ -0,0 +1,75 @@ +/** + * Copyright 2013 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.neo4j.repository.query; + +import org.neo4j.helpers.collection.IteratorUtil; +import org.springframework.data.neo4j.support.Neo4jTemplate; +import org.springframework.data.repository.query.Parameter; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a start clause which makes use of a full text index + * based match against one or more parts. + * + * @author Nicki Watt + */ +public class FullTextIndexBasedStartClause extends IndexBasedStartClause { + + public FullTextIndexBasedStartClause(PartInfo partInfo) { + super(partInfo); + } + + public FullTextIndexBasedStartClause(List partInfos) { + super(partInfos.get(0)); + int i = 0; + for (PartInfo partInfo: partInfos) { + if (i != 0) { + this.merge(partInfo); + } + i++; + } + } + + @Override + public String toString() { + final PartInfo partInfo = getPartInfo(); + final String identifier = partInfo.getIdentifier(); + final String indexName = partInfo.getIndexName(); + final int parameterIndex = partInfo.getParameterIndex(); + return String.format(QueryTemplates.START_CLAUSE_INDEX_QUERY, identifier, indexName, parameterIndex); + } + + @Override + public Map resolveParameters(Map parameters, Neo4jTemplate template) { + Map myParameters = findMyParameters(parameters.keySet()); + + Map result = new LinkedHashMap(parameters); + result.keySet().removeAll(myParameters.keySet()); + + final Map values = matchToPartsAndConvert(myParameters, parameters,template); + + Parameter firstParam = IteratorUtil.first(myParameters.keySet()); + result.put(firstParam, renderQuery(values)); + return result; + } + + + + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/NodeEntityMatchingStartClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphIdStartClause.java similarity index 81% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/NodeEntityMatchingStartClause.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphIdStartClause.java index edc403847..3bd127ae3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/NodeEntityMatchingStartClause.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphIdStartClause.java @@ -1,5 +1,5 @@ /** - * Copyright 2011 the original author or authors. + * Copyright 2013 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. @@ -20,8 +20,15 @@ import org.springframework.data.repository.query.Parameter; import java.util.Map; -public class NodeEntityMatchingStartClause extends StartClause { - public NodeEntityMatchingStartClause(PartInfo partInfo) { +/** + * Represents a start clause where the actual graph id + * of the entity is used. + * + * @author Nicki Watt + */ +public class GraphIdStartClause extends StartClause { + + public GraphIdStartClause(PartInfo partInfo) { super(partInfo); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/IndexRestrictingStartClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/IndexBasedStartClause.java similarity index 60% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/IndexRestrictingStartClause.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/IndexBasedStartClause.java index 64781d5e3..38d61ff87 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/IndexRestrictingStartClause.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/IndexBasedStartClause.java @@ -1,5 +1,5 @@ /** - * Copyright 2011 the original author or authors. + * Copyright 2013 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. @@ -15,16 +15,17 @@ */ package org.springframework.data.neo4j.repository.query; -public class IndexRestrictingStartClause extends StartClause { - private final String className; +/** + * Abstract class which represents a start clause which makes + * use of an indexof some sort. + * + * @author Nicki Watt + */ +abstract class IndexBasedStartClause extends StartClause { - public IndexRestrictingStartClause(PartInfo partInfo, String className) { + public IndexBasedStartClause(PartInfo partInfo) { super(partInfo); - this.className = className; } - @Override - public String toString() { - return String.format(QueryTemplates.DEFAULT_INDEXBASED_START_CLAUSE, getPartInfo().getIdentifier(), className); - } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartInfo.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartInfo.java index 320702af8..07bdc1151 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartInfo.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartInfo.java @@ -22,6 +22,8 @@ import org.springframework.data.repository.query.parser.Part; import org.springframework.util.Assert; /** + * This class represents .... + * All the information about a particular part of a Neo4jPersistentProperty? * @author mh * @since 31.10.11 */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StartClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StartClause.java index d941d4533..779f2bf5a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StartClause.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StartClause.java @@ -27,60 +27,33 @@ import java.util.*; /** * Representation of a Cypher {@literal start} clause. - * + * * @author Oliver Gierke */ -// TODO id-startclause, index-startclause (exact,point,fulltext) -class StartClause { +abstract class StartClause { private final SortedMap partInfos=new TreeMap (); /** - * Creates a new {@link StartClause} from the given {@link Neo4jPersistentProperty}, variable and the given - * parameter index. + * Creates a new {@link StartClause} from the given {@link Neo4jPersistentProperty}, + * variable and the given parameter index. */ public StartClause(PartInfo partInfo) { this.partInfos.put(partInfo.getParameterIndex(), partInfo); } - @Override - public String toString() { - final PartInfo partInfo = getPartInfo(); - final String identifier = partInfo.getIdentifier(); - final String indexName = partInfo.getIndexName(); - final int parameterIndex = partInfo.getParameterIndex(); - // fulltext or multiple - if (shouldRenderQuery()) { - return String.format(QueryTemplates.START_CLAUSE_INDEX_QUERY, identifier, indexName, parameterIndex); - } - // exact and single - return String.format(QueryTemplates.START_CLAUSE_INDEX_LOOKUP, identifier, indexName, partInfo.getNeo4jPropertyName(), parameterIndex); + /** + * Returns true if this start clause comprises of multiple parts + * @return + */ + protected boolean hasMultipleParts() { + return partInfos.size() > 1; } - private boolean shouldRenderQuery() { - PartInfo partInfo = getPartInfo(); - return partInfo.isFullText() || EnumSet.of(Part.Type.LIKE,Part.Type.STARTING_WITH,Part.Type.CONTAINING,Part.Type.ENDING_WITH).contains(partInfo.getType())|| partInfos.size() > 1; - } - public Map resolveParameters(Map parameters, Neo4jTemplate template) { - Map myParameters = findMyParameters(parameters.keySet()); + public abstract Map resolveParameters(Map parameters, Neo4jTemplate template); - Map result = new LinkedHashMap(parameters); - result.keySet().removeAll(myParameters.keySet()); - - final Map values = matchToPartsAndConvert(myParameters, parameters,template); - - Parameter firstParam = IteratorUtil.first(myParameters.keySet()); - if (shouldRenderQuery()) { - result.put(firstParam, renderQuery(values)); - } else { - Object value=IteratorUtil.first(values.values()); - result.put(firstParam, value); - } - return result; - } - - private String renderQuery(Map values) { + protected String renderQuery(Map values) { StringBuilder sb=new StringBuilder(); for (Map.Entry entry : values.entrySet()) { if (sb.length()>0) sb.append(" AND "); @@ -91,7 +64,7 @@ class StartClause { return sb.toString(); } - private Map matchToPartsAndConvert(Map myParameters, Map parameters, Neo4jTemplate template) { + protected Map matchToPartsAndConvert(Map myParameters, Map parameters, Neo4jTemplate template) { Map result = new LinkedHashMap(); for (Map.Entry entry : myParameters.entrySet()) { Object value = parameters.get(entry.getKey()); @@ -103,15 +76,15 @@ class StartClause { return result; } - private Object convertIfNecessary(Neo4jTemplate template, Object value, Neo4jPersistentProperty property) { - if (property.isIndexedNumerically()) return new ValueContext(value).indexNumeric(); + protected Object convertIfNecessary(Neo4jTemplate template, Object value, Neo4jPersistentProperty property) { + if (property.isIndexedNumerically()) return new ValueContext(value).indexNumeric(); if (property.isNeo4jPropertyType() && property.isNeo4jPropertyValue(value)) return value; PropertyConverter converter = new PropertyConverter(template.getConversionService(), property); return converter.serializePropertyValue(value); } - private Map findMyParameters(Set parameters) { + protected Map findMyParameters(Set parameters) { Map result=new LinkedHashMap(); for (Parameter parameter : parameters) { PartInfo partInfo = partInfos.get(parameter.getIndex()); @@ -149,4 +122,10 @@ class StartClause { } return true; } -} \ No newline at end of file + + protected Collection getPartInfos() { + return partInfos.values(); + } +} + + diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StartClauseFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StartClauseFactory.java new file mode 100644 index 000000000..59562f8a2 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StartClauseFactory.java @@ -0,0 +1,71 @@ +/** + * Copyright 2013 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.neo4j.repository.query; + +import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; +import org.springframework.data.repository.query.parser.Part; + +import java.util.EnumSet; +import java.util.List; + +/** + * This class is responsible for creating appropriate StartClause + * instances depending on the part(s) it is being created from. + * + * @author Nicki Watt + */ +public class StartClauseFactory { + + /** + * At present, Multiple parts are always assumed to result in a + * a FullTextIndexBasedStartClause + * @param partInfos The various parts which the start clause + * needs to be created around/for. + * @return A appropriate StartClause + */ + public static StartClause create(List partInfos) { + return (partInfos.size() == 1) + ? create(partInfos.get(0)) + : new FullTextIndexBasedStartClause(partInfos); + } + + /** + * Given a particular partInfo, create an appropriate StartClause + * instance + * + * @param partInfo The part which the start clause needs to be + * created around/for. + * @return A appropriate StartClause + */ + public static StartClause create(PartInfo partInfo) { + if (partInfo.isIndexed()) { + return (partInfo.isFullText() || isTextualSearchLikePart(partInfo)) + ? new FullTextIndexBasedStartClause(partInfo) + : new ExactIndexBasedStartClause(partInfo); + } + + Neo4jPersistentProperty leafProperty = partInfo.getLeafProperty(); + if (leafProperty.isRelationship() || leafProperty.isIdProperty()) { + return new GraphIdStartClause(partInfo); + } + + throw new IllegalArgumentException("Cannot determine an appropriate Start Clause for partInfo=" + partInfo ); + } + + private static boolean isTextualSearchLikePart(PartInfo partInfo) { + return EnumSet.of(Part.Type.LIKE,Part.Type.STARTING_WITH,Part.Type.CONTAINING,Part.Type.ENDING_WITH).contains(partInfo.getType()); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/AbstractDerivedFinderMethodTestBase.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/AbstractDerivedFinderMethodTestBase.java index 6db8d6983..5612e8a05 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/AbstractDerivedFinderMethodTestBase.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/AbstractDerivedFinderMethodTestBase.java @@ -103,7 +103,7 @@ public abstract class AbstractDerivedFinderMethodTestBase { protected Object[] trsSpecificExpectedParams; @Test - public void testCreateIndexQuery() throws Exception { + public void testCypherQueryBuilderWithTwoIndexedParams() throws Exception { CypherQueryBuilder builder = new CypherQueryBuilder(ctx, Thing.class, template); builder.addRestriction(new Part("firstName",Thing.class)); builder.addRestriction(new Part("lastName",Thing.class)); @@ -111,6 +111,7 @@ public abstract class AbstractDerivedFinderMethodTestBase { assertEquals( getExpectedQuery("START `thing`=node:`Thing`({0}) RETURN `thing`"), query.toQueryString()); + } @Test