Repository: gchq/Gaffer (java)

Issue:
ApplyViewToElementsFunction does not apply Schema validation
**Describe the bug**
ApplyViewToElementsFunction will apply the View of an operation to the results from multiple subgraphs. This is meant to ensure that the results are correctly aggregated and validated after a query to a FederatedStore.

However, despite the View validation being applied, **the Schema validation is not applied**. This could be intended, but I suspect it is not, as the intention of this merge function is to ensure all aggregation and validation happens as if the results came from one graph. If this is indeed unintended, I would suggest the merge function should be renamed to something like ValidateAndAggregateElements.

**Additional context**
The reason this happens is that the results are currently merged inside a MapStore and the MapStore does not currently support schema validation: https://github.com/gchq/Gaffer/issues/2613.

Some potential fixes are:
- Add MapStore validation as suggested in https://github.com/gchq/Gaffer/issues/2613.
- Change the ApplyViewToElementsFunction to create an ephemeral AccumuloStore rather than MapStore



Pull request:
Gh 3059 pr refactor
# Related issue

- Resolve #3059

Patch:
diff --git a/core/data/src/main/java/uk/gov/gchq/gaffer/data/elementdefinition/view/View.java b/core/data/src/main/java/uk/gov/gchq/gaffer/data/elementdefinition/view/View.java
index d6447c3a7c..08c198d941 100644
--- a/core/data/src/main/java/uk/gov/gchq/gaffer/data/elementdefinition/view/View.java
+++ b/core/data/src/main/java/uk/gov/gchq/gaffer/data/elementdefinition/view/View.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2023 Crown Copyright
+ * Copyright 2016-2024 Crown Copyright
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -46,6 +46,7 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
 
