From 1dbe3b62d74780991de57da334cd04871bd05e2c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 23 Jun 2015 16:39:54 +0200 Subject: [PATCH] DATAMONGO-1125 - Improve exception message for index creation errors. We now use MongoExceptionTranslator to potentially convert exceptions during index creation into Springs DataAccessException hierarchy. In case we encounter an error code indicating DataIntegrityViolation we try to fetch existing index data and append it to the exceptions message. Original pull request: #302. --- .../core/MongoExceptionTranslator.java | 133 +++++++++++++++++- .../MongoPersistentEntityIndexCreator.java | 61 +++++++- ...entEntityIndexCreatorIntegrationTests.java | 36 +++++ ...PersistentEntityIndexCreatorUnitTests.java | 36 ++++- 4 files changed, 259 insertions(+), 7 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java index 9be051e30..737655e11 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.Set; @@ -25,6 +26,7 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.dao.PermissionDeniedDataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.util.ClassUtils; @@ -86,12 +88,15 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator int code = ((MongoException) ex).getCode(); - if (code == 11000 || code == 11001) { + if (MongoDbErrorCodes.isDuplicateKeyCode(code)) { throw new DuplicateKeyException(ex.getMessage(), ex); - } else if (code == 12000 || code == 13440) { + } else if (MongoDbErrorCodes.isDataAccessResourceFailureCode(code)) { throw new DataAccessResourceFailureException(ex.getMessage(), ex); - } else if (code == 10003 || code == 12001 || code == 12010 || code == 12011 || code == 12012) { + } else if (MongoDbErrorCodes.isInvalidDataAccessApiUsageCode(code) || code == 10003 || code == 12001 + || code == 12010 || code == 12011 || code == 12012) { throw new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) { + throw new PermissionDeniedDataAccessException(ex.getMessage(), ex); } return new UncategorizedMongoDbException(ex.getMessage(), ex); } @@ -101,4 +106,126 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator // that translation should not occur. return null; } + + /** + * {@link MongoDbErrorCodes} holds MongoDB specific error codes outlined in {@literal mongo/base/error_codes.err}. + * + * @author Christoph Strobl + * @since 1.8 + */ + public static final class MongoDbErrorCodes { + + static HashMap dataAccessResourceFailureCodes; + static HashMap dataIntegrityViolationCodes; + static HashMap duplicateKeyCodes; + static HashMap invalidDataAccessApiUsageExeption; + static HashMap permissionDeniedCodes; + + static HashMap errorCodes; + + static { + + dataAccessResourceFailureCodes = new HashMap(10); + dataAccessResourceFailureCodes.put(6, "HostUnreachable"); + dataAccessResourceFailureCodes.put(7, "HostNotFound"); + dataAccessResourceFailureCodes.put(89, "NetworkTimeout"); + dataAccessResourceFailureCodes.put(91, "ShutdownInProgress"); + dataAccessResourceFailureCodes.put(12000, "SlaveDelayDifferential"); + dataAccessResourceFailureCodes.put(10084, "CannotFindMapFile64Bit"); + dataAccessResourceFailureCodes.put(10085, "CannotFindMapFile"); + dataAccessResourceFailureCodes.put(10357, "ShutdownInProgress"); + dataAccessResourceFailureCodes.put(10359, "Header==0"); + dataAccessResourceFailureCodes.put(13440, "BadOffsetInFile"); + dataAccessResourceFailureCodes.put(13441, "BadOffsetInFile"); + dataAccessResourceFailureCodes.put(13640, "DataFileHeaderCorrupt"); + + dataIntegrityViolationCodes = new HashMap(6); + dataIntegrityViolationCodes.put(67, "CannotCreateIndex"); + dataIntegrityViolationCodes.put(68, "IndexAlreadyExists"); + dataIntegrityViolationCodes.put(85, "IndexOptionsConflict"); + dataIntegrityViolationCodes.put(86, "IndexKeySpecsConflict"); + dataIntegrityViolationCodes.put(112, "WriteConflict"); + dataIntegrityViolationCodes.put(117, "ConflictingOperationInProgress"); + + duplicateKeyCodes = new HashMap(3); + duplicateKeyCodes.put(3, "OBSOLETE_DuplicateKey"); + duplicateKeyCodes.put(84, "DuplicateKeyValue"); + duplicateKeyCodes.put(11000, "DuplicateKey"); + duplicateKeyCodes.put(11001, "DuplicateKey"); + + invalidDataAccessApiUsageExeption = new HashMap(); + invalidDataAccessApiUsageExeption.put(5, "GraphContainsCycle"); + invalidDataAccessApiUsageExeption.put(9, "FailedToParse"); + invalidDataAccessApiUsageExeption.put(14, "TypeMismatch"); + invalidDataAccessApiUsageExeption.put(15, "Overflow"); + invalidDataAccessApiUsageExeption.put(16, "InvalidLength"); + invalidDataAccessApiUsageExeption.put(20, "IllegalOperation"); + invalidDataAccessApiUsageExeption.put(21, "EmptyArrayOperation"); + invalidDataAccessApiUsageExeption.put(22, "InvalidBSON"); + invalidDataAccessApiUsageExeption.put(23, "AlreadyInitialized"); + invalidDataAccessApiUsageExeption.put(29, "NonExistentPath"); + invalidDataAccessApiUsageExeption.put(30, "InvalidPath"); + invalidDataAccessApiUsageExeption.put(40, "ConflictingUpdateOperators"); + invalidDataAccessApiUsageExeption.put(45, "UserDataInconsistent"); + invalidDataAccessApiUsageExeption.put(30, "DollarPrefixedFieldName"); + invalidDataAccessApiUsageExeption.put(52, "InvalidPath"); + invalidDataAccessApiUsageExeption.put(53, "InvalidIdField"); + invalidDataAccessApiUsageExeption.put(54, "NotSingleValueField"); + invalidDataAccessApiUsageExeption.put(55, "InvalidDBRef"); + invalidDataAccessApiUsageExeption.put(56, "EmptyFieldName"); + invalidDataAccessApiUsageExeption.put(57, "DottedFieldName"); + invalidDataAccessApiUsageExeption.put(59, "CommandNotFound"); + invalidDataAccessApiUsageExeption.put(60, "DatabaseNotFound"); + invalidDataAccessApiUsageExeption.put(61, "ShardKeyNotFound"); + invalidDataAccessApiUsageExeption.put(62, "OplogOperationUnsupported"); + invalidDataAccessApiUsageExeption.put(66, "ImmutableField"); + invalidDataAccessApiUsageExeption.put(72, "InvalidOptions"); + invalidDataAccessApiUsageExeption.put(115, "CommandNotSupported"); + invalidDataAccessApiUsageExeption.put(116, "DocTooLargeForCapped"); + invalidDataAccessApiUsageExeption.put(130, "SymbolNotFound"); + invalidDataAccessApiUsageExeption.put(17280, "KeyTooLong"); + invalidDataAccessApiUsageExeption.put(13334, "ShardKeyTooBig"); + + permissionDeniedCodes = new HashMap(); + permissionDeniedCodes.put(11, "UserNotFound"); + permissionDeniedCodes.put(18, "AuthenticationFailed"); + permissionDeniedCodes.put(31, "RoleNotFound"); + permissionDeniedCodes.put(32, "RolesNotRelated"); + permissionDeniedCodes.put(33, "PrvilegeNotFound"); + permissionDeniedCodes.put(15847, "CannotAuthenticate"); + permissionDeniedCodes.put(16704, "CannotAuthenticateToAdminDB"); + permissionDeniedCodes.put(16705, "CannotAuthenticateToAdminDB"); + + errorCodes = new HashMap(); + errorCodes.putAll(dataAccessResourceFailureCodes); + errorCodes.putAll(dataIntegrityViolationCodes); + errorCodes.putAll(duplicateKeyCodes); + errorCodes.putAll(invalidDataAccessApiUsageExeption); + errorCodes.putAll(permissionDeniedCodes); + } + + public static boolean isDataIntegrityViolationCode(Integer errorCode) { + return errorCode == null ? false : dataIntegrityViolationCodes.containsKey(errorCode); + } + + public static boolean isDataAccessResourceFailureCode(Integer errorCode) { + return errorCode == null ? false : dataAccessResourceFailureCodes.containsKey(errorCode); + } + + public static boolean isDuplicateKeyCode(Integer errorCode) { + return errorCode == null ? false : duplicateKeyCodes.containsKey(errorCode); + } + + public static boolean isPermissionDeniedCode(Integer errorCode) { + return errorCode == null ? false : permissionDeniedCodes.containsKey(errorCode); + } + + public static boolean isInvalidDataAccessApiUsageCode(Integer errorCode) { + return errorCode == null ? false : invalidDataAccessApiUsageExeption.containsKey(errorCode); + } + + public static String getErrorDescription(Integer errorCode) { + return errorCode == null ? null : errorCodes.get(errorCode); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java index 1b0fbfb61..11cc7db9b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java @@ -21,15 +21,21 @@ import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContextEvent; import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.MongoExceptionTranslator.MongoDbErrorCodes; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import com.mongodb.DBObject; +import com.mongodb.MongoException; /** * Component that inspects {@link MongoPersistentEntity} instances contained in the given {@link MongoMappingContext} @@ -129,9 +135,34 @@ public class MongoPersistentEntityIndexCreator implements ApplicationListener context) { return this.mappingContext.equals(context); } + + private DBObject fetchIndexInformation(IndexDefinitionHolder indexDefinition) { + + if (indexDefinition == null) { + return null; + } + + try { + + Object indexNameToLookUp = indexDefinition.getIndexOptions().get("name"); + + for (DBObject index : mongoDbFactory.getDb().getCollection(indexDefinition.getCollection()).getIndexInfo()) { + if (ObjectUtils.nullSafeEquals(indexNameToLookUp, index.get("name"))) { + return index; + } + } + + } catch (Exception e) { + LOGGER.debug( + String.format("Failed to load index information for collection '%s'.", indexDefinition.getCollection()), e); + } + + return null; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorIntegrationTests.java index 0d6fc90fe..529828fe4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorIntegrationTests.java @@ -18,25 +18,37 @@ package org.springframework.data.mongodb.core.index; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import java.net.UnknownHostException; import java.util.Arrays; import java.util.List; import org.hamcrest.Matchers; +import org.hamcrest.core.IsInstanceOf; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.rules.RuleChain; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.SimpleMongoDbFactory; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.test.util.CleanMongoDB; import org.springframework.data.mongodb.test.util.MongoVersionRule; import org.springframework.data.util.Version; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import com.mongodb.MongoClient; +import com.mongodb.MongoCommandException; + /** * Integration tests for {@link MongoPersistentEntityIndexCreator}. * @@ -54,6 +66,8 @@ public class MongoPersistentEntityIndexCreatorIntegrationTests { public static @ClassRule RuleChain rules = RuleChain.outerRule(MongoVersionRule.atLeast(new Version(2, 6))).around( CleanMongoDB.indexes(Arrays.asList(SAMPLE_TYPE_COLLECTION_NAME, RECURSIVE_TYPE_COLLECTION_NAME))); + public @Rule ExpectedException expectedException = ExpectedException.none(); + @Autowired @Qualifier("mongo1") MongoOperations templateOne; @Autowired @Qualifier("mongo2") MongoOperations templateTwo; @@ -81,6 +95,28 @@ public class MongoPersistentEntityIndexCreatorIntegrationTests { assertThat(indexInfo, Matchers. hasItem(hasProperty("name", is("firstName")))); } + /** + * @DATAMONGO-1125 + */ + @Test + public void createIndexShouldThrowMeaningfulExceptionWhenIndexCreationFails() throws UnknownHostException { + + expectedException.expect(DataIntegrityViolationException.class); + expectedException.expectMessage("collection 'datamongo-1125'"); + expectedException.expectMessage("dalinar.kohlin"); + expectedException.expectMessage("lastname"); + expectedException.expectCause(IsInstanceOf. instanceOf(MongoCommandException.class)); + + MongoPersistentEntityIndexCreator indexCreator = new MongoPersistentEntityIndexCreator(new MongoMappingContext(), + new SimpleMongoDbFactory(new MongoClient(), "issue")); + + indexCreator.createIndex(new IndexDefinitionHolder("dalinar.kohlin", new Index().named("stormlight") + .on("lastname", Direction.ASC).unique(), "datamongo-1125")); + + indexCreator.createIndex(new IndexDefinitionHolder("dalinar.kohlin", new Index().named("stormlight") + .on("lastname", Direction.ASC).sparse(), "datamongo-1125")); + } + @Document(collection = RECURSIVE_TYPE_COLLECTION_NAME) static abstract class RecursiveGenericType> { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java index 815dd775d..093286aab 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 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. @@ -28,11 +28,14 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.context.ApplicationContext; +import org.springframework.dao.DataAccessException; import org.springframework.data.geo.Point; import org.springframework.data.mapping.context.MappingContextEvent; import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.MongoExceptionTranslator; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -43,6 +46,7 @@ import com.mongodb.BasicDBObjectBuilder; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBObject; +import com.mongodb.MongoException; /** * Unit tests for {@link MongoPersistentEntityIndexCreator}. @@ -211,6 +215,36 @@ public class MongoPersistentEntityIndexCreatorUnitTests { assertThat(collectionNameCapturer.getValue(), equalTo("indexedDocumentWrapper")); } + /** + * @see DATAMONGO-1125 + */ + @Test(expected = DataAccessException.class) + public void createIndexShouldUsePersistenceExceptionTranslatorForNonDataIntegrityConcerns() { + + when(factory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator()); + doThrow(new MongoException(6, "HostUnreachable")).when(collection).createIndex(Mockito.any(DBObject.class), + Mockito.any(DBObject.class)); + + MongoMappingContext mappingContext = prepareMappingContext(Person.class); + + new MongoPersistentEntityIndexCreator(mappingContext, factory); + } + + /** + * @see DATAMONGO-1125 + */ + @Test(expected = ClassCastException.class) + public void createIndexShouldNotConvertUnknownExceptionTypes() { + + when(factory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator()); + doThrow(new ClassCastException("o_O")).when(collection).createIndex(Mockito.any(DBObject.class), + Mockito.any(DBObject.class)); + + MongoMappingContext mappingContext = prepareMappingContext(Person.class); + + new MongoPersistentEntityIndexCreator(mappingContext, factory); + } + private static MongoMappingContext prepareMappingContext(Class type) { MongoMappingContext mappingContext = new MongoMappingContext();