diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameter.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameter.java index 9bc1d189b..79d648915 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameter.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameter.java @@ -162,7 +162,7 @@ public final class Parameter { if (isNamedParameter()) { return format(NAMED_PARAMETER_TEMPLATE, getName()); } else { - return format(POSITION_PARAMETER_TEMPLATE, getParameterPosition()); + return format(POSITION_PARAMETER_TEMPLATE, getParameterIndex()); } } @@ -173,7 +173,7 @@ public final class Parameter { * * @return */ - public int getParameterPosition() { + public int getParameterIndex() { return parameters.getPlaceholderPosition(this); } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameters.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameters.java index 5a75c4271..6ca13d1bc 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameters.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameters.java @@ -273,11 +273,11 @@ public final class Parameters implements Iterable { * * @param index * @return the placeholder postion for the parameter with the given index. - * Will return 0 for special parameters. + * Will return -1 for special parameters. */ int getPlaceholderPosition(Parameter parameter) { - return parameter.isSpecialParameter() ? 0 + return parameter.isSpecialParameter() ? -1 : getPlaceholderPositionRecursively(parameter); } @@ -286,7 +286,7 @@ public final class Parameters implements Iterable { int result = parameter.isSpecialParameter() ? 0 : 1; - return parameter.isFirst() ? result : result + return parameter.isFirst() ? result - 1 : result + getPlaceholderPositionRecursively(parameter.getPrevious()); } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/SimpleParameterAccessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/SimpleParameterAccessor.java new file mode 100644 index 000000000..5a0c067b2 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/SimpleParameterAccessor.java @@ -0,0 +1,142 @@ +/* + * Copyright 2008-2010 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.repository.query; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.util.Assert; + + +/** + * {@link SimpleParameterAccessor} is used to bind method parameters. + * + * @author Oliver Gierke + */ +public class SimpleParameterAccessor { + + private final Parameters parameters; + private final Object[] values; + + + /** + * Creates a new {@link SimpleParameterAccessor}. + * + * @param parameters + * @param values + */ + public SimpleParameterAccessor(Parameters parameters, Object[] values) { + + Assert.notNull(parameters); + Assert.notNull(values); + + Assert.isTrue(parameters.getNumberOfParameters() == values.length, + "Invalid number of parameters given!"); + + this.parameters = parameters; + this.values = values; + } + + + /** + * Returns the {@link Pageable} of the parameters, if available. Returns + * {@code null} otherwise. + * + * @return + */ + public Pageable getPageable() { + + if (!parameters.hasPageableParameter()) { + return null; + } + + return (Pageable) values[parameters.getPageableIndex()]; + } + + + /** + * Returns the sort instance to be used for query creation. Will use a + * {@link Sort} parameter if available or the {@link Sort} contained in a + * {@link Pageable} if available. Returns {@code null} if no {@link Sort} + * can be found. + * + * @return + */ + public Sort getSort() { + + if (parameters.hasSortParameter()) { + return (Sort) values[parameters.getSortIndex()]; + } + + if (parameters.hasPageableParameter() && getPageable() != null) { + return getPageable().getSort(); + } + + return null; + } + + + private Object getBindableValue(int index) { + + int bindableCount = 0; + + for (Parameter parameter : parameters) { + + if (parameter.isBindable() && bindableCount == index) { + return values[parameter.getParameterIndex()]; + } + + if (parameter.isBindable()) { + bindableCount++; + } + } + + throw new IllegalArgumentException(); + } + + + /** + * Returns a {@link BindableParameterIterator} to traverse all bindable + * parameters. + * + * @return + */ + public BindableParameterIterator iterator() { + + return new BindableParameterIterator(); + } + + /** + * Iterator class to allow traversing all bindable parameters inside the + * accessor. + * + * @author Oliver Gierke + */ + public class BindableParameterIterator { + + private int currentIndex = 0; + + + /** + * Returns the next bindable parameter. + * + * @return + */ + public Object next() { + + return getBindableValue(currentIndex++); + } + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java new file mode 100644 index 000000000..cbcaf6044 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java @@ -0,0 +1,140 @@ +/* + * Copyright 2008-2010 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.repository.query.parser; + +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.SimpleParameterAccessor; +import org.springframework.data.repository.query.SimpleParameterAccessor.BindableParameterIterator; +import org.springframework.data.repository.query.parser.PartTree.OrPart; +import org.springframework.util.Assert; + + +/** + * Base class for query creators that create criteria based queries from a + * {@link PartTree}. + * + * @param T the actual query type to be created + * @param S the intermediate criteria type + * @author Oliver Gierke + */ +public abstract class AbstractQueryCreator { + + private final SimpleParameterAccessor parameters; + private final PartTree tree; + + + /** + * Creates a new {@link AbstractQueryCreator} for the given {@link PartTree} + * and {@link SimpleParameterAccessor}. + * + * @param tree + * @param parameters + */ + public AbstractQueryCreator(PartTree tree, + SimpleParameterAccessor parameters) { + + Assert.notNull(tree); + Assert.notNull(parameters); + + this.tree = tree; + this.parameters = parameters; + } + + + /** + * Creates the actual query object. + * + * @return + */ + public T createQuery() { + + return finalize(createCriteria(tree), tree.getSort()); + } + + + /** + * Actual query building logic. Traverses the {@link PartTree} and invokes + * callback methods to delegate actual criteria creation and concatenation. + * + * @param tree + * @return + */ + private S createCriteria(PartTree tree) { + + S base = null; + BindableParameterIterator iterator = parameters.iterator(); + + for (OrPart node : tree) { + + S criteria = null; + + for (Part part : node) { + + criteria = + criteria == null ? create(part, iterator) : and(part, + criteria, iterator); + } + + base = base == null ? criteria : or(base, criteria); + } + + return base; + } + + + /** + * Creates a new atomic instance of the criteria object. + * + * @param part + * @param iterator + * @return + */ + protected abstract S create(Part part, BindableParameterIterator iterator); + + + /** + * Creates a new criteria object from the given part and and-concatenates it + * to the given base criteria. + * + * @param part + * @param base will never be {@literal null}. + * @param iterator + * @return + */ + protected abstract S and(Part part, S base, + BindableParameterIterator iterator); + + + /** + * Or-concatenates the given base criteria to the given new criteria. + * + * @param base + * @param criteria + * @return + */ + protected abstract S or(S base, S criteria); + + + /** + * Actually creates the query object applying the given criteria object and + * {@link Sort} definition. + * + * @param criteria + * @param sort + * @return + */ + protected abstract T finalize(S criteria, Sort sort); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java index 7c5a63e43..97e9e2849 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java @@ -65,6 +65,6 @@ public class OrderBySource { public Sort toSort() { - return new Sort(this.orders); + return this.orders.isEmpty() ? null : new Sort(this.orders); } } \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java index 75eac7733..511ad0f0e 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java @@ -61,6 +61,44 @@ public class Part { } + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (obj == this) { + return true; + } + + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + + Part that = (Part) obj; + + return this.property.equals(that.property) + && this.type.equals(that.type); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 37; + result += 17 * property.hashCode(); + result += 17 * type.hashCode(); + return result; + } + + /** * @return the type */ diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java new file mode 100644 index 000000000..f06133275 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java @@ -0,0 +1,188 @@ +/* + * Copyright 2008-2010 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.repository.query.parser; + +import static java.lang.String.*; +import static java.util.regex.Pattern.*; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.parser.PartTree.OrPart; +import org.springframework.util.Assert; + + +/** + * Class to parse a {@link String} into a tree or {@link OrPart}s consisting of + * simple {@link Part} instances in turn. Takes a domain class as well to + * validate that each of the {@link Part}s are refering to a property of the + * domain class. The {@link PartTree} can then be used to build queries based on + * its API instead of parsing the method name for each query execution. + * + * @author Oliver Gierke + */ +public class PartTree implements Iterable { + + private static final String ORDER_BY = "OrderBy"; + private static final String[] PREFIXES = new String[] { "findBy", "find", + "readBy", "read", "getBy", "get" }; + private static final String PREFIX_TEMPLATE = "^%s(?=[A-Z]).*"; + private static final String KEYWORD_TEMPLATE = "(%s)(?=[A-Z])"; + + private final OrderBySource orderBySource; + private final List nodes = new ArrayList(); + + + /** + * Creates a new {@link PartTree} by parsing the given {@link String} + * + * @param source the {@link String} to parse + * @param domainClass the domain class to check indiviual parts against to + * ensure they refer to a property of the class + */ + public PartTree(String source, Class domainClass) { + + Assert.notNull(source); + Assert.notNull(domainClass); + + String foo = strip(source); + String[] parts = split(foo, ORDER_BY); + + if (parts.length > 2) { + throw new IllegalArgumentException( + "OrderBy must not be used more than once in a method name!"); + } + + buildTree(parts[0], domainClass); + this.orderBySource = + parts.length == 2 ? new OrderBySource(parts[1]) : null; + + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Iterable#iterator() + */ + public Iterator iterator() { + + return nodes.iterator(); + } + + + private void buildTree(String source, Class domainClass) { + + String[] split = split(source, "Or"); + + for (String part : split) { + nodes.add(new OrPart(part, domainClass)); + } + } + + + /** + * Returns the {@link Sort} specification parsed from the source. + * + * @return + */ + public Sort getSort() { + + return orderBySource == null ? null : orderBySource.toSort(); + } + + + /** + * Splits the given text at the given keywords. Expects camelcase style to + * only match concrete keywords and not derivatives of it. + * + * @param text + * @param keyword + * @return + */ + private String[] split(String text, String keyword) { + + String regex = format(KEYWORD_TEMPLATE, keyword); + + Pattern pattern = compile(regex); + return pattern.split(text); + } + + + /** + * Strips a prefix from the given method name if it starts with one of + * {@value #PREFIXES}. + * + * @param methodName + * @return + */ + private String strip(String methodName) { + + for (String prefix : PREFIXES) { + + String regex = format(PREFIX_TEMPLATE, prefix); + if (methodName.matches(regex)) { + return methodName.substring(prefix.length()); + } + } + + return methodName; + } + + /** + * A part of the parsed source that results from splitting up the resource + * ar {@literal Or} keywords. Consists of {@link Part}s that have to be + * concatenated by {@literal And}. + * + * @author Oliver Gierke + */ + public class OrPart implements Iterable { + + private final List children = new ArrayList(); + + + /** + * Creates a new {@link OrPart}. + * + * @param source the source to split up into {@literal And} parts in + * turn. + * @param domainClass the domain class to check the resulting + * {@link Part}s against. + */ + OrPart(String source, Class domainClass) { + + String[] split = split(source, "And"); + + for (String part : split) { + children.add(new Part(part, domainClass)); + } + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Iterable#iterator() + */ + public Iterator iterator() { + + return children.iterator(); + } + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java index e6da4358a..9557b66d9 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java @@ -103,8 +103,8 @@ public class ParametersUnitTests { Parameters parameters = new Parameters(method); - assertThat(parameters.getParameter(0).getParameterPosition(), is(0)); - assertThat(parameters.getParameter(1).getParameterPosition(), is(1)); + assertThat(parameters.getParameter(0).getParameterIndex(), is(-1)); + assertThat(parameters.getParameter(1).getParameterIndex(), is(0)); method = SampleDao.class.getMethod("validWithSortInBetween", @@ -112,9 +112,9 @@ public class ParametersUnitTests { parameters = new Parameters(method); - assertThat(parameters.getParameter(0).getParameterPosition(), is(1)); - assertThat(parameters.getParameter(1).getParameterPosition(), is(0)); - assertThat(parameters.getParameter(2).getParameterPosition(), is(2)); + assertThat(parameters.getParameter(0).getParameterIndex(), is(0)); + assertThat(parameters.getParameter(1).getParameterIndex(), is(-1)); + assertThat(parameters.getParameter(2).getParameterIndex(), is(1)); } diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java new file mode 100644 index 000000000..2347fc834 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2008-2010 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.repository.query; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + + +/** + * Unit tests for {@link SimpleParameterAccessor}. + * + * @author Oliver Gierke + */ +public class SimpleParameterAccessorUnitTests { + + Parameters parameters, sortParameters, pageableParameters; + + + @Before + public void setUp() throws SecurityException, NoSuchMethodException { + + parameters = + new Parameters(Sample.class.getMethod("sample", String.class)); + sortParameters = + new Parameters(Sample.class.getMethod("sample1", String.class, + Sort.class)); + pageableParameters = + new Parameters(Sample.class.getMethod("sample2", String.class, + Pageable.class)); + } + + + @Test + public void testname() throws Exception { + + new SimpleParameterAccessor(parameters, new Object[] { "test" }); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullParameters() throws Exception { + + new SimpleParameterAccessor(null, new Object[0]); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullValues() throws Exception { + + new SimpleParameterAccessor(parameters, null); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsTooLittleNumberOfArguments() throws Exception { + + new SimpleParameterAccessor(parameters, new Object[0]); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsTooManyArguments() throws Exception { + + new SimpleParameterAccessor(parameters, new Object[] { "test", "test" }); + } + + + @Test + public void returnsNullForPageableAndSortIfNoneAvailable() throws Exception { + + SimpleParameterAccessor accessor = + new SimpleParameterAccessor(parameters, new Object[] { "test" }); + assertThat(accessor.getPageable(), is(nullValue())); + assertThat(accessor.getSort(), is(nullValue())); + } + + + @Test + public void returnsSortIfAvailable() { + + Sort sort = new Sort("foo"); + SimpleParameterAccessor accessor = + new SimpleParameterAccessor(sortParameters, new Object[] { + "test", sort }); + assertThat(accessor.getSort(), is(sort)); + assertThat(accessor.getPageable(), is(nullValue())); + } + + + @Test + public void returnsPageableIfAvailable() { + + Pageable pageable = new PageRequest(0, 10); + SimpleParameterAccessor accessor = + new SimpleParameterAccessor(pageableParameters, new Object[] { + "test", pageable }); + assertThat(accessor.getPageable(), is(pageable)); + assertThat(accessor.getSort(), is(nullValue())); + } + + + @Test + public void returnsSortFromPageableIfAvailable() throws Exception { + + Sort sort = new Sort("foo"); + Pageable pageable = new PageRequest(0, 10, sort); + SimpleParameterAccessor accessor = + new SimpleParameterAccessor(pageableParameters, new Object[] { + "test", pageable }); + assertThat(accessor.getPageable(), is(pageable)); + assertThat(accessor.getSort(), is(sort)); + } + + interface Sample { + + void sample(String firstname); + + + void sample1(String firstname, Sort sort); + + + void sample2(String firstname, Pageable pageable); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java new file mode 100644 index 000000000..05b44cdff --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2008-2010 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.repository.query.parser; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Iterator; + +import org.junit.Test; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.repository.query.parser.PartTree.OrPart; + + +/** + * Unit tests for {@link PartTree}. + * + * @author Oliver Gierke + */ +public class PartTreeUnitTests { + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullSource() throws Exception { + + new PartTree(null, getClass()); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullDomainClass() throws Exception { + + new PartTree("test", null); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsMultipleOrderBy() throws Exception { + + new PartTree("firstnameOrderByLastnameOrderByFirstname", User.class); + } + + + @Test + public void parsesSimplePropertyCorrectly() throws Exception { + + PartTree partTree = new PartTree("firstname", User.class); + assertPart(partTree, new Part[] { new Part("firstname", User.class) }); + } + + + @Test + public void parsesAndPropertiesCorrectly() throws Exception { + + PartTree partTree = new PartTree("firstnameAndLastname", User.class); + assertPart(partTree, new Part[] { new Part("firstname", User.class), + new Part("lastname", User.class) }); + assertThat(partTree.getSort(), is(nullValue())); + } + + + @Test + public void parsesOrPropertiesCorrectly() throws Exception { + + PartTree partTree = new PartTree("firstnameOrLastname", User.class); + assertPart(partTree, new Part[] { new Part("firstname", User.class) }, + new Part[] { new Part("lastname", User.class) }); + assertThat(partTree.getSort(), is(nullValue())); + } + + + @Test + public void hasSortIfOrderByIsGiven() throws Exception { + + PartTree partTree = + new PartTree("firstnameOrderByLastnameDesc", User.class); + assertThat(partTree.getSort(), is(new Sort(Direction.DESC, "lastname"))); + } + + + private void assertPart(PartTree tree, Part[]... parts) { + + Iterator iterator = tree.iterator(); + for (Part[] part : parts) { + assertThat(iterator.hasNext(), is(true)); + Iterator partIterator = iterator.next().iterator(); + for (int k = 0; k < part.length; k++) { + assertThat(String.format("Expected %d parts but have %d", + part.length, k + 1), partIterator.hasNext(), is(true)); + Part next = partIterator.next(); + assertThat( + String.format("Expected %s but got %s!", part[k], next), + part[k], is(next)); + } + assertThat("Too many parts!", partIterator.hasNext(), is(false)); + } + assertThat("Too many or parts!", iterator.hasNext(), is(false)); + } + + class User { + + String firstname; + String lastname; + } +}