@@ -473,7 +474,14 @@ public class View extends ElementDefinitions<ViewElementDefinition, ViewElementD
         @Override
         @JsonIgnore
         public CHILD_CLASS merge(final View view) {
-            if (null != view) {
+            return view == null ? merge(Optional.empty()) : merge(Optional.of(view));
+
+        }
+
+        @JsonIgnore
+        public CHILD_CLASS merge(final Optional<View> viewOptional) {
+            if (viewOptional.isPresent()) {
+                final View view = viewOptional.get();
                 if (!(getThisView().canMerge(view, getThisView()) && view.canMerge(view, getThisView()))) {
                     throw new IllegalArgumentException("A " + view.getClass().getSimpleName() +
                             " cannot be merged into a " + getThisView().getClass().getSimpleName());
@@ -563,6 +571,11 @@ public class View extends ElementDefinitions<ViewElementDefinition, ViewElementD
             merge(view);
         }
 
+        public Builder(final Optional<View> view) {
+            this();
+            merge(view);
+        }
+
         @Override
         protected Builder self() {
             return this;
diff --git a/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/FederatedStoreUtil.java b/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/FederatedStoreUtil.java
index b543d8336b..bd2e8264bb 100644
--- a/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/FederatedStoreUtil.java
+++ b/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/FederatedStoreUtil.java
@@ -59,6 +59,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.regex.Pattern;
@@ -66,11 +67,11 @@ import java.util.stream.Collectors;
 
 import static java.util.Objects.isNull;
 import static java.util.Objects.nonNull;
-import static uk.gov.gchq.gaffer.federatedstore.util.MergeElementFunction.SCHEMA;
-import static uk.gov.gchq.gaffer.federatedstore.util.MergeElementFunction.USER;
-import static uk.gov.gchq.gaffer.federatedstore.util.MergeElementFunction.VIEW;
 
 public final class FederatedStoreUtil {
+    public static final String SCHEMA = "schema";
+    public static final String VIEW = "view";
+    public static final String USER = "user";
     private static final Logger LOGGER = LoggerFactory.getLogger(FederatedStoreUtil.class);
     private static final String SCHEMA_DEL_REGEX = Pattern.quote(",");
 
@@ -342,7 +343,7 @@ public final class FederatedStoreUtil {
                         .graphIds(graphIds)
                         .build(), operationContext);
 
-                functionContext.put(SCHEMA, schema);
+                putSchema(functionContext, schema);
             } catch (final OperationException e) {
                 throw new GafferCheckedException(String.format("Error getting Schema for SpecificMergeFunction, due to:%s", e.getMessage()), e);
             }
@@ -415,6 +416,23 @@ public final class FederatedStoreUtil {
         }
 
 
+
+    }
+    public static void putSchema(final HashMap<String, Object> functionContext, final Schema schema) {
+        functionContext.put(SCHEMA, schema);
+    }
+
+    public static Optional<Schema> getSchema(final Map<String, Object> context) {
+        final Object o = context.get(SCHEMA);
+        return o instanceof Schema ? Optional.of((Schema) o) : Optional.empty();
     }
 
+    public static Optional<View> getView(final Map<String, Object> context) {
+        final Object o = context.get(VIEW);
+        return o instanceof View ? Optional.of((View) o) : Optional.empty();
+    }
+
+    public static boolean containsUser(final Map<String, Object> context) {
+        return context.containsKey(USER);
+    }
 }
diff --git a/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/MergeElementFunction.java b/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/MergeElementFunction.java
index 1df3498ceb..4f660378ba 100644
--- a/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/MergeElementFunction.java
+++ b/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/MergeElementFunction.java
@@ -49,10 +49,18 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Random;
 import java.util.Set;
 import java.util.stream.Stream;
 
+import static uk.gov.gchq.gaffer.federatedstore.util.FederatedStoreUtil.SCHEMA;
+import static uk.gov.gchq.gaffer.federatedstore.util.FederatedStoreUtil.USER;
+import static uk.gov.gchq.gaffer.federatedstore.util.FederatedStoreUtil.VIEW;
+import static uk.gov.gchq.gaffer.federatedstore.util.FederatedStoreUtil.containsUser;
+import static uk.gov.gchq.gaffer.federatedstore.util.FederatedStoreUtil.getSchema;
+import static uk.gov.gchq.gaffer.federatedstore.util.FederatedStoreUtil.getView;
+
 /**
  * This class is used to address some of the issues with having the same element groups distributed amongst multiple graphs.
  * Such as the re-application of View filter or Schema Validation after the local aggregation of results from multiple graphs.
@@ -61,9 +69,6 @@ import java.util.stream.Stream;
  */
 public class MergeElementFunction implements ContextSpecificMergeFunction<Object, Iterable<Object>, Iterable<Object>> {
     private static final Logger LOGGER = LoggerFactory.getLogger(MergeElementFunction.class);
-    public static final String VIEW = "view";
-    public static final String SCHEMA = "schema";
-    public static final String USER = "user";
     public static final String TEMP_RESULTS_GRAPH = "temporaryResultsGraph";
     private static final Random RANDOM = new Random();
 
@@ -73,28 +78,19 @@ public class MergeElementFunction implements ContextSpecificMergeFunction<Object
     public MergeElementFunction() {
     }
 
-    public MergeElementFunction(final Map<String, Object> context) throws GafferCheckedException {
-        this();
+    @Override
+    public MergeElementFunction createFunctionWithContext(final HashMap<String, Object> context) throws GafferCheckedException {
+        final MergeElementFunction mergeElementFunction = new MergeElementFunction();
+
         try {
-            // Check if results graph, hasn't already be supplied, otherwise make a default results graph.
-            if (!context.containsKey(TEMP_RESULTS_GRAPH)) {
-                final Graph resultsGraph = new Graph.Builder()
-                        .config(new GraphConfig(String.format("%s%s%d", TEMP_RESULTS_GRAPH, MergeElementFunction.class.getSimpleName(), RANDOM.nextInt(Integer.MAX_VALUE))))
-                        .addSchema((Schema) context.get(SCHEMA))
-                        //MapStore easy in memory Store. Large results size may not be suitable, a graph could be provided via Context.
-                        .addStoreProperties(new MapStoreProperties())
-                        .build();
-
-                LOGGER.debug("A Temporary results graph named:{} is being made with schema:{}", resultsGraph.getGraphId(), resultsGraph.getSchema());
-
-                context.put(TEMP_RESULTS_GRAPH, resultsGraph);
-            }
-            // Validate the supplied context before using
             validate(context);
 
+            makeTempResultsGraphIfRequired(context);
+
             updateViewWithValidationFromSchema(context);
 
-            this.context = Collections.unmodifiableMap(context);
+            mergeElementFunction.context = Collections.unmodifiableMap(context);
+            return mergeElementFunction;
         } catch (final Exception e) {
             throw new GafferCheckedException("Unable to create TemporaryResultsGraph", e);
         }
@@ -103,25 +99,37 @@ public class MergeElementFunction implements ContextSpecificMergeFunction<Object
 
     private static void updateViewWithValidationFromSchema(final Map<String, Object> context) {
         //Only do this for MapStore, not required for other stores.
-        if (MapStore.class.getName().equals(getGraph(context).getStoreProperties().getStoreClass())) {
+        final Optional<Graph> graph = getGraph(context);
+        if (graph.isPresent() && MapStore.class.getName().equals(graph.get().getStoreProperties().getStoreClass())) {
             //Update View with
-            final View view = (View) context.get(VIEW);
-            final Schema schema = (Schema) context.get(SCHEMA);
-            final View.Builder updatedView = new View.Builder(view);
+            final Optional<View> view = getView(context);
+            final Schema schema = getSchema(context).orElseThrow(() -> new IllegalStateException("No Schema was found from context, which should have been validated"));
+            final View.Builder editableView = new View.Builder(view);
 
             //getUpdatedDefs and add to new view.
             getUpdatedViewDefsFromSchemaDefs(schema.getEdges(), view)
-                    .forEach(e -> updatedView.edge(e.getKey(), e.getValue()));
+                    .forEach(e -> editableView.edge(e.getKey(), e.getValue()));
             getUpdatedViewDefsFromSchemaDefs(schema.getEntities(), view)
-                    .forEach(e -> updatedView.entity(e.getKey(), e.getValue()));
+                    .forEach(e -> editableView.entity(e.getKey(), e.getValue()));
 
-            context.put(VIEW, updatedView.build());
+            context.put(VIEW, editableView.build());
         }
     }
 
-    @Override
-    public MergeElementFunction createFunctionWithContext(final HashMap<String, Object> context) throws GafferCheckedException {
-        return new MergeElementFunction(context);
+    private static void makeTempResultsGraphIfRequired(final HashMap<String, Object> context) {
+        // Check if results graph, hasn't already be supplied, otherwise make a default results graph.
+        if (!containsTempResultsGraph(context)) {
+            final Graph resultsGraph = new Graph.Builder()
+                    .config(new GraphConfig(String.format("%s%s%d", TEMP_RESULTS_GRAPH, MergeElementFunction.class.getSimpleName(), RANDOM.nextInt(Integer.MAX_VALUE))))
+                    .addSchema((Schema) context.get(SCHEMA))
+                    //MapStore easy in memory Store. Large results size may not be suitable, a graph could be provided via Context.
+                    .addStoreProperties(new MapStoreProperties())
+                    .build();
+
+            LOGGER.debug("A Temporary results graph named:{} is being made with schema:{}", resultsGraph.getGraphId(), resultsGraph.getSchema());
+
+            context.put(TEMP_RESULTS_GRAPH, resultsGraph);
+        }
     }
 
     /**
@@ -129,39 +137,54 @@ public class MergeElementFunction implements ContextSpecificMergeFunction<Object
      *
      * @param context The context e.g. view, schema and user
      */
-    private static void validate(final Map<String, Object> context) {
-        View view = (View) context.get(VIEW);
-        if (view != null && view.hasTransform()) {
-            throw new UnsupportedOperationException("Error: context invalid: can not use this function with a POST AGGREGATION TRANSFORM VIEW, " +
-                    "because transformation may have created items that does not exist in the schema. " +
-                    "The re-applying of the View to the collected federated results would not be be possible. " +
-                    "Try a simple concat merge that doesn't require the re-application of view");
-            // Solution is to derive and use the "Transformed schema" from the uk.gov.gchq.gaffer.data.elementdefinition.view.ViewElementDefinition.
+    public static void validate(final Map<String, Object> context) {
+        if (!containsUser(context)) {
+            throw new IllegalArgumentException("Error: context invalid, requires a User");
         }
 
-        Schema schema = (Schema) context.get(SCHEMA);
-        if (schema == null || !schema.hasGroups()) {
-            throw new IllegalArgumentException("Error: context invalid, requires a populated schema.");
-        }
+        if (!containsTempResultsGraph(context)) {
+            if (!containsValidView(context)) {
+                throw new UnsupportedOperationException("Error: context invalid: can not use this function with a POST AGGREGATION TRANSFORM VIEW, " +
+                        "because transformation may have created items that does not exist in the schema. " +
+                        "The re-applying of the View to the collected federated results would not be be possible. " +
+                        "Try a simple concat merge that doesn't require the re-application of view");
+                // Solution is to derive and use the "Transformed schema" from the uk.gov.gchq.gaffer.data.elementdefinition.view.ViewElementDefinition.
+            }
+
+            if (!containsValidSchema(context)) {
+                throw new IllegalArgumentException("Error: context invalid, requires a populated schema.");
+            }
 
-        if (!context.containsKey(TEMP_RESULTS_GRAPH)) {
-            throw new IllegalStateException("Error: context invalid, did not contain a Temporary Results Graph.");
-        } else if (!(context.get(TEMP_RESULTS_GRAPH) instanceof Graph)
-                && !(context.get(TEMP_RESULTS_GRAPH) instanceof GraphSerialisable)) {
+        } else if (!containsValidTempResultsGraph(context)) {
             throw new IllegalArgumentException(String.format("Error: context invalid, value for %s was not a Graph, found: %s", TEMP_RESULTS_GRAPH, context.get(TEMP_RESULTS_GRAPH)));
         }
+    }
 
-        if (!context.containsKey(USER)) {
-            throw new IllegalArgumentException("Error: context invalid, requires a User");
-        }
+    private static boolean containsValidTempResultsGraph(final Map<String, Object> context) {
+        final Object o = context.get(TEMP_RESULTS_GRAPH);
+        return o instanceof Graph || o instanceof GraphSerialisable;
+    }
+
+    private static boolean containsTempResultsGraph(final Map<String, Object> context) {
+        return context.containsKey(TEMP_RESULTS_GRAPH);
     }
 
-    private static Stream<Map.Entry<String, ViewElementDefinition>> getUpdatedViewDefsFromSchemaDefs(final Map<String, ? extends SchemaElementDefinition> groupDefs, final View view) {
+    private static boolean containsValidSchema(final Map<String, Object> context) {
+        final Optional<Schema> schema = getSchema(context);
+        return schema.isPresent() && (schema.get()).hasGroups();
+    }
+
+    private static boolean containsValidView(final Map<String, Object> context) {
+        final Optional<View> view = getView(context);
+        return !view.isPresent() || !view.get().hasTransform();
+    }
+
+    private static Stream<Map.Entry<String, ViewElementDefinition>> getUpdatedViewDefsFromSchemaDefs(final Map<String, ? extends SchemaElementDefinition> groupDefs, final Optional<View> view) {
         return groupDefs.entrySet().stream()
                 .map(e -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), getUpdatedViewDefFromSchemaDef(e.getKey(), e.getValue(), view)));
     }
 
-    private static ViewElementDefinition getUpdatedViewDefFromSchemaDef(final String groupName, final SchemaElementDefinition schemaElementDef, final View view) {
+    private static ViewElementDefinition getUpdatedViewDefFromSchemaDef(final String groupName, final SchemaElementDefinition schemaElementDef, final Optional<View> view) {
         final ViewElementDefinition.Builder updatePreAggregationFilter;
         final ArrayList<TupleAdaptedPredicate<String, ?>> updatedFilterFunctions = new ArrayList<>();
 
@@ -170,8 +193,8 @@ public class MergeElementFunction implements ContextSpecificMergeFunction<Object
             updatedFilterFunctions.addAll(schemaElementDef.getValidator().getComponents());
         }
 
-        if (view != null) {
-            final ViewElementDefinition viewElementDef = view.getElement(groupName);
+        if (view.isPresent()) {
+            final ViewElementDefinition viewElementDef = view.get().getElement(groupName);
             //Add View Validation
             if (viewElementDef != null && viewElementDef.hasPostAggregationFilters()) {
                 updatedFilterFunctions.addAll(viewElementDef.getPostAggregationFilter().getComponents());
@@ -205,12 +228,14 @@ public class MergeElementFunction implements ContextSpecificMergeFunction<Object
             }
         }
 
-        final Graph resultsGraph = getGraph(context);
+        final Graph resultsGraph;
         final Context userContext = new Context((User) context.get(USER));
+
         try {
+            resultsGraph = getGraph(context).orElseThrow(() -> new GafferCheckedException("No results Graph available for merging "));
             // The update object might be a lazy AccumuloElementRetriever and might be MASSIVE.
             resultsGraph.execute(new AddElements.Builder().input((Iterable<Element>) update).build(), userContext);
-        } catch (final OperationException e) {
+        } catch (final GafferCheckedException e) {
             throw new GafferRuntimeException("Error adding elements to temporary results graph, due to:" + e.getMessage(), e);
         }
 
@@ -221,11 +246,16 @@ public class MergeElementFunction implements ContextSpecificMergeFunction<Object
         }
     }
 
-    private static Graph getGraph(final Map<String, Object> context) {
+    private static Optional<Graph> getGraph(final Map<String, Object> context) {
         final Object o = context.get(TEMP_RESULTS_GRAPH);
-        final Graph resultsGraph = o instanceof GraphSerialisable
-                ? ((GraphSerialisable) o).getGraph()
-                : (Graph) o;
+        final Optional<Graph> resultsGraph;
+        if (o instanceof GraphSerialisable) {
+            resultsGraph = Optional.of(((GraphSerialisable) o).getGraph());
+        } else if (o instanceof Graph) {
+            resultsGraph = Optional.of((Graph) o);
+        } else {
+            resultsGraph = Optional.empty();
+        }
         return resultsGraph;
     }
 }
diff --git a/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/MergeSchema.java b/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/MergeSchema.java
index 65b2dd4565..e7362d2904 100644
--- a/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/MergeSchema.java
+++ b/store-implementation/federated-store/src/main/java/uk/gov/gchq/gaffer/federatedstore/util/MergeSchema.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2023 Crown Copyright
+ * Copyright 2022-2024 Crown Copyright
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@ package uk.gov.gchq.gaffer.federatedstore.util;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import uk.gov.gchq.gaffer.core.exception.GafferCheckedException;
 import uk.gov.gchq.gaffer.data.elementdefinition.exception.SchemaException;
 import uk.gov.gchq.gaffer.serialisation.Serialiser;
 import uk.gov.gchq.gaffer.store.schema.Schema;
@@ -29,6 +28,7 @@ import uk.gov.gchq.gaffer.store.schema.exception.VisibilityPropertySchemaExcepti
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Map;
 import java.util.Set;
 import java.util.function.BiFunction;
 
@@ -43,16 +43,11 @@ public class MergeSchema implements BiFunction<Schema, Schema, Schema>, ContextS
     public static final String FORMAT_CAUGHT_SCHEMA_EXCEPTION_ATTEMPTING_TO_RE_MERGE_BUT_WITHOUT_S_ERROR_MESSAGE_S = "Caught SchemaException, attempting to re-merge but without %s. Error message:%s";
     public static final String MERGE_FUNCTION_UNABLE_TO_RECOVER_FROM_ERROR_DUE_TO = MergeSchema.class.getSimpleName() + " function unable to recover from error, due to: ";
     public static final String MATCHING_ELEMENT_GROUPS_HAVING_NO_SHARED_PROPERTIES_CAUSED_BY = "Matching element groups having no shared properties, caused by:";
-    private HashMap<String, Object> context;
+    private Map<String, Object> context;
 
     public MergeSchema() {
     }
 
-    public MergeSchema(final HashMap<String, Object> context) {
-        this();
-        this.context = new HashMap<>(validate(context));
-    }
-
     public Schema apply(final Schema update, final Schema state) {
         if (state == null) {
             return nonNull(update) ? update : new Schema();
@@ -109,9 +104,10 @@ public class MergeSchema implements BiFunction<Schema, Schema, Schema>, ContextS
         }
     }
 
-    @Override
-    public ContextSpecificMergeFunction<Schema, Schema, Schema> createFunctionWithContext(final HashMap<String, Object> context) throws GafferCheckedException {
-        return new MergeSchema(context);
+    public MergeSchema createFunctionWithContext(final HashMap<String, Object> context) {
+        final MergeSchema function = new MergeSchema();
+        function.context = new HashMap<>(validate(context));
+        return function;
     }
 
 
@@ -120,7 +116,7 @@ public class MergeSchema implements BiFunction<Schema, Schema, Schema>, ContextS
         return Collections.emptySet();
     }
 
-    private static HashMap<String, Object> validate(final HashMap<String, Object> context) {
+    private static Map<String, Object> validate(final Map<String, Object> context) {
         return context;
     }
 }


Test patch:
diff --git a/core/data/src/test/java/uk/gov/gchq/gaffer/data/elementdefinition/view/ViewTest.java b/core/data/src/test/java/uk/gov/gchq/gaffer/data/elementdefinition/view/ViewTest.java
index db63733bf7..d382dd39cc 100644
--- a/core/data/src/test/java/uk/gov/gchq/gaffer/data/elementdefinition/view/ViewTest.java
+++ b/core/data/src/test/java/uk/gov/gchq/gaffer/data/elementdefinition/view/ViewTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2023 Crown Copyright
+ * Copyright 2016-2024 Crown Copyright
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -1254,7 +1255,7 @@ public class ViewTest extends JSONSerialisationTest<View> {
         // Then
         assertThat(view.hasPreAggregationFilters()).isTrue();
         assertThat(view.getEntity(TestGroups.ENTITY).getPreAggregationFilter()
-                        .getComponents().get(0).getPredicate())
+                .getComponents().get(0).getPredicate())
                 .isExactlyInstanceOf(Exists.class);
     }
 
@@ -1287,10 +1288,10 @@ public class ViewTest extends JSONSerialisationTest<View> {
         // Then
         assertThat(view.hasPreAggregationFilters()).isTrue();
         assertThat(view.getEntity(TestGroups.ENTITY).getPreAggregationFilter()
-                        .getComponents().get(0).getPredicate())
+                .getComponents().get(0).getPredicate())
                 .isExactlyInstanceOf(Exists.class);
         assertThat(view.getEntity(TestGroups.ENTITY).getPreAggregationFilter()
-                        .getComponents().get(1).getPredicate())
+                .getComponents().get(1).getPredicate())
                 .isExactlyInstanceOf(ExampleFilterFunction.class);
     }
 
@@ -1316,7 +1317,7 @@ public class ViewTest extends JSONSerialisationTest<View> {
         // Then
         assertThat(view.hasPostAggregationFilters()).isTrue();
         assertThat(view.getEntity(TestGroups.ENTITY).getPostAggregationFilter()
-                        .getComponents().get(0).getPredicate())
+                .getComponents().get(0).getPredicate())
                 .isExactlyInstanceOf(Exists.class);
     }
 
@@ -1349,10 +1350,10 @@ public class ViewTest extends JSONSerialisationTest<View> {
         // Then
         assertThat(view.hasPostAggregationFilters()).isTrue();
         assertThat(view.getEntity(TestGroups.ENTITY).getPostAggregationFilter()
-                        .getComponents().get(0).getPredicate())
+                .getComponents().get(0).getPredicate())
                 .isExactlyInstanceOf(Exists.class);
         assertThat(view.getEntity(TestGroups.ENTITY).getPostAggregationFilter()
-                        .getComponents().get(1).getPredicate())
+                .getComponents().get(1).getPredicate())
                 .isExactlyInstanceOf(ExampleFilterFunction.class);
     }
 
@@ -1378,7 +1379,7 @@ public class ViewTest extends JSONSerialisationTest<View> {
         // Then
         assertThat(view.hasPostTransformFilters()).isTrue();
         assertThat(view.getEntity(TestGroups.ENTITY).getPostTransformFilter()
-                        .getComponents().get(0).getPredicate())
+                .getComponents().get(0).getPredicate())
                 .isExactlyInstanceOf(Exists.class);
     }
 
@@ -1411,10 +1412,10 @@ public class ViewTest extends JSONSerialisationTest<View> {
         // Then
         assertThat(view.hasPostTransformFilters()).isTrue();
         assertThat(view.getEntity(TestGroups.ENTITY).getPostTransformFilter()
-                        .getComponents().get(0).getPredicate())
+                .getComponents().get(0).getPredicate())
                 .isExactlyInstanceOf(Exists.class);
         assertThat(view.getEntity(TestGroups.ENTITY).getPostTransformFilter()
-                        .getComponents().get(1).getPredicate())
+                .getComponents().get(1).getPredicate())
                 .isExactlyInstanceOf(ExampleFilterFunction.class);
     }
 
