diff --git a/spring-data-commons-core/pom.xml b/spring-data-commons-core/pom.xml index d03a3c6e3..5e825ae0c 100644 --- a/spring-data-commons-core/pom.xml +++ b/spring-data-commons-core/pom.xml @@ -77,6 +77,13 @@ junit junit + + + joda-time + joda-time + 1.6 + true + diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Auditable.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Auditable.java new file mode 100644 index 000000000..fe9f37d17 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Auditable.java @@ -0,0 +1,97 @@ +/* + * 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.domain; + +import java.io.Serializable; + +import org.joda.time.DateTime; + + +/** + * Interface for auditable entities. Allows storing and retrieving creation and + * modification information. The changing instance (typically some user) is to + * be defined by a generics definition. + * + * @author Oliver Gierke + * @param the auditing type. Typically some kind of user. + * @param the type of the auditing type's idenifier + */ +public interface Auditable extends Persistable { + + /** + * Returns the user who created this entity. + * + * @return the createdBy + */ + U getCreatedBy(); + + + /** + * Sets the user who created this entity. + * + * @param createdBy the creating entity to set + */ + void setCreatedBy(final U createdBy); + + + /** + * Returns the creation date of the entity. + * + * @return the createdDate + */ + DateTime getCreatedDate(); + + + /** + * Sets the creation date of the entity. + * + * @param creationDate the creation date to set + */ + void setCreatedDate(final DateTime creationDate); + + + /** + * Returns the user who modified the entity lastly. + * + * @return the lastModifiedBy + */ + U getLastModifiedBy(); + + + /** + * Sets the user who modified the entity lastly. + * + * @param lastModifiedBy the last modifying entity to set + */ + void setLastModifiedBy(final U lastModifiedBy); + + + /** + * Returns the date of the last modification. + * + * @return the lastModifiedDate + */ + DateTime getLastModifiedDate(); + + + /** + * Sets the date of the last modification. + * + * @param lastModifiedDate the date of the last modification to set + */ + void setLastModifiedDate(final DateTime lastModifiedDate); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/AuditorAware.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/AuditorAware.java new file mode 100644 index 000000000..e8b6dedb3 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/AuditorAware.java @@ -0,0 +1,34 @@ +/* + * 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.domain; + +/** + * Interface for components that are aware of the application's current auditor. + * This will be some kind of user mostly. + * + * @author Oliver Gierke + * @param the type of the auditing instance + */ +public interface AuditorAware { + + /** + * Returns the current auditor of the application. + * + * @return the current auditor + */ + T getCurrentAuditor(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Page.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Page.java new file mode 100644 index 000000000..822e5e025 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Page.java @@ -0,0 +1,127 @@ +/* + * 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.domain; + +import java.util.Iterator; +import java.util.List; + + +/** + * A page is a sublist of a list of objects. It allows gain information about + * the position of it in the containing entire list. + * + * @author Oliver Gierke + * @param + */ +public interface Page extends Iterable { + + /** + * Returns the number of the current page. Is always greater than zero and + * less that {@code Page#getTotalPages()}. + * + * @return the number of the current page + */ + int getNumber(); + + + /** + * Returns the size of the page. + * + * @return the size of the page + */ + int getSize(); + + + /** + * Returns the number of total pages. + * + * @return the number of toral pages + */ + int getTotalPages(); + + + /** + * Returns the number of elements currently on this page. + * + * @return the number of elements currently on this page + */ + int getNumberOfElements(); + + + /** + * Returns the total amount of elements. + * + * @return the total amount of elements + */ + long getTotalElements(); + + + /** + * Returns if there is a previous page. + * + * @return if there is a previous page + */ + boolean hasPreviousPage(); + + + /** + * Returns whether the current page is the first one. + * + * @return + */ + boolean isFirstPage(); + + + /** + * Returns if there is a next page. + * + * @return if there is a next page + */ + boolean hasNextPage(); + + + /** + * Returns whether the current page is the last one. + * + * @return + */ + boolean isLastPage(); + + + /* + * (non-Javadoc) + * + * @see java.lang.Iterable#iterator() + */ + Iterator iterator(); + + + /** + * Returns the page content as {@link List}. + * + * @return + */ + List getContent(); + + + /** + * Returns the sorting parameters for the page. + * + * @return + */ + Sort getSort(); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/PageImpl.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/PageImpl.java new file mode 100644 index 000000000..2d58a539f --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/PageImpl.java @@ -0,0 +1,265 @@ +/* + * 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.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + + +/** + * Basic {@code Page} implementation. + * + * @author Oliver Gierke + * @param the type of which the page consists. + */ +public class PageImpl implements Page { + + private final List content = new ArrayList(); + private final Pageable pageable; + private final long total; + + + /** + * Constructor of {@code PageImpl}. + * + * @param content the content of this page + * @param pageable the paging information + * @param total the total amount of items available + */ + public PageImpl(List content, Pageable pageable, long total) { + + if (null == content) { + throw new IllegalArgumentException("Content must not be null!"); + } + + this.content.addAll(content); + this.total = total; + + this.pageable = + null == pageable ? new PageRequest(0, content.size()) + : pageable; + } + + + /** + * Creates a new {@link PageImpl} with the given content. This will result + * in the created {@link Page} being identical to the entire {@link List}. + * + * @param content + */ + public PageImpl(List content) { + + this(content, null, (null == content) ? 0 : content.size()); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#getNumber() + */ + public int getNumber() { + + return pageable.getPageNumber(); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#getSize() + */ + public int getSize() { + + return pageable.getPageSize(); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#getTotalPages() + */ + public int getTotalPages() { + + return (int) Math.ceil((double) total / (double) getSize()); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#getNumberOfElements() + */ + public int getNumberOfElements() { + + return content.size(); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#getTotalElements() + */ + public long getTotalElements() { + + return total; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#hasPreviousPage() + */ + public boolean hasPreviousPage() { + + return getNumber() > 0; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#isFirstPage() + */ + public boolean isFirstPage() { + + return !hasPreviousPage(); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#hasNextPage() + */ + public boolean hasNextPage() { + + return ((getNumber() + 1) * getSize()) < total; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#isLastPage() + */ + public boolean isLastPage() { + + return !hasNextPage(); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#iterator() + */ + public Iterator iterator() { + + return content.iterator(); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#asList() + */ + public List getContent() { + + return Collections.unmodifiableList(content); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Page#getSort() + */ + public Sort getSort() { + + return pageable.getSort(); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + String contentType = "UNKNOWN"; + + if (content.size() > 0) { + contentType = content.get(0).getClass().getName(); + } + + return String.format("Page %s of %d containing %s instances", + getNumber(), getTotalPages(), contentType); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof PageImpl)) { + return false; + } + + PageImpl that = (PageImpl) obj; + + boolean totalEqual = this.total == that.total; + boolean contentEqual = this.content.equals(that.content); + boolean pageableEqual = this.pageable.equals(that.pageable); + + return totalEqual && contentEqual && pageableEqual; + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result = 31 * result + (int) (total ^ total >>> 32); + result = 31 * result + pageable.hashCode(); + result = 31 * result + content.hashCode(); + + return result; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/PageRequest.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/PageRequest.java new file mode 100644 index 000000000..431699130 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/PageRequest.java @@ -0,0 +1,176 @@ +/* + * 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.domain; + +import org.springframework.data.domain.Sort.Direction; + + +/** + * Basic Java Bean implementation of {@code Pageable}. + * + * @author Oliver Gierke + */ +public class PageRequest implements Pageable { + + private final int page; + private final int size; + private final Sort sort; + + + /** + * Creates a new {@link PageRequest}. Pages are zero indexed, thus providing + * 0 for {@code page} will return the first page. + * + * @param size + * @param page + */ + public PageRequest(int page, int size) { + + this(page, size, null); + } + + + /** + * Creates a new {@link PageRequest} with sort parameters applied. + * + * @param page + * @param size + * @param direction + * @param properties + */ + public PageRequest(int page, int size, Direction direction, + String... properties) { + + this(page, size, new Sort(direction, properties)); + } + + + /** + * Creates a new {@link PageRequest} with sort parameters applied. + * + * @param page + * @param size + * @param sort + */ + public PageRequest(int page, int size, Sort sort) { + + if (0 > page) { + throw new IllegalArgumentException( + "Page index must not be less than zero!"); + } + + if (0 > size) { + throw new IllegalArgumentException( + "Page size must not be less than or equal to zero!"); + } + + this.page = page; + this.size = size; + this.sort = sort; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Pageable#getPageSize() + */ + public int getPageSize() { + + return size; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Pageable#getPageNumber() + */ + public int getPageNumber() { + + return page; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Pageable#getFirstItem() + */ + public int getFirstItem() { + + return page * size; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Pageable#getSort() + */ + public Sort getSort() { + + return sort; + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof PageRequest)) { + return false; + } + + PageRequest that = (PageRequest) obj; + + boolean pageEqual = this.page == that.page; + boolean sizeEqual = this.size == that.size; + + boolean sortEqual = + this.sort == null ? that.sort == null : this.sort + .equals(that.sort); + + return pageEqual && sizeEqual && sortEqual; + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result = 31 * result + page; + result = 31 * result + size; + result = 31 * result + (null == sort ? 0 : sort.hashCode()); + + return result; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Pageable.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Pageable.java new file mode 100644 index 000000000..6498c6637 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Pageable.java @@ -0,0 +1,56 @@ +/* + * 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.domain; + +/** + * Abstract interface for pagination information. + * + * @author Oliver Gierke + */ +public interface Pageable { + + /** + * Returns the page to be returned. + * + * @return the page to be returned. + */ + int getPageNumber(); + + + /** + * Returns the number of items to be returned. + * + * @return the number of items of that page + */ + int getPageSize(); + + + /** + * Returns the first item relatively to the total number of items. + * + * @return the first item to be returned + */ + int getFirstItem(); + + + /** + * Returns the sorting parameters. + * + * @return + */ + Sort getSort(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java new file mode 100644 index 000000000..69c587a75 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java @@ -0,0 +1,44 @@ +/* + * 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.domain; + +import java.io.Serializable; + + +/** + * Simple interface for entities. + * + * @author Oliver Gierke + * @param the type of the identifier + */ +public interface Persistable extends Serializable { + + /** + * Returns the id of the entity. + * + * @return the id + */ + PK getId(); + + + /** + * Returns if the {@code Persistable} is new or was persisted already. + * + * @return if the object is new + */ + boolean isNew(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java new file mode 100644 index 000000000..0060b145f --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java @@ -0,0 +1,364 @@ +/* + * 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.domain; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import org.springframework.util.StringUtils; + + +/** + * Sort option for queries. You have to provide at least a list of properties to + * sort for that must not include {@code null} or empty strings. The direction + * defaults to {@value Sort#DEFAULT_DIRECTION}. + * + * @author Oliver Gierke + */ +public class Sort implements + Iterable { + + public static final Direction DEFAULT_DIRECTION = Direction.ASC; + + private List orders; + + + public Sort(Order... orders) { + + this(Arrays.asList(orders)); + } + + + /** + * Creates a new {@link Sort} instance. + * + * @param orders must not be {@literal null} or contain {@literal null} or + * empty strings + */ + public Sort(List orders) { + + if (null == orders || orders.isEmpty()) { + throw new IllegalArgumentException( + "You have to provide at least one sort property to sort by!"); + } + + this.orders = orders; + } + + + /** + * Creates a new {@link Sort} instance. Order defaults to + * {@value Direction#ASC}. + * + * @param properties must not be {@literal null} or contain {@literal null} + * or empty strings + */ + public Sort(String... properties) { + + this(DEFAULT_DIRECTION, properties); + } + + + /** + * Creates a new {@link Sort} instance. + * + * @param direction defaults to {@value Sort#DEFAULT_DIRECTION} (for + * {@literal null} cases, too) + * @param properties must not be {@literal null} or contain {@literal null} + * or empty strings + */ + public Sort(Direction direction, String... properties) { + + this(direction, properties == null ? new ArrayList() : Arrays + .asList(properties)); + } + + + /** + * Creates a new {@link Sort} instance. + * + * @param direction + * @param properties + */ + public Sort(Direction direction, List properties) { + + if (properties == null || properties.isEmpty()) { + throw new IllegalArgumentException( + "You have to provide at least one property to sort by!"); + } + + this.orders = new ArrayList(properties.size()); + + for (String property : properties) { + this.orders.add(new Order(direction, property)); + } + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Iterable#iterator() + */ + public Iterator iterator() { + + return this.orders.iterator(); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof Sort)) { + return false; + } + + Sort that = (Sort) obj; + + return this.orders.equals(that.orders); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + result = 31 * result + orders.hashCode(); + return result; + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return StringUtils.collectionToCommaDelimitedString(orders); + } + + /** + * Enumeration for sort directions. + * + * @author Oliver Gierke + */ + public static enum Direction { + + ASC, DESC; + + /** + * Returns the {@link Direction} enum for the given {@link String} + * value. + * + * @param value + * @return + */ + public static Direction fromString(String value) { + + try { + return Direction.valueOf(value.toUpperCase(Locale.US)); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format( + "Invalid value '%s' for orders given! Has to be either 'desc' or 'asc'.", + value), e); + } + } + } + + /** + * Property implements the pairing of an {@code Order} and a property. It is + * used to provide input for {@link Sort} + * + * @author Oliver Gierke + */ + public static class Order { + + private final Direction direction; + private final String property; + + + /** + * Creates a new {@link Order} instance. if order is {@literal null} + * then order defaults to {@value Sort#DEFAULT_DIRECTION} + * + * @param direction can be {@code null} + * @param property must not be {@code null} or empty + */ + public Order(Direction direction, String property) { + + if (property == null || "".equals(property.trim())) { + throw new IllegalArgumentException( + "Property must not null or empty!"); + } + + this.direction = direction == null ? DEFAULT_DIRECTION : direction; + this.property = property; + } + + + /** + * Creates a new {@link Order} instance. Takes a single property. Order + * defaults to {@value Sort.DEFAULT_ORDER} + * + * @param property - must not be {@code null} or empty + */ + public Order(String property) { + + this(DEFAULT_DIRECTION, property); + } + + + public static List create(Direction direction, + Iterable properties) { + + List orders = new ArrayList(); + for (String property : properties) { + orders.add(new Order(direction, property)); + } + return orders; + } + + + /** + * Returns the order the property shall be sorted for. + * + * @return + */ + public Direction getDirection() { + + return direction; + } + + + /** + * Returns the property to order for. + * + * @return + */ + public String getProperty() { + + return property; + } + + + /** + * Returns whether sorting for this property shall be ascending. + * + * @return + */ + public boolean isAscending() { + + return this.direction.equals(Direction.ASC); + } + + + /** + * Returns a new {@link Order} with the given {@link Order}. + * + * @param order + * @return + */ + public Order with(Direction order) { + + return new Order(order, this.property); + } + + + /** + * Returns a new {@link Sort} instance for the given properties. + * + * @param properties + * @return + */ + public Sort withProperties(String... properties) { + + return new Sort(this.direction, properties); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result = 31 * result + direction.hashCode(); + result = 31 * result + property.hashCode(); + + return result; + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof Order)) { + return false; + } + + Order that = (Order) obj; + + return this.direction.equals(that.direction) + && this.property.equals(that.property); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return String.format("%s: %s", property, direction); + } + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/NoRepositoryBean.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/NoRepositoryBean.java new file mode 100644 index 000000000..376bf3e1d --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/NoRepositoryBean.java @@ -0,0 +1,42 @@ +/* + * 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Annotation to exclude repository interfaces from being picked up and thus in + * consequence getting an instance being created. + *

+ * This will typically be used when providing an extended base interface for all + * repositories in combination with a custom repository base class to implement + * methods declared in that intermediate interface. In this case you typically + * derive your concrete repository interfaces from the intermediate one but + * don't want to create a Spring bean for the intermediate interface. + * + * @author Oliver Gierke + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +public @interface NoRepositoryBean { + +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/PagingAndSortingRepository.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/PagingAndSortingRepository.java new file mode 100644 index 000000000..438781798 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/PagingAndSortingRepository.java @@ -0,0 +1,40 @@ +package org.springframework.data.repository; + +import java.io.Serializable; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + + +/** + * Extension of {@link Repository} to provide additional methods to retrieve + * entities using the pagination and sorting abstraction. + * + * @see Sort + * @see Pageable + * @see Page + * @author Oliver Gierke + */ +public interface PagingAndSortingRepository extends + Repository { + + /** + * Returns all entities sorted by the given options. + * + * @param sort + * @return all entities sorted by the given options + */ + List findAll(Sort sort); + + + /** + * Returns a {@link Page} of entities meeting the paging restriction + * provided in the {@code Pageable} object. + * + * @param pageable + * @return a page of entities + */ + Page findAll(Pageable pageable); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/Repository.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/Repository.java new file mode 100644 index 000000000..a20ec8415 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/Repository.java @@ -0,0 +1,107 @@ +/* + * 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; + +import java.io.Serializable; +import java.util.List; + + +/** + * Interface for generic CRUD operations on a repository for a specific type. + * + * @author Oliver Gierke + * @author Eberhard Wolff + */ +public interface Repository { + + /** + * Saves a given entity. Use the returned instance for further operations as + * the save operation might have changed the entity instance completely. + * + * @param entity + * @return the saved entity + */ + T save(T entity); + + + /** + * Saves all given entities. + * + * @param entities + * @return + */ + List save(Iterable entities); + + + /** + * Retrives an entity by its primary key. + * + * @param id + * @return the entity with the given primary key or {@code null} if none + * found + * @throws IllegalArgumentException if primaryKey is {@code null} + */ + T findById(ID id); + + + /** + * Returns whether an entity with the given id exists. + * + * @param id + * @return true if an entity with the given id exists, alse otherwise + * @throws IllegalArgumentException if primaryKey is {@code null} + */ + boolean exists(ID id); + + + /** + * Returns all instances of the type. + * + * @return all entities + */ + List findAll(); + + + /** + * Returns the number of entities available. + * + * @return the number of entities + */ + Long count(); + + + /** + * Deletes a given entity. + * + * @param entity + */ + void delete(T entity); + + + /** + * Deletes the given entities. + * + * @param entities + */ + void delete(Iterable entities); + + + /** + * Deletes all entities managed by the DAO. + */ + void deleteAll(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AbstractRepositoryConfigDefinitionParser.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AbstractRepositoryConfigDefinitionParser.java new file mode 100644 index 000000000..dd876f9b0 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AbstractRepositoryConfigDefinitionParser.java @@ -0,0 +1,478 @@ +/* + * 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.config; + +import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*; +import static org.springframework.data.repository.util.ClassUtils.*; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ReaderContext; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; +import org.springframework.data.repository.NoRepositoryBean; +import org.w3c.dom.Element; + + +/** + * Base class to implement repository namespaces. These will typically consist + * of a main XML element potentially having child elements. The parser will wrap + * the XML element into a {@link GlobalRepositoryConfigInformation} object and + * allow either manual configuration or automatic detection of repository + * interfaces. + * + * @author Oliver Gierke + */ +public abstract class AbstractRepositoryConfigDefinitionParser, T extends SingleRepositoryConfigInformation> + implements BeanDefinitionParser { + + private static final Logger LOG = LoggerFactory + .getLogger(AbstractRepositoryConfigDefinitionParser.class); + + private static final Class PET_POST_PROCESSOR = + PersistenceExceptionTranslationPostProcessor.class; + private static final String DAO_INTERFACE_POST_PROCESSOR = + "org.springframework.data.repository.support.RepositoryInterfaceAwareBeanPostProcessor"; + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.beans.factory.xml.BeanDefinitionParser#parse(org. + * w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext) + */ + public BeanDefinition parse(Element element, ParserContext parser) { + + try { + S configContext = getGlobalRepositoryConfigInformation(element); + + if (configContext.configureManually()) { + doManualConfiguration(configContext, parser); + } else { + doAutoConfiguration(configContext, parser); + } + + Object beanSource = parser.extractSource(element); + registerPostProcessors(parser.getRegistry(), beanSource); + + } catch (RuntimeException e) { + handleError(e, element, parser.getReaderContext()); + } + + return null; + } + + + /** + * Executes repository auto configuration by scanning the provided base + * package for repository interfaces. + * + * @param config + * @param parser + */ + private void doAutoConfiguration(S config, ParserContext parser) { + + LOG.debug("Triggering auto repository detection"); + + ResourceLoader resourceLoader = + parser.getReaderContext().getResourceLoader(); + + // Detect available DAO interfaces + Set repositoryInterfaces = + getRepositoryInterfacesForAutoConfig(config, resourceLoader, + parser.getReaderContext()); + + for (String daoInterface : repositoryInterfaces) { + registerGenericRepositoryFactoryBean(parser, + config.getAutoconfigRepositoryInformation(daoInterface)); + } + } + + + private Set getRepositoryInterfacesForAutoConfig(S config, + ResourceLoader loader, ReaderContext reader) { + + ClassPathScanningCandidateComponentProvider scanner = + new RepositoryComponentProvider( + config.getRepositoryBaseInterface()); + scanner.setResourceLoader(loader); + + TypeFilterParser parser = + new TypeFilterParser(loader.getClassLoader(), reader); + parser.parseFilters(config.getSource(), scanner); + + Set findCandidateComponents = + scanner.findCandidateComponents(config.getBasePackage()); + + Set interfaceNames = new HashSet(); + for (BeanDefinition definition : findCandidateComponents) { + interfaceNames.add(definition.getBeanClassName()); + } + + return interfaceNames; + } + + + /** + * Returns a {@link GlobalRepositoryConfigInformation} implementation for + * the given element. + * + * @param element + * @return + */ + protected abstract S getGlobalRepositoryConfigInformation(Element element); + + + /** + * Proceeds manual configuration by traversing the context's + * {@link SingleRepositoryConfigInformation}s. + * + * @param context + * @param parser + */ + private void doManualConfiguration(S context, ParserContext parser) { + + LOG.debug("Triggering manual repository detection"); + + for (T daoContext : context.getSingleRepositoryConfigInformations()) { + registerGenericRepositoryFactoryBean(parser, daoContext); + } + } + + + private void handleError(Exception e, Element source, ReaderContext reader) { + + reader.error(e.getMessage(), reader.extractSource(source), e.getCause()); + } + + + /** + * Registers a generic repository factory bean for a bean with the given + * name and the provided configuration context. + * + * @param parser + * @param name + * @param context + */ + private void registerGenericRepositoryFactoryBean(ParserContext parser, + T context) { + + try { + + Object beanSource = parser.extractSource(context.getSource()); + + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.rootBeanDefinition(context + .getRepositoryFactoryBeanClassName()); + + builder.addPropertyValue("repositoryInterface", + context.getInterfaceName()); + builder.addPropertyValue("queryLookupStrategyKey", + context.getQueryLookupStrategyKey()); + + String customImplementationBeanName = + registerCustomImplementation(context, parser, beanSource); + + if (customImplementationBeanName != null) { + builder.addPropertyReference("customImplementation", + customImplementationBeanName); + } + + postProcessBeanDefinition(context, builder, beanSource); + + AbstractBeanDefinition beanDefinition = builder.getBeanDefinition(); + beanDefinition.setSource(beanSource); + + LOG.debug( + "Registering repository: %s - Interface: %s - Factory: %s, - Custom implementation: %s", + new Object[] { context.getBeanId(), + context.getInterfaceName(), + context.getRepositoryFactoryBeanClassName(), + customImplementationBeanName }); + + BeanComponentDefinition definition = + new BeanComponentDefinition(beanDefinition, + context.getBeanId()); + parser.registerBeanComponent(definition); + } catch (RuntimeException e) { + handleError(e, context.getSource(), parser.getReaderContext()); + } + } + + + /** + * Callback to post process a repository bean definition prior to actual + * registration. + * + * @param context + * @param builder + * @param beanSource + */ + protected void postProcessBeanDefinition(T context, + BeanDefinitionBuilder builder, Object beanSource) { + + } + + + /** + * Registers a possibly available custom repository implementation on the + * repository bean. Tries to find an already registered bean to reference or + * tries to detect a custom implementation itself. + * + * @param config + * @param parser + * @param source + * @return the bean name of the custom implementation or {@code null} if + * none available + */ + private String registerCustomImplementation(T config, ParserContext parser, + Object source) { + + String beanName = config.getImplementationBeanName(); + + // Already a bean configured? + if (parser.getRegistry().containsBeanDefinition(beanName)) { + return beanName; + } + + // Autodetect implementation + if (config.autodetectCustomImplementation()) { + + AbstractBeanDefinition beanDefinition = + detectCustomImplementation(config, parser); + + if (null == beanDefinition) { + return null; + } + + LOG.debug("Registering custom repository implementation: %s %s", + config.getImplementationBeanName(), + beanDefinition.getBeanClassName()); + + beanDefinition.setSource(source); + parser.registerBeanComponent(new BeanComponentDefinition( + beanDefinition, beanName)); + + } else { + beanName = config.getCustomImplementationRef(); + } + + return beanName; + } + + + /** + * Tries to detect a custom implementation for a repository bean by + * classpath scanning. + * + * @param config + * @param parser + * @return the {@code AbstractBeanDefinition} of the custom implementation + * or {@literal null} if none found + */ + private AbstractBeanDefinition detectCustomImplementation(T config, + ParserContext parser) { + + // Build pattern to lookup implementation class + Pattern pattern = + Pattern.compile(".*" + config.getImplementationClassName()); + + // Build classpath scanner and lookup bean definition + ClassPathScanningCandidateComponentProvider provider = + new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(parser.getReaderContext() + .getResourceLoader()); + provider.addIncludeFilter(new RegexPatternTypeFilter(pattern)); + Set definitions = + provider.findCandidateComponents(config.getBasePackage()); + + return (0 == definitions.size() ? null + : (AbstractBeanDefinition) definitions.iterator().next()); + } + + + /** + * Registers necessary (Bean)PostProcessor instances if they have not + * already been registered. + * + * @param registry + * @param source + */ + protected void registerPostProcessors(BeanDefinitionRegistry registry, + Object source) { + + // Create PersistenceExceptionTranslationPostProcessor definition + if (!hasBean(PET_POST_PROCESSOR, registry)) { + + AbstractBeanDefinition definition = + BeanDefinitionBuilder + .rootBeanDefinition(PET_POST_PROCESSOR) + .getBeanDefinition(); + + registerWithSourceAndGeneratedBeanName(registry, definition, source); + } + + AbstractBeanDefinition definition = + BeanDefinitionBuilder.rootBeanDefinition( + DAO_INTERFACE_POST_PROCESSOR).getBeanDefinition(); + + registerWithSourceAndGeneratedBeanName(registry, definition, source); + } + + + /** + * Returns whether the given {@link BeanDefinitionRegistry} already contains + * a bean of the given type assuming the bean name has been autogenerated. + * + * @param type + * @param registry + * @return + */ + protected static boolean hasBean(Class type, + BeanDefinitionRegistry registry) { + + String name = + String.format("%s%s0", type.getName(), + GENERATED_BEAN_NAME_SEPARATOR); + return registry.containsBeanDefinition(name); + } + + + /** + * Sets the given source on the given {@link AbstractBeanDefinition} and + * registers it inside the given {@link BeanDefinitionRegistry}. + * + * @param registry + * @param bean + * @param source + * @return + */ + protected static String registerWithSourceAndGeneratedBeanName( + BeanDefinitionRegistry registry, AbstractBeanDefinition bean, + Object source) { + + bean.setSource(source); + + String beanName = generateBeanName(bean, registry); + registry.registerBeanDefinition(beanName, bean); + + return beanName; + } + + /** + * Custom {@link ClassPathScanningCandidateComponentProvider} scanning for + * interfaces extending the given base interface. Skips interfaces annotated + * with {@link NoRepositoryBean}. + * + * @author Oliver Gierke + */ + static class RepositoryComponentProvider extends + ClassPathScanningCandidateComponentProvider { + + /** + * Creates a new {@link RepositoryComponentProvider}. + * + * @param repositoryInterface the interface to scan for + */ + public RepositoryComponentProvider(Class repositoryInterface) { + + super(false); + addIncludeFilter(new InterfaceTypeFilter(repositoryInterface)); + addExcludeFilter(new AnnotationTypeFilter(NoRepositoryBean.class)); + } + + + /* + * (non-Javadoc) + * + * @seeorg.springframework.context.annotation. + * ClassPathScanningCandidateComponentProvider + * #isCandidateComponent(org.springframework + * .beans.factory.annotation.AnnotatedBeanDefinition) + */ + @Override + protected boolean isCandidateComponent( + AnnotatedBeanDefinition beanDefinition) { + + boolean isNonHadesInterfaces = + !isGenericRepositoryInterface(beanDefinition + .getBeanClassName()); + boolean isTopLevelType = + !beanDefinition.getMetadata().hasEnclosingClass(); + + return isNonHadesInterfaces && isTopLevelType; + } + + /** + * {@link org.springframework.core.type.filter.TypeFilter} that only + * matches interfaces. Thus setting this up makes only sense providing + * an interface type as {@code targetType}. + * + * @author Oliver Gierke + */ + private static class InterfaceTypeFilter extends AssignableTypeFilter { + + /** + * Creates a new {@link InterfaceTypeFilter}. + * + * @param targetType + */ + public InterfaceTypeFilter(Class targetType) { + + super(targetType); + } + + + /* + * (non-Javadoc) + * + * @seeorg.springframework.core.type.filter. + * AbstractTypeHierarchyTraversingFilter + * #match(org.springframework.core.type.classreading.MetadataReader, + * org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public boolean match(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) + throws IOException { + + return metadataReader.getClassMetadata().isInterface() + && super.match(metadataReader, metadataReaderFactory); + } + } + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AutomaticRepositoryConfigInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AutomaticRepositoryConfigInformation.java new file mode 100644 index 000000000..91df44cd6 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AutomaticRepositoryConfigInformation.java @@ -0,0 +1,61 @@ +package org.springframework.data.repository.config; + +import static org.springframework.util.ClassUtils.*; +import static org.springframework.util.StringUtils.*; + +import org.springframework.util.Assert; + + +/** + * A {@link SingleRepositoryConfigInformation} implementation that is not backed + * by an XML element but by a scanned interface. As this is derived from the + * parent, most of the lookup logic is delegated to the parent as well. + * + * @author Oliver Gierke + */ +public class AutomaticRepositoryConfigInformation + extends ParentDelegatingRepositoryConfigInformation { + + private final String interfaceName; + + + /** + * Creates a new {@link AutomaticRepositoryConfigInformation} for the given + * interface name and {@link CommonRepositoryConfigInformation} parent. + * + * @param interfaceName + * @param parent + */ + public AutomaticRepositoryConfigInformation(String interfaceName, S parent) { + + super(parent); + Assert.notNull(interfaceName); + this.interfaceName = interfaceName; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.config.SingleRepositoryConfigInformation + * #getBeanId() + */ + public String getBeanId() { + + return uncapitalize(getShortName(interfaceName)); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.config.SingleRepositoryConfigInformation + * #getInterfaceName() + */ + public String getInterfaceName() { + + return interfaceName; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/CommonRepositoryConfigInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/CommonRepositoryConfigInformation.java new file mode 100644 index 000000000..9d827d536 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/CommonRepositoryConfigInformation.java @@ -0,0 +1,64 @@ +package org.springframework.data.repository.config; + +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.transaction.PlatformTransactionManager; +import org.w3c.dom.Element; + + +/** + * Interface for shared repository information. + * + * @author Oliver Gierke + */ +public interface CommonRepositoryConfigInformation { + + /** + * Returns the element the repository information is derived from. + * + * @return + */ + Element getSource(); + + + /** + * Returns the base package. + * + * @return + */ + String getBasePackage(); + + + /** + * Returns the suffix to use for implementation bean lookup or class + * detection. + * + * @return + */ + String getRepositoryImplementationSuffix(); + + + /** + * Returns the configured repository factory class. + * + * @return + */ + String getRepositoryFactoryBeanClassName(); + + + /** + * Returns the bean name of the {@link PlatformTransactionManager} to be + * used. + * + * @return + */ + String getTransactionManagerRef(); + + + /** + * Returns the strategy finder methods should be resolved. + * + * @return + */ + Key getQueryLookupStrategyKey(); + +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/GlobalRepositoryConfigInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/GlobalRepositoryConfigInformation.java new file mode 100644 index 000000000..c4b990ad2 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/GlobalRepositoryConfigInformation.java @@ -0,0 +1,43 @@ +package org.springframework.data.repository.config; + +/** + * @author Oliver Gierke + */ +public interface GlobalRepositoryConfigInformation> + extends CommonRepositoryConfigInformation { + + /** + * Returns the + * + * @param interfaceName + * @return + */ + T getAutoconfigRepositoryInformation(String interfaceName); + + + /** + * Returns all {@link SingleRepositoryConfigInformation} instances used for + * manual configuration. + * + * @return + */ + Iterable getSingleRepositoryConfigInformations(); + + + /** + * Returns whether to consider manual configuration. If this returns true, + * clients should use {@link #getSingleRepositoryConfigInformations()} to + * lookup configuration information for individual repository beans. + * + * @return + */ + boolean configureManually(); + + + /** + * Returns the base interface to use + * + * @return + */ + Class getRepositoryBaseInterface(); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/ManualRepositoryConfigInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/ManualRepositoryConfigInformation.java new file mode 100644 index 000000000..939c45e74 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/ManualRepositoryConfigInformation.java @@ -0,0 +1,165 @@ +package org.springframework.data.repository.config; + +import static org.springframework.util.StringUtils.*; + +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.w3c.dom.Element; + + +/** + * @author Oliver Gierke + */ +public class ManualRepositoryConfigInformation + extends ParentDelegatingRepositoryConfigInformation { + + private static final String CUSTOM_IMPL_REF = "custom-impl-ref"; + + private Element element; + + + /** + * @param parent + */ + public ManualRepositoryConfigInformation(Element element, T parent) { + + super(parent); + this.element = element; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getBeanName() + */ + public String getBeanId() { + + return element.getAttribute("id"); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getInterfaceName() + */ + public String getInterfaceName() { + + return getBasePackage() + "." + capitalize(getBeanId()); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getCustomImplementationRef() + */ + @Override + public String getCustomImplementationRef() { + + return element.getAttribute(CUSTOM_IMPL_REF); + } + + + /** + * Returns if a custom DAO implementation shall be autodetected. + * + * @return + */ + @Override + public boolean autodetectCustomImplementation() { + + return !hasText(getCustomImplementationRef()); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.AbstractRepositoryInformation + * #getRepositoryImplementationSuffix() + */ + @Override + public String getRepositoryImplementationSuffix() { + + String value = + element.getAttribute(RepositoryConfig.REPOSITORY_IMPL_POSTFIX); + return hasText(value) ? value : getParent() + .getRepositoryImplementationSuffix(); + } + + + @Override + public String getTransactionManagerRef() { + + return getAttribute(RepositoryConfig.TRANSACTION_MANAGER_REF); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.CommonRepositoryInformation + * #getSource() + */ + @Override + public Element getSource() { + + return element; + } + + + /** + * Returns the attribute of the current context. If it's not set the method + * will fall back to the parent's source. + * + * @param attribute + * @return + */ + protected String getAttribute(String attribute) { + + String value = getSource().getAttribute(attribute); + return hasText(value) ? value : getParent().getSource().getAttribute( + attribute); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.CommonRepositoryInformation + * #getRepositoryFactoryClassName() + */ + @Override + public String getRepositoryFactoryBeanClassName() { + + String value = + element.getAttribute(RepositoryConfig.REPOSITORY_FACTORY_CLASS_NAME); + return hasText(value) ? value : getParent() + .getRepositoryFactoryBeanClassName(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.CommonRepositoryInformation + * #getQueryLookupStrategyKey() + */ + @Override + public Key getQueryLookupStrategyKey() { + + return Key + .create(getAttribute(RepositoryConfig.QUERY_LOOKUP_STRATEGY)); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/ParentDelegatingRepositoryConfigInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/ParentDelegatingRepositoryConfigInformation.java new file mode 100644 index 000000000..571f05242 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/ParentDelegatingRepositoryConfigInformation.java @@ -0,0 +1,177 @@ +package org.springframework.data.repository.config; + +import static org.springframework.util.StringUtils.*; + +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.util.Assert; +import org.w3c.dom.Element; + + +/** + * Base class for {@link SingleRepositoryConfigInformation} implementations. So these + * implementations will capture information for XML elements manually + * configuring a single repository bean. + * + * @author Oliver Gierke + */ +public abstract class ParentDelegatingRepositoryConfigInformation + implements SingleRepositoryConfigInformation { + + private final T parent; + + + /** + * Creates a new {@link ParentDelegatingRepositoryConfigInformation} with the given + * {@link CommonRepositoryConfigInformation} as parent. + * + * @param parent + */ + public ParentDelegatingRepositoryConfigInformation(T parent) { + + Assert.notNull(parent); + this.parent = parent; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getParent() + */ + protected T getParent() { + + return parent; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.CommonRepositoryInformation + * #getBasePackage() + */ + public String getBasePackage() { + + return parent.getBasePackage(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getImplementationClassName() + */ + public String getImplementationClassName() { + + return capitalize(getBeanId()) + getRepositoryImplementationSuffix(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getImplementationBeanName() + */ + public String getImplementationBeanName() { + + return getBeanId() + getRepositoryImplementationSuffix(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * autodetectCustomImplementation() + */ + public boolean autodetectCustomImplementation() { + + return true; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getCustomImplementationRef() + */ + public String getCustomImplementationRef() { + + return getBeanId() + getRepositoryImplementationSuffix(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.CommonRepositoryInformation + * #getSource() + */ + public Element getSource() { + + return parent.getSource(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getDaoImplPostfix() + */ + public String getRepositoryImplementationSuffix() { + + return parent.getRepositoryImplementationSuffix(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getDaoFactoryClassName() + */ + public String getRepositoryFactoryBeanClassName() { + + return parent.getRepositoryFactoryBeanClassName(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getTransactionManagerRef() + */ + public String getTransactionManagerRef() { + + return parent.getTransactionManagerRef(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.RepositoryInformation# + * getQueryLookupStrategyKey() + */ + public Key getQueryLookupStrategyKey() { + + return parent.getQueryLookupStrategyKey(); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/RepositoryConfig.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/RepositoryConfig.java new file mode 100644 index 000000000..3b3b93611 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/RepositoryConfig.java @@ -0,0 +1,223 @@ +/* + * 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.config; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.util.TxUtils; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + + +/** + * Class defining access to the repository configuration abstracting the content + * of the {@code repositories} element in XML namespcae configuration. Defines + * default values to populate resulting repository beans with. + * + * @author Oliver Gierke + */ +public abstract class RepositoryConfig, S extends CommonRepositoryConfigInformation> + implements GlobalRepositoryConfigInformation { + + public static final String DEFAULT_DAO_IMPL_POSTFIX = "Impl"; + public static final String QUERY_LOOKUP_STRATEGY = "query-lookup-strategy"; + public static final String BASE_PACKAGE = "base-package"; + public static final String REPOSITORY_IMPL_POSTFIX = "dao-impl-postfix"; + public static final String REPOSITORY_FACTORY_CLASS_NAME = "factory-class"; + public static final String TRANSACTION_MANAGER_REF = + "transaction-manager-ref"; + + private final Element element; + private final String defaultRepositoryFactoryBeanClassName; + + + /** + * Creates an instance of {@code RepositoryConfig}. + * + * @param repositoriesElement + */ + protected RepositoryConfig(Element repositoriesElement, + String defaultRepositoryFactoryBeanClassName) { + + Assert.notNull(repositoriesElement, "Element must not be null!"); + Assert.notNull(defaultRepositoryFactoryBeanClassName, + "Default repository factory bean class name must not be null!"); + + this.element = repositoriesElement; + this.defaultRepositoryFactoryBeanClassName = + defaultRepositoryFactoryBeanClassName; + + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.config.CommonRepositoryConfigInformation + * #getSource() + */ + public Element getSource() { + + return element; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.config.GlobalRepositoryConfigInformation + * #configureManually() + */ + public boolean configureManually() { + + return getRepositoryNodes().size() > 0; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.config.CommonRepositoryConfigInformation + * #getQueryLookupStrategyKey() + */ + public Key getQueryLookupStrategyKey() { + + String createFinderQueries = + element.getAttribute(QUERY_LOOKUP_STRATEGY); + + return StringUtils.hasText(createFinderQueries) ? Key + .create(createFinderQueries) : null; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.config.CommonRepositoryConfigInformation + * #getBasePackage() + */ + public String getBasePackage() { + + return element.getAttribute(BASE_PACKAGE); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.config.CommonRepositoryConfigInformation + * #getRepositoryFactoryClassName() + */ + public String getRepositoryFactoryBeanClassName() { + + String factoryClassName = + getSource().getAttribute(REPOSITORY_FACTORY_CLASS_NAME); + return StringUtils.hasText(factoryClassName) ? factoryClassName + : defaultRepositoryFactoryBeanClassName; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.config.CommonRepositoryConfigInformation + * #getRepositoryImplementationSuffix() + */ + public String getRepositoryImplementationSuffix() { + + String postfix = element.getAttribute(REPOSITORY_IMPL_POSTFIX); + return StringUtils.hasText(postfix) ? postfix + : DEFAULT_DAO_IMPL_POSTFIX; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.config.CommonRepositoryConfigInformation + * #getTransactionManagerRef() + */ + public String getTransactionManagerRef() { + + String ref = element.getAttribute(TRANSACTION_MANAGER_REF); + return StringUtils.hasText(ref) ? ref + : TxUtils.DEFAULT_TRANSACTION_MANAGER; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.config.GlobalRepositoryInformation + * #getManualRepositoryInformations() + */ + public Iterable getSingleRepositoryConfigInformations() { + + Set infos = new HashSet(); + for (Element element : getRepositoryNodes()) { + infos.add(createSingleRepositoryConfigInformationFor(element)); + } + + return infos; + } + + + private Collection getRepositoryNodes() { + + NodeList nodes = element.getChildNodes(); + Set result = new HashSet(); + + for (int i = 0; i < nodes.getLength(); i++) { + + Node node = nodes.item(i); + + boolean isElement = Node.ELEMENT_NODE == node.getNodeType(); + boolean isDao = "repository".equals(node.getLocalName()); + + if (isElement && isDao) { + result.add((Element) node); + } + } + + return result; + } + + + /** + * Creates a {@link SingleRepositoryConfigInformation} for the given + * {@link Element}. + * + * @param element + * @return + */ + protected abstract T createSingleRepositoryConfigInformationFor( + Element element); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/SingleRepositoryConfigInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/SingleRepositoryConfigInformation.java new file mode 100644 index 000000000..4d59bc8bd --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/SingleRepositoryConfigInformation.java @@ -0,0 +1,60 @@ +package org.springframework.data.repository.config; + +/** + * Interface to capture configuration information necessary to set up a single + * repository instance. + * + * @author Oliver Gierke + */ +public interface SingleRepositoryConfigInformation + extends CommonRepositoryConfigInformation { + + /** + * Returns the bean name to be used for the repository. + * + * @return + */ + String getBeanId(); + + + /** + * Returns the name of the repository interface. + * + * @return + */ + String getInterfaceName(); + + + /** + * Returns the class name of a possible custom repository implementation + * class to detect. + * + * @return + */ + String getImplementationClassName(); + + + /** + * Returns the bean name a possibly found custom implementation shall be + * registered under. + * + * @return + */ + String getImplementationBeanName(); + + + /** + * Returns the bean reference to the custom repository implementation. + * + * @return + */ + String getCustomImplementationRef(); + + + /** + * Returns whether to try to autodetect a custom implementation. + * + * @return + */ + boolean autodetectCustomImplementation(); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/TypeFilterParser.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/TypeFilterParser.java new file mode 100644 index 000000000..43171957a --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/TypeFilterParser.java @@ -0,0 +1,286 @@ +/* + * 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.config; + +import java.lang.annotation.Annotation; +import java.util.regex.Pattern; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.parsing.ReaderContext; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AspectJTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + + +/** + * Parser to populate the given + * {@link ClassPathScanningCandidateComponentProvider} with {@link TypeFilter}s + * parsed from the given {@link Element}'s children. + * + * @author Oliver Gierke + */ +class TypeFilterParser { + + private static final String FILTER_TYPE_ATTRIBUTE = "type"; + private static final String FILTER_EXPRESSION_ATTRIBUTE = "expression"; + + private final ClassLoader classLoader; + private final ReaderContext readerContext; + + + /** + * Creates a new {@link TypeFilterParser} with the given {@link ClassLoader} + * and {@link ReaderContext}. + * + * @param classLoader + * @param readerContext + */ + public TypeFilterParser(ClassLoader classLoader, ReaderContext readerContext) { + + this.classLoader = classLoader; + this.readerContext = readerContext; + } + + + /** + * Parses include and exclude filters form the given {@link Element}'s child + * elements and populates the given + * {@link ClassPathScanningCandidateComponentProvider} with the according + * {@link TypeFilter}s. + * + * @param element + * @param scanner + */ + public void parseFilters(Element element, + ClassPathScanningCandidateComponentProvider scanner) { + + parseTypeFilters(element, scanner, Type.INCLUDE); + parseTypeFilters(element, scanner, Type.EXCLUDE); + } + + + private void parseTypeFilters(Element element, + ClassPathScanningCandidateComponentProvider scanner, Type type) { + + NodeList nodeList = element.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + + Element childElement = type.getElement(node); + + if (childElement != null) { + + try { + + type.addFilter( + createTypeFilter((Element) node, classLoader), + scanner); + + } catch (RuntimeException e) { + readerContext.error(e.getMessage(), + readerContext.extractSource(element), e.getCause()); + } + } + } + } + + + protected TypeFilter createTypeFilter(Element element, + ClassLoader classLoader) { + + String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE); + String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE); + + try { + + FilterType filter = FilterType.fromString(filterType); + return filter.getFilter(expression, classLoader); + + } catch (ClassNotFoundException ex) { + throw new FatalBeanException("Type filter class not found: " + + expression, ex); + } + } + + /** + * Enum representing all the filter types available for {@code include} and + * {@code exclude} elements. This acts as factory for {@link TypeFilter} + * instances. + * + * @see #getFilter(String, ClassLoader) + * @author Oliver Gierke + */ + private static enum FilterType { + + ANNOTATION { + + @Override + @SuppressWarnings("unchecked") + public TypeFilter getFilter(String expression, + ClassLoader classLoader) throws ClassNotFoundException { + + return new AnnotationTypeFilter( + (Class) classLoader.loadClass(expression)); + } + }, + + ASSIGNABLE { + + @Override + public TypeFilter getFilter(String expression, + ClassLoader classLoader) throws ClassNotFoundException { + + return new AssignableTypeFilter( + classLoader.loadClass(expression)); + } + + }, + + ASPECTJ { + + @Override + public TypeFilter getFilter(String expression, + ClassLoader classLoader) { + + return new AspectJTypeFilter(expression, classLoader); + } + + }, + + REGEX { + + @Override + public TypeFilter getFilter(String expression, + ClassLoader classLoader) { + + return new RegexPatternTypeFilter(Pattern.compile(expression)); + } + + }, + + CUSTOM { + + @Override + public TypeFilter getFilter(String expression, + ClassLoader classLoader) throws ClassNotFoundException { + + Class filterClass = classLoader.loadClass(expression); + if (!TypeFilter.class.isAssignableFrom(filterClass)) { + throw new IllegalArgumentException( + "Class is not assignable to [" + + TypeFilter.class.getName() + "]: " + + expression); + } + return (TypeFilter) BeanUtils.instantiateClass(filterClass); + } + }; + + /** + * Returns the {@link TypeFilter} for the given expression and + * {@link ClassLoader}. + * + * @param expression + * @param classLoader + * @return + * @throws ClassNotFoundException + */ + abstract TypeFilter getFilter(String expression, ClassLoader classLoader) + throws ClassNotFoundException; + + + /** + * Returns the {@link FilterType} for the given type as {@link String}. + * + * @param typeString + * @return + * @throws IllegalArgumentException if no {@link FilterType} could be + * found for the given argument. + */ + static FilterType fromString(String typeString) { + + for (FilterType filter : FilterType.values()) { + if (filter.name().equalsIgnoreCase(typeString)) { + return filter; + } + } + + throw new IllegalArgumentException("Unsupported filter type: " + + typeString); + } + } + + private static enum Type { + + INCLUDE("include-filter") { + + @Override + public void addFilter(TypeFilter filter, + ClassPathScanningCandidateComponentProvider scanner) { + + scanner.addIncludeFilter(filter); + } + + }, + EXCLUDE("exclude-filter") { + + @Override + public void addFilter(TypeFilter filter, + ClassPathScanningCandidateComponentProvider scanner) { + + scanner.addExcludeFilter(filter); + } + }; + + private String elementName; + + + private Type(String elementName) { + + this.elementName = elementName; + } + + + /** + * Returns the {@link Element} if the given {@link Node} is an + * {@link Element} and it's name equals the one of the type. + * + * @param node + * @return + */ + Element getElement(Node node) { + + if (node.getNodeType() == Node.ELEMENT_NODE) { + String localName = node.getLocalName(); + if (elementName.equals(localName)) { + return (Element) node; + } + } + + return null; + } + + + abstract void addFilter(TypeFilter filter, + ClassPathScanningCandidateComponentProvider scanner); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Param.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Param.java new file mode 100644 index 000000000..7a0f49d5b --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Param.java @@ -0,0 +1,22 @@ +package org.springframework.data.repository.query; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Annotation to bind let method parameters be bound to a query via a named + * parameter. + * + * @author Oliver Gierke + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Param { + + String value(); +} 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 new file mode 100644 index 000000000..9bc1d189b --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameter.java @@ -0,0 +1,239 @@ +/* + * 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 java.lang.String.*; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.util.Assert; + + +/** + * Class to abstract a single parameter of a query method. It is held in the + * context of a {@link Parameters} instance. + * + * @author Oliver Gierke + */ +public final class Parameter { + + @SuppressWarnings("unchecked") + static final List> TYPES = Arrays.asList(Pageable.class, + Sort.class); + + private static final String PARAM_ON_SPECIAL = format( + "You must not user @%s on a parameter typed %s or %s", + Param.class.getSimpleName(), Pageable.class.getSimpleName(), + Sort.class.getSimpleName()); + + private static final String NAMED_PARAMETER_TEMPLATE = ":%s"; + private static final String POSITION_PARAMETER_TEMPLATE = "?%s"; + + private final Class type; + private final Parameters parameters; + private final int index; + private final String name; + + + /** + * Creates a new {@link Parameter} for the given type, {@link Annotation}s, + * positioned at the given index inside the given {@link Parameters}. + * + * @param type + * @param parameters + * @param index + * @param name + */ + Parameter(Class type, Parameters parameters, int index, String name) { + + Assert.notNull(type); + Assert.notNull(parameters); + + this.parameters = parameters; + this.index = index; + + this.type = type; + this.name = name; + + if (isSpecialParameter() && isNamedParameter()) { + throw new IllegalArgumentException(PARAM_ON_SPECIAL); + } + } + + + /** + * Copy constructor to put a {@link Parameter} into another context. + * + * @param parameter + * @param parameters + * @param index + */ + Parameter(Parameter parameter, Parameters parameters, int index) { + + this(parameter.type, parameters, index, parameter.name); + } + + + /** + * Returns whether the {@link Parameter} is the first one. + * + * @return + */ + boolean isFirst() { + + return index == 0; + } + + + /** + * Returns the next {@link Parameter} from the surrounding + * {@link Parameters}. + * + * @throws ParameterOutOfBoundsException + * @return + */ + public Parameter getNext() { + + return parameters.getParameter(index + 1); + } + + + /** + * Returns the previous {@link Parameter}. + * + * @return + */ + Parameter getPrevious() { + + return parameters.getParameter(index - 1); + } + + + /** + * Returns whether the parameter is a special parameter. + * + * @see #TYPES + * @param index + * @return + */ + public boolean isSpecialParameter() { + + return TYPES.contains(type); + } + + + /** + * Returns whether the {@link Parameter} is to be bound to a query. + * + * @return + */ + public boolean isBindable() { + + return !isSpecialParameter(); + } + + + /** + * Returns the placeholder to be used for the parameter. Can either be a + * named one or positional. + * + * @param index + * @return + */ + public String getPlaceholder() { + + if (isNamedParameter()) { + return format(NAMED_PARAMETER_TEMPLATE, getName()); + } else { + return format(POSITION_PARAMETER_TEMPLATE, getParameterPosition()); + } + } + + + /** + * Returns the position index the parameter is bound to in the context of + * its surrounding {@link Parameters}. + * + * @return + */ + public int getParameterPosition() { + + return parameters.getPlaceholderPosition(this); + } + + + /** + * Returns whether the parameter is annotated with {@link Param}. + * + * @param index + * @return + */ + public boolean isNamedParameter() { + + return !isSpecialParameter() && getName() != null; + } + + + /** + * Returns the name of the parameter (through {@link Param} annotation) or + * null if none can be found. + * + * @return + */ + public String getName() { + + return name; + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return format("%s:%s", isNamedParameter() ? getName() : "#" + index, + type.getName()); + } + + + /** + * Returns whether the {@link Parameter} is a {@link Pageable} parameter. + * + * @return + */ + boolean isPageable() { + + return Pageable.class.isAssignableFrom(type); + } + + + /** + * Returns whether the {@link Parameter} is a {@link Sort} parameter. + * + * @return + */ + boolean isSort() { + + return Sort.class.isAssignableFrom(type); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterOutOfBoundsException.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterOutOfBoundsException.java new file mode 100644 index 000000000..62c8156da --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterOutOfBoundsException.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * Exception to be thrown when trying to access a {@link Parameter} with an + * invalid index inside a {@link Parameters} instance. + * + * @author Oliver Gierke + */ +public class ParameterOutOfBoundsException extends RuntimeException { + + private static final long serialVersionUID = 8433209953653278886L; + + + /** + * Creates a new {@link ParameterOutOfBoundsException} with the given + * exception as cause. + * + * @param cause + */ + public ParameterOutOfBoundsException(Throwable cause) { + + super(cause); + } +} 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 new file mode 100644 index 000000000..5a75c4271 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/Parameters.java @@ -0,0 +1,337 @@ +/* + * 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 java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.util.Assert; + + +/** + * Abstracts method parameters that have to be bound to query parameters or + * applied to the query independently. + * + * @author Oliver Gierke + */ +public final class Parameters implements Iterable { + + @SuppressWarnings("unchecked") + public static final List> TYPES = Arrays.asList(Pageable.class, + Sort.class); + + private static final String ALL_OR_NOTHING = + String.format( + "Either use @%s " + + "on all parameters except %s and %s typed once, or none at all!", + Param.class.getSimpleName(), + Pageable.class.getSimpleName(), Sort.class.getSimpleName()); + + private final int pageableIndex; + private final int sortIndex; + + private final List parameters; + private final ParameterNameDiscoverer discoverer = + new LocalVariableTableParameterNameDiscoverer(); + + + /** + * Creates a new instance of {@link Parameters}. + * + * @param method + */ + public Parameters(Method method) { + + Assert.notNull(method); + + this.parameters = new ArrayList(); + + List> types = Arrays.asList(method.getParameterTypes()); + + for (int i = 0; i < types.size(); i++) { + String name = getParameterName(method, i); + parameters.add(new Parameter(types.get(i), this, i, name)); + } + + this.pageableIndex = types.indexOf(Pageable.class); + this.sortIndex = types.indexOf(Sort.class); + + assertEitherAllParamAnnotatedOrNone(); + } + + + /** + * Returns the name of the parameter of the given {@link Method} with the + * given index. Inspects {@link Param} annotation before falling back to a + * {@link ParameterNameDiscoverer}. + * + * @param method + * @param index + * @return + */ + private String getParameterName(Method method, int index) { + + for (Annotation annotation : method.getParameterAnnotations()[index]) { + if (annotation instanceof Param) { + return ((Param) annotation).value(); + } + } + + String[] parameterNames = discoverer.getParameterNames(method); + + if (parameterNames != null) { + return parameterNames[index]; + } + + return null; + } + + + /** + * Creates a new {@link Parameters} instance with the given + * {@link Parameter}s put into new context. + * + * @param originals + */ + private Parameters(List originals) { + + this.parameters = new ArrayList(); + + int pageableIndexTemp = -1; + int sortIndexTemp = -1; + + for (int i = 0; i < originals.size(); i++) { + + Parameter original = originals.get(i); + + this.parameters.add(new Parameter(original, this, i)); + + pageableIndexTemp = original.isPageable() ? i : -1; + sortIndexTemp = original.isSort() ? i : -1; + } + + this.pageableIndex = pageableIndexTemp; + this.sortIndex = sortIndexTemp; + } + + + /** + * Returns whether the method the {@link Parameters} was created for + * contains a {@link Pageable} argument. + * + * @return + */ + public boolean hasPageableParameter() { + + return pageableIndex != -1; + } + + + /** + * Returns the index of the {@link Pageable} {@link Method} parameter if + * available. Will return {@literal -1} if there is no {@link Pageable} + * argument in the {@link Method}'s parameter list. + * + * @return the pageableIndex + */ + public int getPageableIndex() { + + return pageableIndex; + } + + + /** + * Returns the index of the {@link Sort} {@link Method} parameter if + * available. Will return {@literal -1} if there is no {@link Sort} argument + * in the {@link Method}'s parameter list. + * + * @return + */ + public int getSortIndex() { + + return sortIndex; + } + + + /** + * Returns whether the method the {@link Parameters} was created for + * contains a {@link Sort} argument. + * + * @return + */ + public boolean hasSortParameter() { + + return sortIndex != -1; + } + + + /** + * Returns the parameter with the given index. + * + * @param index + * @return + */ + public Parameter getParameter(int index) { + + try { + return parameters.get(index); + } catch (IndexOutOfBoundsException e) { + throw new ParameterOutOfBoundsException(e); + } + } + + + /** + * Returns whether we have a parameter at the given position. + * + * @param position + * @return + */ + public boolean hasParameterAt(int position) { + + try { + return null != getParameter(position); + } catch (ParameterOutOfBoundsException e) { + return false; + } + } + + + /** + * Returns whether the method signature contains one of the special + * parameters ({@link Pageable}, {@link Sort}). + * + * @return + */ + public boolean hasSpecialParameter() { + + return hasSortParameter() || hasPageableParameter(); + } + + + /** + * Returns the number of parameters. + * + * @return + */ + public int getNumberOfParameters() { + + return parameters.size(); + } + + + /** + * Returns a {@link Parameters} instance with effectively all special + * parameters removed. + * + * @see Parameter#TYPES + * @see Parameter#isSpecialParameter() + * @return + */ + public Parameters getBindableParameters() { + + List bindables = new ArrayList(); + + for (Parameter candidate : this) { + + if (candidate.isBindable()) { + bindables.add(candidate); + } + } + + return new Parameters(bindables); + } + + + /** + * Returns the index of the placeholder inside a query for the parameter + * with the given index. They might differ from the parameter index as the + * method signature can contain special parameters (e.g. {@link Sort}, + * {@link Pageable}) that are not bound as plain query parameters but rather + * handled differently. + * + * @param index + * @return the placeholder postion for the parameter with the given index. + * Will return 0 for special parameters. + */ + int getPlaceholderPosition(Parameter parameter) { + + return parameter.isSpecialParameter() ? 0 + : getPlaceholderPositionRecursively(parameter); + } + + + private int getPlaceholderPositionRecursively(Parameter parameter) { + + int result = parameter.isSpecialParameter() ? 0 : 1; + + return parameter.isFirst() ? result : result + + getPlaceholderPositionRecursively(parameter.getPrevious()); + } + + + /** + * Asserts that either all of the non special parameters ({@link Pageable}, + * {@link Sort}) are annotated with {@link Param} or none of them is. + * + * @param method + */ + private void assertEitherAllParamAnnotatedOrNone() { + + boolean nameFound = false; + + for (Parameter parameter : this.getBindableParameters()) { + + if (parameter.isNamedParameter()) { + Assert.isTrue(nameFound || parameter.isFirst(), ALL_OR_NOTHING); + nameFound = true; + } else { + Assert.isTrue(!nameFound, ALL_OR_NOTHING); + } + } + } + + + /** + * Returns whether the given type is a bindable parameter. + * + * @param type + * @return + */ + public static boolean isBindable(Class type) { + + return !TYPES.contains(type); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Iterable#iterator() + */ + public Iterator iterator() { + + return parameters.iterator(); + } +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryCreationException.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryCreationException.java new file mode 100644 index 000000000..1e52aa369 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryCreationException.java @@ -0,0 +1,70 @@ +package org.springframework.data.repository.query; + +/** + * Exception to be thrown if a query cannot be created from a + * {@link QueryMethod}. + * + * @author Oliver Gierke + */ +public final class QueryCreationException extends RuntimeException { + + private static final long serialVersionUID = -1238456123580L; + private static final String MESSAGE_TEMPLATE = + "Could not create query for method %s! Could not find property %s on domain class %s."; + + + /** + * Creates a new {@link QueryCreationException}. + * + * @param method + */ + private QueryCreationException(String message) { + + super(message); + } + + + /** + * Rejects the given domain class property. + * + * @param method + * @param propertyName + * @return + */ + public static QueryCreationException invalidProperty(QueryMethod method, + String propertyName) { + + return new QueryCreationException(String.format(MESSAGE_TEMPLATE, + method, propertyName, method.getDomainClass().getName())); + } + + + /** + * Creates a new {@link QueryCreationException}. + * + * @param method + * @param message + * @return + */ + public static QueryCreationException create(QueryMethod method, + String message) { + + return new QueryCreationException(String.format( + "Could not create query for %s! Reason: %s", method, message)); + } + + + /** + * Creates a new {@link QueryCreationException} for the given + * {@link QueryMethod} and {@link Throwable} as cause. + * + * @param method + * @param cause + * @return + */ + public static QueryCreationException create(QueryMethod method, + Throwable cause) { + + return create(method, cause.getMessage()); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java new file mode 100644 index 000000000..5c21ab50d --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java @@ -0,0 +1,44 @@ +package org.springframework.data.repository.query; + +import java.util.Locale; + +import org.springframework.util.StringUtils; + + +/** + * Strategy interface for which way to lookup {@link RepositoryQuery}s. + * + * @author Oliver Gierke + */ +public interface QueryLookupStrategy { + + public static enum Key { + + CREATE, USE_DECLARED_QUERY, CREATE_IF_NOT_FOUND; + + /** + * Returns a strategy key from the given XML value. + * + * @param xml + * @return a strategy key from the given XML value + */ + public static Key create(String xml) { + + if (!StringUtils.hasText(xml)) { + return null; + } + + return valueOf(xml.toUpperCase(Locale.US).replace("-", "_")); + } + } + + + /** + * Resolves a {@link RepositoryQuery} from the given {@link QueryMethod} + * that can be executed afterwards. + * + * @param method + * @return + */ + RepositoryQuery resolveQuery(QueryMethod method); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java new file mode 100644 index 000000000..94e53139f --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java @@ -0,0 +1,159 @@ +/* + * 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.springframework.data.repository.util.ClassUtils.*; + +import java.lang.reflect.Method; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.util.Assert; + + +/** + * Abstraction of a method that is designated to execute a finder query. + * Enriches the standard {@link Method} interface with Hades specific + * information that is necessary to construct {@link HadesQuery}s for the + * method. + * + * @author Oliver Gierke + */ +public class QueryMethod { + + private final Method method; + private final Parameters parameters; + + + /** + * Creates a new {@link QueryMethod} from the given parameters. Looks up the + * correct query to use for following invocations of the method given. + * + * @param method must not be {@literal null} + */ + public QueryMethod(Method method) { + + Assert.notNull(method, "Method must not be null!"); + + for (Class type : Parameters.TYPES) { + if (getNumberOfOccurences(method, type) > 1) { + throw new IllegalStateException(String.format( + "Method must only one argument of type %s!", + type.getSimpleName())); + } + } + + if (hasParameterOfType(method, Pageable.class)) { + assertReturnType(method, Page.class, List.class); + if (hasParameterOfType(method, Sort.class)) { + throw new IllegalStateException( + "Method must not have Pageable *and* Sort parameter. " + + "Use sorting capabilities on Pageble instead!"); + } + } + + this.method = method; + this.parameters = new Parameters(method); + } + + + /** + * Returns the method's name. + * + * @return + */ + public String getName() { + + return method.getName(); + } + + + /** + * Returns whether the given + * + * @param number + * @return + */ + public boolean isCorrectNumberOfParameters(int number) { + + return number == parameters.getBindableParameters() + .getNumberOfParameters(); + } + + + /** + * Returns the domain class for this method. + * + * @return + */ + public Class getDomainClass() { + + return getReturnedDomainClass(method); + } + + + /** + * Returns whether the finder will actually return a collection of entities + * or a single one. + * + * @return + */ + protected boolean isCollectionQuery() { + + Class returnType = method.getReturnType(); + return org.springframework.util.ClassUtils.isAssignable(List.class, + returnType); + } + + + /** + * Returns whether the finder will return a {@link Page} of results. + * + * @return + */ + protected boolean isPageQuery() { + + Class returnType = method.getReturnType(); + return org.springframework.util.ClassUtils.isAssignable(Page.class, + returnType); + } + + + /** + * Returns the {@link Parameters} wrapper to gain additional information + * about {@link Method} parameters. + * + * @return + */ + public Parameters getParameters() { + + return parameters; + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return method.toString(); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java new file mode 100644 index 000000000..2f4524f10 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Interface for a query abstraction. + * + * @author Oliver Gierke + */ +public interface RepositoryQuery { + + /** + * Executes the {@link RepositoryQuery} with the given parameters. + * + * @param store + * @param parameters + * @return + */ + public Object execute(Object[] parameters); +} \ No newline at end of file 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 new file mode 100644 index 000000000..7c5a63e43 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java @@ -0,0 +1,70 @@ +package org.springframework.data.repository.query.parser; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + + +/** + * Simple helper class to create a {@link Sort} instance from a method name end. + * It expects the last part of the method name to be given and supports lining + * up multiple properties ending with the sorting direction. So the following + * method ends are valid: {@code LastnameUsernameDesc}, + * {@code LastnameAscUsernameDesc}. + * + * @author Oliver Gierke + */ +public class OrderBySource { + + private final List orders; + + + public OrderBySource(String clause) { + + this.orders = new ArrayList(); + List properties = new ArrayList(); + + for (String part : clause.split("(?<=[a-z])(?=[A-Z])")) { + + Direction direction = defaultedFrom(part); + + if (direction == null) { + properties.add(StringUtils.uncapitalize(part)); + } else { + Assert.notEmpty( + properties, + "Invalid order syntax! You have to provide at least one property before the sort direction."); + orders.addAll(Order.create(direction, properties)); + properties.clear(); + } + } + } + + + /** + * Tries to resolve a {@link Direction} for the given {@link String}. + * Returns {@literal null} if resolving fails. + * + * @param candidate + * @return + */ + private Direction defaultedFrom(String candidate) { + + try { + return Direction.fromString(candidate); + } catch (IllegalArgumentException e) { + return null; + } + } + + + public Sort toSort() { + + return 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 new file mode 100644 index 000000000..75eac7733 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java @@ -0,0 +1,221 @@ +package org.springframework.data.repository.query.parser; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.data.repository.util.ClassUtils; +import org.springframework.util.StringUtils; + + +/** + * A single part of a method name that has to be transformed into a query part. + * The actual transformation is defined by a {@link Type} that is determined + * from inspecting the given part. The query part can then be looked up via + * {@link #getQueryPart()}. + * + * @author Oliver Gierke + */ +public class Part { + + private final String property; + private final Part.Type type; + + + /** + * Creates a new {@link Part} from the given method name part, the + * {@link Class} the part originates from and the start parameter index. + * + * @param part + * @param clazz + */ + public Part(String part, Class clazz) { + + this.type = Type.fromProperty(part, clazz); + this.property = type.extractProperty(part); + } + + + public boolean getParameterRequired() { + + return getNumberOfArguments() > 0; + } + + + /** + * Returns how many method parameters are bound by this part. + * + * @return + */ + public int getNumberOfArguments() { + + return type.getNumberOfArguments(); + } + + + /** + * @return the part + */ + public String getProperty() { + + return property; + } + + + /** + * @return the type + */ + public Part.Type getType() { + + return type; + } + + /** + * The type of a method name part. Used to create query parts in various + * ways. + * + * @author Oliver Gierke + */ + public static enum Type { + + BETWEEN(null, 2, "Between"), + + IS_NOT_NULL(null, 0, "IsNotNull", "NotNull"), + + IS_NULL(null, 0, "IsNull", "Null"), + + LESS_THAN("<", "LessThan"), + + GREATER_THAN(">", "GreaterThan"), + + NOT_LIKE("not like", "NotLike"), + + LIKE("like", "Like"), + + NEGATING_SIMPLE_PROPERTY("<>", "Not"), + + SIMPLE_PROPERTY("="); + + // Need to list them again explicitly as the order is important + // (esp. for IS_NULL, IS_NOT_NULL) + private static final List ALL = Arrays.asList(IS_NOT_NULL, + IS_NULL, BETWEEN, LESS_THAN, GREATER_THAN, NOT_LIKE, LIKE, + NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY); + private List keywords; + private String operator; + private int numberOfArguments; + + + /** + * Creates a new {@link Type} using the given keyword, number of + * arguments to be bound and operator. Keyword and operator can be + * {@literal null}. + * + * @param operator + * @param numberOfArguments + * @param keywords + */ + private Type(String operator, int numberOfArguments, String... keywords) { + + this.operator = operator; + this.numberOfArguments = numberOfArguments; + this.keywords = Arrays.asList(keywords); + } + + + private Type(String operator, String... keywords) { + + this(operator, 1, keywords); + } + + + /** + * Returns the {@link Type} of the {@link Part} for the given raw + * property and the given {@link Class}. This will try to detect e.g. + * keywords contained in the raw property that trigger special query + * creation. Returns {@link #SIMPLE_PROPERTY} by default. + * + * @param rawProperty + * @param clazz + * @return + */ + public static Part.Type fromProperty(String rawProperty, Class clazz) { + + for (Part.Type type : ALL) { + if (type.supports(rawProperty, clazz)) { + return type; + } + } + + return SIMPLE_PROPERTY; + } + + + public String getOperator() { + + return this.operator; + } + + + /** + * Returns whether the the type supports the given raw property. Default + * implementation checks whether the property ends with the registered + * keyword. Does not support the keyword if the property is a valid + * field as is. + * + * @param property + * @param clazz + * @return + */ + protected boolean supports(String property, Class clazz) { + + if (keywords == null) { + return true; + } + + if (ClassUtils.hasProperty(clazz, property)) { + return false; + } + + for (String keyword : keywords) { + if (property.endsWith(keyword)) { + return true; + } + } + + return false; + } + + + /** + * Returns the number of arguments the property binds. By default this + * exactly one argument. + * + * @return + */ + public int getNumberOfArguments() { + + return numberOfArguments; + } + + + /** + * Callback method to extract the actual property to be bound from the + * given part. Strips the keyword from the part's end if available. + * + * @param part + * @return + */ + public String extractProperty(String part) { + + String candidate = StringUtils.uncapitalize(part); + + for (String keyword : keywords) { + if (candidate.endsWith(keyword)) { + return candidate.substring(0, candidate.indexOf(keyword)); + } + } + + return candidate; + } + } +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartSource.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartSource.java new file mode 100644 index 000000000..ea81acdf5 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartSource.java @@ -0,0 +1,125 @@ +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; + + +/** + * Helper class to split a method name into all of its logical parts (prefix, + * properties, postfix). + * + * @author Oliver Gierke + */ +public class PartSource { + + 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 String cleanedUpString; + private final OrderBySource orderBySource; + + + public PartSource(String methodName) { + + String removedPrefixes = strip(methodName); + + String[] parts = split(removedPrefixes, ORDER_BY); + + if (parts.length > 2) { + throw new IllegalArgumentException( + "OrderBy must not be used more than once in a method name!"); + } + + this.cleanedUpString = parts[0]; + this.orderBySource = + parts.length == 2 ? getOrderBySourceFor(parts[1]) : null; + } + + + public OrderBySource getOrderBySource() { + + return orderBySource; + } + + + public boolean hasOrderByClause() { + + return orderBySource != null; + } + + + protected OrderBySource getOrderBySourceFor(String postfix) { + + return new OrderBySource(postfix); + } + + + /** + * Returns an iterator over all the {@link PartSource}s created by spliting + * up the current one with the given keyword. + * + * @param keyword + * @return + */ + public Iterator getParts(String keyword) { + + List parts = new ArrayList(); + for (String part : split(cleanedUpString, keyword)) { + parts.add(new PartSource(part)); + } + + return parts.iterator(); + } + + + public String cleanedUp() { + + return cleanedUpString; + } + + + /** + * 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; + } + + + /** + * 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); + } +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java new file mode 100644 index 000000000..d9d7a2149 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java @@ -0,0 +1,17 @@ +package org.springframework.data.repository.support; + +/** + * Interface to abstract the ways to retrieve the id of the given entity. + * + * @author Oliver Gierke + */ +public interface IdAware { + + /** + * Returns the id of the given entity. + * + * @param entity + * @return + */ + Object getId(Object entity); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IsNewAware.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IsNewAware.java new file mode 100644 index 000000000..cf1c7c6bc --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IsNewAware.java @@ -0,0 +1,18 @@ +package org.springframework.data.repository.support; + +/** + * Interface to abstract the ways to determine if the given entity is to be + * considered as new. + * + * @author Oliver Gierke + */ +public interface IsNewAware { + + /** + * Returns whether the given entity is considered to be new. + * + * @param entity + * @return + */ + boolean isNew(Object entity); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java new file mode 100644 index 000000000..b406cf99d --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java @@ -0,0 +1,37 @@ +package org.springframework.data.repository.support; + +import org.springframework.data.domain.Persistable; + + +/** + * Implementation of {@link IsNewAware} that assumes the entity handled + * implements {@link Persistable} and uses {@link Persistable#isNew()} for the + * {@link #isNew(Object)} check. + * + * @author Oliver Gierke + */ +public class PersistableEntityInformation implements IsNewAware, IdAware { + + /* + * (non-Javadoc) + * + * @see org.synyx.hades.dao.orm.GenericJpaDao.IsNewStrategy#isNew(java.lang + * .Object) + */ + public boolean isNew(Object entity) { + + return ((Persistable) entity).isNew(); + } + + + /* + * (non-Javadoc) + * + * @see org.synyx.hades.dao.orm.GenericDaoSupport.IdAware#getId(java.lang + * .Object) + */ + public Object getId(Object entity) { + + return ((Persistable) entity).getId(); + } +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java new file mode 100644 index 000000000..c6b39da9a --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java @@ -0,0 +1,131 @@ +package org.springframework.data.repository.support; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; +import org.springframework.util.ReflectionUtils.MethodCallback; + + +/** + * {@link IsNewAware} and {@link IdAware} implementation that reflectively + * checks a {@link Field} or {@link Method} annotated with the given + * annotations. Subclasses usually simply have to provide the persistence + * technology specific annotations. + * + * @author Oliver Gierke + */ +public class ReflectiveEntityInformationSupport implements IsNewAware, IdAware { + + private Field field; + private Method method; + + + /** + * Creates a new {@link ReflectiveEntityInformationSupport} by inspecting + * the given class for a {@link Field} or {@link Method} for and {@link Id} + * annotation. + * + * @param domainClass not {@literal null}, must be annotated with + * {@link Entity} and carry an anootation defining the id + * property. + */ + public ReflectiveEntityInformationSupport(Class domainClass, + final Class... annotationsToScanFor) { + + Assert.notNull(domainClass); + + ReflectionUtils.doWithFields(domainClass, new FieldCallback() { + + public void doWith(Field field) { + + if (ReflectiveEntityInformationSupport.this.field != null) { + return; + } + + if (hasAnnotation(field, annotationsToScanFor)) { + ReflectiveEntityInformationSupport.this.field = field; + } + } + }); + + if (field != null) { + return; + } + + ReflectionUtils.doWithMethods(domainClass, new MethodCallback() { + + public void doWith(Method method) { + + if (ReflectiveEntityInformationSupport.this.method != null) { + return; + } + + if (hasAnnotation(method, annotationsToScanFor)) { + ReflectiveEntityInformationSupport.this.method = method; + } + } + }); + + Assert.isTrue(this.field != null || this.method != null, + "No id method or field found!"); + } + + + /** + * Checks whether the given {@link AnnotatedElement} carries one of the + * given {@link Annotation}s. + * + * @param annotatedElement + * @param annotations + * @return + */ + private boolean hasAnnotation(AnnotatedElement annotatedElement, + Class... annotations) { + + for (Class annotation : annotations) { + + if (annotatedElement.getAnnotation(annotation) != null) { + return true; + } + } + + return false; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.RepositorySupport.IsNewAware + * #isNew(java.lang.Object) + */ + public boolean isNew(Object entity) { + + return getId(entity) == null; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.RepositorySupport.IdAware + * #getId(java.lang.Object) + */ + public Object getId(Object entity) { + + if (field != null) { + ReflectionUtils.makeAccessible(field); + return ReflectionUtils.getField(field, entity); + } + + ReflectionUtils.makeAccessible(method); + return ReflectionUtils.invokeMethod(method, entity); + } +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java new file mode 100644 index 000000000..6c6dbaa13 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java @@ -0,0 +1,174 @@ +/* + * 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.support; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.Required; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.util.TxUtils; +import org.springframework.util.Assert; + + +/** + * Adapter for Springs {@link FactoryBean} interface to allow easy setup of + * repository factories via Spring configuration. + * + * @author Oliver Gierke + * @param the type of the repository + */ +public abstract class RepositoryFactoryBeanSupport> + implements FactoryBean, InitializingBean, BeanFactoryAware { + + private RepositoryFactorySupport factory; + + private Key queryLookupStrategyKey; + private Class repositoryInterface; + private Object customImplementation; + + private String transactionManagerName = TxUtils.DEFAULT_TRANSACTION_MANAGER; + private RepositoryProxyPostProcessor txPostProcessor; + + + /** + * Setter to inject the repository interface to implement. + * + * @param repositoryInterface the repository interface to set + */ + @Required + public void setRepositoryInterface(Class repositoryInterface) { + + Assert.notNull(repositoryInterface); + this.repositoryInterface = repositoryInterface; + } + + + public void setQueryLookupStrategyKey(Key queryLookupStrategyKey) { + + this.queryLookupStrategyKey = queryLookupStrategyKey; + } + + + /** + * Setter to configure which transaction manager to be used. We have to use + * the bean name explicitly as otherwise the qualifier of the + * {@link org.springframework.transaction.annotation.Transactional} + * annotation is used. By explicitly defining the transaction manager bean + * name we favour let this one be the default one chosen. + * + * @param transactionManager + */ + public void setTransactionManager(String transactionManager) { + + this.transactionManagerName = + transactionManager == null ? TxUtils.DEFAULT_TRANSACTION_MANAGER + : transactionManager; + } + + + /** + * Setter to inject a custom repository implementation. + * + * @param customImplementation + */ + public void setCustomImplementation(Object customImplementation) { + + this.customImplementation = customImplementation; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ + public T getObject() { + + return factory.getRepository(repositoryInterface, + customImplementation); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + @SuppressWarnings("unchecked") + public Class getObjectType() { + + return (Class) (null == repositoryInterface ? Repository.class + : repositoryInterface); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.beans.factory.FactoryBean#isSingleton() + */ + public boolean isSingleton() { + + return true; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() { + + this.factory = createRepositoryFactory(); + this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); + this.factory.validate(repositoryInterface, + customImplementation); + this.factory.addDaoProxyPostProcessor(txPostProcessor); + } + + + /** + * Create the actual {@link RepositoryFactorySupport} instance. + * + * @return + */ + protected abstract RepositoryFactorySupport createRepositoryFactory(); + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org + * .springframework.beans.factory.BeanFactory) + */ + public void setBeanFactory(BeanFactory beanFactory) { + + Assert.isInstanceOf(ListableBeanFactory.class, beanFactory); + + this.txPostProcessor = + new TransactionalRepositoryProxyPostProcessor( + (ListableBeanFactory) beanFactory, + transactionManagerName); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java new file mode 100644 index 000000000..99626d0ac --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java @@ -0,0 +1,458 @@ +/* + * 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.support; + +import static org.springframework.data.repository.util.ClassUtils.*; +import static org.springframework.util.ReflectionUtils.*; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.util.ClassUtils; +import org.springframework.util.Assert; + + +/** + * Factory bean to create instances of a given repository interface. Creates a + * proxy implementing the configured repository interface and apply an advice + * handing the control to the {@code QueryExecuterMethodInterceptor}. Query + * detection strategy can be configured by setting + * {@link QueryLookupStrategy.Key}. + * + * @author Oliver Gierke + */ +public abstract class RepositoryFactorySupport { + + private QueryLookupStrategy.Key queryLookupStrategyKey; + + private final Map methodCache = + new ConcurrentHashMap(); + private final List postProcessors = + new ArrayList(); + + + /** + * Sets the strategy of how to lookup a query to execute finders. + * + * @param queryLookupStrategy the createFinderQueries to set + */ + public void setQueryLookupStrategyKey(Key key) { + + this.queryLookupStrategyKey = key; + } + + + /** + * Adds {@link RepositoryProxyPostProcessor}s to the factory to allow + * manipulation of the {@link ProxyFactory} before the proxy gets created. + * Note that the {@link QueryExecuterMethodInterceptor} will be added to the + * proxy after the {@link RepositoryProxyPostProcessor}s are + * considered. + * + * @param processor + */ + protected void addDaoProxyPostProcessor( + RepositoryProxyPostProcessor processor) { + + Assert.notNull(processor); + this.postProcessors.add(processor); + } + + + /** + * Returns a repository instance for the given interface. + * + * @param + * @param repositoryInterface + * @return + */ + public > T getRepository( + Class repositoryInterface) { + + return getRepository(repositoryInterface, null); + } + + + /** + * Returns a repository instance for the given interface backed by an + * instance providing implementation logic for custom logic. + * + * @param + * @param repositoryInterface + * @param customDaoImplementation + * @return + */ + @SuppressWarnings("unchecked") + public > T getRepository( + Class repositoryInterface, Object customDaoImplementation) { + + validate(repositoryInterface, customDaoImplementation); + + Class domainClass = getDomainClass(repositoryInterface); + RepositorySupport target = + getTargetRepository(domainClass); + + // Create proxy + ProxyFactory result = new ProxyFactory(); + result.setTarget(target); + result.setInterfaces(new Class[] { repositoryInterface }); + + for (RepositoryProxyPostProcessor processor : postProcessors) { + processor.postProcess(result); + } + + result.addAdvice(new QueryExecuterMethodInterceptor( + repositoryInterface, customDaoImplementation, target)); + + return (T) result.getProxy(); + } + + + /** + * Create a {@link RepositorySupport} instance as backing for the + * query proxy. + * + * @param + * @param domainClass + * @return + */ + protected abstract RepositorySupport getTargetRepository( + Class domainClass); + + + /** + * Create a {@link QueryMethod} instance for the given {@link Method}. + * + * @param method + * @return + */ + protected abstract QueryMethod getQueryMethod(Method method); + + + /** + * Determines the base class for the repository to be created. + * + * @return + */ + @SuppressWarnings("rawtypes") + protected abstract Class getRepositoryClass(); + + + /** + * Returns the {@link QueryLookupStrategy} for the given {@link Key}. + * + * @param key can be {@literal null} + * @return + */ + protected abstract QueryLookupStrategy getQueryLookupStrategy(Key key); + + + /** + * Returns if the configured DAO interface has custom methods, that might + * have to be delegated to a custom DAO implementation. This is used to + * verify DAO configuration. + * + * @return + */ + private boolean hasCustomMethod( + Class> daoInterface) { + + boolean hasCustomMethod = false; + + // No detection required if no typing interface was configured + if (isGenericRepositoryInterface(daoInterface)) { + return false; + } + + for (Method method : daoInterface.getMethods()) { + + if (isCustomMethod(method, daoInterface) + && !isBaseClassMethod(method, daoInterface)) { + return true; + } + } + + return hasCustomMethod; + } + + + /** + * Returns whether the given method is considered to be a DAO base class + * method. + * + * @param method + * @return + */ + private boolean isBaseClassMethod(Method method, Class daoInterface) { + + Assert.notNull(method); + + if (method.getDeclaringClass().isAssignableFrom(getRepositoryClass())) { + return true; + } + + return !method.equals(getBaseClassMethod(method, daoInterface)); + } + + + /** + * Returns the base class method that is backing the given method. This can + * be necessary if a DAO interface redeclares a method in {@link GenericDao} + * (e.g. for transaction behaviour customization). Returns the method itself + * if the base class does not implement the given method. + * + * @param method + * @return + */ + private Method getBaseClassMethod(Method method, Class daoInterface) { + + Assert.notNull(method); + + Method result = methodCache.get(method); + + if (null != result) { + return result; + } + + result = + getBaseClassMethodFor(method, getRepositoryClass(), + daoInterface); + methodCache.put(method, result); + + return result; + } + + + /** + * Returns whether the given method is a custom DAO method. + * + * @param method + * @param daoInterface + * @return + */ + private boolean isCustomMethod(Method method, Class daoInterface) { + + Class declaringClass = method.getDeclaringClass(); + + boolean isQueryMethod = declaringClass.equals(daoInterface); + boolean isHadesDaoInterface = + isGenericRepositoryInterface(declaringClass); + boolean isBaseClassMethod = isBaseClassMethod(method, daoInterface); + + return !(isHadesDaoInterface || isBaseClassMethod || isQueryMethod); + } + + + /** + * Returns all methods considered to be finder methods. + * + * @param daoInterface + * @return + */ + private Iterable getFinderMethods(Class daoInterface) { + + Set result = new HashSet(); + + for (Method method : daoInterface.getDeclaredMethods()) { + if (!isCustomMethod(method, daoInterface) + && !isBaseClassMethod(method, daoInterface)) { + result.add(method); + } + } + + return result; + } + + + /** + * Validates the given repository interface. + * + * @param daoInterface + */ + private void validate(Class daoInterface) { + + Assert.notNull(daoInterface); + Assert.notNull( + getDomainClass(daoInterface), + "Could not retrieve domain class from interface. Make sure it extends GenericRepository."); + + } + + + /** + * Validates the given repository interface as well as the given custom + * implementation. + * + * @param repositoryInterface + * @param customImplementation + */ + protected void validate( + Class> repositoryInterface, + Object customImplementation) { + + validate(repositoryInterface); + + if (null == customImplementation + && hasCustomMethod(repositoryInterface)) { + + throw new IllegalArgumentException( + String.format( + "You have custom methods in %s but not provided a custom implementation!", + repositoryInterface)); + } + } + + /** + * This {@code MethodInterceptor} intercepts calls to methods of the custom + * implementation and delegates the to it if configured. Furthermore it + * resolves method calls to finders and triggers execution of them. You can + * rely on having a custom repository implementation instance set if this + * returns true. + * + * @author Oliver Gierke + */ + public class QueryExecuterMethodInterceptor implements MethodInterceptor { + + private final Map queries = + new ConcurrentHashMap(); + + private final Object customImplementation; + private final Class repositoryInterface; + private final RepositorySupport target; + + + /** + * Creates a new {@link QueryExecuterMethodInterceptor}. Builds a model + * of {@link QueryMethod}s to be invoked on execution of repository + * interface methods. + */ + public QueryExecuterMethodInterceptor(Class repositoryInterface, + Object customImplementation, + RepositorySupport target) { + + this.repositoryInterface = repositoryInterface; + this.customImplementation = customImplementation; + this.target = target; + + QueryLookupStrategy strategy = + getQueryLookupStrategy(queryLookupStrategyKey); + + for (Method method : getFinderMethods(repositoryInterface)) { + QueryMethod queryMethod = getQueryMethod(method); + queries.put(method, strategy.resolveQuery(queryMethod)); + } + } + + + /* + * (non-Javadoc) + * + * @see + * org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance + * .intercept.MethodInvocation) + */ + public Object invoke(MethodInvocation invocation) throws Throwable { + + Method method = invocation.getMethod(); + + if (isCustomMethodInvocation(invocation)) { + + makeAccessible(method); + return executeMethodOn(customImplementation, method, + invocation.getArguments()); + } + + if (hasQueryFor(method)) { + return queries.get(method).execute(invocation.getArguments()); + } + + // Lookup actual method as it might be redeclared in the interface + // and we have to use the repository instance nevertheless + Method actualMethod = + getBaseClassMethod(method, repositoryInterface); + return executeMethodOn(target, actualMethod, + invocation.getArguments()); + } + + + /** + * Executes the given method on the given target. Correctly unwraps + * exceptions not caused by the reflection magic. + * + * @param target + * @param method + * @param parameters + * @return + * @throws Throwable + */ + private Object executeMethodOn(Object target, Method method, + Object[] parameters) throws Throwable { + + try { + return method.invoke(target, parameters); + } catch (Exception e) { + ClassUtils.unwrapReflectionException(e); + } + + throw new IllegalStateException("Should not occur!"); + } + + + /** + * Returns whether we know of a query to execute for the given + * {@link Method}; + * + * @param method + * @return + */ + private boolean hasQueryFor(Method method) { + + return queries.containsKey(method); + } + + + /** + * Returns whether the given {@link MethodInvocation} is considered to + * be targeted as an invocation of a custom method. + * + * @param method + * @return + */ + private boolean isCustomMethodInvocation(MethodInvocation invocation) { + + if (null == customImplementation) { + return false; + } + + return isCustomMethod(invocation.getMethod(), repositoryInterface); + } + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryInterfaceAwareBeanPostProcessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryInterfaceAwareBeanPostProcessor.java new file mode 100644 index 000000000..f3fb59d45 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryInterfaceAwareBeanPostProcessor.java @@ -0,0 +1,115 @@ +/* + * 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.support; + +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.util.ClassUtils; + + +/** + * A + * {@link org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor} + * implementing {@code #predictBeanType(Class, String)} to return the configured + * DAO interface from {@link GenericDaoFactoryBean}s. This is done as shortcut + * to prevent the need of instantiating {@link GenericDaoFactoryBean}s just to + * find out what DAO interface they actually create. + * + * @author Oliver Gierke + */ +class RepositoryInterfaceAwareBeanPostProcessor extends + InstantiationAwareBeanPostProcessorAdapter implements BeanFactoryAware { + + private static final Class REPOSITORY_TYPE = + RepositoryFactorySupport.class; + + private ConfigurableListableBeanFactory context; + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org + * .springframework.beans.factory.BeanFactory) + */ + public void setBeanFactory(BeanFactory beanFactory) { + + if (beanFactory instanceof ConfigurableListableBeanFactory) { + + this.context = (ConfigurableListableBeanFactory) beanFactory; + } + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.beans.factory.config. + * InstantiationAwareBeanPostProcessorAdapter + * #predictBeanType(java.lang.Class, java.lang.String) + */ + @Override + public Class predictBeanType(Class beanClass, String beanName) { + + if (null == context || !REPOSITORY_TYPE.isAssignableFrom(beanClass)) { + return null; + } + + BeanDefinition definition = context.getBeanDefinition(beanName); + PropertyValue value = + definition.getPropertyValues().getPropertyValue("daoInterface"); + + return getClassForPropertyValue(value); + } + + + /** + * Returns the class which is configured in the given {@link PropertyValue}. + * In case it is not a {@link TypedStringValue} or the value contained + * cannot be interpreted as {@link Class} it will return null. + * + * @param propertyValue + * @return + */ + private Class getClassForPropertyValue(PropertyValue propertyValue) { + + Object value = propertyValue.getValue(); + String className = null; + + if (value instanceof TypedStringValue) { + className = ((TypedStringValue) value).getValue(); + } else if (value instanceof String) { + className = (String) value; + } else if (value instanceof Class) { + return (Class) value; + } else { + return null; + } + + try { + return ClassUtils.resolveClassName(className, + RepositoryInterfaceAwareBeanPostProcessor.class.getClassLoader()); + } catch (IllegalArgumentException ex) { + return null; + } + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryProxyPostProcessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryProxyPostProcessor.java new file mode 100644 index 000000000..4f931000e --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryProxyPostProcessor.java @@ -0,0 +1,36 @@ +/* + * 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.support; + +import org.springframework.aop.framework.ProxyFactory; + + +/** + * Callback interface used during DAO proxy creation. Allows manipulating the + * {@link ProxyFactory} creating the DAO. + * + * @author Oliver Gierke + */ +public interface RepositoryProxyPostProcessor { + + /** + * Manipulates the {@link ProxyFactory}, e.g. add further interceptors to + * it. + * + * @param factory + */ + void postProcess(ProxyFactory factory); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java new file mode 100644 index 000000000..b52c47e25 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java @@ -0,0 +1,87 @@ +/* + * 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.support; + +import java.io.Serializable; + +import org.springframework.data.domain.Persistable; +import org.springframework.data.repository.Repository; +import org.springframework.util.Assert; + + +/** + * Abstract base class for generic repositories. Captures information about the + * domain class to be managed. + * + * @author Oliver Gierke + * @param the type of entity to be handled + */ +public abstract class RepositorySupport implements + Repository { + + private final Class domainClass; + private final IsNewAware isNewStrategy; + + + /** + * Creates a new {@link RepositorySupport}. + * + * @param domainClass + */ + public RepositorySupport(Class domainClass) { + + Assert.notNull(domainClass); + this.domainClass = domainClass; + this.isNewStrategy = createIsNewStrategy(domainClass); + Assert.notNull(isNewStrategy); + } + + + /** + * Returns the domain class to handle. + * + * @return the domain class + */ + protected Class getDomainClass() { + + return domainClass; + } + + + /** + * Return whether the given entity is to be regarded as new. Default + * implementation will inspect the given domain class and use either + * {@link PersistableEntityInformation} if the class implements + * {@link Persistable} or {@link ReflectiveEntityInformation} otherwise. + * + * @param entity + * @return + */ + protected abstract IsNewAware createIsNewStrategy(Class domainClass); + + + /** + * Returns the strategy how to determine whether an entity is to be regarded + * as new. + * + * @return the isNewStrategy + */ + protected IsNewAware getIsNewStrategy() { + + return isNewStrategy; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryProxyPostProcessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryProxyPostProcessor.java new file mode 100644 index 000000000..36a30fe16 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryProxyPostProcessor.java @@ -0,0 +1,75 @@ +/* + * 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.support; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.dao.support.PersistenceExceptionTranslationInterceptor; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.util.Assert; + + +/** + * {@link DaoProxyPostProcessor} to add transactional behaviour to DAO proxies. + * Adds a {@link PersistenceExceptionTranslationInterceptor} as well as an + * annotation based {@link TransactionInterceptor} to the proxy. + * + * @author Oliver Gierke + */ +class TransactionalRepositoryProxyPostProcessor implements + RepositoryProxyPostProcessor { + + private final TransactionInterceptor transactionInterceptor; + private final PersistenceExceptionTranslationInterceptor petInterceptor; + + + /** + * Creates a new {@link TransactionalRepositoryProxyPostProcessor}. + */ + public TransactionalRepositoryProxyPostProcessor( + ListableBeanFactory beanFactory, String transactionManagerName) { + + Assert.notNull(beanFactory); + Assert.notNull(transactionManagerName); + + this.petInterceptor = new PersistenceExceptionTranslationInterceptor(); + this.petInterceptor.setBeanFactory(beanFactory); + this.petInterceptor.afterPropertiesSet(); + + this.transactionInterceptor = + new TransactionInterceptor(null, + new AnnotationTransactionAttributeSource()); + this.transactionInterceptor + .setTransactionManagerBeanName(transactionManagerName); + this.transactionInterceptor.setBeanFactory(beanFactory); + this.transactionInterceptor.afterPropertiesSet(); + } + + + /* + * (non-Javadoc) + * + * @see + * org.synyx.hades.dao.orm.DaoProxyPostProcessor#postProcess(org.springframework + * .aop.framework.ProxyFactory) + */ + public void postProcess(ProxyFactory factory) { + + factory.addAdvice(petInterceptor); + factory.addAdvice(transactionInterceptor); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java new file mode 100644 index 000000000..35e9618a9 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java @@ -0,0 +1,356 @@ +/* + * 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.util; + +import static org.springframework.core.GenericTypeResolver.*; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.data.repository.Repository; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + + +/** + * Utility class to work with classes. + * + * @author Oliver Gierke + */ +public abstract class ClassUtils { + + @SuppressWarnings("rawtypes") + private static final TypeVariable>[] PARAMETERS = + Repository.class.getTypeParameters(); + private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName(); + private static final String ID_TYPE_NAME = PARAMETERS[1].getName(); + + + /** + * Private constructor to prevent instantiation. + */ + private ClassUtils() { + + } + + + /** + * Returns the domain class the given class is declared for. Will introspect + * the given class for extensions of {@link Repository} and retrieve + * the domain class type from its generics declaration. + * + * @param clazz + * @return the domain class the given class is repository for or + * {@code null} if none found. + */ + public static Class getDomainClass(Class clazz) { + + Class[] arguments = + resolveTypeArguments(clazz, Repository.class); + return arguments == null ? null : arguments[0]; + } + + + /** + * Returns the id class the given class is declared for. Will introspect the + * given class for extensions of {@link Repository} or and retrieve + * the {@link Serializable} type from its generics declaration. + * + * @param clazz + * @return the id class the given class is repository for or {@code null} if + * none found. + */ + @SuppressWarnings("unchecked") + public static Class getIdClass(Class clazz) { + + Class[] arguments = + resolveTypeArguments(clazz, Repository.class); + return (Class) (arguments == null ? null + : arguments[1]); + } + + + /** + * Returns the domain class returned by the given {@link Method}. Will + * extract the type from {@link Collection}s and + * {@link org.springframework.data.domain.Page} as well. + * + * @param method + * @return + */ + public static Class getReturnedDomainClass(Method method) { + + Type type = method.getGenericReturnType(); + + if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type) + .getActualTypeArguments()[0]; + + } else { + return method.getReturnType(); + } + } + + + /** + * Returns whether the given class contains a property with the given name. + * + * @param fieldName + * @return + */ + public static boolean hasProperty(Class type, String property) { + + if (null != ReflectionUtils.findMethod(type, "get" + property)) { + return true; + } + + return null != ReflectionUtils.findField(type, + StringUtils.uncapitalize(property)); + } + + + /** + * Returns wthere the given type is the {@link Repository} interface. + * + * @param interfaze + * @return + */ + public static boolean isGenericRepositoryInterface(Class interfaze) { + + return Repository.class.equals(interfaze); + } + + + /** + * Returns whether the given type name is a repository interface name. + * + * @param interfaceName + * @return + */ + public static boolean isGenericRepositoryInterface(String interfaceName) { + + return Repository.class.getName().equals(interfaceName); + } + + + /** + * Returns the number of occurences of the given type in the given + * {@link Method}s parameters. + * + * @param method + * @param type + * @return + */ + public static int getNumberOfOccurences(Method method, Class type) { + + int result = 0; + for (Class clazz : method.getParameterTypes()) { + if (type.equals(clazz)) { + result++; + } + } + + return result; + } + + + /** + * Asserts the given {@link Method}'s return type to be one of the given + * types. + * + * @param method + * @param types + */ + public static void assertReturnType(Method method, Class... types) { + + if (!Arrays.asList(types).contains(method.getReturnType())) { + throw new IllegalStateException( + "Method has to have one of the following return types! " + + Arrays.toString(types)); + } + } + + + /** + * Returns whether the given object is of one of the given types. Will + * return {@literal false} for {@literal null}. + * + * @param object + * @param types + * @return + */ + public static boolean isOfType(Object object, Collection> types) { + + if (null == object) { + return false; + } + + for (Class type : types) { + if (type.isAssignableFrom(object.getClass())) { + return true; + } + } + + return false; + } + + + /** + * Returns whether the given {@link Method} has a parameter of the given + * type. + * + * @param method + * @param type + * @return + */ + public static boolean hasParameterOfType(Method method, Class type) { + + return Arrays.asList(method.getParameterTypes()).contains(type); + } + + + /** + * Helper method to extract the original exception that can possibly occur + * during a reflection call. + * + * @param ex + * @throws Throwable + */ + public static void unwrapReflectionException(Exception ex) throws Throwable { + + if (ex instanceof InvocationTargetException) { + throw ((InvocationTargetException) ex).getTargetException(); + } + + throw ex; + } + + + /** + * Returns the given base class' method if the given method (declared in the + * interface) was also declared at the base class. Returns the given method + * if the given base class does not declare the method given. Takes generics + * into account. + * + * @param method + * @param baseClass + * @param repositoryInterface + * @return + */ + public static Method getBaseClassMethodFor(Method method, + Class baseClass, Class repositoryInterface) { + + for (Method baseClassMethod : baseClass.getDeclaredMethods()) { + + // Wrong name + if (!method.getName().equals(baseClassMethod.getName())) { + continue; + } + + // Wrong number of arguments + if (!(method.getParameterTypes().length == baseClassMethod + .getParameterTypes().length)) { + continue; + } + + // Check whether all parameters match + if (!parametersMatch(method, baseClassMethod, repositoryInterface)) { + continue; + } + + return baseClassMethod; + } + + return method; + } + + + /** + * Checks the given method's parameters to match the ones of the given base + * class method. Matches generic arguments agains the ones bound in the + * given repository interface. + * + * @param method + * @param baseClassMethod + * @param repositoryInterface + * @return + */ + private static boolean parametersMatch(Method method, + Method baseClassMethod, Class repositoryInterface) { + + Type[] genericTypes = baseClassMethod.getGenericParameterTypes(); + Class[] types = baseClassMethod.getParameterTypes(); + Class[] methodParameters = method.getParameterTypes(); + + for (int i = 0; i < genericTypes.length; i++) { + + Type type = genericTypes[i]; + + if (type instanceof TypeVariable) { + + String name = ((TypeVariable) type).getName(); + + if (!matchesGenericType(name, methodParameters[i], + repositoryInterface)) { + return false; + } + + } else { + + if (!types[i].equals(methodParameters[i])) { + return false; + } + } + } + + return true; + } + + + /** + * Checks whether the given parameter type matches the generic type of the + * given parameter. Thus when {@literal PK} is declared, the method ensures + * that given method parameter is the primary key type declared in the given + * repository interface e.g. + * + * @param name + * @param parameterType + * @param repositoryInterface + * @return + */ + private static boolean matchesGenericType(String name, + Class parameterType, Class repositoryInterface) { + + Class entityType = getDomainClass(repositoryInterface); + Class idClass = getIdClass(repositoryInterface); + + if (ID_TYPE_NAME.equals(name) && parameterType.equals(idClass)) { + return true; + } + + if (DOMAIN_TYPE_NAME.equals(name) && parameterType.equals(entityType)) { + return true; + } + + return false; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/TxUtils.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/TxUtils.java new file mode 100644 index 000000000..f77c3c21b --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/TxUtils.java @@ -0,0 +1,31 @@ +/* + * 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.util; + +/** + * Simple constants holder. + * + * @author Oliver Gierke + */ +public abstract class TxUtils { + + private TxUtils() { + + } + + public static final String DEFAULT_TRANSACTION_MANAGER = + "transactionManager"; +} diff --git a/spring-data-commons-core/src/main/resources/META-INF/spring.schemas b/spring-data-commons-core/src/main/resources/META-INF/spring.schemas new file mode 100644 index 000000000..22d3f4bfa --- /dev/null +++ b/spring-data-commons-core/src/main/resources/META-INF/spring.schemas @@ -0,0 +1,2 @@ +http\://www.springframework.org/schema/data/repository/spring-repository-1.0.xsd=org/springframework/data/repository/config/spring-repository-1.0.xsd +http\://www.springframework.org/schema/data/repository/spring-repository.xsd=org/springframework/data/repository/config/spring-repository-1.0.xsd diff --git a/spring-data-commons-core/src/main/resources/org/springframework/data/repository/config/spring-repository-1.0.xsd b/spring-data-commons-core/src/main/resources/org/springframework/data/repository/config/spring-repository-1.0.xsd new file mode 100644 index 000000000..cea1bab0d --- /dev/null +++ b/spring-data-commons-core/src/main/resources/org/springframework/data/repository/config/spring-repository-1.0.xsd @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Declares a single DAO instance. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/domain/DirectionUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/domain/DirectionUnitTests.java new file mode 100644 index 000000000..8582d653a --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/domain/DirectionUnitTests.java @@ -0,0 +1,29 @@ +package org.springframework.data.domain; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.domain.Sort.Direction; + + +/** + * Unit test for {@link Direction}. + * + * @author Oliver Gierke + */ +public class DirectionUnitTests { + + @Test + public void jpaValueMapping() throws Exception { + + assertEquals(Direction.ASC, Direction.fromString("asc")); + assertEquals(Direction.DESC, Direction.fromString("desc")); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsInvalidString() throws Exception { + + Direction.fromString("foo"); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/domain/PageImplUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/domain/PageImplUnitTests.java new file mode 100644 index 000000000..ce51a8bf8 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/domain/PageImplUnitTests.java @@ -0,0 +1,81 @@ +/* + * 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.domain; + +import static org.springframework.data.domain.UnitTestUtils.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + + +/** + * Unit test for {@link PageImpl}. + * + * @author Oliver Gierke + */ +public class PageImplUnitTests { + + @Test + public void assertEqualsForSimpleSetup() throws Exception { + + PageImpl page = new PageImpl(Arrays.asList("Foo")); + + assertEqualsAndHashcode(page, page); + assertEqualsAndHashcode(page, + new PageImpl(Arrays.asList("Foo"))); + } + + + @Test + public void assertEqualsForComplexSetup() throws Exception { + + Pageable pageable = new PageRequest(0, 10); + List content = Arrays.asList("Foo"); + + PageImpl page = new PageImpl(content, pageable, 100); + + assertEqualsAndHashcode(page, page); + + assertEqualsAndHashcode(page, new PageImpl(content, pageable, + 100)); + + assertNotEqualsAndHashcode(page, new PageImpl(content, + pageable, 90)); + + assertNotEqualsAndHashcode(page, new PageImpl(content, + new PageRequest(1, 10), 100)); + + assertNotEqualsAndHashcode(page, new PageImpl(content, + new PageRequest(0, 15), 100)); + } + + + @Test(expected = IllegalArgumentException.class) + public void preventsNullContentForSimpleSetup() throws Exception { + + new PageImpl(null); + } + + + @Test(expected = IllegalArgumentException.class) + public void preventsNullContentForAdvancedSetup() throws Exception { + + new PageImpl(null, null, 0); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/domain/PageRequestUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/domain/PageRequestUnitTests.java new file mode 100644 index 000000000..9895c8148 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/domain/PageRequestUnitTests.java @@ -0,0 +1,87 @@ +/* + * 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.domain; + +import static org.springframework.data.domain.UnitTestUtils.*; + +import org.junit.Test; +import org.springframework.data.domain.Sort.Direction; + + +/** + * Unit test for {@link PageRequest}. + * + * @author Oliver Gierke + */ +public class PageRequestUnitTests { + + @Test(expected = IllegalArgumentException.class) + public void preventsNegativePage() { + + new PageRequest(-1, 10); + } + + + @Test(expected = IllegalArgumentException.class) + public void preventsNegativeSize() { + + new PageRequest(0, -1); + } + + + @Test + public void equalsRegardsSortCorrectly() { + + Sort sort = new Sort(Direction.DESC, "foo"); + PageRequest request = new PageRequest(0, 10, sort); + + // Equals itself + assertEqualsAndHashcode(request, request); + + // Equals another instance with same setup + assertEqualsAndHashcode(request, new PageRequest(0, 10, sort)); + + // Equals without sort entirely + assertEqualsAndHashcode(new PageRequest(0, 10), new PageRequest(0, 10)); + + // Is not equal to instance without sort + assertNotEqualsAndHashcode(request, new PageRequest(0, 10)); + + // Is not equal to instance with another sort + assertNotEqualsAndHashcode(request, new PageRequest(0, 10, + Direction.ASC, "foo")); + } + + + @Test + public void equalsHonoursPageAndSize() { + + PageRequest request = new PageRequest(0, 10); + + // Equals itself + assertEqualsAndHashcode(request, request); + + // Equals same setup + assertEqualsAndHashcode(request, new PageRequest(0, 10)); + + // Does not equal on different page + assertNotEqualsAndHashcode(request, new PageRequest(1, 10)); + + // Does not equal on different size + assertNotEqualsAndHashcode(request, new PageRequest(0, 11)); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/domain/SortUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/domain/SortUnitTests.java new file mode 100644 index 000000000..c6323910e --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/domain/SortUnitTests.java @@ -0,0 +1,95 @@ +/* + * 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.domain; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.domain.Sort.Direction; + + +/** + * Unit test for {@link Sort}. + * + * @author Oliver Gierke + */ +public class SortUnitTests { + + /** + * Asserts that the class applies the default sort order if no order or + * {@code null} was provided. + * + * @throws Exception + */ + @Test + public void appliesDefaultForOrder() throws Exception { + + assertEquals(Sort.DEFAULT_DIRECTION, new Sort("foo").iterator().next() + .getDirection()); + assertEquals(Sort.DEFAULT_DIRECTION, new Sort((Direction) null, "foo") + .iterator().next().getDirection()); + } + + + /** + * Asserts that the class rejects {@code null} as properties array. + * + * @throws Exception + */ + @Test(expected = IllegalArgumentException.class) + public void preventsNullProperties() throws Exception { + + new Sort(Direction.ASC, (String[]) null); + } + + + /** + * Asserts that the class rejects {@code null} values in the properties + * array. + * + * @throws Exception + */ + @Test(expected = IllegalArgumentException.class) + public void preventsNullProperty() throws Exception { + + new Sort(Direction.ASC, (String) null); + } + + + /** + * Asserts that the class rejects empty strings in the properties array. + * + * @throws Exception + */ + @Test(expected = IllegalArgumentException.class) + public void preventsEmptyProperty() throws Exception { + + new Sort(Direction.ASC, ""); + } + + + /** + * Asserts that the class rejects no properties given at all. + * + * @throws Exception + */ + @Test(expected = IllegalArgumentException.class) + public void preventsNoProperties() throws Exception { + + new Sort(Direction.ASC); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/domain/UnitTestUtils.java b/spring-data-commons-core/src/test/java/org/springframework/data/domain/UnitTestUtils.java new file mode 100644 index 000000000..752a20186 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/domain/UnitTestUtils.java @@ -0,0 +1,44 @@ +package org.springframework.data.domain; + +import static org.junit.Assert.*; + + +/** + * @author Oliver Gierke + */ +public abstract class UnitTestUtils { + + private UnitTestUtils() { + + } + + + /** + * Asserts that delivered objects both equal each other as well as return + * the same hash code. + * + * @param first + * @param second + */ + public static void assertEqualsAndHashcode(Object first, Object second) { + + assertEquals(first, second); + assertEquals(second, first); + assertEquals(first.hashCode(), second.hashCode()); + } + + + /** + * Asserts that both objects are not equal to each other and differ in hash + * code, too. + * + * @param first + * @param second + */ + public static void assertNotEqualsAndHashcode(Object first, Object second) { + + assertFalse(first.equals(second)); + assertFalse(second.equals(first)); + assertFalse(first.hashCode() == second.hashCode()); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/config/TypeFilterParserUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/config/TypeFilterParserUnitTests.java new file mode 100644 index 000000000..bc1c24603 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/config/TypeFilterParserUnitTests.java @@ -0,0 +1,108 @@ +/* + * 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.config; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +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.beans.factory.parsing.ReaderContext; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + + +/** + * Unit test for {@link TypeFilterParser}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class TypeFilterParserUnitTests { + + private TypeFilterParser parser; + private Element documentElement; + + @Mock + private ClassLoader classLoader; + + @Mock + private ReaderContext context; + + @Mock + private ClassPathScanningCandidateComponentProvider scanner; + + + @Before + public void setUp() throws SAXException, IOException, + ParserConfigurationException { + + parser = new TypeFilterParser(classLoader, context); + + Resource sampleXmlFile = + new ClassPathResource("type-filter-test.xml", + TypeFilterParserUnitTests.class); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + + documentElement = + factory.newDocumentBuilder() + .parse(sampleXmlFile.getInputStream()) + .getDocumentElement(); + } + + + @Test + public void parsesIncludesCorrectly() throws Exception { + + Element element = + DomUtils.getChildElementByTagName(documentElement, + "firstSample"); + + parser.parseFilters(element, scanner); + + verify(scanner, atLeastOnce()).addIncludeFilter( + isA(AssignableTypeFilter.class)); + } + + + @Test + public void parsesExcludesCorrectly() throws Exception { + + Element element = + DomUtils.getChildElementByTagName(documentElement, + "secondSample"); + + parser.parseFilters(element, scanner); + + verify(scanner, atLeastOnce()).addExcludeFilter( + isA(AssignableTypeFilter.class)); + } +} 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 new file mode 100644 index 000000000..e6da4358a --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java @@ -0,0 +1,167 @@ +/* + * 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 java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + + +/** + * Unit test for {@link Parameters}. + * + * @author Oliver Gierke + */ +public class ParametersUnitTests { + + private Method valid; + + + @Before + public void setUp() throws SecurityException, NoSuchMethodException { + + valid = SampleDao.class.getMethod("valid", String.class); + } + + + @Test + public void checksValidMethodCorrectly() throws Exception { + + Method validWithPageable = + SampleDao.class.getMethod("validWithPageable", String.class, + Pageable.class); + Method validWithSort = + SampleDao.class.getMethod("validWithSort", String.class, + Sort.class); + + new Parameters(valid); + new Parameters(validWithPageable); + new Parameters(validWithSort); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsInvalidMethodWithParamMissing() throws Exception { + + Method method = + SampleDao.class.getMethod("invalidParamMissing", String.class, + String.class); + new Parameters(method); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullMethod() throws Exception { + + new Parameters(null); + } + + + @Test + public void detectsNamedParameterCorrectly() throws Exception { + + Parameters parameters = + getParametersFor("validWithSort", String.class, Sort.class); + + Parameter parameter = parameters.getParameter(0); + + assertThat(parameter.isNamedParameter(), is(true)); + assertThat(parameter.getPlaceholder(), is(":username")); + + parameter = parameters.getParameter(1); + + assertThat(parameter.isNamedParameter(), is(false)); + assertThat(parameter.isSpecialParameter(), is(true)); + } + + + @Test + public void calculatesPlaceholderPositionCorrectly() throws Exception { + + Method method = + SampleDao.class.getMethod("validWithSortFirst", Sort.class, + String.class); + + Parameters parameters = new Parameters(method); + + assertThat(parameters.getParameter(0).getParameterPosition(), is(0)); + assertThat(parameters.getParameter(1).getParameterPosition(), is(1)); + + method = + SampleDao.class.getMethod("validWithSortInBetween", + String.class, Sort.class, String.class); + + 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)); + } + + + @Test + public void detectsEmptyParameterListCorrectly() throws Exception { + + Parameters parameters = getParametersFor("emptyParameters"); + assertThat(parameters.hasParameterAt(0), is(false)); + } + + + private Parameters getParametersFor(String methodName, + Class... parameterTypes) throws SecurityException, + NoSuchMethodException { + + Method method = SampleDao.class.getMethod(methodName, parameterTypes); + + return new Parameters(method); + } + + static class User { + + } + + static interface SampleDao { + + User valid(@Param("username") String username); + + + User invalidParamMissing(@Param("username") String username, + String lastname); + + + User validWithPageable(@Param("username") String username, + Pageable pageable); + + + User validWithSort(@Param("username") String username, Sort sort); + + + User validWithSortFirst(Sort sort, String username); + + + User validWithSortInBetween(String firstname, Sort sort, String lastname); + + + User emptyParameters(); + + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/OrderBySourceUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/OrderBySourceUnitTests.java new file mode 100644 index 000000000..122e0f1f2 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/OrderBySourceUnitTests.java @@ -0,0 +1,66 @@ +/* + * 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 static org.springframework.data.domain.Sort.Direction.*; + +import org.junit.Test; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + + +/** + * Unit test for {@link OrderBySource}. + * + * @author Oliver Gierke + */ +public class OrderBySourceUnitTests { + + @Test + public void handlesSingleDirectionAndPropertyCorrectly() throws Exception { + + assertThat(new OrderBySource("UsernameDesc").toSort(), is(new Sort( + DESC, "username"))); + } + + + @Test + public void handlesSingleDirectionAndMultiplePropertiesCorrectly() + throws Exception { + + assertThat(new OrderBySource("LastnameUsernameDesc").toSort(), + is(new Sort(DESC, "lastname", "username"))); + } + + + @Test + public void handlesMultipleDirectionsCorrectly() throws Exception { + + OrderBySource orderBySource = + new OrderBySource("LastnameAscUsernameDesc"); + assertThat(orderBySource.toSort(), is(new Sort(new Order(ASC, + "lastname"), new Order(DESC, "username")))); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsMissingProperty() throws Exception { + + new OrderBySource("Desc"); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java new file mode 100644 index 000000000..16fdc8dd7 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java @@ -0,0 +1,81 @@ +/* + * 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.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.domain.Persistable; + + +/** + * Unit test for {@link PersistableEntityInformation}. + * + * @author Oliver Gierke + */ +public class PersistableEntityInformationTests { + + @Test + public void detectsPersistableCorrectly() throws Exception { + + PersistableEntityInformation info = new PersistableEntityInformation(); + + assertNewAndNoId(info, new PersistableEntity(null)); + assertNotNewAndId(info, new PersistableEntity(1L), 1L); + } + + + private void assertNewAndNoId(T info, + Object entity) { + + assertThat(info.isNew(entity), is(true)); + assertThat(info.getId(entity), is(nullValue())); + } + + + private void assertNotNewAndId(T info, + Object entity, Object id) { + + assertThat(info.isNew(entity), is(false)); + assertThat(info.getId(entity), is(id)); + } + + static class PersistableEntity implements Persistable { + + private static final long serialVersionUID = -5898780128204716452L; + + private final Long id; + + + public PersistableEntity(Long id) { + + this.id = id; + } + + + public Long getId() { + + return id; + } + + + public boolean isNew() { + + return id == null; + } + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryInterfaceAwareBeanPostProcessorUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryInterfaceAwareBeanPostProcessorUnitTests.java new file mode 100644 index 000000000..0108deedb --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryInterfaceAwareBeanPostProcessorUnitTests.java @@ -0,0 +1,126 @@ +/* + * 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.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +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.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.data.repository.Repository; + + +/** + * Unit tests for {@link RepositoryInterfaceAwareBeanPostProcessor}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class RepositoryInterfaceAwareBeanPostProcessorUnitTests { + + private static final Class FACTORY_CLASS = + RepositoryFactorySupport.class; + private static final String BEAN_NAME = "foo"; + private static final String DAO_INTERFACE_PROPERTY = "daoInterface"; + + private RepositoryInterfaceAwareBeanPostProcessor processor; + + @Mock + private ConfigurableListableBeanFactory beanFactory; + private BeanDefinition beanDefinition; + + + @Before + public void setUp() { + + BeanDefinitionBuilder builder = + BeanDefinitionBuilder + .rootBeanDefinition(FACTORY_CLASS) + .addPropertyValue(DAO_INTERFACE_PROPERTY, UserDao.class); + this.beanDefinition = builder.getBeanDefinition(); + + when(beanFactory.getBeanDefinition(BEAN_NAME)).thenReturn( + beanDefinition); + + processor = new RepositoryInterfaceAwareBeanPostProcessor(); + + } + + + @Test + public void returnsDaoInterfaceClassForFactoryBean() throws Exception { + + processor.setBeanFactory(beanFactory); + assertEquals(UserDao.class, + processor.predictBeanType(FACTORY_CLASS, BEAN_NAME)); + } + + + @Test + public void doesNotResolveInterfaceForNonFactoryClasses() throws Exception { + + processor.setBeanFactory(beanFactory); + assertNotTypeDetected(BeanFactory.class); + } + + + @Test + public void doesNotResolveInterfaceForUnloadableClass() throws Exception { + + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.rootBeanDefinition(FACTORY_CLASS) + .addPropertyValue(DAO_INTERFACE_PROPERTY, + "com.acme.Foo"); + + when(beanFactory.getBeanDefinition(BEAN_NAME)).thenReturn( + builder.getBeanDefinition()); + + assertNotTypeDetected(FACTORY_CLASS); + } + + + @Test + public void doesNotResolveTypeForPlainBeanFactory() throws Exception { + + BeanFactory beanFactory = mock(BeanFactory.class); + processor.setBeanFactory(beanFactory); + + assertNotTypeDetected(FACTORY_CLASS); + } + + + private void assertNotTypeDetected(Class beanClass) { + + assertThat(processor.predictBeanType(beanClass, BEAN_NAME), + is(nullValue())); + } + + private class User { + + } + + private interface UserDao extends Repository { + + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/TransactionRepositoryProxyPostProcessorUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/TransactionRepositoryProxyPostProcessorUnitTests.java new file mode 100644 index 000000000..0d7d459c6 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/TransactionRepositoryProxyPostProcessorUnitTests.java @@ -0,0 +1,91 @@ +/* + * 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.support; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Map; + +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.aop.framework.ProxyFactory; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.dao.support.PersistenceExceptionTranslationInterceptor; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.transaction.interceptor.TransactionInterceptor; + + +/** + * Unit test for {@link TransactionalRepositoryProxyPostProcessor}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class TransactionRepositoryProxyPostProcessorUnitTests { + + TransactionalRepositoryProxyPostProcessor processor; + + @Mock + ListableBeanFactory beanFactory; + @Mock + ProxyFactory proxyFactory; + + + @Before + public void setUp() { + + Map beans = + new HashMap(); + beans.put("foo", mock(PersistenceExceptionTranslator.class)); + when( + beanFactory.getBeansOfType( + eq(PersistenceExceptionTranslator.class), anyBoolean(), + anyBoolean())).thenReturn(beans); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullBeanFactory() throws Exception { + + new TransactionalRepositoryProxyPostProcessor(null, "transactionManager"); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullTxManagerName() throws Exception { + + new TransactionalRepositoryProxyPostProcessor(beanFactory, null); + } + + + @Test + public void setsUpBasicInstance() throws Exception { + + RepositoryProxyPostProcessor postProcessor = + new TransactionalRepositoryProxyPostProcessor(beanFactory, "txManager"); + + postProcessor.postProcess(proxyFactory); + + verify(proxyFactory).addAdvice( + isA(PersistenceExceptionTranslationInterceptor.class)); + verify(proxyFactory).addAdvice(isA(TransactionInterceptor.class)); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java new file mode 100644 index 000000000..8f7c9989f --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java @@ -0,0 +1,211 @@ +/* + * 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.util; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.repository.util.ClassUtils.*; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.support.RepositorySupport; + + +/** + * Unit test for {@link ClassUtils}. + * + * @author Oliver Gierke + */ +public class ClassUtilsUnitTests { + + @Test + public void looksUpDomainClassCorrectly() throws Exception { + + assertEquals(User.class, getDomainClass(UserRepository.class)); + assertEquals(User.class, getDomainClass(SomeDao.class)); + assertNull(getDomainClass(Serializable.class)); + } + + + @Test + public void looksUpIdClassCorrectly() throws Exception { + + assertEquals(Integer.class, getIdClass(UserRepository.class)); + assertNull(getIdClass(Serializable.class)); + } + + + @Test(expected = IllegalStateException.class) + public void rejectsInvalidReturnType() throws Exception { + + assertReturnType(SomeDao.class.getMethod("findByFirstname", + Pageable.class, String.class), User.class); + } + + + @Test + public void findsDomainClassOnExtensionOfDaoInterface() throws Exception { + + assertEquals(User.class, + getDomainClass(ExtensionOfUserCustomExtendedDao.class)); + } + + + @Test + public void determinesValidFieldsCorrectly() { + + assertTrue(hasProperty(User.class, "firstname")); + assertTrue(hasProperty(User.class, "Firstname")); + assertFalse(hasProperty(User.class, "address")); + } + + + /** + * References #256. + */ + @Test + public void detectsParameterizedEntitiesCorrectly() { + + assertEquals(GenericEntity.class, + getDomainClass(GenericEntityDao.class)); + } + + + /** + * #301 + */ + @Test + public void discoversDaoBaseClassMethod() throws Exception { + + Method method = FooDao.class.getMethod("findById", Integer.class); + + Method reference = + getBaseClassMethodFor(method, + DummyGenericRepositorySupport.class, FooDao.class); + assertEquals(DummyGenericRepositorySupport.class, + reference.getDeclaringClass()); + assertThat(reference.getName(), is("findById")); + } + + + /** + * #301 + */ + @Test + public void discoveresNonDaoBaseClassMethod() throws Exception { + + Method method = FooDao.class.getMethod("readById", Long.class); + + assertThat( + getBaseClassMethodFor(method, RepositorySupport.class, + FooDao.class), is(method)); + } + + @SuppressWarnings("unused") + private class User { + + private String firstname; + + + public String getAddress() { + + return null; + } + } + + static interface UserRepository extends Repository { + + } + + /** + * Sample interface to serve two purposes: + *
    + *
  1. Check that {@link ClassUtils#getDomainClass(Class)} skips non + * {@link GenericDao} interfaces
  2. + *
  3. Check that {@link ClassUtils#getDomainClass(Class)} traverses + * interface hierarchy
  4. + *
+ * + * @author Oliver Gierke + */ + private interface SomeDao extends Serializable, UserRepository { + + Page findByFirstname(Pageable pageable, String firstname); + } + + /** + * Sample interface to test recursive lookup of domain class. + * + * @author Oliver Gierke + */ + static interface ExtensionOfUserCustomExtendedDao extends + UserCustomExtendedRepository { + + } + + static interface UserCustomExtendedRepository extends + Repository { + + } + + /** + * Helper class to reproduce #256. + * + * @author Oliver Gierke + */ + static class GenericEntity { + } + + static interface GenericEntityDao extends + Repository, Long> { + + } + + /** + * Sample DAO interface to test redeclaration of {@link GenericDao} methods. + * + * @author Oliver Gierke + */ + private static interface FooDao extends Repository { + + // Redeclared method + User findById(Integer primaryKey); + + + // Not a redeclared method + User readById(Long primaryKey); + } + + static abstract class DummyGenericRepositorySupport + extends RepositorySupport { + + public DummyGenericRepositorySupport(Class domainClass) { + + super(domainClass); + } + + + public T findById(ID id) { + + return null; + } + } +} diff --git a/spring-data-commons-core/src/test/resources/org/springframework/data/repository/config/type-filter-test.xml b/spring-data-commons-core/src/test/resources/org/springframework/data/repository/config/type-filter-test.xml new file mode 100644 index 000000000..6ef49f137 --- /dev/null +++ b/spring-data-commons-core/src/test/resources/org/springframework/data/repository/config/type-filter-test.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-data-commons-core/template.mf b/spring-data-commons-core/template.mf index 34dfc4514..af09e6b73 100644 --- a/spring-data-commons-core/template.mf +++ b/spring-data-commons-core/template.mf @@ -4,9 +4,11 @@ Bundle-Vendor: SpringSource Bundle-ManifestVersion: 2 Import-Package: sun.reflect;version="0";resolution:=optional -Import-Template: +Import-Template: + org.springframework.aop.*;version="[3.0.0, 4.0.0)", org.springframework.beans.*;version="[3.0.0, 4.0.0)", org.springframework.core.*;version="[3.0.0, 4.0.0)", + org.springframework.context.*;version="[3.0.0, 4.0.0)", org.springframework.dao.*;version="[3.0.0, 4.0.0)", org.springframework.util.*;version="[3.0.0, 4.0.0)", org.springframework.transaction.*;version="[3.0.0, 4.0.0)", @@ -14,6 +16,8 @@ Import-Template: org.springframework.data.persistence.*;version="[1.0.0, 2.0.0)", org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.joda.time.*;version="[1.6.0,2.0.0)", + org.slf4j.*;version="[1.5.0,1.6.0)", org.w3c.dom.*;version="0" diff --git a/spring-data-commons-parent/pom.xml b/spring-data-commons-parent/pom.xml index e5a58b360..8df5e8ec0 100644 --- a/spring-data-commons-parent/pom.xml +++ b/spring-data-commons-parent/pom.xml @@ -205,6 +205,13 @@ ${log4j.version} test + + + org.hamcrest + hamcrest-all + 1.1 + test +