From caad3db37fb99dd776738783cc08e6f8895d9c98 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Wed, 30 Apr 2014 11:07:53 +0200 Subject: [PATCH] DATAJPA-523 - Make Specifications implement Serializable. Specifications now implements Serializable to be able to transfer them across systems boundaries. Introduced special Serializable Specification types for composed and negated Specifications. Note that currently only Hibernate and EclipseLink expression structures implement Serializable - OpenJPA's don't. Original pull request: #86. --- .../data/jpa/domain/Specification.java | 3 +- .../data/jpa/domain/Specifications.java | 144 ++++++++++++++---- .../jpa/domain/SpecificationsUnitTests.java | 58 +++++-- 3 files changed, 162 insertions(+), 43 deletions(-) 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())); + } + }