@@ -1621,4 +1622,15 @@ public class ViewTest extends JSONSerialisationTest<View> {
                 .config("key1", "value1")
                 .build();
     }
+
+    @Test
+    void shouldWorkWithBuilder() {
+        final View view = createView();
+
+        final View builderView = new View.Builder(view).build();
+        final View optionalBuilderView = new View.Builder(Optional.of(view)).build();
+
+        assertThat(view).isEqualTo(builderView);
+        assertThat(view).isEqualTo(optionalBuilderView);
+    }
 }
diff --git a/store-implementation/federated-store/src/test/java/uk/gov/gchq/gaffer/federatedstore/util/MergeElementFunctionTest.java b/store-implementation/federated-store/src/test/java/uk/gov/gchq/gaffer/federatedstore/util/MergeElementFunctionTest.java
index cd4a962807..da729ca3eb 100644
--- a/store-implementation/federated-store/src/test/java/uk/gov/gchq/gaffer/federatedstore/util/MergeElementFunctionTest.java
+++ b/store-implementation/federated-store/src/test/java/uk/gov/gchq/gaffer/federatedstore/util/MergeElementFunctionTest.java
@@ -31,7 +31,6 @@ import uk.gov.gchq.gaffer.data.elementdefinition.view.View;
 import uk.gov.gchq.gaffer.data.elementdefinition.view.ViewElementDefinition;
 import uk.gov.gchq.gaffer.federatedstore.FederatedStore;
 import uk.gov.gchq.gaffer.federatedstore.FederatedStoreProperties;
