diff --git a/src/main/java/org/springframework/data/jpa/domain/Specification.java b/src/main/java/org/springframework/data/jpa/domain/Specification.java index 13a477145..39500da7d 100644 --- a/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2011 the original author or authors. + * Copyright 2008-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import javax.persistence.criteria.Root; * Specification in the sense of Domain Driven Design. * * @author Oliver Gierke + * @author Thomas Darimont */ public interface Specification { diff --git a/src/main/java/org/springframework/data/jpa/domain/Specifications.java b/src/main/java/org/springframework/data/jpa/domain/Specifications.java index c6c933402..fed75d0bc 100644 --- a/src/main/java/org/springframework/data/jpa/domain/Specifications.java +++ b/src/main/java/org/springframework/data/jpa/domain/Specifications.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2012 the original author or authors. + * Copyright 2008-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,26 @@ */ package org.springframework.data.jpa.domain; +import static org.springframework.data.jpa.domain.Specifications.CompositionType.*; + +import java.io.Serializable; + import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import org.springframework.util.Assert; + /** * Helper class to easily combine {@link Specification} instances. * * @author Oliver Gierke + * @author Thomas Darimont */ -public class Specifications implements Specification { +public class Specifications implements Specification, Serializable { + + private static final long serialVersionUID = 1L; private final Specification spec; @@ -56,18 +65,8 @@ public class Specifications implements Specification { * @param other can be {@literal null}. * @return */ - public Specifications and(final Specification other) { - - return new Specifications(new Specification() { - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { - - Predicate otherPredicate = other == null ? null : other.toPredicate(root, query, builder); - Predicate thisPredicate = spec == null ? null : spec.toPredicate(root, query, builder); - - return thisPredicate == null ? otherPredicate : otherPredicate == null ? thisPredicate : builder.and( - thisPredicate, otherPredicate); - } - }); + public Specifications and(Specification other) { + return new Specifications(new ComposedSpecification(spec, other, AND)); } /** @@ -77,18 +76,8 @@ public class Specifications implements Specification { * @param other can be {@literal null}. * @return */ - public Specifications or(final Specification other) { - - return new Specifications(new Specification() { - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { - - Predicate otherPredicate = other == null ? null : other.toPredicate(root, query, builder); - Predicate thisPredicate = spec == null ? null : spec.toPredicate(root, query, builder); - - return thisPredicate == null ? otherPredicate : otherPredicate == null ? thisPredicate : builder.or( - thisPredicate, otherPredicate); - } - }); + public Specifications or(Specification other) { + return new Specifications(new ComposedSpecification(spec, other, OR)); } /** @@ -98,12 +87,8 @@ public class Specifications implements Specification { * @param spec can be {@literal null}. * @return */ - public static Specifications not(final Specification spec) { - return new Specifications(new Specification() { - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { - return spec == null ? null : builder.not(spec.toPredicate(root, query, builder)); - } - }); + public static Specifications not(Specification spec) { + return new Specifications(new NegatedSpecification(spec)); } /* @@ -113,4 +98,99 @@ public class Specifications implements Specification { public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { return spec == null ? null : spec.toPredicate(root, query, builder); } + + /** + * Enum for the composition types for {@link Predicate}s. + * + * @author Thomas Darimont + */ + enum CompositionType { + + AND { + @Override + public Predicate combine(CriteriaBuilder builder, Predicate lhs, Predicate rhs) { + return builder.and(lhs, rhs); + } + }, + + OR { + @Override + public Predicate combine(CriteriaBuilder builder, Predicate lhs, Predicate rhs) { + return builder.or(lhs, rhs); + } + }; + + abstract Predicate combine(CriteriaBuilder builder, Predicate lhs, Predicate rhs); + } + + /** + * A {@link Specification} that negates a given {@code Specification}. + * + * @author Thomas Darimont + * @since 1.6 + */ + private static class NegatedSpecification implements Specification, Serializable { + + private static final long serialVersionUID = 1L; + + private final Specification spec; + + /** + * Creates a new {@link NegatedSpecification} from the given {@link Specification} + * + * @param spec may be {@iteral null} + */ + public NegatedSpecification(Specification spec) { + this.spec = spec; + } + + public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { + return spec == null ? null : builder.not(spec.toPredicate(root, query, builder)); + } + } + + /** + * A {@link Specification} that combines two given {@code Specification}s via a given {@link CompositionType}. + * + * @author Thomas Darimont + * @since 1.6 + */ + private static class ComposedSpecification implements Specification, Serializable { + + private static final long serialVersionUID = 1L; + + private final Specification lhs; + private final Specification rhs; + private final CompositionType compositionType; + + /** + * Creates a new {@link ComposedSpecification} from the given {@link Specification} for the left-hand-side and the + * right-hand-side with the given {@link CompositionType}. + * + * @param lhs may be {@literal null} + * @param rhs may be {@literal null} + * @param compositionType must not be {@literal null} + */ + private ComposedSpecification(Specification lhs, Specification rhs, CompositionType compositionType) { + + Assert.notNull(compositionType, "CompositionType must not be null!"); + + this.lhs = lhs; + this.rhs = rhs; + this.compositionType = compositionType; + } + + /** + * Returns {@link Predicate} for the given {@link Root} and {@link CriteriaQuery} that is constructed via the given + * {@link CriteriaBuilder}. + */ + public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { + + Predicate otherPredicate = rhs == null ? null : rhs.toPredicate(root, query, builder); + Predicate thisPredicate = lhs == null ? null : lhs.toPredicate(root, query, builder); + + return thisPredicate == null ? otherPredicate : otherPredicate == null ? thisPredicate : this.compositionType + .combine(builder, thisPredicate, otherPredicate); + } + } } diff --git a/src/test/java/org/springframework/data/jpa/domain/SpecificationsUnitTests.java b/src/test/java/org/springframework/data/jpa/domain/SpecificationsUnitTests.java index 4b0e5e653..e3f81d546 100644 --- a/src/test/java/org/springframework/data/jpa/domain/SpecificationsUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/domain/SpecificationsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.springframework.data.jpa.domain.Specifications.*; +import static org.springframework.util.SerializationUtils.*; + +import java.io.Serializable; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; @@ -33,24 +36,24 @@ import org.mockito.runners.MockitoJUnitRunner; /** * @author Oliver Gierke + * @author Thomas Darimont */ @RunWith(MockitoJUnitRunner.class) public class SpecificationsUnitTests { - @Mock Specification mockSpec; - @Mock - Root root; - @Mock - CriteriaQuery query; - @Mock - CriteriaBuilder builder; + @Mock(extraInterfaces = Serializable.class) Root root; + @Mock(extraInterfaces = Serializable.class) CriteriaQuery query; + @Mock(extraInterfaces = Serializable.class) CriteriaBuilder builder; - @Mock - Predicate predicate; + @Mock(extraInterfaces = Serializable.class) Predicate predicate; @Before + @SuppressWarnings("unchecked") public void setUp() { + + mockSpec = (Specification) mock(Specification.class, withSettings().serializable()); + when(mockSpec.toPredicate(root, query, builder)).thenReturn(predicate); } @@ -128,4 +131,39 @@ public class SpecificationsUnitTests { assertThat(specification, is(notNullValue())); assertThat(specification.toPredicate(root, query, builder), is(predicate)); } + + /** + * @see DATAJPA-523 + */ + @Test + public void specificationsShouldBeSerializable() { + + Specifications specification = where(mockSpec); + specification = specification.and(mockSpec); + + assertThat(specification, is(notNullValue())); + + @SuppressWarnings("unchecked") + Specifications transferedSpecification = (Specifications) deserialize(serialize(specification)); + + assertThat(transferedSpecification, is(notNullValue())); + } + + /** + * @see DATAJPA-523 + */ + @Test + public void complexSpecificationsShouldBeSerializable() { + + Specifications specification = where(mockSpec); + specification = Specifications.not(specification.and(mockSpec).or(mockSpec)); + + assertThat(specification, is(notNullValue())); + + @SuppressWarnings("unchecked") + Specifications transferedSpecification = (Specifications) deserialize(serialize(specification)); + + assertThat(transferedSpecification, is(notNullValue())); + } + }