diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..205aa81 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +# Test +script: + - sh -c 'cd jaggr && mvn clean test' \ No newline at end of file diff --git a/jaggr/pom.xml b/jaggr/pom.xml new file mode 100644 index 0000000..43dc46d --- /dev/null +++ b/jaggr/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + com.caffinc + jaggr + 1.0-SNAPSHOT + jaggr - JSON Aggregator + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.7 + 1.7 + + + + + + + + com.google.code.gson + gson + 2.6.2 + + + + junit + junit + 4.12 + test + + + \ No newline at end of file diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/Aggregation.java b/jaggr/src/main/java/com/caffinc/jaggr/core/Aggregation.java new file mode 100644 index 0000000..cf8e5c2 --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/Aggregation.java @@ -0,0 +1,60 @@ +package com.caffinc.jaggr.core; + +import com.caffinc.jaggr.core.operations.Operation; +import com.caffinc.jaggr.core.utils.FieldValueExtractor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Aggregates list of objects based on operations + * + * @author Sriram + * @since 11/26/2016 + */ +public class Aggregation { + private String _id; + private String[] idSplit; + private Map operationMap; + + Aggregation(String _id, Map operationMap) { + this._id = _id; + this.idSplit = _id != null ? _id.split("\\.") : null; + this.operationMap = operationMap; + } + + public List> aggregate(List> objects) { + Map> workspace = new HashMap<>(); + for (Map object : objects) { + String id = "0"; + if (_id != null) { + id = String.valueOf(FieldValueExtractor.getValue(idSplit, object)); + } + if (!workspace.containsKey(id)) { + Map groupWorkspace = new HashMap<>(); + groupWorkspace.put("_id", id); + workspace.put(id, groupWorkspace); + } + Map groupWorkspace = workspace.get(id); + for (Map.Entry operationEntry : operationMap.entrySet()) { + String field = operationEntry.getKey(); + Operation operation = operationEntry.getValue(); + Object t0 = groupWorkspace.get(field); + Object t1 = operation.aggregate(t0, object); + groupWorkspace.put(field, t1); + } + } + List> resultList = new ArrayList<>(); + for (Map groupWorkspace : workspace.values()) { + for (Map.Entry operationEntry : operationMap.entrySet()) { + String field = operationEntry.getKey(); + Operation operation = operationEntry.getValue(); + groupWorkspace.put(field, operation.result(groupWorkspace.get(field))); + } + resultList.add(groupWorkspace); + } + return resultList; + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/AggregationBuilder.java b/jaggr/src/main/java/com/caffinc/jaggr/core/AggregationBuilder.java new file mode 100644 index 0000000..20ad62b --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/AggregationBuilder.java @@ -0,0 +1,32 @@ +package com.caffinc.jaggr.core; + + +import com.caffinc.jaggr.core.operations.Operation; + +import java.util.HashMap; +import java.util.Map; + +/** + * Builder for Aggregations + * + * @author Sriram + * @since 11/26/2016 + */ +public class AggregationBuilder { + private String _id = null; + private Map operationMap = new HashMap<>(); + + public AggregationBuilder setGroupBy(String field) { + _id = field; + return this; + } + + public AggregationBuilder addOperation(String field, Operation operation) { + operationMap.put(field, operation); + return this; + } + + public Aggregation getAggregation() { + return new Aggregation(_id, operationMap); + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/entities/Tuple2.java b/jaggr/src/main/java/com/caffinc/jaggr/core/entities/Tuple2.java new file mode 100644 index 0000000..c82c10c --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/entities/Tuple2.java @@ -0,0 +1,17 @@ +package com.caffinc.jaggr.core.entities; + +/** + * Holds a pair of values + * + * @author Sriram + * @since 11/26/2016 + */ +public class Tuple2 { + public T1 _1; + public T2 _2; + + public Tuple2(T1 _1, T2 _2) { + this._1 = _1; + this._2 = _2; + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/operations/AverageOperation.java b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/AverageOperation.java new file mode 100644 index 0000000..4f49d80 --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/AverageOperation.java @@ -0,0 +1,54 @@ +package com.caffinc.jaggr.core.operations; + +import com.caffinc.jaggr.core.entities.Tuple2; +import com.caffinc.jaggr.core.utils.FieldValueExtractor; + +import java.util.Map; + +/** + * Performs averaging aggregation + * + * @author Sriram + * @since 11/26/2016 + */ +public class AverageOperation implements Operation { + private String[] field; + private String unsplitField; + + public AverageOperation(String field) { + this.unsplitField = field; + this.field = field.split("\\."); + } + + @Override + public Object aggregate(Object previousAccumulatedValue, Map object) { + Object value = FieldValueExtractor.getValue(field, object); + if (value == null) + return previousAccumulatedValue; + Tuple2 accumulator = previousAccumulatedValue == null + ? new Tuple2<>(0.0d, 0) + : (Tuple2) previousAccumulatedValue; + + if (value instanceof Double) { + accumulator._1 = accumulator._1 + (Double) value; + } else if (value instanceof Float) { + accumulator._1 = accumulator._1 + (Float) value; + } else if (value instanceof Long) { + accumulator._1 = accumulator._1 + (Long) value; + } else if (value instanceof Integer) { + accumulator._1 = accumulator._1 + (Integer) value; + } else { + throw new IllegalArgumentException("Field " + unsplitField + " isn't a Double, Float, Long or Integer"); + } + accumulator._2++; + return accumulator; + } + + @Override + public Object result(Object accumulatedValue) { + Tuple2 accumulator = accumulatedValue == null + ? new Tuple2<>(0.0d, 1) + : (Tuple2) accumulatedValue; + return accumulator._1 / accumulator._2; + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/operations/CollectOperation.java b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/CollectOperation.java new file mode 100644 index 0000000..6eea987 --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/CollectOperation.java @@ -0,0 +1,41 @@ +package com.caffinc.jaggr.core.operations; + + +import com.caffinc.jaggr.core.utils.FieldValueExtractor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Collects all values into a list + * + * @author Sriram + * @since 11/26/2016 + */ +public class CollectOperation implements Operation { + private String[] field; + private String unsplitField; + + public CollectOperation(String field) { + this.unsplitField = field; + this.field = field.split("\\."); + } + + @Override + public Object aggregate(Object previousAccumulatedValue, Map object) { + Object value = FieldValueExtractor.getValue(field, object); + if (value == null) + return previousAccumulatedValue; + List accumulator = previousAccumulatedValue == null + ? new ArrayList<>() + : (List) previousAccumulatedValue; + accumulator.add(value); + return accumulator; + } + + @Override + public Object result(Object accumulatedValue) { + return accumulatedValue; + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/operations/CollectSetOperation.java b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/CollectSetOperation.java new file mode 100644 index 0000000..cc871c1 --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/CollectSetOperation.java @@ -0,0 +1,41 @@ +package com.caffinc.jaggr.core.operations; + + +import com.caffinc.jaggr.core.utils.FieldValueExtractor; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Collects all values into a set + * + * @author Sriram + * @since 11/26/2016 + */ +public class CollectSetOperation implements Operation { + private String[] field; + private String unsplitField; + + public CollectSetOperation(String field) { + this.unsplitField = field; + this.field = field.split("\\."); + } + + @Override + public Object aggregate(Object previousAccumulatedValue, Map object) { + Object value = FieldValueExtractor.getValue(field, object); + if (value == null) + return previousAccumulatedValue; + Set accumulator = previousAccumulatedValue == null + ? new HashSet<>() + : (Set) previousAccumulatedValue; + accumulator.add(value); + return accumulator; + } + + @Override + public Object result(Object accumulatedValue) { + return accumulatedValue; + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/operations/CountOperation.java b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/CountOperation.java new file mode 100644 index 0000000..c7f7952 --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/CountOperation.java @@ -0,0 +1,31 @@ +package com.caffinc.jaggr.core.operations; + +import java.util.Map; + +/** + * Performs count aggregation + * + * @author Sriram + * @since 11/26/2016 + */ +public class CountOperation implements Operation { + private Integer counterValue; + + public CountOperation(Integer counterValue) { + this.counterValue = counterValue; + } + + public CountOperation() { + this(1); + } + + @Override + public Object aggregate(Object previousAccumulatedValue, Map object) { + return (previousAccumulatedValue == null ? 0 : (Integer) previousAccumulatedValue) + counterValue; + } + + @Override + public Object result(Object accumulatedValue) { + return accumulatedValue; + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/operations/MaxOperation.java b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/MaxOperation.java new file mode 100644 index 0000000..bf5a42a --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/MaxOperation.java @@ -0,0 +1,56 @@ +package com.caffinc.jaggr.core.operations; + + +import com.caffinc.jaggr.core.utils.FieldValueExtractor; + +import java.util.Map; + +/** + * Performs maximum value aggregation + * + * @author Sriram + * @since 11/26/2016 + */ +public class MaxOperation implements Operation { + private String[] field; + private String unsplitField; + private boolean isDouble = false; + + public MaxOperation(String field) { + this.unsplitField = field; + this.field = field.split("\\."); + } + + @Override + public Object aggregate(Object previousAccumulatedValue, Map object) { + Object value = FieldValueExtractor.getValue(field, object); + if (value == null) + return previousAccumulatedValue; + Double accumulator = previousAccumulatedValue == null + ? Double.MIN_VALUE + : (Double) previousAccumulatedValue; + if (value instanceof Double) { + isDouble = true; + return (accumulator < (Double) value) ? value : accumulator; + } + if (value instanceof Float) { + isDouble = true; + return (accumulator < (Float) value) ? value : accumulator; + } + if (value instanceof Long) { + return (accumulator < (Long) value) ? value : accumulator; + } + if (value instanceof Integer) { + return (accumulator < (Integer) value) ? value : accumulator; + } + throw new IllegalArgumentException("Field " + unsplitField + " isn't a Double, Float, Long or Integer"); + } + + @Override + public Object result(Object accumulatedValue) { + if (accumulatedValue != null) + return isDouble ? accumulatedValue : ((Double) accumulatedValue).longValue(); + else + return null; + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/operations/MinOperation.java b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/MinOperation.java new file mode 100644 index 0000000..b0e558e --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/MinOperation.java @@ -0,0 +1,56 @@ +package com.caffinc.jaggr.core.operations; + + +import com.caffinc.jaggr.core.utils.FieldValueExtractor; + +import java.util.Map; + +/** + * Performs minimum value aggregation + * + * @author Sriram + * @since 11/26/2016 + */ +public class MinOperation implements Operation { + private String[] field; + private String unsplitField; + private boolean isDouble = false; + + public MinOperation(String field) { + this.unsplitField = field; + this.field = field.split("\\."); + } + + @Override + public Object aggregate(Object previousAccumulatedValue, Map object) { + Object value = FieldValueExtractor.getValue(field, object); + if (value == null) + return previousAccumulatedValue; + Double accumulator = previousAccumulatedValue == null + ? Double.MAX_VALUE + : (Double) previousAccumulatedValue; + if (value instanceof Double) { + isDouble = true; + return (accumulator > (Double) value) ? value : accumulator; + } + if (value instanceof Float) { + isDouble = true; + return (accumulator > (Float) value) ? value : accumulator; + } + if (value instanceof Long) { + return (accumulator > (Long) value) ? value : accumulator; + } + if (value instanceof Integer) { + return (accumulator > (Integer) value) ? value : accumulator; + } + throw new IllegalArgumentException("Field " + unsplitField + " isn't a Double, Float, Long or Integer"); + } + + @Override + public Object result(Object accumulatedValue) { + if (accumulatedValue != null) + return isDouble ? accumulatedValue : ((Double) accumulatedValue).longValue(); + else + return null; + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/operations/Operation.java b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/Operation.java new file mode 100644 index 0000000..9e56da4 --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/Operation.java @@ -0,0 +1,15 @@ +package com.caffinc.jaggr.core.operations; + +import java.util.Map; + +/** + * Interface for an operation in the aggregation + * + * @author Sriram + * @since 11/24/2016 + */ +public interface Operation { + Object aggregate(Object previousAccumulatedValue, Map object); + + Object result(Object accumulatedValue); +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/operations/SumOperation.java b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/SumOperation.java new file mode 100644 index 0000000..7ff50b2 --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/operations/SumOperation.java @@ -0,0 +1,54 @@ +package com.caffinc.jaggr.core.operations; + + +import com.caffinc.jaggr.core.utils.FieldValueExtractor; + +import java.util.Map; + +/** + * Performs summing aggregation + * + * @author Sriram + * @since 11/24/2016 + */ +public class SumOperation implements Operation { + private String[] field; + private String unsplitField; + private boolean isDouble = false; + + public SumOperation(String field) { + this.unsplitField = field; + this.field = field.split("\\."); + } + + @Override + public Object aggregate(Object previousAccumulatedValue, Map object) { + Object value = FieldValueExtractor.getValue(field, object); + if (value == null) + return previousAccumulatedValue; + Double accumulator = previousAccumulatedValue == null ? 0.0d : (Double) previousAccumulatedValue; + if (value instanceof Double) { + isDouble = true; + return accumulator + (Double) value; + } + if (value instanceof Float) { + isDouble = true; + return accumulator + (Float) value; + } + if (value instanceof Long) { + return accumulator + (Long) value; + } + if (value instanceof Integer) { + return accumulator + (Integer) value; + } + throw new IllegalArgumentException("Field " + unsplitField + " isn't a Double, Float, Long or Integer"); + } + + @Override + public Object result(Object accumulatedValue) { + if (accumulatedValue != null) + return isDouble ? accumulatedValue : ((Double) accumulatedValue).longValue(); + else + return null; + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/utils/FieldValueExtractor.java b/jaggr/src/main/java/com/caffinc/jaggr/core/utils/FieldValueExtractor.java new file mode 100644 index 0000000..c34b5b6 --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/utils/FieldValueExtractor.java @@ -0,0 +1,48 @@ +package com.caffinc.jaggr.core.utils; + +import java.util.Map; + +/** + * Extracts a nested field value from a Map + * + * @author Sriram + * @since 11/26/2016 + */ +public class FieldValueExtractor { + + /** + * Extracts a nested value for the specified field from the given object + * + * @param field Field to extract + * @param object Object to extract from + * @return Extracted value + */ + public static Object getValue(String field, Map object) { + return getValue(field.split("\\."), object); + } + + /** + * Extracts a nested value for the specified nested field from the given object + * + * @param split Nested field to extract + * @param object Object to extract from + * @return Extracted value + */ + public static Object getValue(String[] split, Map object) { + return getValue(split, 0, object); + } + + private static Object getValue(String[] split, int i, Map object) { + if (i == split.length - 1) { + // This is the value we need, extract it + return object.get(split[i]); + } else { + // Go deeper if possible, else return null + if (object.containsKey(split[i]) && object.get(split[i]) instanceof Map) { + return getValue(split, i + 1, (Map) object.get(split[i])); + } else { + return null; + } + } + } +} diff --git a/jaggr/src/main/java/com/caffinc/jaggr/core/utils/JsonFileReader.java b/jaggr/src/main/java/com/caffinc/jaggr/core/utils/JsonFileReader.java new file mode 100644 index 0000000..fc5ac22 --- /dev/null +++ b/jaggr/src/main/java/com/caffinc/jaggr/core/utils/JsonFileReader.java @@ -0,0 +1,74 @@ +package com.caffinc.jaggr.core.utils; + +import com.google.gson.Gson; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Reads a JSON file + * + * @author Sriram + * @since 11/26/2016 + */ +public class JsonFileReader { + private static final Gson gson = new Gson(); + + /** + * Reads lines from a resource file and converts it to JSON + * + * @param resourceName Resource to read from + * @return List of JSON lines from the file + * @throws IOException thrown if there is a problem accessing the file + */ + public static List> readJsonFromResource(String resourceName) throws IOException { + List> jsonList = new ArrayList<>(); + String json; + try (BufferedReader br = new BufferedReader( + new InputStreamReader(JsonFileReader.class.getClassLoader().getResourceAsStream(resourceName)))) { + while ((json = br.readLine()) != null) { + jsonList.add(gson.fromJson(json, HashMap.class)); + } + } + return jsonList; + } + + /** + * Reads lines from a text file and converts it to JSON + * + * @param fileName File name to read from + * @return List of JSON lines from the file + * @throws IOException thrown if there is a problem accessing the file + */ + public static List> readJsonFromFile(String fileName) throws IOException { + List> jsonList = new ArrayList<>(); + for (String json : getFileLines(fileName)) { + jsonList.add(gson.fromJson(json, HashMap.class)); + } + return jsonList; + } + + /** + * Reads lines from a text file + * + * @param fileName File name to read from + * @return List of lines from the file + * @throws IOException thrown if there is a problem accessing the file + */ + public static List getFileLines(String fileName) throws IOException { + List text = new ArrayList<>(); + try (BufferedReader br = new BufferedReader(new FileReader(fileName))) { + String line; + while ((line = br.readLine()) != null) { + text.add(line); + } + } + return text; + } +} diff --git a/jaggr/src/test/java/com/caffinc/jaggr/core/AggregationBuilderTest.java b/jaggr/src/test/java/com/caffinc/jaggr/core/AggregationBuilderTest.java new file mode 100644 index 0000000..6d272f8 --- /dev/null +++ b/jaggr/src/test/java/com/caffinc/jaggr/core/AggregationBuilderTest.java @@ -0,0 +1,205 @@ +package com.caffinc.jaggr.core; + +import com.caffinc.jaggr.core.operations.*; +import com.caffinc.jaggr.core.utils.FieldValueExtractor; +import com.caffinc.jaggr.core.utils.JsonFileReader; +import com.google.gson.Gson; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.assertEquals; + +/** + * Tests the Aggregation Builder + * + * @author Sriram + * @since 11/26/2016 + */ +public class AggregationBuilderTest { + private static final Gson gson = new Gson(); + private static List> jsonList = new ArrayList<>(); + + private static T roughen(Object o, Class t) { + return gson.fromJson(gson.toJson(o), t); + } + + @BeforeClass + public static void setUp() throws Exception { + jsonList = JsonFileReader.readJsonFromResource("raw.json"); + } + + @Test + public void testSimpleGrouping() throws Exception { + String field = "f"; + Set expectedResult = new HashSet<>(); + for (Map obj : jsonList) { + expectedResult.add(String.valueOf(obj.get(field))); + } + + Set result = new HashSet<>(); + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field).getAggregation(); + List> resultList = aggregation.aggregate(jsonList); + for (Map resultObj : resultList) { + result.add(resultObj.get("_id")); + } + + assertEquals("Grouping by ID should match", expectedResult, result); + } + + @Test + public void testNestedGrouping() throws Exception { + String field = "test.f"; + Set expectedResult = new HashSet<>(); + for (Map obj : jsonList) { + expectedResult.add(String.valueOf(FieldValueExtractor.getValue(field, obj))); + } + + Set result = new HashSet<>(); + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field).getAggregation(); + List> resultList = aggregation.aggregate(jsonList); + for (Map resultObj : resultList) { + result.add(resultObj.get("_id")); + } + + assertEquals("Grouping by field should match", expectedResult, result); + } + + @Test + public void testCountOperation() throws Exception { + String field = "f"; + Set> expected = new HashSet<>(JsonFileReader.readJsonFromResource("countResult.json")); + + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field) + .addOperation("count", new CountOperation()) + .getAggregation(); + Set> result = roughen(aggregation.aggregate(jsonList), HashSet.class); + assertEquals("Counts should be as expected", expected, result); + } + + @Test + public void testMaxOperation() throws Exception { + String field = "f"; + String maxField = "test.f"; + Set> expected = new HashSet<>(JsonFileReader.readJsonFromResource("maxResult.json")); + + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field) + .addOperation("max", new MaxOperation(maxField)) + .getAggregation(); + Set> result = roughen(aggregation.aggregate(jsonList), HashSet.class); + assertEquals("Max should be as expected", expected, result); + } + + @Test + public void testMinOperation() throws Exception { + String field = "f"; + String minField = "test.f"; + Set> expected = new HashSet<>(JsonFileReader.readJsonFromResource("minResult.json")); + + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field) + .addOperation("min", new MinOperation(minField)) + .getAggregation(); + Set> result = roughen(aggregation.aggregate(jsonList), HashSet.class); + assertEquals("Min should be as expected", expected, result); + } + + @Test + public void testCollectOperation() throws Exception { + String field = "f"; + String collectField = "_id"; + Set> expected = new HashSet<>(JsonFileReader.readJsonFromResource("collectResult.json")); + + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field) + .addOperation("list", new CollectOperation(collectField)) + .getAggregation(); + Set> result = roughen(aggregation.aggregate(jsonList), HashSet.class); + assertEquals("Collected lists should be as expected", expected, result); + } + + @Test + public void testCollectSetOperation() throws Exception { + String field = "f"; + String collectField = "test.f"; + Map expectedMap = new HashMap<>(); + for (Map expectedObject : JsonFileReader.readJsonFromResource("collectSetResult.json")) { + expectedMap.put(String.valueOf(expectedObject.get("_id")), new HashSet((List) expectedObject.get("set"))); + } + + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field) + .addOperation("set", new CollectSetOperation(collectField)) + .getAggregation(); + Map resultMap = new HashMap<>(); + for (Map resultObject : (Set>) roughen(aggregation.aggregate(jsonList), HashSet.class)) { + resultMap.put(String.valueOf(resultObject.get("_id")), new HashSet((List) resultObject.get("set"))); + } + assertEquals("Collected sets should be as expected", expectedMap, resultMap); + } + + @Test + public void testSumOperation() throws Exception { + String field = "f"; + String sumField = "test.f"; + Set> expected = new HashSet<>(JsonFileReader.readJsonFromResource("sumResult.json")); + + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field) + .addOperation("sum", new SumOperation(sumField)) + .getAggregation(); + Set> result = roughen(aggregation.aggregate(jsonList), HashSet.class); + assertEquals("Sum should be as expected", expected, result); + } + + @Test + public void testAverageOperation() throws Exception { + String field = "f"; + String avgField = "test.f"; + Set> expected = new HashSet<>(JsonFileReader.readJsonFromResource("avgResult.json")); + + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field) + .addOperation("avg", new AverageOperation(avgField)) + .getAggregation(); + Set> result = roughen(aggregation.aggregate(jsonList), HashSet.class); + assertEquals("Average should be as expected", expected, result); + } + + @Test + public void testOperationWithoutGrouping() throws Exception { + Set> expected = new HashSet<>(JsonFileReader.readJsonFromResource("grouplessResult.json")); + + Aggregation aggregation = new AggregationBuilder() + .addOperation("count", new CountOperation()) + .getAggregation(); + Set> result = roughen(aggregation.aggregate(jsonList), HashSet.class); + assertEquals("Counts without grouping should be as expected", expected, result); + } + + @Test + public void testMultiOperation() throws Exception { + String field = "f"; + String avgField = "test.f"; + String sumField = "test.f"; + String minField = "test.f"; + String maxField = "test.f"; + Set> expected = new HashSet<>(JsonFileReader.readJsonFromResource("multiResult.json")); + + Aggregation aggregation = new AggregationBuilder() + .setGroupBy(field) + .addOperation("avg", new AverageOperation(avgField)) + .addOperation("sum", new SumOperation(sumField)) + .addOperation("min", new MinOperation(minField)) + .addOperation("max", new MaxOperation(maxField)) + .addOperation("count", new CountOperation()) + .getAggregation(); + Set> result = roughen(aggregation.aggregate(jsonList), HashSet.class); + assertEquals("Multiple aggregations result should be as expected", expected, result); + } +} diff --git a/jaggr/src/test/java/com/caffinc/jaggr/core/utils/FieldValueExtractorTest.java b/jaggr/src/test/java/com/caffinc/jaggr/core/utils/FieldValueExtractorTest.java new file mode 100644 index 0000000..a0e718d --- /dev/null +++ b/jaggr/src/test/java/com/caffinc/jaggr/core/utils/FieldValueExtractorTest.java @@ -0,0 +1,41 @@ +package com.caffinc.jaggr.core.utils; + +import com.google.gson.Gson; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * Tests the FieldValueExtractor + * + * @author Sriram + * @since 11/26/2016 + */ +public class FieldValueExtractorTest { + private static final Gson gson = new Gson(); + + @Test + public void testSimpleFieldExtraction() throws Exception { + Map obj = gson.fromJson("{\"_id\" : \"a\"}", HashMap.class); + Assert.assertEquals("Value extracted should match value in the JSON", + "a", + FieldValueExtractor.getValue("_id", obj)); + } + + @Test + public void testNestedFieldExtraction() throws Exception { + Map obj = gson.fromJson("{\"l1\" : {\"l2\" : \"a\"}}", HashMap.class); + Assert.assertEquals("Value extracted should match value in the JSON", + "a", + FieldValueExtractor.getValue("l1.l2", obj)); + } + + @Test + public void testNonexistentFieldExtraction() throws Exception { + Map obj = gson.fromJson("{\"l1\" : {\"l2\" : \"a\"}}", HashMap.class); + Assert.assertNull("Non-existent value should be extracted as null from the JSON", + FieldValueExtractor.getValue("l1.l3", obj)); + } +} diff --git a/jaggr/src/test/resources/avgResult.json b/jaggr/src/test/resources/avgResult.json new file mode 100644 index 0000000..3df455b --- /dev/null +++ b/jaggr/src/test/resources/avgResult.json @@ -0,0 +1,2 @@ +{"_id": "a", "avg": 2.0} +{"_id": "b", "avg": 1.0} diff --git a/jaggr/src/test/resources/collectResult.json b/jaggr/src/test/resources/collectResult.json new file mode 100644 index 0000000..5f519c3 --- /dev/null +++ b/jaggr/src/test/resources/collectResult.json @@ -0,0 +1,2 @@ +{"_id": "a", "list": [1,2,3,4,5]} +{"_id": "b", "list": [6,7,8,9,10]} diff --git a/jaggr/src/test/resources/collectSetResult.json b/jaggr/src/test/resources/collectSetResult.json new file mode 100644 index 0000000..1b18bf9 --- /dev/null +++ b/jaggr/src/test/resources/collectSetResult.json @@ -0,0 +1,2 @@ +{"_id": "a", "set": [3,2,1,5,-1]} +{"_id": "b", "set": [1]} diff --git a/jaggr/src/test/resources/countResult.json b/jaggr/src/test/resources/countResult.json new file mode 100644 index 0000000..60851e1 --- /dev/null +++ b/jaggr/src/test/resources/countResult.json @@ -0,0 +1,2 @@ +{"_id": "a", "count": 5} +{"_id": "b", "count": 5} diff --git a/jaggr/src/test/resources/grouplessResult.json b/jaggr/src/test/resources/grouplessResult.json new file mode 100644 index 0000000..c1a80ed --- /dev/null +++ b/jaggr/src/test/resources/grouplessResult.json @@ -0,0 +1 @@ +{"_id": "0", "count": 10} diff --git a/jaggr/src/test/resources/maxResult.json b/jaggr/src/test/resources/maxResult.json new file mode 100644 index 0000000..608c3e4 --- /dev/null +++ b/jaggr/src/test/resources/maxResult.json @@ -0,0 +1,2 @@ +{"_id": "a", "max": 5} +{"_id": "b", "max": 1} diff --git a/jaggr/src/test/resources/minResult.json b/jaggr/src/test/resources/minResult.json new file mode 100644 index 0000000..2abf79a --- /dev/null +++ b/jaggr/src/test/resources/minResult.json @@ -0,0 +1,2 @@ +{"_id": "a", "min": -1} +{"_id": "b", "min": 1} diff --git a/jaggr/src/test/resources/multiResult.json b/jaggr/src/test/resources/multiResult.json new file mode 100644 index 0000000..18bde28 --- /dev/null +++ b/jaggr/src/test/resources/multiResult.json @@ -0,0 +1,2 @@ +{"_id": "a", "avg": 2.0, "sum": 10, "min": -1, "max": 5, "count": 5} +{"_id": "b", "avg": 1.0, "sum": 5, "min": 1, "max": 1, "count": 5} diff --git a/jaggr/src/test/resources/raw.json b/jaggr/src/test/resources/raw.json new file mode 100644 index 0000000..f967260 --- /dev/null +++ b/jaggr/src/test/resources/raw.json @@ -0,0 +1,10 @@ +{"_id": 1, "f": "a", "test": {"f": 3}} +{"_id": 2, "f": "a", "test": {"f": 2}} +{"_id": 3, "f": "a", "test": {"f": 1}} +{"_id": 4, "f": "a", "test": {"f": 5}} +{"_id": 5, "f": "a", "test": {"f": -1}} +{"_id": 6, "f": "b", "test": {"f": 1}} +{"_id": 7, "f": "b", "test": {"f": 1}} +{"_id": 8, "f": "b", "test": {"f": 1}} +{"_id": 9, "f": "b", "test": {"f": 1}} +{"_id": 10, "f": "b", "test": {"f": 1}} diff --git a/jaggr/src/test/resources/sumResult.json b/jaggr/src/test/resources/sumResult.json new file mode 100644 index 0000000..5f9a8e3 --- /dev/null +++ b/jaggr/src/test/resources/sumResult.json @@ -0,0 +1,2 @@ +{"_id": "a", "sum": 10} +{"_id": "b", "sum": 5} diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 006251c..0000000 --- a/pom.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - 4.0.0 - - com.caffinc - jaggr - 1.0-SNAPSHOT - - - \ No newline at end of file