-import uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil;
 import uk.gov.gchq.gaffer.operation.OperationException;
 import uk.gov.gchq.gaffer.operation.impl.add.AddElements;
 import uk.gov.gchq.gaffer.operation.impl.get.GetAllElements;
@@ -42,8 +41,10 @@ import uk.gov.gchq.koryphe.impl.predicate.IsLessThan;
 import java.util.HashMap;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.ACCUMULO_STORE_SINGLE_USE_PROPERTIES;
 import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.GRAPH_ID_ACCUMULO;
+import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.GRAPH_ID_TEST_FEDERATED_STORE;
 import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.GROUP_BASIC_EDGE;
 import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.PROPERTY_1;
 import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.SCHEMA_EDGE_BASIC_JSON;
@@ -51,12 +52,15 @@ import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.contextBl
 import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.edgeBasic;
 import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.loadAccumuloStoreProperties;
 import static uk.gov.gchq.gaffer.federatedstore.FederatedStoreTestUtil.loadSchemaFromJson;
+import static uk.gov.gchq.gaffer.federatedstore.util.FederatedStoreUtil.SCHEMA;
+import static uk.gov.gchq.gaffer.federatedstore.util.FederatedStoreUtil.USER;
+import static uk.gov.gchq.gaffer.federatedstore.util.FederatedStoreUtil.VIEW;
 import static uk.gov.gchq.gaffer.user.StoreUser.blankUser;
 import static uk.gov.gchq.gaffer.user.StoreUser.testUser;
 
 class MergeElementFunctionTest {
 
-    public static final Schema SCHEMA = loadSchemaFromJson(SCHEMA_EDGE_BASIC_JSON);
+    public static final Schema EDGE_SCHEMA = loadSchemaFromJson(SCHEMA_EDGE_BASIC_JSON);
     public static final AccumuloProperties ACCUMULO_PROPERTIES = loadAccumuloStoreProperties(ACCUMULO_STORE_SINGLE_USE_PROPERTIES);
 
     @Test
@@ -92,7 +96,7 @@ class MergeElementFunctionTest {
         final MergeElementFunction function = new MergeElementFunction().createFunctionWithContext(
                 makeContext(
                         new View.Builder().edge(GROUP_BASIC_EDGE).build(),
-                        SCHEMA.clone()));
+                        EDGE_SCHEMA.clone()));
 
         //when
         Iterable<Object> iterable = null;
@@ -112,7 +116,7 @@ class MergeElementFunctionTest {
 
     private static FederatedStore getFederatedStore() throws StoreException {
         final FederatedStore federatedStore = new FederatedStore();
-        federatedStore.initialise(FederatedStoreTestUtil.GRAPH_ID_TEST_FEDERATED_STORE, new Schema(), new FederatedStoreProperties());
+        federatedStore.initialise(GRAPH_ID_TEST_FEDERATED_STORE, new Schema(), new FederatedStoreProperties());
         return federatedStore;
     }
 
@@ -132,7 +136,7 @@ class MergeElementFunctionTest {
                                                 .execute(new IsLessThan(3))
                                                 .build())
                                         .build()).build(),
-                        SCHEMA.clone()));
+                        EDGE_SCHEMA.clone()));
 
         //when
         Iterable<Object> iterable = null;
@@ -162,15 +166,51 @@ class MergeElementFunctionTest {
         //This line allows different MiniAccumuloStore
         //tableName = NameSpace.GraphId.
         clone.setNamespace(instanceName);
-        accumuloStore.initialise(GRAPH_ID_ACCUMULO, SCHEMA.clone(), clone);
+        accumuloStore.initialise(GRAPH_ID_ACCUMULO, EDGE_SCHEMA.clone(), clone);
         return accumuloStore;
     }
 
     private static HashMap<String, Object> makeContext(final View view, final Schema schema) {
         final HashMap<String, Object> map = new HashMap<>();
-        map.put(MergeElementFunction.VIEW, view);
-        map.put(MergeElementFunction.SCHEMA, schema);
-        map.put(MergeElementFunction.USER, testUser());
+        map.put(VIEW, view);
+        map.put(SCHEMA, schema);
+        map.put(USER, testUser());
         return map;
     }
+
+    @Test
+    void shouldRequireUserInFunctionContext() {
+        final HashMap<String, Object> context = getTestFunctionContext();
+        context.remove(USER);
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> MergeElementFunction.validate(context))
+                .withMessageContaining("context invalid, requires a User");
+    }
+
+    @Test
+    void shouldRequireSchemaInFunctionContext() {
+        final HashMap<String, Object> context = getTestFunctionContext();
+        context.remove(SCHEMA);
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> MergeElementFunction.validate(context))
+                .withMessageContaining("context invalid, requires a populated schema");
+    }
+
+    @Test
+    void shouldRequireSchemaWithGroupsInFunctionContext() {
+        final HashMap<String, Object> context = getTestFunctionContext();
+        context.put(SCHEMA, new Schema());
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> MergeElementFunction.validate(context))
+                .withMessageContaining("context invalid, requires a populated schema");
+    }
+
+    private static HashMap<String, Object> getTestFunctionContext() {
+        final HashMap<String, Object> context = new HashMap<>();
+        context.put(USER, testUser());
+        context.put(SCHEMA, EDGE_SCHEMA);
+        return context;
+    }
+
+
 }