Merge branch 'pr-197'
diff --git a/gora-redis/pom.xml b/gora-redis/pom.xml
new file mode 100755
index 0000000..ee33cb1
--- /dev/null
+++ b/gora-redis/pom.xml
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.gora</groupId>
+ <artifactId>gora</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <relativePath>../</relativePath>
+ </parent>
+ <artifactId>gora-redis</artifactId>
+ <packaging>bundle</packaging>
+
+ <name>Apache Gora :: Redis</name>
+ <url>http://gora.apache.org</url>
+ <description>The Apache Gora open source framework provides an in-memory data model and
+ persistence for big data. Gora supports persisting to column stores, key value stores,
+ document stores and RDBMSs, and analyzing the data with extensive Apache Hadoop MapReduce
+ support.</description>
+ <inceptionYear>2010</inceptionYear>
+ <organization>
+ <name>The Apache Software Foundation</name>
+ <url>http://www.apache.org/</url>
+ </organization>
+ <issueManagement>
+ <system>JIRA</system>
+ <url>https://issues.apache.org/jira/browse/GORA</url>
+ </issueManagement>
+ <ciManagement>
+ <system>Jenkins</system>
+ <url>https://builds.apache.org/job/Gora-trunk/</url>
+ </ciManagement>
+
+ <properties>
+ <osgi.import>*</osgi.import>
+ <osgi.export>org.apache.gora.redis*;version="${project.version}";-noimport:=true</osgi.export>
+ </properties>
+
+ <profiles>
+ <profile>
+ <id>redis-with-test</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.4.2</version>
+ <configuration>
+ <skipTests>false</skipTests>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
+ <build>
+ <directory>target</directory>
+ <outputDirectory>target/classes</outputDirectory>
+ <finalName>${project.artifactId}-${project.version}</finalName>
+ <testOutputDirectory>target/test-classes</testOutputDirectory>
+ <testSourceDirectory>src/test/java</testSourceDirectory>
+ <sourceDirectory>src/main/java</sourceDirectory>
+ <testResources>
+ <testResource>
+ <directory>${project.basedir}/src/test/resources</directory>
+ <includes>
+ <include>**/*</include>
+ </includes>
+ <!--targetPath>${project.basedir}/target/classes/</targetPath -->
+ </testResource>
+ </testResources>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.4.2</version>
+ <configuration>
+ <skipTests>true</skipTests>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <version>${build-helper-maven-plugin.version}</version>
+ <executions>
+ <execution>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>add-source</goal>
+ </goals>
+ <configuration>
+ <sources>
+ <source>src/examples/java</source>
+ </sources>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <!-- Gora Internal Dependencies -->
+ <dependency>
+ <groupId>org.apache.gora</groupId>
+ <artifactId>gora-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.gora</groupId>
+ <artifactId>gora-core</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <!--Redis Dependency -->
+ <dependency>
+ <groupId>org.redisson</groupId>
+ <artifactId>redisson-all</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-all</artifactId>
+ <version>4.1.36.Final</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.zookeeper</groupId>
+ <artifactId>zookeeper</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.avro</groupId>
+ <artifactId>avro</artifactId>
+ </dependency>
+
+ <!-- Logging Dependencies -->
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>javax.jms</groupId>
+ <artifactId>jms</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- Testing Dependencies -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-minicluster</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/package-info.java b/gora-redis/src/main/java/org/apache/gora/redis/package-info.java
new file mode 100755
index 0000000..d8ccc55
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/package-info.java
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/**
+ * This package contains Redis datastore related all classes.
+ */
+package org.apache.gora.redis;
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/query/RedisQuery.java b/gora-redis/src/main/java/org/apache/gora/redis/query/RedisQuery.java
new file mode 100755
index 0000000..5a8d2eb
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/query/RedisQuery.java
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.query;
+
+import org.apache.gora.persistency.impl.PersistentBase;
+import org.apache.gora.query.impl.QueryBase;
+import org.apache.gora.store.DataStore;
+
+/**
+ * Redis specific implementation of the {@link org.apache.gora.query.Query}
+ * interface.
+ */
+public class RedisQuery<K, T extends PersistentBase> extends QueryBase<K, T> {
+
+ /**
+ * Constructor for the query
+ */
+ public RedisQuery() {
+ super(null);
+ }
+
+ /**
+ * Constructor for the query
+ *
+ * @param dataStore Data store used
+ *
+ */
+ public RedisQuery(DataStore<K, T> dataStore) {
+ super(dataStore);
+ }
+
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/query/RedisResult.java b/gora-redis/src/main/java/org/apache/gora/redis/query/RedisResult.java
new file mode 100755
index 0000000..5c9656f
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/query/RedisResult.java
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.query;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Iterator;
+import org.apache.gora.persistency.impl.PersistentBase;
+import org.apache.gora.query.Query;
+import org.apache.gora.query.impl.ResultBase;
+import org.apache.gora.redis.store.RedisStore;
+import org.apache.gora.store.DataStore;
+
+/**
+ * Redis specific implementation of the {@link org.apache.gora.query.Result}
+ * interface.
+ */
+public class RedisResult<K, T extends PersistentBase> extends ResultBase<K, T> {
+
+ private Iterator<K> range;
+ private final int size;
+
+ /**
+ * Constructor of RedisResult
+ *
+ * @param dataStore Query's data store
+ * @param query Query
+ * @param idsRange Collection of found keys
+ */
+ public RedisResult(DataStore<K, T> dataStore, Query<K, T> query, Collection<K> idsRange) {
+ super(dataStore, query);
+ this.size = idsRange.size();
+ this.range = idsRange.iterator();
+ }
+
+ /**
+ * Gets the items reading progress
+ *
+ * @return a float value representing progress of the job
+ * @throws java.io.IOException if there is an error obtaining progress
+ */
+ @Override
+ public float getProgress() throws IOException {
+ if (this.limit != -1) {
+ return (float) this.offset / (float) this.limit;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Gets the next item
+ *
+ * @return true if another result exists
+ * @throws java.io.IOException if for some reason we reach a result which does
+ * not exist
+ */
+ @Override
+ protected boolean nextInner() throws IOException {
+ if (range == null) {
+ return false;
+ }
+ boolean next = range.hasNext();
+ if (next) {
+ key = (K) range.next();
+ persistent = ((RedisStore<K, T>) getDataStore()).get(key, query.getFields());
+ }
+
+ return next;
+ }
+
+ @Override
+ public int size() {
+ return this.size;
+ }
+
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/query/package-info.java b/gora-redis/src/main/java/org/apache/gora/redis/query/package-info.java
new file mode 100755
index 0000000..9270674
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/query/package-info.java
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/**
+ * This package contains all the Redis store query representation class as well as Result set representing class
+ * when query is executed over the Redis dataStore.
+ */
+package org.apache.gora.redis.query;
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/store/RedisMapping.java b/gora-redis/src/main/java/org/apache/gora/redis/store/RedisMapping.java
new file mode 100755
index 0000000..09be3d8
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/store/RedisMapping.java
@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.store;
+
+import java.util.Map;
+
+/**
+ * Mapping definitions for Redis.
+ */
+public class RedisMapping {
+
+ private int database;
+ private String prefix;
+ private Map<String, String> fields;
+ private Map<String, RedisType> types;
+
+ /**
+ * Gets database number
+ *
+ * @return database number
+ */
+ public int getDatabase() {
+ return database;
+ }
+
+ /**
+ * Sets database number
+ *
+ * @param datebase database number
+ */
+ public void setDatabase(int datebase) {
+ this.database = datebase;
+ }
+
+ /**
+ * Gets key prefix
+ *
+ * @return prefix
+ */
+ public String getPrefix() {
+ return prefix;
+ }
+
+ /**
+ * Sets key prefix
+ *
+ * @param prefix String prefix for the creation of redis keys.
+ */
+ public void setPrefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+ /**
+ * Gets mapped fields
+ *
+ * @return mapped fields
+ */
+ public Map<String, String> getFields() {
+ return fields;
+ }
+
+ /**
+ * Sets mapped fields
+ *
+ * @param fields mapped fields
+ */
+ public void setFields(Map<String, String> fields) {
+ this.fields = fields;
+ }
+
+ /**
+ * Gets types mapping
+ *
+ * @return Types mapping
+ */
+ public Map<String, RedisType> getTypes() {
+ return types;
+ }
+
+ /**
+ * Sets types mapping
+ *
+ * @param types Types mapping
+ */
+ public void setTypes(Map<String, RedisType> types) {
+ this.types = types;
+ }
+
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/store/RedisMappingBuilder.java b/gora-redis/src/main/java/org/apache/gora/redis/store/RedisMappingBuilder.java
new file mode 100755
index 0000000..f2e10c1
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/store/RedisMappingBuilder.java
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.gora.redis.store;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.apache.gora.persistency.impl.PersistentBase;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+/**
+ * Mapping builder for Redis
+ */
+public class RedisMappingBuilder<K, T extends PersistentBase> {
+
+ private final RedisStore<K, T> dataStore;
+
+ public RedisMappingBuilder(RedisStore<K, T> dataStore) {
+ this.dataStore = dataStore;
+ }
+
+ public RedisMapping readMapping(InputStream inputStream) throws IOException {
+ try {
+ RedisMapping redisMapping = new RedisMapping();
+ DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ Document dom = db.parse(inputStream);
+ Element root = dom.getDocumentElement();
+ NodeList classesNodes = root.getElementsByTagName("class");
+ for (int indexClasses = 0; indexClasses < classesNodes.getLength(); indexClasses++) {
+ Element classElement = (Element) classesNodes.item(indexClasses);
+ if (classElement.getAttribute("keyClass").equals(dataStore.getKeyClass().getCanonicalName())
+ && classElement.getAttribute("name").equals(dataStore.getPersistentClass().getCanonicalName())) {
+ redisMapping.setDatabase(Integer.parseInt(classElement.getAttribute("database")));
+ redisMapping.setPrefix(classElement.getAttribute("prefix"));
+ NodeList elementsByTagName = classElement.getElementsByTagName("field");
+ Map<String, String> mapFields = new HashMap<>();
+ Map<String, RedisType> mapTypes = new HashMap<>();
+ for (int indexFields = 0; indexFields < elementsByTagName.getLength(); indexFields++) {
+ Element item = (Element) elementsByTagName.item(indexFields);
+ String name = item.getAttribute("name");
+ String column = item.getAttribute("column");
+ String type = item.getAttribute("type");
+ mapFields.put(name, column);
+ mapTypes.put(name, RedisType.valueOf(type));
+ }
+ redisMapping.setTypes(mapTypes);
+ redisMapping.setFields(mapFields);
+ }
+ }
+ return redisMapping;
+ } catch (Exception ex) {
+ throw new IOException(ex);
+ }
+ }
+
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/store/RedisStore.java b/gora-redis/src/main/java/org/apache/gora/redis/store/RedisStore.java
new file mode 100755
index 0000000..14334e5
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/store/RedisStore.java
@@ -0,0 +1,540 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.store;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.invoke.MethodHandles;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import org.apache.avro.Schema;
+import org.apache.commons.io.IOUtils;
+import org.apache.gora.persistency.impl.PersistentBase;
+import org.apache.gora.query.PartitionQuery;
+import org.apache.gora.query.Query;
+import org.apache.gora.query.Result;
+import org.apache.gora.query.impl.PartitionQueryImpl;
+import org.apache.gora.redis.query.RedisQuery;
+import org.apache.gora.redis.query.RedisResult;
+import org.apache.gora.redis.util.DatumHandler;
+import static org.apache.gora.redis.util.RedisStoreConstants.PREFIX;
+import static org.apache.gora.redis.util.RedisStoreConstants.END_TAG;
+import static org.apache.gora.redis.util.RedisStoreConstants.FIELD_SEPARATOR;
+import static org.apache.gora.redis.util.RedisStoreConstants.GORA_REDIS_ADDRESS;
+import static org.apache.gora.redis.util.RedisStoreConstants.GORA_REDIS_MASTERNAME;
+import static org.apache.gora.redis.util.RedisStoreConstants.GORA_REDIS_MODE;
+import static org.apache.gora.redis.util.RedisStoreConstants.GORA_REDIS_READMODE;
+import static org.apache.gora.redis.util.RedisStoreConstants.GORA_REDIS_STORAGE;
+import static org.apache.gora.redis.util.RedisStoreConstants.INDEX;
+import static org.apache.gora.redis.util.RedisStoreConstants.START_TAG;
+import static org.apache.gora.redis.util.RedisStoreConstants.WILDCARD;
+import org.apache.gora.redis.util.ServerMode;
+import org.apache.gora.redis.util.StorageMode;
+import org.apache.gora.store.impl.DataStoreBase;
+import org.apache.gora.util.GoraException;
+import org.redisson.Redisson;
+import org.redisson.api.RBatch;
+import org.redisson.api.RBucket;
+import org.redisson.api.RBucketAsync;
+import org.redisson.api.RFuture;
+import org.redisson.api.RLexSortedSet;
+import org.redisson.api.RLexSortedSetAsync;
+import org.redisson.api.RList;
+import org.redisson.api.RListAsync;
+import org.redisson.api.RMap;
+import org.redisson.api.RMapAsync;
+import org.redisson.api.RScoredSortedSet;
+import org.redisson.api.RScoredSortedSetAsync;
+import org.redisson.api.RedissonClient;
+import org.redisson.client.protocol.ScoredEntry;
+import org.redisson.config.Config;
+import org.redisson.config.ReadMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of a Redis data store to be used by gora.
+ *
+ * @param <K> class to be used for the key
+ * @param <T> class to be persisted within the store
+ */
+public class RedisStore<K, T extends PersistentBase> extends DataStoreBase<K, T> {
+
+ protected static final String PARSE_MAPPING_FILE_KEY = "gora.redis.mapping.file";
+ protected static final String DEFAULT_MAPPING_FILE = "gora-redis-mapping.xml";
+ protected static final String XML_MAPPING_DEFINITION = "gora.mapping";
+ private RedissonClient redisInstance;
+ private RedisMapping mapping;
+ public static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final DatumHandler handler = new DatumHandler();
+ private StorageMode mode;
+
+ /**
+ * Initialize the data store by reading the credentials, setting the client's
+ * properties up and reading the mapping file. Initialize is called when then
+ * the call to {@link org.apache.gora.store.DataStoreFactory#createDataStore}
+ * is made.
+ *
+ * @param keyClass Gora's key class
+ * @param persistentClass Persistent class
+ * @param properties Configurations for the data store
+ * @throws org.apache.gora.util.GoraException Unexpected exception during initialization
+ */
+ @Override
+ public void initialize(Class<K> keyClass, Class<T> persistentClass, Properties properties) throws GoraException {
+ try {
+ super.initialize(keyClass, persistentClass, properties);
+
+ InputStream mappingStream;
+ if (properties.containsKey(XML_MAPPING_DEFINITION)) {
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("{} = {}", XML_MAPPING_DEFINITION, properties.getProperty(XML_MAPPING_DEFINITION));
+ }
+ mappingStream = IOUtils.toInputStream(properties.getProperty(XML_MAPPING_DEFINITION), (Charset) null);
+ } else {
+ mappingStream = getClass().getClassLoader().getResourceAsStream(getConf().get(PARSE_MAPPING_FILE_KEY, DEFAULT_MAPPING_FILE));
+ }
+ RedisMappingBuilder mappingBuilder = new RedisMappingBuilder(this);
+ mapping = mappingBuilder.readMapping(mappingStream);
+ Config config = new Config();
+ String storage = getConf().get(GORA_REDIS_STORAGE, properties.getProperty(GORA_REDIS_STORAGE));
+ mode = StorageMode.valueOf(storage);
+ String modeString = getConf().get(GORA_REDIS_MODE, properties.getProperty(GORA_REDIS_MODE));
+ ServerMode connectionMode = ServerMode.valueOf(modeString);
+ String name = getConf().get(GORA_REDIS_MASTERNAME, properties.getProperty(GORA_REDIS_MASTERNAME));
+ String readm = getConf().get(GORA_REDIS_READMODE, properties.getProperty(GORA_REDIS_READMODE));
+ //Override address in tests
+ String[] hosts = getConf().get(GORA_REDIS_ADDRESS, properties.getProperty(GORA_REDIS_ADDRESS)).split(",");
+ for (int indexHosts = 0; indexHosts < hosts.length; indexHosts++) {
+ hosts[indexHosts] = PREFIX + hosts[indexHosts];
+ }
+ switch (connectionMode) {
+ case SINGLE:
+ config.useSingleServer()
+ .setAddress(hosts[0])
+ .setDatabase(mapping.getDatabase());
+ break;
+ case CLUSTER:
+ config.useClusterServers()
+ .addNodeAddress(hosts);
+ break;
+ case REPLICATED:
+ config.useReplicatedServers()
+ .addNodeAddress(hosts)
+ .setDatabase(mapping.getDatabase());
+ break;
+ case SENTINEL:
+ config.useSentinelServers()
+ .setMasterName(name)
+ .setReadMode(ReadMode.valueOf(readm))
+ .addSentinelAddress(hosts);
+ break;
+ default:
+ throw new AssertionError(connectionMode.name());
+ }
+ redisInstance = Redisson.create(config);
+ if (autoCreateSchema && !schemaExists()) {
+ createSchema();
+ }
+ } catch (IOException ex) {
+ throw new GoraException(ex);
+ }
+ }
+
+ /**
+ * Redis, being a schemaless database does not support explicit schema
+ * creation. When the records are added to the database, the schema is created
+ * on the fly. Thus, schema operations are unavailable in gora-redis module.
+ *
+ * @return null
+ */
+ @Override
+ public String getSchemaName() {
+ return null;
+ }
+
+ /**
+ * Redis, being a schemaless database does not support explicit schema
+ * creation. When the records are added to the database, the schema is created
+ * on the fly. Thus, schema operations are unavailable in gora-redis module.
+ *
+ * @throws org.apache.gora.util.GoraException Unexpected exception.
+ */
+ @Override
+ public void createSchema() throws GoraException {
+ }
+
+ @Override
+ public void deleteSchema() throws GoraException {
+ redisInstance.getKeys().deleteByPattern(mapping.getPrefix() + FIELD_SEPARATOR + WILDCARD);
+ }
+
+ /**
+ * Redis, being a schemaless database does not support explicit schema
+ * creation. When the records are added to the database, the schema is created
+ * on the fly. Thus, schema operations are unavailable in gora-redis module.
+ *
+ * @return true
+ * @throws org.apache.gora.util.GoraException Unexpected exception.
+ */
+ @Override
+ public boolean schemaExists() throws GoraException {
+ return true;
+ }
+
+ private String generateKeyHash(K baseKey) {
+ return mapping.getPrefix() + FIELD_SEPARATOR + START_TAG + baseKey + END_TAG;
+ }
+
+ private String generateKeyString(String field, K baseKey) {
+ return generateKeyStringBase(baseKey) + field;
+ }
+
+ private String generateKeyStringBase(K baseKey) {
+ return mapping.getPrefix() + FIELD_SEPARATOR + START_TAG + baseKey + END_TAG + FIELD_SEPARATOR;
+ }
+
+ private String generateIndexKey() {
+ return mapping.getPrefix() + FIELD_SEPARATOR + INDEX;
+ }
+
+ public T newInstanceFromString(K key, String[] fields) throws GoraException, IOException {
+ fields = getFieldsToQuery(fields);
+ T persistent = newPersistent();
+ int countRetrieved = 0;
+ for (String f : fields) {
+ Schema.Field field = fieldMap.get(f);
+ String redisField = mapping.getFields().get(field.name());
+ RedisType redisType = mapping.getTypes().get(field.name());
+ Object redisVal = null;
+ switch (redisType) {
+ case STRING:
+ RBucket<Object> bucket = redisInstance.getBucket(generateKeyString(redisField, key));
+ redisVal = bucket.isExists() ? handler.deserializeFieldValue(field, field.schema(), bucket.get(), persistent) : null;
+ break;
+ case LIST:
+ RList<Object> list = redisInstance.getList(generateKeyString(redisField, key));
+ redisVal = list.isExists() ? handler.deserializeFieldList(field, field.schema(), list, persistent) : null;
+ break;
+ case HASH:
+ RMap<Object, Object> map = redisInstance.getMap(generateKeyString(redisField, key));
+ redisVal = map.isExists() ? handler.deserializeFieldMap(field, field.schema(), map, persistent) : null;
+ break;
+ default:
+ throw new AssertionError(redisType.name());
+ }
+ if (redisVal == null) {
+ continue;
+ }
+ countRetrieved++;
+ persistent.put(field.pos(), redisVal);
+ persistent.setDirty(field.pos());
+ }
+ return countRetrieved > 0 ? persistent : null;
+ }
+
+ public T newInstanceFromHash(RMap<String, Object> map, String[] fields) throws GoraException, IOException {
+ fields = getFieldsToQuery(fields);
+ T persistent = newPersistent();
+ for (String fieldName : fields) {
+ Schema.Field field = fieldMap.get(fieldName);
+ Object fValue = map.get(mapping.getFields().get(field.name()));
+ if (fValue == null) {
+ continue;
+ }
+ Object fieldValue = handler.deserializeFieldValue(field, field.schema(), fValue, persistent);
+ persistent.put(field.pos(), fieldValue);
+ persistent.setDirty(field.pos());
+ }
+ return persistent;
+ }
+
+ @Override
+ public T get(K key, String[] fields) throws GoraException {
+ try {
+ if (mode == StorageMode.SINGLEKEY) {
+ RMap<String, Object> map = redisInstance.getMap(generateKeyHash(key));
+ if (!map.isEmpty()) {
+ return newInstanceFromHash(map, fields);
+ } else {
+ return null;
+ }
+ } else {
+ return newInstanceFromString(key, fields);
+ }
+ } catch (IOException ex) {
+ throw new GoraException(ex);
+ }
+ }
+
+ @Override
+ public void put(K key, T obj) throws GoraException {
+ try {
+ if (obj.isDirty()) {
+ Schema objectSchema = obj.getSchema();
+ List<Schema.Field> fields = objectSchema.getFields();
+ RBatch batchInstance = redisInstance.createBatch();
+ //update secundary index
+ if (isNumericKey()) {
+ RScoredSortedSetAsync<Object> secundaryIndex = batchInstance.getScoredSortedSet(generateIndexKey());
+ secundaryIndex.addAsync(obtainDoubleValue(key), key);
+ } else {
+ RLexSortedSetAsync secundaryIndex = batchInstance.getLexSortedSet(generateIndexKey());
+ secundaryIndex.addAsync(key.toString());
+ }
+ if (mode == StorageMode.SINGLEKEY) {
+ RMapAsync<Object, Object> map = batchInstance.getMap(generateKeyHash(key));
+ fields.forEach((field) -> {
+ Object fieldValue = handler.serializeFieldValue(field.schema(), obj.get(field.pos()));
+ if (fieldValue != null) {
+ map.fastPutAsync(mapping.getFields().get(field.name()), fieldValue);
+ } else {
+ map.fastRemoveAsync(mapping.getFields().get(field.name()));
+ }
+ });
+ } else {
+ for (Schema.Field field : fields) {
+ Object fieldValue = obj.get(field.pos());
+ String redisField = mapping.getFields().get(field.name());
+ RedisType redisType = mapping.getTypes().get(field.name());
+ switch (redisType) {
+ case STRING:
+ RBucketAsync<Object> bucket = batchInstance.getBucket(generateKeyString(redisField, key));
+ bucket.deleteAsync();
+ if (fieldValue != null) {
+ fieldValue = handler.serializeFieldValue(field.schema(), fieldValue);
+ bucket.setAsync(fieldValue);
+ }
+ break;
+ case LIST:
+ RListAsync<Object> rlist = batchInstance.getList(generateKeyString(redisField, key));
+ rlist.deleteAsync();
+ if (fieldValue != null) {
+ List<Object> list = handler.serializeFieldList(field.schema(), fieldValue);
+ rlist.addAllAsync(list);
+ }
+ break;
+ case HASH:
+ RMapAsync<Object, Object> map = batchInstance.getMap(generateKeyString(redisField, key));
+ map.deleteAsync();
+ if (fieldValue != null) {
+ Map<Object, Object> mp = handler.serializeFieldMap(field.schema(), fieldValue);
+ map.putAllAsync(mp);
+ }
+ break;
+ default:
+ throw new AssertionError(redisType.name());
+ }
+ }
+ }
+ batchInstance.execute();
+ } else {
+ LOG.info("Ignored putting object {} in the store as it is neither "
+ + "new, neither dirty.", new Object[]{obj});
+ }
+ } catch (Exception e) {
+ throw new GoraException(e);
+ }
+ }
+
+ @Override
+ public boolean delete(K key) throws GoraException {
+ try {
+ RBatch batchInstance = redisInstance.createBatch();
+ //update secundary index
+ if (isNumericKey()) {
+ RScoredSortedSetAsync<Object> secundaryIndex = batchInstance.getScoredSortedSet(generateIndexKey());
+ secundaryIndex.removeAsync(key);
+ } else {
+ RLexSortedSetAsync secundaryIndex = batchInstance.getLexSortedSet(generateIndexKey());
+ secundaryIndex.removeAsync(key.toString());
+ }
+ if (mode == StorageMode.SINGLEKEY) {
+ RMapAsync<Object, Object> map = batchInstance.getMap(generateKeyHash(key));
+ RFuture<Boolean> deleteAsync = map.deleteAsync();
+ batchInstance.execute();
+ return deleteAsync.get();
+ } else {
+ batchInstance.execute();
+ return redisInstance.getKeys().deleteByPattern(generateKeyStringBase(key) + WILDCARD) > 0;
+ }
+ } catch (Exception ex) {
+ throw new GoraException(ex);
+ }
+ }
+
+ @Override
+ public long deleteByQuery(Query<K, T> query) throws GoraException {
+ Collection<K> range = runQuery(query);
+ RBatch batchInstance = redisInstance.createBatch();
+ RLexSortedSetAsync secundaryIndex = batchInstance.getLexSortedSet(generateIndexKey());
+ if (query.getFields() != null && query.getFields().length < mapping.getFields().size()) {
+ List<String> dbFields = new ArrayList<>();
+ List<RedisType> dbTypes = new ArrayList<>();
+ for (String af : query.getFields()) {
+ dbFields.add(mapping.getFields().get(af));
+ dbTypes.add(mapping.getTypes().get(af));
+ }
+ for (K key : range) {
+ if (mode == StorageMode.SINGLEKEY) {
+ RMapAsync<Object, Object> map = batchInstance.getMap(generateKeyHash(key));
+ dbFields.forEach((field) -> {
+ map.removeAsync(field);
+ });
+ } else {
+ for (int indexField = 0; indexField < dbFields.size(); indexField++) {
+ String field = dbFields.get(indexField);
+ RedisType type = dbTypes.get(indexField);
+ switch (type) {
+ case STRING:
+ RBucketAsync<Object> bucket = batchInstance.getBucket(generateKeyString(field, key));
+ bucket.deleteAsync();
+ break;
+ case LIST:
+ RListAsync<Object> rlist = batchInstance.getList(generateKeyString(field, key));
+ rlist.deleteAsync();
+ break;
+ case HASH:
+ RMapAsync<Object, Object> map = batchInstance.getMap(generateKeyString(field, key));
+ map.deleteAsync();
+ break;
+ default:
+ throw new AssertionError(type.name());
+ }
+ }
+ }
+ }
+ } else {
+ range.stream().map((key) -> {
+ secundaryIndex.removeAsync(key);
+ return key;
+ }).forEachOrdered((key) -> {
+ if (mode == StorageMode.SINGLEKEY) {
+ RMapAsync<Object, Object> map = batchInstance.getMap(generateKeyHash(key));
+ map.deleteAsync();
+ } else {
+ redisInstance.getKeys().deleteByPattern(generateKeyStringBase(key) + WILDCARD);
+ }
+ });
+ }
+ batchInstance.execute();
+ return range.size();
+ }
+
+ /**
+ * Execute the query and return the result.
+ *
+ * @param query Query sent to Redis
+ * @return Query result
+ * @throws org.apache.gora.util.GoraException Unexpected exception in querying process.
+ */
+ @Override
+ public Result<K, T> execute(Query<K, T> query) throws GoraException {
+ return new RedisResult<>(this, query, runQuery(query));
+ }
+
+ @Override
+ public Query<K, T> newQuery() {
+ RedisQuery<K, T> query = new RedisQuery<>(this);
+ query.setFields(getFieldsToQuery(null));
+ return query;
+ }
+
+ @Override
+ public List<PartitionQuery<K, T>> getPartitions(Query<K, T> query) throws GoraException {
+ List<PartitionQuery<K, T>> partitions = new ArrayList<>();
+ PartitionQueryImpl<K, T> partitionQuery = new PartitionQueryImpl<>(
+ query);
+ partitionQuery.setConf(getConf());
+ partitions.add(partitionQuery);
+ return partitions;
+ }
+
+ @Override
+ public void flush() throws GoraException {
+ }
+
+ @Override
+ public void close() {
+ redisInstance.shutdown();
+ }
+
+ @Override
+ public boolean exists(K key) throws GoraException {
+ if (mode == StorageMode.SINGLEKEY) {
+ return redisInstance.getKeys().countExists(generateKeyHash(key)) != 0;
+ } else {
+ Iterator<String> respKeys = redisInstance.getKeys().getKeysByPattern(generateKeyStringBase(key) + WILDCARD, 1).iterator();
+ return respKeys.hasNext();
+ }
+ }
+
+ private Collection<K> runQuery(Query<K, T> query) {
+ Collection<K> range;
+ if (isNumericKey()) {
+ RScoredSortedSet<Object> index = redisInstance.getScoredSortedSet(generateIndexKey());
+ Collection<ScoredEntry<Object>> rangeResponse;
+ int limit = query.getLimit() > -1 ? (int) query.getLimit() : Integer.MAX_VALUE;
+ if (query.getStartKey() != null && query.getEndKey() != null) {
+ rangeResponse = index.entryRange(obtainDoubleValue(query.getStartKey()), true, obtainDoubleValue(query.getEndKey()), true, 0, limit);
+ } else if (query.getStartKey() != null && query.getEndKey() == null) {
+ rangeResponse = index.entryRange(obtainDoubleValue(query.getStartKey()), true, Double.MAX_VALUE, true, 0, limit);
+ } else if (query.getStartKey() == null && query.getEndKey() != null) {
+ rangeResponse = index.entryRange(Double.MIN_VALUE, true, obtainDoubleValue(query.getEndKey()), true, 0, limit);
+ } else {
+ rangeResponse = index.entryRange(Double.MIN_VALUE, true, Double.MAX_VALUE, true, 0, limit);
+ }
+ range = new ArrayList<>();
+ for (ScoredEntry<Object> indexVal : rangeResponse) {
+ range.add((K) indexVal.getValue());
+ }
+ } else {
+ RLexSortedSet index = redisInstance.getLexSortedSet(generateIndexKey());
+ Collection<String> rangeResponse;
+ int limit = query.getLimit() > -1 ? (int) query.getLimit() : Integer.MAX_VALUE;
+ if (query.getStartKey() != null && query.getEndKey() != null) {
+ rangeResponse = index.range(query.getStartKey().toString(), true, query.getEndKey().toString(), true, 0, limit);
+ } else if (query.getStartKey() != null && query.getEndKey() == null) {
+ rangeResponse = index.rangeTail(query.getStartKey().toString(), true, 0, limit);
+ } else if (query.getStartKey() == null && query.getEndKey() != null) {
+ rangeResponse = index.rangeHead(query.getEndKey().toString(), true, 0, limit);
+ } else {
+ rangeResponse = index.stream().limit(limit).collect(Collectors.toList());
+ }
+ range = new ArrayList<>();
+ for (String indexVal : rangeResponse) {
+ range.add((K) indexVal);
+ }
+ }
+ return range;
+ }
+
+ private boolean isNumericKey() {
+ return Number.class.isAssignableFrom(keyClass);
+ }
+
+ private double obtainDoubleValue(K key) {
+ return Double.parseDouble(key.toString());
+ }
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/store/RedisType.java b/gora-redis/src/main/java/org/apache/gora/redis/store/RedisType.java
new file mode 100755
index 0000000..7e8f832
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/store/RedisType.java
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.store;
+
+/**
+ * Supported data types for Redis.
+ *
+ * Refer to: https://redis.io/topics/data-types
+ */
+public enum RedisType {
+ /**
+ * Strings are the most basic kind of Redis value. Redis Strings are binary
+ * safe, this means that a Redis string can contain any kind of data.
+ */
+ STRING,
+ /**
+ * Redis Lists are simply lists of strings, sorted by insertion order.
+ */
+ LIST,
+ /**
+ * Redis Hashes are maps between string fields and string values, so they are
+ * the perfect data type to represent objects.
+ */
+ HASH
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/store/package-info.java b/gora-redis/src/main/java/org/apache/gora/redis/store/package-info.java
new file mode 100755
index 0000000..e3cc092
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/store/package-info.java
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/**
+ * This package contains all the Redis store related classes.
+ */
+package org.apache.gora.redis.store;
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/util/DatumHandler.java b/gora-redis/src/main/java/org/apache/gora/redis/util/DatumHandler.java
new file mode 100755
index 0000000..6a1cfa9
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/util/DatumHandler.java
@@ -0,0 +1,399 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.util;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.avro.Schema;
+import org.apache.avro.specific.SpecificDatumReader;
+import org.apache.avro.specific.SpecificDatumWriter;
+import org.apache.avro.util.Utf8;
+import org.apache.gora.persistency.Persistent;
+import org.apache.gora.persistency.impl.DirtyListWrapper;
+import org.apache.gora.persistency.impl.DirtyMapWrapper;
+import org.apache.gora.persistency.impl.PersistentBase;
+import org.apache.gora.util.AvroUtils;
+import org.apache.gora.util.IOUtils;
+import org.redisson.api.RList;
+import org.redisson.api.RMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class for serialization and deserialization of values from redis.
+ */
+public class DatumHandler<T extends PersistentBase> {
+
+ public static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private final ConcurrentHashMap<Schema, SpecificDatumReader<?>> readerMap = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<Schema, SpecificDatumWriter<?>> writerMap = new ConcurrentHashMap<>();
+
+ public DatumHandler() {
+ }
+
+ /**
+ * Serialize an object
+ *
+ * @param fieldSchema The avro schema to be used.
+ * @param fieldValue The object to be serialized.
+ * @return Serialized object.
+ */
+ @SuppressWarnings("unchecked")
+ public Object serializeFieldValue(Schema fieldSchema, Object fieldValue) {
+ Object output = fieldValue;
+ switch (fieldSchema.getType()) {
+ case ARRAY:
+ case MAP:
+ case RECORD:
+ byte[] data = null;
+ try {
+ @SuppressWarnings("rawtypes")
+ SpecificDatumWriter writer = getDatumWriter(fieldSchema);
+ data = IOUtils.serialize(writer, fieldValue);
+ } catch (IOException e) {
+ LOG.error(e.getMessage(), e);
+ }
+ output = data;
+ break;
+ case UNION:
+ if (fieldSchema.getTypes().size() == 2 && isNullable(fieldSchema)) {
+ int schemaPos = getUnionSchema(fieldValue, fieldSchema);
+ Schema unionSchema = fieldSchema.getTypes().get(schemaPos);
+ output = serializeFieldValue(unionSchema, fieldValue);
+ } else {
+ data = null;
+ try {
+ @SuppressWarnings("rawtypes")
+ SpecificDatumWriter writer = getDatumWriter(fieldSchema);
+ data = IOUtils.serialize(writer, fieldValue);
+ } catch (IOException e) {
+ LOG.error(e.getMessage(), e);
+ }
+ output = data;
+ }
+ break;
+ case FIXED:
+ break;
+ case ENUM:
+ case STRING:
+ output = fieldValue.toString();
+ break;
+ case BYTES:
+ output = ((ByteBuffer) fieldValue).array();
+ break;
+ case INT:
+ case LONG:
+ case FLOAT:
+ case DOUBLE:
+ case BOOLEAN:
+ output = fieldValue;
+ break;
+ case NULL:
+ break;
+ default:
+ throw new AssertionError(fieldSchema.getType().name());
+ }
+ return output;
+ }
+
+ /**
+ * Serialize an object as a Map
+ *
+ * @param fieldSchema The avro schema to be used.
+ * @param fieldValue The object to be serialized.
+ * @return Serialized object as a map.
+ */
+ @SuppressWarnings("unchecked")
+ public Map<Object, Object> serializeFieldMap(Schema fieldSchema, Object fieldValue) {
+ Map<Object, Object> map = new HashMap();
+ switch (fieldSchema.getType()) {
+ case UNION:
+ for (Schema sc : fieldSchema.getTypes()) {
+ if (sc.getType() == Schema.Type.MAP) {
+ map = serializeFieldMap(sc, fieldValue);
+ }
+ }
+ break;
+ case MAP:
+ Map<CharSequence, ?> mp = (Map<CharSequence, ?>) fieldValue;
+ for (Entry<CharSequence, ?> e : mp.entrySet()) {
+ String mapKey = e.getKey().toString();
+ Object mapValue = e.getValue();
+ mapValue = serializeFieldValue(fieldSchema.getValueType(), mapValue);
+ map.put(mapKey, mapValue);
+ }
+ break;
+ default:
+ throw new AssertionError(fieldSchema.getType().name());
+ }
+ return map;
+ }
+
+ /**
+ * Serialize an object as a List
+ *
+ * @param fieldSchema The avro schema to be used.
+ * @param fieldValue The object to be serialized.
+ * @return Serialized object as a List.
+ */
+ @SuppressWarnings("unchecked")
+ public List<Object> serializeFieldList(Schema fieldSchema, Object fieldValue) {
+ List<Object> serializedList = new ArrayList();
+ switch (fieldSchema.getType()) {
+ case ARRAY:
+ List<?> rawdataList = (List<?>) fieldValue;
+ rawdataList.stream().map((lsValue) -> serializeFieldValue(fieldSchema.getElementType(), lsValue)).forEachOrdered((lsValue_) -> {
+ serializedList.add(lsValue_);
+ });
+ break;
+ default:
+ throw new AssertionError(fieldSchema.getType().name());
+ }
+ return serializedList;
+ }
+
+ /**
+ * Deserialize an object into a gora bean using avro
+ *
+ * @param field The field schema.
+ * @param fieldSchema The object schema.
+ * @param redisValue Object from redis.
+ * @param persistent Persistent object
+ * @return Deserialized object
+ * @throws java.io.IOException Deserialization exception
+ */
+ @SuppressWarnings("unchecked")
+ public Object deserializeFieldValue(Schema.Field field, Schema fieldSchema,
+ Object redisValue, T persistent) throws IOException {
+ Object fieldValue = null;
+ switch (fieldSchema.getType()) {
+ case MAP:
+ case ARRAY:
+ case RECORD:
+ @SuppressWarnings("rawtypes") SpecificDatumReader reader = getDatumReader(fieldSchema);
+ fieldValue = IOUtils.deserialize((byte[]) redisValue, reader,
+ persistent.get(field.pos()));
+ break;
+ case ENUM:
+ fieldValue = AvroUtils.getEnumValue(fieldSchema, redisValue.toString());
+ break;
+ case FIXED:
+ break;
+ case BYTES:
+ fieldValue = ByteBuffer.wrap((byte[]) redisValue);
+ break;
+ case STRING:
+ fieldValue = new Utf8(redisValue.toString());
+ break;
+ case UNION:
+ if (fieldSchema.getTypes().size() == 2 && isNullable(fieldSchema)) {
+ int schemaPos = getUnionSchema(redisValue, fieldSchema);
+ Schema unionSchema = fieldSchema.getTypes().get(schemaPos);
+ fieldValue = deserializeFieldValue(field, unionSchema, redisValue, persistent);
+ } else {
+ reader = getDatumReader(fieldSchema);
+ fieldValue = IOUtils.deserialize((byte[]) redisValue, reader,
+ persistent.get(field.pos()));
+ }
+ break;
+ default:
+ fieldValue = redisValue;
+ }
+ return fieldValue;
+ }
+
+ /**
+ * Deserialize an Map into a gora bean using avro
+ *
+ * @param field The field schema.
+ * @param fieldSchema The object schema.
+ * @param redisMap Map from redis.
+ * @param persistent Persistent object
+ * @return Deserialized object
+ * @throws java.io.IOException Deserialization exception
+ */
+ @SuppressWarnings("unchecked")
+ public Object deserializeFieldMap(Schema.Field field, Schema fieldSchema,
+ RMap<Object, Object> redisMap, T persistent) throws IOException {
+ Map<Utf8, Object> fieldValue = new HashMap<>();
+ switch (fieldSchema.getType()) {
+ case UNION:
+ for (Schema sc : fieldSchema.getTypes()) {
+ if (sc.getType() == Schema.Type.MAP) {
+ return deserializeFieldMap(field, sc, redisMap, persistent);
+ }
+ }
+ break;
+ case MAP:
+ for (Entry<Object, Object> aEntry : redisMap.entrySet()) {
+ String key = aEntry.getKey().toString();
+ Object value = deserializeFieldValue(field, fieldSchema.getValueType(), aEntry.getValue(), persistent);
+ fieldValue.put(new Utf8(key), value);
+ }
+ break;
+ default:
+ throw new AssertionError(fieldSchema.getType().name());
+ }
+ return new DirtyMapWrapper<>(fieldValue);
+ }
+
+ /**
+ * Deserialize an List into a gora bean using avro
+ *
+ * @param field The field schema.
+ * @param fieldSchema The object schema.
+ * @param redisList List from redis.
+ * @param persistent Persistent object
+ * @return Deserialized object
+ * @throws java.io.IOException Deserialization exception
+ */
+ @SuppressWarnings("unchecked")
+ public Object deserializeFieldList(Schema.Field field, Schema fieldSchema,
+ RList<Object> redisList, T persistent) throws IOException {
+ List<Object> fieldValue = new ArrayList<>();
+ switch (fieldSchema.getType()) {
+ case ARRAY:
+ for (Object ob : redisList) {
+ Object value = deserializeFieldValue(field, fieldSchema.getElementType(), ob, persistent);
+ fieldValue.add(value);
+ }
+ break;
+ default:
+ throw new AssertionError(fieldSchema.getType().name());
+ }
+ return new DirtyListWrapper<>(fieldValue);
+ }
+
+ /**
+ * Gets the Datum reader for a Schema
+ *
+ * @param fieldSchema The avro schema to be used
+ * @return SpecificDatumReader for the schema
+ */
+ @SuppressWarnings("rawtypes")
+ private SpecificDatumReader getDatumReader(Schema fieldSchema) {
+ SpecificDatumReader<?> reader = readerMap.get(fieldSchema);
+ if (reader == null) {
+ reader = new SpecificDatumReader(fieldSchema);
+ SpecificDatumReader localReader;
+ if ((localReader = readerMap.putIfAbsent(fieldSchema, reader)) != null) {
+ reader = localReader;
+ }
+ }
+ return reader;
+ }
+
+ /**
+ * Gets the Datum writer for a Schema
+ *
+ * @param fieldSchema The avro schema to be used
+ * @return SpecificDatumWriter for the schema
+ */
+ @SuppressWarnings("rawtypes")
+ private SpecificDatumWriter getDatumWriter(Schema fieldSchema) {
+ SpecificDatumWriter writer = writerMap.get(fieldSchema);
+ if (writer == null) {
+ writer = new SpecificDatumWriter(fieldSchema);
+ writerMap.put(fieldSchema, writer);
+ }
+ return writer;
+ }
+
+ /**
+ * Verify if a schema is Nullable
+ *
+ * @param unionSchema The schema to be verified
+ * @return result
+ */
+ private boolean isNullable(Schema unionSchema) {
+ if (unionSchema.getTypes().stream().anyMatch((innerSchema) -> (innerSchema.getType().equals(Schema.Type.NULL)))) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Method to retrieve the corresponding schema type index of a particular
+ * object having UNION schema. As UNION type can have one or more types and at
+ * a given instance, it holds an object of only one type of the defined types,
+ * this method is used to figure out the corresponding instance's schema type
+ * index.
+ *
+ * @param instanceValue value that the object holds
+ * @param unionSchema union schema containing all of the data types
+ * @return the unionSchemaPosition corresponding schema position
+ */
+ private int getUnionSchema(Object instanceValue, Schema unionSchema) {
+ int unionSchemaPos = 0;
+ for (Schema currentSchema : unionSchema.getTypes()) {
+ Schema.Type schemaType = currentSchema.getType();
+ if (instanceValue instanceof CharSequence && schemaType.equals(Schema.Type.STRING)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof ByteBuffer && schemaType.equals(Schema.Type.BYTES)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof byte[] && schemaType.equals(Schema.Type.BYTES)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof Integer && schemaType.equals(Schema.Type.INT)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof Long && schemaType.equals(Schema.Type.LONG)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof Double && schemaType.equals(Schema.Type.DOUBLE)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof Float && schemaType.equals(Schema.Type.FLOAT)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof Boolean && schemaType.equals(Schema.Type.BOOLEAN)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof Map && schemaType.equals(Schema.Type.MAP)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof List && schemaType.equals(Schema.Type.ARRAY)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof Persistent && schemaType.equals(Schema.Type.RECORD)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof byte[] && schemaType.equals(Schema.Type.MAP)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof byte[] && schemaType.equals(Schema.Type.RECORD)) {
+ return unionSchemaPos;
+ }
+ if (instanceValue instanceof byte[] && schemaType.equals(Schema.Type.ARRAY)) {
+ return unionSchemaPos;
+ }
+ unionSchemaPos++;
+ }
+ return 0;
+ }
+
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/util/RedisStoreConstants.java b/gora-redis/src/main/java/org/apache/gora/redis/util/RedisStoreConstants.java
new file mode 100755
index 0000000..793b0b5
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/util/RedisStoreConstants.java
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.gora.redis.util;
+
+/**
+ * Redis constants
+ */
+public class RedisStoreConstants {
+
+ //Redis constants
+ public static final String FIELD_SEPARATOR = ".";
+ public static final String WILDCARD = "*";
+ public static final String INDEX = "index";
+ public static final String START_TAG = "{";
+ public static final String END_TAG = "}";
+ public static final String PREFIX = "redis://";
+
+ public static final String GORA_REDIS_ADDRESS = "gora.datastore.redis.address";
+ public static final String GORA_REDIS_READMODE = "gora.datastore.redis.readMode";
+ public static final String GORA_REDIS_MASTERNAME = "gora.datastore.redis.masterName";
+ public static final String GORA_REDIS_MODE = "gora.datastore.redis.mode";
+ public static final String GORA_REDIS_STORAGE = "gora.datastore.redis.storage";
+
+ private RedisStoreConstants() {
+ }
+
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/util/ServerMode.java b/gora-redis/src/main/java/org/apache/gora/redis/util/ServerMode.java
new file mode 100755
index 0000000..dfde4dc
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/util/ServerMode.java
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.util;
+
+/**
+ * Connection mode to the Redis Server
+ *
+ * More details in : https://github.com/redisson/redisson/wiki/2.-Configuration
+ */
+public enum ServerMode {
+ /**
+ * Redis single server configuration.
+ */
+ SINGLE,
+ /**
+ * Redis server cluster configuration.
+ */
+ CLUSTER,
+ /**
+ * Redis replicated mode configuration.
+ */
+ REPLICATED,
+ /**
+ * Redis server sentinel configuration.
+ */
+ SENTINEL
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/util/StorageMode.java b/gora-redis/src/main/java/org/apache/gora/redis/util/StorageMode.java
new file mode 100755
index 0000000..26408aa
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/util/StorageMode.java
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.util;
+
+/**
+ * Storage mode of Objects for gora-redis
+ *
+ * More details in:
+ * https://cwiki.apache.org/confluence/display/GORA/Redis+backend+documentation
+ */
+public enum StorageMode {
+ /**
+ * The records are stored in a single hash in redis.
+ */
+ SINGLEKEY,
+ /**
+ * The records are stored in multiple keys.
+ */
+ MULTIKEY
+}
diff --git a/gora-redis/src/main/java/org/apache/gora/redis/util/package-info.java b/gora-redis/src/main/java/org/apache/gora/redis/util/package-info.java
new file mode 100755
index 0000000..6afb613
--- /dev/null
+++ b/gora-redis/src/main/java/org/apache/gora/redis/util/package-info.java
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/**
+ * This package contains Redis store related util classes.
+ */
+package org.apache.gora.redis.util;
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/GoraRedisTestDriver.java b/gora-redis/src/test/java/org/apache/gora/redis/GoraRedisTestDriver.java
new file mode 100755
index 0000000..9af838e
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/GoraRedisTestDriver.java
@@ -0,0 +1,115 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.gora.redis;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.apache.gora.GoraTestDriver;
+import org.apache.gora.redis.store.RedisStore;
+import org.apache.gora.redis.util.RedisStartupLogWaitStrategy;
+import org.apache.gora.redis.util.ServerMode;
+import org.apache.gora.redis.util.StorageMode;
+import org.testcontainers.containers.FixedHostPortGenericContainer;
+import org.testcontainers.containers.GenericContainer;
+
+/**
+ * Helper class to execute tests in a embedded instance of Redis.
+ *
+ */
+public class GoraRedisTestDriver extends GoraTestDriver {
+
+ private static final String DOCKER_IMAGE = "grokzen/redis-cluster:latest";
+ private final FixedHostPortGenericContainer redisContainer;
+
+ private final StorageMode storageMode;
+ private final ServerMode serverMode;
+
+ public GoraRedisTestDriver(StorageMode storageMode, ServerMode serverMode) {
+ super(RedisStore.class);
+ this.storageMode = storageMode;
+ this.serverMode = serverMode;
+ GenericContainer container = new FixedHostPortGenericContainer(DOCKER_IMAGE)
+ .withFixedExposedPort(7000, 7000)
+ .withFixedExposedPort(7001, 7001)
+ .withFixedExposedPort(7002, 7002)
+ .withFixedExposedPort(7003, 7003)
+ .withFixedExposedPort(7004, 7004)
+ .withFixedExposedPort(7005, 7005)
+ .withFixedExposedPort(7006, 7006)
+ .withFixedExposedPort(7007, 7007)
+ .withFixedExposedPort(5000, 5000)
+ .withFixedExposedPort(5001, 5001)
+ .withFixedExposedPort(5002, 5002)
+ .waitingFor(new RedisStartupLogWaitStrategy())
+ .withStartupTimeout(Duration.ofMinutes(3))
+ .withEnv("STANDALONE", "true")
+ .withEnv("SENTINEL", "true");
+ redisContainer = (FixedHostPortGenericContainer) container;
+
+ }
+
+ @Override
+ public void setUpClass() throws IOException {
+ redisContainer.start();
+ log.info("Setting up Redis test driver");
+ conf.set("gora.datastore.redis.storage", storageMode.name());
+ conf.set("gora.datastore.redis.mode", serverMode.name());
+ String bridgeIpAddress = redisContainer.getContainerInfo()
+ .getNetworkSettings()
+ .getNetworks()
+ .values()
+ .iterator()
+ .next()
+ .getIpAddress();
+ switch (serverMode) {
+ case SINGLE:
+ conf.set("gora.datastore.redis.address", bridgeIpAddress + ":" + 7006);
+ break;
+ case CLUSTER:
+ conf.set("gora.datastore.redis.address",
+ bridgeIpAddress + ":" + 7000 + ","
+ + bridgeIpAddress + ":" + 7001 + ","
+ + bridgeIpAddress + ":" + 7002
+ );
+ break;
+ case REPLICATED:
+ conf.set("gora.datastore.redis.address",
+ bridgeIpAddress + ":" + 7000 + ","
+ + bridgeIpAddress + ":" + 7004
+ );
+ break;
+ case SENTINEL:
+ conf.set("gora.datastore.redis.masterName", "sentinel7000");
+ conf.set("gora.datastore.redis.readMode", "MASTER");
+ conf.set("gora.datastore.redis.address",
+ bridgeIpAddress + ":" + 5000 + ","
+ + bridgeIpAddress + ":" + 5001 + ","
+ + bridgeIpAddress + ":" + 5000
+ );
+ break;
+ default:
+ throw new AssertionError(serverMode.name());
+ }
+ }
+
+ @Override
+ public void tearDownClass() throws Exception {
+ redisContainer.stop();
+ log.info("Tearing down Redis test driver");
+ }
+}
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/mapreduce/RedisStoreMapReduceTest.java b/gora-redis/src/test/java/org/apache/gora/redis/mapreduce/RedisStoreMapReduceTest.java
new file mode 100755
index 0000000..711a3f1
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/mapreduce/RedisStoreMapReduceTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.gora.redis.mapreduce;
+
+import java.io.IOException;
+import org.apache.gora.examples.generated.WebPage;
+import org.apache.gora.mapreduce.DataStoreMapReduceTestBase;
+import org.apache.gora.redis.GoraRedisTestDriver;
+import org.apache.gora.redis.util.ServerMode;
+import org.apache.gora.redis.util.StorageMode;
+import org.apache.gora.store.DataStore;
+import org.apache.gora.store.DataStoreFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+
+/**
+ * Executes tests for MR jobs over Redis dataStore.
+ *
+ * Mapreduce tests are disable due to failure which only occur in Maven environment.
+ * Test passes in local IDE environment.
+ */
+@Ignore
+public class RedisStoreMapReduceTest extends DataStoreMapReduceTestBase {
+
+ private final GoraRedisTestDriver driver;
+
+ public RedisStoreMapReduceTest() throws IOException {
+ super();
+ driver = new GoraRedisTestDriver(StorageMode.SINGLEKEY, ServerMode.SINGLE);
+ }
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ driver.setUpClass();
+ super.setUp();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ driver.tearDownClass();
+ }
+
+ @Override
+ protected DataStore<String, WebPage> createWebPageDataStore() throws IOException {
+ try {
+ return DataStoreFactory.getDataStore(String.class, WebPage.class, driver.getConfiguration());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/mapreduce/package-info.java b/gora-redis/src/test/java/org/apache/gora/redis/mapreduce/package-info.java
new file mode 100755
index 0000000..f7c087d
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/mapreduce/package-info.java
@@ -0,0 +1,17 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.mapreduce;
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/package-info.java b/gora-redis/src/test/java/org/apache/gora/redis/package-info.java
new file mode 100755
index 0000000..4363d31
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/package-info.java
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/**
+ * Tests for <code>gora-redis</code> including the test driver for
+ * {@link org.apache.gora.redis.store.RedisStoreTest}
+ */
+package org.apache.gora.redis;
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/store/RedisStoreClusterTest.java b/gora-redis/src/test/java/org/apache/gora/redis/store/RedisStoreClusterTest.java
new file mode 100755
index 0000000..ae4c8fe
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/store/RedisStoreClusterTest.java
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.store;
+
+import org.apache.gora.redis.GoraRedisTestDriver;
+import org.apache.gora.redis.util.ServerMode;
+import org.apache.gora.redis.util.StorageMode;
+import org.apache.gora.store.DataStoreTestBase;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Tests extending {@link org.apache.gora.store.DataStoreTestBase} which run the
+ * base JUnit test suite for Gora.
+ */
+public class RedisStoreClusterTest extends DataStoreTestBase {
+
+ static {
+ setTestDriver(new GoraRedisTestDriver(StorageMode.SINGLEKEY, ServerMode.CLUSTER));
+ }
+
+ // Unsupported functionality due to the limitations in Redis
+ @Test
+ @Ignore("Explicit schema creation related functionality is not supported in Redis")
+ @Override
+ public void testTruncateSchema() throws Exception {
+ super.testTruncateSchema();
+ }
+
+ @Test
+ @Ignore("Explicit schema creation related functionality is not supported in Redis")
+ @Override
+ public void testDeleteSchema() throws Exception {
+ super.testDeleteSchema();
+ }
+
+ @Test
+ @Ignore("Explicit schema creation related functionality is not supported in Redis")
+ @Override
+ public void testSchemaExists() throws Exception {
+ super.testSchemaExists();
+ }
+}
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/store/RedisStoreHashTest.java b/gora-redis/src/test/java/org/apache/gora/redis/store/RedisStoreHashTest.java
new file mode 100755
index 0000000..9ac9935
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/store/RedisStoreHashTest.java
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.store;
+
+import org.apache.gora.redis.GoraRedisTestDriver;
+import org.apache.gora.redis.util.ServerMode;
+import org.apache.gora.redis.util.StorageMode;
+import org.apache.gora.store.DataStoreTestBase;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Tests extending {@link org.apache.gora.store.DataStoreTestBase} which run the
+ * base JUnit test suite for Gora.
+ */
+public class RedisStoreHashTest extends DataStoreTestBase {
+
+ static {
+ setTestDriver(new GoraRedisTestDriver(StorageMode.SINGLEKEY, ServerMode.SINGLE));
+ }
+
+ // Unsupported functionality due to the limitations in Redis
+ @Test
+ @Ignore("Explicit schema creation related functionality is not supported in Redis")
+ @Override
+ public void testTruncateSchema() throws Exception {
+ super.testTruncateSchema();
+ }
+
+ @Test
+ @Ignore("Explicit schema creation related functionality is not supported in Redis")
+ @Override
+ public void testDeleteSchema() throws Exception {
+ super.testDeleteSchema();
+ }
+
+ @Test
+ @Ignore("Explicit schema creation related functionality is not supported in Redis")
+ @Override
+ public void testSchemaExists() throws Exception {
+ super.testSchemaExists();
+ }
+
+}
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/store/RedisStoreStringTest.java b/gora-redis/src/test/java/org/apache/gora/redis/store/RedisStoreStringTest.java
new file mode 100755
index 0000000..347b800
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/store/RedisStoreStringTest.java
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.gora.redis.store;
+
+import org.apache.gora.redis.GoraRedisTestDriver;
+import org.apache.gora.redis.util.ServerMode;
+import org.apache.gora.redis.util.StorageMode;
+import org.apache.gora.store.DataStoreTestBase;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Tests extending {@link org.apache.gora.store.DataStoreTestBase} which run the
+ * base JUnit test suite for Gora.
+ */
+public class RedisStoreStringTest extends DataStoreTestBase {
+
+ static {
+ setTestDriver(new GoraRedisTestDriver(StorageMode.MULTIKEY, ServerMode.SINGLE));
+ }
+
+ // Unsupported functionality due to the limitations in Redis
+ @Test
+ @Ignore("Explicit schema creation related functionality is not supported in Redis")
+ @Override
+ public void testTruncateSchema() throws Exception {
+ super.testTruncateSchema();
+ }
+
+ @Test
+ @Ignore("Explicit schema creation related functionality is not supported in Redis")
+ @Override
+ public void testDeleteSchema() throws Exception {
+ super.testDeleteSchema();
+ }
+
+ @Test
+ @Ignore("Explicit schema creation related functionality is not supported in Redis")
+ @Override
+ public void testSchemaExists() throws Exception {
+ super.testSchemaExists();
+ }
+
+}
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/store/package-info.java b/gora-redis/src/test/java/org/apache/gora/redis/store/package-info.java
new file mode 100755
index 0000000..30d0832
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/store/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/**
+ * This package contains all the unit tests for basic CRUD operations
+ * functionality of the Redis dataStore.
+ */
+package org.apache.gora.redis.store;
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/util/RedisStartupLogWaitStrategy.java b/gora-redis/src/test/java/org/apache/gora/redis/util/RedisStartupLogWaitStrategy.java
new file mode 100755
index 0000000..b9857b1
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/util/RedisStartupLogWaitStrategy.java
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.gora.redis.util;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+import org.testcontainers.containers.ContainerLaunchException;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.OutputFrame;
+import org.testcontainers.containers.output.WaitingConsumer;
+
+/**
+ * Utility class for detecting when the docker container is ready.
+ */
+public class RedisStartupLogWaitStrategy extends GenericContainer.AbstractWaitStrategy {
+
+ private static final String REGEX = ".*Background AOF rewrite finished successfully.*";
+ private final int times = 3;
+
+ @Override
+ protected void waitUntilReady() {
+ WaitingConsumer waitingConsumer = new WaitingConsumer();
+ this.container.followOutput(waitingConsumer);
+ Predicate waitPredicate = (outputFrame) -> {
+ String trimmedFrameText = ((OutputFrame) outputFrame).getUtf8String().replaceFirst("\n$", "");
+ return trimmedFrameText.matches(REGEX);
+ };
+
+ try {
+ waitingConsumer.waitUntil(waitPredicate, this.startupTimeout.getSeconds(), TimeUnit.SECONDS,
+ this.times);
+ } catch (TimeoutException var4) {
+ throw new ContainerLaunchException(
+ "Timed out waiting for log output matching Redis server startup Log \'" + REGEX
+ + "\'");
+ }
+ }
+}
diff --git a/gora-redis/src/test/java/org/apache/gora/redis/util/package-info.java b/gora-redis/src/test/java/org/apache/gora/redis/util/package-info.java
new file mode 100755
index 0000000..a771d86
--- /dev/null
+++ b/gora-redis/src/test/java/org/apache/gora/redis/util/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/**
+ * This package contains all the unit tests for utils of the Redis dataStore.
+ */
+package org.apache.gora.redis.util;
diff --git a/gora-redis/src/test/resources/gora-redis-mapping.xml b/gora-redis/src/test/resources/gora-redis-mapping.xml
new file mode 100755
index 0000000..87f9450
--- /dev/null
+++ b/gora-redis/src/test/resources/gora-redis-mapping.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+
+<gora-otd>
+ <class name="org.apache.gora.examples.generated.Employee" keyClass="java.lang.String" prefix="employee" database="0" >
+ <field name="ssn" column="ssn" type="STRING"/>
+ <field name="name" column="name" type="STRING"/>
+ <field name="dateOfBirth" column="dateOfBirth" type="STRING"/>
+ <field name="salary" column="salary" type="STRING"/>
+ <field name="boss" column="boss" type="STRING"/>
+ <field name="webpage" column="webpage" type="STRING"/>
+ <field name="value" column="value" type="STRING"/>
+ </class>
+ <class name="org.apache.gora.examples.generated.WebPage" keyClass="java.lang.String" prefix="webpage" database="1" >
+ <field name="url" column="url" type="STRING"/>
+ <field name="content" column="content" type="STRING"/>
+ <field name="parsedContent" column="parsedContent" type="LIST"/>
+ <field name="outlinks" column="outlinks" type="HASH"/>
+ <field name="headers" column="headers" type="HASH"/>
+ <field name="metadata" column="metadata" type="STRING"/>
+ <field name="byteData" column="byteData" type="HASH"/>
+ <field name="stringData" column="stringData" type="HASH"/>
+ </class>
+</gora-otd>
diff --git a/gora-redis/src/test/resources/gora.properties b/gora-redis/src/test/resources/gora.properties
new file mode 100755
index 0000000..81e8151
--- /dev/null
+++ b/gora-redis/src/test/resources/gora.properties
@@ -0,0 +1,34 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+gora.datastore.default=org.apache.gora.redis.store.RedisStore
+gora.datastore.redis.storage=SINGLEKEY
+gora.datastore.redis.mode=SINGLE
+gora.datastore.redis.address=localhost:7006
+
+#Minimal cluster configuration requires to contain at least three master nodes
+#gora.datastore.redis.mode=CLUSTER
+#gora.datastore.redis.address=localhost:6387,localhost:6388,localhost:6389
+
+#Multiple nodes at once could be added. All nodes (master and slaves) should be provided.
+#gora.datastore.redis.mode=REPLICATED
+#gora.datastore.redis.address=localhost:6387,localhost:6388,localhost:6389
+
+#Sentinel mode
+#gora.datastore.redis.mode=SENTINEL
+#gora.datastore.redis.masterName=mock
+#gora.datastore.redis.readMode=MASTER
+#gora.datastore.redis.address=localhost:6387,localhost:6388,localhost:6389
+
diff --git a/pom.xml b/pom.xml
old mode 100644
new mode 100755
index da3b1aa..c501ce4
--- a/pom.xml
+++ b/pom.xml
@@ -452,7 +452,7 @@
</dependency>
</dependencies>
</plugin>
- <!--This plugin's configuration is used to store Eclipse m2e settings
+ <!--This plugin's configuration is used to store Eclipse m2e settings
only. It has no influence on the Maven build itself. -->
<plugin>
<groupId>org.eclipse.m2e</groupId>
@@ -666,7 +666,7 @@
<id>release</id>
<build>
<plugins>
- <!-- <plugin> <groupId>org.apache.rat</groupId> <artifactId>apache-rat-plugin</artifactId>
+ <!-- <plugin> <groupId>org.apache.rat</groupId> <artifactId>apache-rat-plugin</artifactId>
<version>${apache-rat-plugin.version}</version> <executions> <execution>
<id>rat-verify</id> <phase>test</phase> <goals> <goal>check</goal> </goals>
</execution> </executions> <configuration> <licenses> <license implementation="org.apache.rat.analysis.license.SimplePatternBasedLicense">
@@ -791,6 +791,7 @@
<module>gora-ignite</module>
<module>gora-kudu</module>
<module>gora-hive</module>
+ <module>gora-redis</module>
<module>gora-tutorial</module>
<module>gora-benchmark</module>
<module>sources-dist</module>
@@ -816,6 +817,8 @@
<!-- Ignite Dependencies -->
<ignite.version>2.6.0</ignite.version>
<sqlbuilder.version>2.1.7</sqlbuilder.version>
+ <!-- Redis Dependencies -->
+ <redisson.version>3.11.0</redisson.version>
<!-- Kudu Dependencies -->
<kudu.version>1.9.0</kudu.version>
<!-- Solr Dependencies -->
@@ -850,10 +853,10 @@
<orientdb.version>2.2.22</orientdb.version>
<orientqb.version>0.2.0</orientqb.version>
-
+
<!-- CouchDB Dependencies -->
<couchdb.version>1.4.2</couchdb.version>
-
+
<!-- MongoDB Dependencies -->
<mongo.embed.version>2.0.0</mongo.embed.version>
@@ -866,11 +869,11 @@
<!-- Testing Dependencies -->
<junit.version>4.10</junit.version>
<test.container.version>1.4.2</test.container.version>
-
+
<!-- gora-benchmark and version dependencies -->
<site.ycsb.version>0.17.0</site.ycsb.version>
<datafactory.version>0.8</datafactory.version>
-
+
<!-- Maven Plugin Dependencies -->
<maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
@@ -1048,7 +1051,7 @@
<artifactId>gora-mongodb</artifactId>
<version>${project.version}</version>
</dependency>
-
+
<!--Kudu DataStore dependencies -->
<dependency>
<groupId>org.apache.kudu</groupId>
@@ -1140,6 +1143,24 @@
<scope>compile</scope>
</dependency>
+ <!-- Redis DataStore dependencies -->
+ <dependency>
+ <groupId>org.apache.gora</groupId>
+ <artifactId>gora-redis</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.gora</groupId>
+ <artifactId>gora-redis</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ </dependency>
+ <dependency>
+ <groupId>org.redisson</groupId>
+ <artifactId>redisson-all</artifactId>
+ <version>${redisson.version}</version>
+ </dependency>
+
<!--Hadoop dependencies -->
<dependency>
<groupId>org.apache.hadoop</groupId>
@@ -1738,7 +1759,7 @@
<artifactId>aerospike-client</artifactId>
<version>${aerospike.version}</version>
</dependency>
-
+
<!-- CouchDB Dependency -->
<dependency>
<groupId>org.ektorp</groupId>
@@ -1770,21 +1791,21 @@
<artifactId>testcontainers</artifactId>
<version>${test.container.version}</version>
</dependency>
-
+
<!-- Gora Benchmark Dependencies -->
<dependency>
<groupId>site.ycsb</groupId>
<artifactId>core</artifactId>
<version>${site.ycsb.version}</version>
</dependency>
-
+
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<version>${mongo.embed.version}</version>
<scope>test</scope>
</dependency>
-
+
<dependency>
<groupId>org.fluttercode.datafactory</groupId>
<artifactId>datafactory</artifactId>