diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntity.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntity.java index 712351514..a87e226be 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntity.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntity.java @@ -27,7 +27,6 @@ import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.data.cassandra.util.CassandraNamingUtils; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.AssociationHandler; -import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.mapping.model.MappingException; diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/DefaultCassandraPersistentEntityMetadataVerifier.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/DefaultCassandraPersistentEntityMetadataVerifier.java index 61f242002..97126abfb 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/DefaultCassandraPersistentEntityMetadataVerifier.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/DefaultCassandraPersistentEntityMetadataVerifier.java @@ -15,9 +15,13 @@ */ package org.springframework.data.cassandra.mapping; +import java.io.Serializable; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import org.springframework.cassandra.core.PrimaryKeyType; +import org.springframework.data.annotation.Persistent; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.MappingException; @@ -33,16 +37,38 @@ public class DefaultCassandraPersistentEntityMetadataVerifier implements Cassand @Override public void verify(CassandraPersistentEntity entity) throws MappingException { - boolean todo = true; - if (todo) { - return; - } + VerifierMappingExceptions exceptions = new VerifierMappingExceptions( + "Mapping Exceptions from DefaultCassandraPersistentEntityMetadataVerifier"); - // TODO - Determine total list. final List idProperties = new ArrayList(); final List compositePrimaryKeys = new ArrayList(); final List primaryKeyColumns = new ArrayList(); + /* + * Determine how this type is annotated + */ + Class thisType = entity.getType(); + + boolean isTable = (thisType.isAnnotationPresent(Table.class) || thisType.isAnnotationPresent(Persistent.class)); + boolean isPrimaryKeyClass = thisType.isAnnotationPresent(PrimaryKeyClass.class); + + /* + * Ensure that this is not both a @Table(@Persistent) and a @PrimaryKey + */ + if (isTable && isPrimaryKeyClass) { + exceptions.add(new MappingException("Entity cannot be of type Table and PrimaryKey")); + throw exceptions; + } + + /* + * Ensure that this is either a @Table(@Persistent) or a @PrimaryKey + */ + if (!isTable && !isPrimaryKeyClass) { + exceptions.add(new MappingException( + "Cassandra entities must have the @Table, @Persistent or @PrimaryKeyClass Annotation")); + throw exceptions; + } + /* * Parse the properties */ @@ -63,32 +89,132 @@ public class DefaultCassandraPersistentEntityMetadataVerifier implements Cassand }); /* - * Verify that the Primary Key annotations are correct + * Perform rules verification on PrimaryKeyClass */ - if (entity.isCompositePrimaryKey()) { + if (isPrimaryKeyClass) { + /* + * Must have at least 1 attribute annotated with @PrimaryKeyColumn + */ if (primaryKeyColumns.size() == 0) { - throw new MappingException(String.format("composite primary key type [%s] has no fields annotated with @%s", - entity.getType().getName(), PrimaryKeyColumn.class.getSimpleName())); + exceptions.add(new MappingException(String.format( + "composite primary key type [%s] has no fields annotated with @%s", entity.getType().getName(), + PrimaryKeyColumn.class.getSimpleName()))); } - // there can also be no @PrimaryKey or @Id fields that aren't composite primary keys themselves - for (CassandraPersistentProperty p : idProperties) { - if (!p.getType().isAnnotationPresent(PrimaryKeyClass.class)) { - throw new MappingException(String.format( - "composite primary key type [%s] property [%s] can only be a composite primary key type itself", entity - .getType().getName(), p.getName())); + /* + * At least one of the PrimaryKeyColumns must have a type PARTIONED + */ + boolean partitionKeyExists = false; + for (CassandraPersistentProperty p : primaryKeyColumns) { + if (p.getField().getAnnotation(PrimaryKeyColumn.class).type() == PrimaryKeyType.PARTITIONED) { + partitionKeyExists = true; + } + } + if (!partitionKeyExists) { + exceptions.add(new MappingException( + "At least on of the PrimaryKeyColumn annotation must have a type of PARTITIONED")); + } + + /* + * Cannot have any Id or PrimaryKey Annotations + */ + if (idProperties.size() > 0) { + exceptions.add(new MappingException( + "Annotations @Id and @PrimaryKey are invalid for type annoated as @PrimaryKeyClass")); + } + + /* + * Ensure that PrimaryKeyColumn is a supported Type. + */ + for (CassandraPersistentProperty p : primaryKeyColumns) { + if (CassandraSimpleTypeHolder.getDataTypeFor(p.getType()) == null) { + exceptions.add(new MappingException("Fields annotated with @PrimaryKeyColumn must be simple CassandraTypes")); } } - } else { + /* + * Ensure PrimaryKeyClass is Serializable + */ + Class[] interfaces = thisType.getInterfaces(); + boolean isTypeSerializable = false; + for (Class c : interfaces) { + if (c.equals(Serializable.class)) { + isTypeSerializable = true; + } + } + if (!isTypeSerializable) { + exceptions.add(new MappingException("@PrimaryKeyClass must be Serializable")); + } /* - * Not a Composite Primary Key so there must be at + * Ensure PrimaryKeyClass only extends Object */ + if (!thisType.getSuperclass().equals(Object.class)) { + exceptions.add(new MappingException("@PrimaryKeyClass must only extend Object")); + } - // TODO Index, potential verify against TableMetaData...DO NOT CREATE INDEX. + /* + * Ensure PrimaryKeyClass overrides "boolean equals(Object)" + */ + try { + Method equalsMethod = thisType.getDeclaredMethod("equals", Object.class); + if (equalsMethod == null || !equalsMethod.getDeclaringClass().equals(thisType)) { + throw new NoSuchMethodException(); + } + } catch (NoSuchMethodException e) { + exceptions.add(new MappingException("@PrimaryKeyClass must override 'boolean equals(Object)' method")); + } + + /* + * Ensure PrimaryKeyClass overrides "int hashCode()" + */ + try { + Method hashCodeMethod = thisType.getDeclaredMethod("hashCode", (Class[]) null); + if (hashCodeMethod == null || !hashCodeMethod.getDeclaringClass().equals(thisType)) { + throw new NoSuchMethodException(); + } + } catch (NoSuchMethodException e) { + exceptions.add(new MappingException("@PrimaryKeyClass must override 'int hashCode()' method")); + } + } + + /* + * Perform rules verification on Table/Persistent + */ + if (isTable) { + + /* + * Ensure at least 1 PK + */ + if (idProperties.size() == 0 && compositePrimaryKeys.size() == 0) { + exceptions.add(new MappingException("@Table/@Persistent types must have at least 1 @PrimaryKey attribute")); + } + + /* + * Ensure no more than 1 PK + */ + if (idProperties.size() + compositePrimaryKeys.size() > 1) { + exceptions.add(new MappingException("@Table/@Persistent must have only 1 @PrimaryKey attribute")); + } + + /* + * Ensure that Id is a supported Type. At the point there is only 1. + */ + if (idProperties.size() == 1) { + Class typeClass = idProperties.get(0).getType(); + if (!typeClass.isAnnotationPresent(PrimaryKeyClass.class) + && CassandraSimpleTypeHolder.getDataTypeFor(typeClass) == null) { + exceptions.add(new MappingException("Fields annotated with @PrimaryKey must be simple CassandraTypes")); + } + } + } + + /* + * Determine whether or not to throw Exception based on errors found + */ + if (exceptions.getErrorCount() > 0) { + throw exceptions; } } - } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/VerifierMappingExceptions.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/VerifierMappingExceptions.java new file mode 100644 index 000000000..51d5190a4 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/VerifierMappingExceptions.java @@ -0,0 +1,94 @@ +/* + * Copyright 2011-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.cassandra.mapping; + +import java.util.Collection; +import java.util.LinkedList; + +import org.springframework.data.mapping.model.MappingException; + +/** + * Aggregator of multiple {@link MappingException} for convenience when verifying persistent entities. This allows the + * framework to communicate all verification errors to the user of the framework, rather than one at a time. + * + * @author David Webb + * + */ +public class VerifierMappingExceptions extends MappingException { + + Collection messages = new LinkedList(); + Collection errors = new LinkedList(); + + /** + * @param s + */ + public VerifierMappingExceptions(String s) { + super(s); + } + + /** + * @param s + */ + public void add(MappingException e) { + messages.add(e.getMessage()); + errors.add(e); + } + + /** + * @param s + */ + public void add(String s, MappingException e) { + messages.add(s); + errors.add(e); + } + + /** + * Returns a list of the MappingExceptions aggregated within. + * + * @return The Collection of MappingException + */ + public Collection getMappingExceptions() { + return errors; + } + + /** + * Returns a list of the MappingException messages aggregated within. + * + * @return The Collection of Messages + */ + public Collection getMappingExceptionMessages() { + return messages; + } + + /** + * Returns the number of errors that have been added to this Exception Class. + * + * @return Number of Errors present + */ + public int getErrorCount() { + return errors.size(); + } + + @Override + public String getMessage() { + StringBuilder builder = new StringBuilder(); + for (String s : messages) { + builder.append(s).append("\n"); + } + return builder.toString(); + } + +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/BasicCassandraPersistentEntityVerifierTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/BasicCassandraPersistentEntityVerifierIntegrationTest.java similarity index 62% rename from spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/BasicCassandraPersistentEntityVerifierTest.java rename to spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/BasicCassandraPersistentEntityVerifierIntegrationTest.java index 00a6bbc9b..e7a02b5c3 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/BasicCassandraPersistentEntityVerifierTest.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/BasicCassandraPersistentEntityVerifierIntegrationTest.java @@ -15,6 +15,8 @@ */ package org.springframework.data.cassandra.test.integration.mapping; +import java.io.Serializable; + import org.junit.Before; import org.junit.Test; import org.springframework.cassandra.core.Ordering; @@ -26,12 +28,13 @@ import org.springframework.data.cassandra.mapping.PrimaryKey; import org.springframework.data.cassandra.mapping.PrimaryKeyClass; import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; import org.springframework.data.cassandra.mapping.Table; +import org.springframework.data.mapping.model.MappingException; /** * @author dwebb * */ -public class BasicCassandraPersistentEntityVerifierTest { +public class BasicCassandraPersistentEntityVerifierIntegrationTest { CassandraMappingContext mappingContext; @@ -42,6 +45,20 @@ public class BasicCassandraPersistentEntityVerifierTest { } + @Test(expected = MappingException.class) + public void testNonPersistentType() { + + mappingContext.getPersistentEntity(NonPersistentClass.class); + + } + + @Test(expected = MappingException.class) + public void testTooManyAnnotations() { + + mappingContext.getPersistentEntity(TooManyAnnotations.class); + + } + @Test public void testNonPrimaryKeyClass() { @@ -49,6 +66,13 @@ public class BasicCassandraPersistentEntityVerifierTest { } + @Test(expected = MappingException.class) + public void testPrimaryKeyClassNotFullyImplemented() { + + mappingContext.getPersistentEntity(AnimalPkNoOverrides.class); + + } + @Test public void testPrimaryKeyClass() { @@ -58,6 +82,16 @@ public class BasicCassandraPersistentEntityVerifierTest { } + static class NonPersistentClass { + + @Id + private String id; + + private String foo; + private String bar; + + } + @Table static class Person { @@ -79,7 +113,17 @@ public class BasicCassandraPersistentEntityVerifierTest { } @PrimaryKeyClass - static class AnimalPK { + static class AnimalPK implements Serializable { + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) private String species; @@ -92,4 +136,24 @@ public class BasicCassandraPersistentEntityVerifierTest { } + @PrimaryKeyClass + static class AnimalPkNoOverrides { + + @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) + private String species; + + @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED) + private String breed; + + @PrimaryKeyColumn(ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING) + private String color; + + } + + @Table + @PrimaryKeyClass + static class TooManyAnnotations { + + } + }