Created
November 15, 2019 21:28
-
-
Save MuellerConstantin/c0ba4d7f2cc1fe6e5b321e5f226afb73 to your computer and use it in GitHub Desktop.
Execution listener for loading initial data into MongoDB database
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.springframework.test.context.TestExecutionListeners; | |
import java.lang.annotation.Documented; | |
import java.lang.annotation.Inherited; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.Target; | |
import static java.lang.annotation.ElementType.METHOD; | |
import static java.lang.annotation.ElementType.TYPE; | |
import static java.lang.annotation.RetentionPolicy.RUNTIME; | |
/** | |
* Enables {@link MongoData @MongoData} related {@code TestExecutionListener} for supporting | |
* MongoDB database seeding. | |
* | |
* @author 0x1C1B | |
* @see MongoData | |
* @see MongoDataTestExecutionListener | |
*/ | |
@Documented | |
@Inherited | |
@Retention(RUNTIME) | |
@Target({TYPE, METHOD}) | |
@TestExecutionListeners( | |
value = MongoDataTestExecutionListener.class, | |
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS) | |
public @interface EnableMongoData { | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.annotation.*; | |
import static java.lang.annotation.ElementType.METHOD; | |
import static java.lang.annotation.ElementType.TYPE; | |
import static java.lang.annotation.RetentionPolicy.RUNTIME; | |
/** | |
* {@link MongoData @MongoData} is used to annotate a test class or test method to configure | |
* insertion of JSON based documents into a given database during integration tests. The actual | |
* insertion is performed by the {@link MongoDataTestExecutionListener}, which is | |
* <i>disabled</i> by default. | |
* | |
* <p> | |
* Each collection requires a separate JSON file and therefore an own {@link MongoData @MongoData} | |
* annotation for populating data. However it's possible to populate multiple documents of the | |
* same collection using a single one. Please note: Method-level declarations of the annotation | |
* overrides class-level declarations. | |
* </p> | |
* | |
* <p> | |
* Since the release of Java 8, {@code @MongoData} can be used as a | |
* <em>{@linkplain Repeatable repeatable}</em> annotation. Otherwise, | |
* {@link MongoDataGroup @MongoDataGroup} can be used as an explicit container for declaring | |
* multiple instances of {@code @MongoData}. | |
* </p> | |
* | |
* @author 0x1C1B | |
* @see MongoDataTestExecutionListener | |
* @see MongoDataGroup | |
*/ | |
@Documented | |
@Inherited | |
@Retention(RUNTIME) | |
@Target({TYPE, METHOD}) | |
@Repeatable(MongoDataGroup.class) | |
public @interface MongoData { | |
/** | |
* Path to the JSON file to insert. | |
* | |
* <p> | |
* The path will be interpreted as a string and relative to the classpath root, this means | |
* it is <b>not</b> possible to load external resources. A plain path | |
* — for example, {@code "data.json"} — will be treated as a | |
* classpath resource that is <em>relative</em> to the package in which the actual test class | |
* is defined. All other paths, respectively paths starting with a slash, will be treated as an | |
* <em>absolute</em> classpath resource, for example: {@code "/example/data.json"}. | |
* </p> | |
* | |
* @return Returns the JSON file path as string | |
*/ | |
String value(); | |
/** | |
* Collection's name to insert provided records. | |
* | |
* <p> | |
* It's assumed that a collection named like the given one already exists in the used MongoDB | |
* database instance. | |
* </p> | |
* | |
* @return Returns the collection's name | |
*/ | |
String collection(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.annotation.*; | |
import static java.lang.annotation.ElementType.METHOD; | |
import static java.lang.annotation.ElementType.TYPE; | |
import static java.lang.annotation.RetentionPolicy.RUNTIME; | |
/** | |
* Container annotation that aggregates multiple {@link MongoData @MongoData} annotations. | |
* | |
* <p> | |
* Usage required for Java 7 and below without support for | |
* <em>{@linkplain Repeatable repeatable}</em> annotation. For higher versions | |
* this annotation can also be used in conjunction with Java 8's support for repeatable ones, | |
* where {@link MongoData @MongoData} can simply be declared several times. | |
* </p> | |
* | |
* @author 0x1C1B | |
* @see MongoData | |
*/ | |
@Documented | |
@Inherited | |
@Retention(RUNTIME) | |
@Target({TYPE, METHOD}) | |
public @interface MongoDataGroup { | |
MongoData[] value(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.bson.BsonArray; | |
import org.bson.BsonDocument; | |
import org.bson.Document; | |
import org.bson.codecs.BsonArrayCodec; | |
import org.bson.codecs.BsonDocumentCodec; | |
import org.bson.codecs.DecoderContext; | |
import org.bson.json.JsonParseException; | |
import org.bson.json.JsonReader; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.data.mongodb.core.MongoOperations; | |
import org.springframework.test.context.TestContext; | |
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; | |
import java.io.File; | |
import java.io.FileReader; | |
import java.util.Arrays; | |
import java.util.List; | |
import java.util.stream.Collectors; | |
/** | |
* {@code TestExecutionListener} that provides support for inserting JSON | |
* {@link MongoData#value() files} configured via the {@link MongoData @MongoData} annotation. | |
* | |
* <p> | |
* Each time an annotated method is called, the JSON data specified using | |
* {@link MongoData @MongoData} is inserted before by this class. If an method is called | |
* without {@link MongoData @MongoData} annotation, the implementation will try to fetch it | |
* from the class level. If the annotation is also missing at class level, the execution | |
* listener is skipped. | |
* </p> | |
* | |
* @author 0x1C1B | |
* @see MongoData | |
*/ | |
public class MongoDataTestExecutionListener extends DependencyInjectionTestExecutionListener { | |
private static final Logger log = LoggerFactory.getLogger(MongoDataTestExecutionListener.class); | |
private MongoOperations mongoOperations; | |
@Override | |
protected void injectDependencies(TestContext testContext) { | |
mongoOperations = testContext.getApplicationContext().getBean(MongoOperations.class); | |
} | |
@Override | |
public void beforeTestMethod(TestContext testContext) { | |
insertMongoData(testContext); | |
} | |
private void insertMongoData(TestContext testContext) { | |
MongoData[] annotations = testContext.getTestMethod().getAnnotationsByType(MongoData.class); | |
if (0 == annotations.length) { | |
annotations = testContext.getTestClass().getAnnotationsByType(MongoData.class); | |
if (0 == annotations.length) return; | |
} | |
Arrays.stream(annotations).forEach(annotation -> insertMongoData(annotation, testContext)); | |
} | |
private void insertMongoData(MongoData annotation, TestContext testContext) { | |
try { | |
File file = new File(testContext.getTestClass().getResource(annotation.value()).toURI()); | |
try (JsonReader reader = new JsonReader(new FileReader(file))) { | |
switch (reader.readBsonType()) { | |
case ARRAY: { | |
BsonArrayCodec arrayCodec = new BsonArrayCodec(); | |
BsonArray bsonArray = arrayCodec.decode(reader, DecoderContext.builder().build()); | |
List<Document> documents = bsonArray.stream() | |
.map(bsonValue -> Document.parse(bsonValue.asDocument().toJson())) | |
.collect(Collectors.toList()); | |
mongoOperations.getCollection(annotation.collection()).insertMany(documents); | |
break; | |
} | |
case DOCUMENT: { | |
BsonDocumentCodec documentCodec = new BsonDocumentCodec(); | |
BsonDocument bsonDocument = documentCodec.decode(reader, DecoderContext.builder().build()); | |
Document document = Document.parse(bsonDocument.toJson()); | |
mongoOperations.getCollection(annotation.collection()).insertOne(document); | |
break; | |
} | |
default: { | |
throw new JsonParseException("Root element must be either DOCUMENT or ARRAY"); | |
} | |
} | |
} | |
log.info(String.format("Populated JSON source '%s' for collection '%s' successfully", | |
annotation.value(), annotation.collection())); | |
} catch (Exception exc) { | |
log.error(String.format("Populating JSON source '%s' for collection '%s' failed", | |
annotation.value(), annotation.collection()), exc); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment