Repository: gchq/Gaffer (java)

Issue:
Track creation date of sub graphs
**Describe the new feature you'd like**
The federated store should be able to track the creation date of new sub graphs

**Why do you want this feature?**
So old graphs can be aged off

**Additional context**
For implementation, a date field could be added to [GraphSerialisable](https://github.com/gchq/Gaffer/blob/6ce0a4e3fceb097943d00d53aeacad1e59a310c5/core/graph/src/main/java/uk/gov/gchq/gaffer/graph/GraphSerialisable.java#L50) that gets populated as current datetime when created



Pull request description:
Gh 2839 track creation date of sub graphs
- Adds /config/graphCreatedTime endpoint to retrieve graph creation timestamp associated with REST API
- Adds operation to retrieve list of all graphIds & associated creation timestamps. In a federated store this will return all sub-graph ids and creations times
- Adds custom property to Accumulostore on table creation to save graph creation time
- Saves MapStore based graph creation times as Store variable on build 


# Related issue

- Resolve #2839

Patch:
diff --git a/core/graph/src/main/java/uk/gov/gchq/gaffer/graph/Graph.java b/core/graph/src/main/java/uk/gov/gchq/gaffer/graph/Graph.java
index 34fa107db6..770e06c6fe 100644
--- a/core/graph/src/main/java/uk/gov/gchq/gaffer/graph/Graph.java
+++ b/core/graph/src/main/java/uk/gov/gchq/gaffer/graph/Graph.java
@@ -520,6 +520,10 @@ public final class Graph {
         return store.getCaches();
     }
 
+    public String getCreatedTime() {
+        return store.getCreatedTime();
+    }
+
     @FunctionalInterface
     private interface StoreExecuter<O> {
         O execute(final OperationChain<O> operation, final Context context) throws OperationException;
diff --git a/core/operation/src/main/java/uk/gov/gchq/gaffer/operation/impl/get/GetGraphCreatedTime.java b/core/operation/src/main/java/uk/gov/gchq/gaffer/operation/impl/get/GetGraphCreatedTime.java
new file mode 100644
index 0000000000..5806ea06cc
--- /dev/null
+++ b/core/operation/src/main/java/uk/gov/gchq/gaffer/operation/impl/get/GetGraphCreatedTime.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 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.
+ * 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 uk.gov.gchq.gaffer.operation.impl.get;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.apache.commons.lang3.exception.CloneFailedException;
+
+import uk.gov.gchq.gaffer.operation.Operation;
+import uk.gov.gchq.gaffer.operation.io.Output;
+import uk.gov.gchq.koryphe.Since;
+import uk.gov.gchq.koryphe.Summary;
+
+import java.util.Map;
+
+@Since("2.3.1")
+@Summary("Operation to retrieve Graph created date")
+@JsonPropertyOrder(alphabetic = true)
+public class GetGraphCreatedTime implements Output<Map<String, String>> {
+    private Map<String, String> options;
+
+    @Override
+    public Operation shallowClone() throws CloneFailedException {
+        return new Builder()
+                .options(options)
+                .build();
+    }
+
+    @Override
+    public Map<String, String> getOptions() {
+        return options;
+    }
+
+    @Override
+    public void setOptions(final Map<String, String> options) {
+        this.options = options;
+    }
+
+    @Override
+    public TypeReference<Map<String, String>> getOutputTypeReference() {
+        return new TypeReference<Map<String, String>>() { };
+
+    }
+
+    public static class Builder extends Operation.BaseBuilder<GetGraphCreatedTime, Builder> implements Output.Builder<GetGraphCreatedTime, Map<String, String>, Builder> {
+        public Builder() {
+            super(new GetGraphCreatedTime());
+        }
+
+    }
+}
diff --git a/core/operation/src/main/java/uk/gov/gchq/gaffer/operation/serialisation/TypeReferenceImpl.java b/core/operation/src/main/java/uk/gov/gchq/gaffer/operation/serialisation/TypeReferenceImpl.java
index a5028f6750..b0b8562f2c 100644
--- a/core/operation/src/main/java/uk/gov/gchq/gaffer/operation/serialisation/TypeReferenceImpl.java
+++ b/core/operation/src/main/java/uk/gov/gchq/gaffer/operation/serialisation/TypeReferenceImpl.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.
@@ -95,6 +95,8 @@ public final class TypeReferenceImpl {
     public static class MapStringObject extends TypeReference<java.util.Map<java.lang.String, java.lang.Object>> {
     }
 
+    public static class MapStringString extends TypeReference<java.util.Map<java.lang.String, java.lang.String>> {
+    }
     public static class MapStringSet extends TypeReference<java.util.Map<java.lang.String, java.util.Set<java.lang.Object>>> {
     }
 
diff --git a/core/store/src/main/java/uk/gov/gchq/gaffer/store/Store.java b/core/store/src/main/java/uk/gov/gchq/gaffer/store/Store.java
index a4211b90d9..d153548bac 100644
--- a/core/store/src/main/java/uk/gov/gchq/gaffer/store/Store.java
+++ b/core/store/src/main/java/uk/gov/gchq/gaffer/store/Store.java
@@ -79,6 +79,7 @@ import uk.gov.gchq.gaffer.operation.impl.generate.GenerateObjects;
 import uk.gov.gchq.gaffer.operation.impl.get.GetAdjacentIds;
 import uk.gov.gchq.gaffer.operation.impl.get.GetAllElements;
 import uk.gov.gchq.gaffer.operation.impl.get.GetElements;
+import uk.gov.gchq.gaffer.operation.impl.get.GetGraphCreatedTime;
 import uk.gov.gchq.gaffer.operation.impl.job.CancelScheduledJob;
 import uk.gov.gchq.gaffer.operation.impl.job.GetAllJobDetails;
 import uk.gov.gchq.gaffer.operation.impl.job.GetJobDetails;
@@ -114,6 +115,7 @@ import uk.gov.gchq.gaffer.store.operation.handler.CountGroupsHandler;
 import uk.gov.gchq.gaffer.store.operation.handler.CountHandler;
 import uk.gov.gchq.gaffer.store.operation.handler.DiscardOutputHandler;
 import uk.gov.gchq.gaffer.store.operation.handler.ForEachHandler;
+import uk.gov.gchq.gaffer.store.operation.handler.GetGraphCreatedTimeHandler;
 import uk.gov.gchq.gaffer.store.operation.handler.GetSchemaHandler;
 import uk.gov.gchq.gaffer.store.operation.handler.GetVariableHandler;
 import uk.gov.gchq.gaffer.store.operation.handler.GetVariablesHandler;
@@ -174,6 +176,7 @@ import uk.gov.gchq.gaffer.user.User;
 import uk.gov.gchq.koryphe.ValidationResult;
 import uk.gov.gchq.koryphe.util.ReflectionUtil;
 
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -213,6 +216,7 @@ public abstract class Store {
     protected final OperationChainValidator opChainValidator;
     private final SchemaOptimiser schemaOptimiser;
     private final Boolean addCoreOpHandlers;
+    private final LocalDateTime createdTime = LocalDateTime.now();
 
     /**
      * The schema - contains the type of {@link uk.gov.gchq.gaffer.data.element.Element}s
@@ -313,6 +317,14 @@ public abstract class Store {
             }
         }
     }
+    /**
+     * Gets this Store's creation timestamp for {@link GetGraphCreatedTime} operation.
+     *
+     * @return the Store's creation timestamp.
+     */
+    public String getCreatedTime() {
+        return createdTime.toString();
+    }
 
     private void rescheduleJob(final JobDetail jobDetail) {
 
@@ -1109,6 +1121,8 @@ public abstract class Store {
         addOperationHandler(ToSingletonList.class, new ToSingletonListHandler());
         addOperationHandler(Reduce.class, new ReduceHandler());
         addOperationHandler(Join.class, new JoinHandler());
+        addOperationHandler(GetGraphCreatedTime.class, new GetGraphCreatedTimeHandler());
+
 
         // Context variables
         addOperationHandler(SetVariable.class, new SetVariableHandler());
diff --git a/core/store/src/main/java/uk/gov/gchq/gaffer/store/operation/handler/GetGraphCreatedTimeHandler.java b/core/store/src/main/java/uk/gov/gchq/gaffer/store/operation/handler/GetGraphCreatedTimeHandler.java
new file mode 100644
index 0000000000..081cd98aac
--- /dev/null
+++ b/core/store/src/main/java/uk/gov/gchq/gaffer/store/operation/handler/GetGraphCreatedTimeHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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.
+ * 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 uk.gov.gchq.gaffer.store.operation.handler;
+
+import uk.gov.gchq.gaffer.operation.OperationException;
+import uk.gov.gchq.gaffer.operation.impl.get.GetGraphCreatedTime;
+import uk.gov.gchq.gaffer.store.Context;
+import uk.gov.gchq.gaffer.store.Store;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+  * A {@code GetGraphCreatedTimeHandler} handles {@link GetGraphCreatedTime} operations.
+  */
+ public class GetGraphCreatedTimeHandler implements OperationHandler<GetGraphCreatedTime> {
+     @Override
+     public Map<String, String> doOperation(final GetGraphCreatedTime operation, final Context context, final Store store)
+             throws OperationException {
+        Map<String, String> timestamps = new HashMap<>();
+        timestamps.put(store.getGraphId(), store.getCreatedTime());
+
+        return timestamps;
+     }
+ }
diff --git a/example/federated-demo/README.md b/example/federated-demo/README.md
index 7b8f461a52..9157f56a30 100644
--- a/example/federated-demo/README.md
+++ b/example/federated-demo/README.md
@@ -71,8 +71,7 @@ To add a Map graph, execute this operation (or run addMapEdgesGraph.sh):
            ]
          }
        }
-     }
-    },
+     },
     "isPublic": true
 }
 ```
diff --git a/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/controller/GraphConfigurationController.java b/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/controller/GraphConfigurationController.java
index 647abc722f..3dc1c4ee33 100644
--- a/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/controller/GraphConfigurationController.java
+++ b/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/controller/GraphConfigurationController.java
@@ -84,6 +84,12 @@ public class GraphConfigurationController {
         return graphFactory.getGraph().getGraphId();
     }
 
+    @GetMapping(path = "/graphCreatedTime", produces = TEXT_PLAIN_VALUE)
+    @Operation(summary = "Gets the creation time for the store associated with this REST API")
+    public String getGraphCreatedTime() {
+        return graphFactory.getGraph().getCreatedTime();
+    }
+
     @GetMapping(path = "/filterFunctions", produces = APPLICATION_JSON_VALUE)
     @Operation(summary = "Gets the available filter functions")
     public Set<Class> getFilterFunctions() {
diff --git a/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/AccumuloProperties.java b/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/AccumuloProperties.java
index 474eb9482b..baf239784d 100644
--- a/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/AccumuloProperties.java
+++ b/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/AccumuloProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2022 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.
@@ -52,7 +52,7 @@ public class AccumuloProperties extends StoreProperties {
     public static final String TABLE_REPLICATION_FACTOR = "accumulo.file.replication";
     public static final String ENABLE_VALIDATOR_ITERATOR = "gaffer.store.accumulo.enable.validator.iterator";
     public static final String HDFS_SKIP_PERMISSIONS = "accumulostore.operation.hdfs.skip_permissions";
-
+    public static final String TABLE_CREATED_TIME = "table.custom.tableCreatedTime";
     // default values
     private static final String NUM_THREADS_FOR_BATCH_WRITER_DEFAULT = "10";
     private static final String MAX_ENTRIES_FOR_BATCH_SCANNER_DEFAULT = "50000";
diff --git a/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/AccumuloStore.java b/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/AccumuloStore.java
index b961006496..8e2ea59d92 100644
--- a/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/AccumuloStore.java
+++ b/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/AccumuloStore.java
@@ -19,12 +19,14 @@ package uk.gov.gchq.gaffer.accumulostore;
 import com.google.common.collect.Sets;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
+import org.apache.accumulo.core.client.AccumuloException;
 import org.apache.accumulo.core.client.AccumuloSecurityException;
 import org.apache.accumulo.core.client.BatchWriter;
 import org.apache.accumulo.core.client.ClientConfiguration;
 import org.apache.accumulo.core.client.Connector;
 import org.apache.accumulo.core.client.IteratorSetting;
 import org.apache.accumulo.core.client.MutationsRejectedException;
+import org.apache.accumulo.core.client.TableNotFoundException;
 import org.apache.accumulo.core.client.mapreduce.AccumuloInputFormat;
 import org.apache.accumulo.core.client.security.tokens.PasswordToken;
 import org.apache.accumulo.core.data.Key;
@@ -168,7 +170,28 @@ public class AccumuloStore extends Store {
         preInitialise(graphId, schema, properties);
         TableUtils.ensureTableExists(this);
     }
+    /**
+     * Retrieves Accumulo Table created time property.
+     * @return Accumulo Table created time string.
+     */
+    @Override
+    public String getCreatedTime() {
 
+        String tableName = TableUtils.getTableName(getProperties(), this.getGraphId());
+        Iterable<Entry<String, String>> properties;
+        try {
+            properties = TableUtils.getConnector(getProperties()).tableOperations().getProperties(tableName);
+            for (final Entry<String, String>  entry : properties) {
+                LOGGER.debug("Comparing Accumulo Table property: {}", entry.getKey());
+                if (entry.getKey().equals(AccumuloProperties.TABLE_CREATED_TIME)) {
+                    return entry.getValue();
+                }
+            }
+        } catch (final StoreException | AccumuloException | TableNotFoundException e) {
+            throw new GafferRuntimeException("Error getting timestamp.", e);
+        }
+        throw new GafferRuntimeException("Timestamp not found on table");
+    }
     /**
      * Performs general initialisation without creating the table.
      *
diff --git a/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/utils/TableUtils.java b/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/utils/TableUtils.java
index 2c931ccc64..8e1801e0ff 100644
--- a/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/utils/TableUtils.java
+++ b/store-implementation/accumulo-store/src/main/java/uk/gov/gchq/gaffer/accumulostore/utils/TableUtils.java
@@ -47,6 +47,7 @@ import uk.gov.gchq.gaffer.accumulostore.key.exception.IteratorSettingException;
 import uk.gov.gchq.gaffer.store.StoreException;
 import uk.gov.gchq.koryphe.ValidationResult;
 
+import java.time.LocalDateTime;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -139,6 +140,10 @@ public final class TableUtils {
             connector.tableOperations().setProperty(tableName, Property.TABLE_BLOOM_KEY_FUNCTOR.getKey(),
                     store.getKeyPackage().getKeyFunctor().getClass().getName());
 
+            //  Set table creation timestamp
+            LOGGER.info("Storing creation timestamp for table {}", tableName);
+            connector.tableOperations().setProperty(tableName, AccumuloProperties.TABLE_CREATED_TIME, LocalDateTime.now().toString());
+
             // Remove versioning iterator from table for all scopes
             LOGGER.info("Removing versioning iterator from table {}", tableName);
             final EnumSet<IteratorScope> iteratorScopes = EnumSet.allOf(IteratorScope.class);


Test patch:
diff --git a/core/graph/src/test/java/uk/gov/gchq/gaffer/graph/GraphTest.java b/core/graph/src/test/java/uk/gov/gchq/gaffer/graph/GraphTest.java
index 26a5de75fb..ae42ec9d58 100644
--- a/core/graph/src/test/java/uk/gov/gchq/gaffer/graph/GraphTest.java
+++ b/core/graph/src/test/java/uk/gov/gchq/gaffer/graph/GraphTest.java
@@ -105,6 +105,8 @@ import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -118,6 +120,7 @@ import java.util.regex.Pattern;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.within;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -2688,6 +2691,31 @@ public class GraphTest {
         assertNotNull(mergedView.getGlobalEdges().get(0).getPostAggregationFilter());
 
     }
+    @Test
+    void shouldGetGraphCreatedTime() {
+        // Given
+        final Store store = new TestStoreImpl();
+        final Schema schema = new Schema.Builder().build();
+        Graph graph = new Graph.Builder()
+        .config(new GraphConfig.Builder()
+                .graphId(GRAPH_ID)
+                .build())
+        .storeProperties(StreamUtil.storeProps(getClass()))
+        .store(store)
+        .addSchema(schema)
+        .build();
+
+        // When
+        String graphCreatedTime = graph.getCreatedTime();
+
+        // Then
+        assertThat(graphCreatedTime)
+                .isNotNull()
+                .isInstanceOf(String.class);
+        assertThat(LocalDateTime.parse(graphCreatedTime))
+                .isInstanceOf(LocalDateTime.class)
+                .isCloseTo(LocalDateTime.now(), within(20, ChronoUnit.SECONDS));
+    }
 
     public static class TestStoreImpl extends Store {
         @Override
diff --git a/core/operation/src/test/java/uk/gov/gchq/gaffer/operation/impl/get/GetGraphCreatedTimeTest.java b/core/operation/src/test/java/uk/gov/gchq/gaffer/operation/impl/get/GetGraphCreatedTimeTest.java
new file mode 100644
index 0000000000..6bb5cb5bcb
--- /dev/null
+++ b/core/operation/src/test/java/uk/gov/gchq/gaffer/operation/impl/get/GetGraphCreatedTimeTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 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.
+ * 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 uk.gov.gchq.gaffer.operation.impl.get;
+
+import org.junit.jupiter.api.Test;
+
+import uk.gov.gchq.gaffer.operation.Operation;
+import uk.gov.gchq.gaffer.operation.OperationTest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GetGraphCreatedTimeTest extends OperationTest<GetGraphCreatedTime> {
+
+    @Test
+    public void builderShouldCreatePopulatedOperation() {
+        final GetGraphCreatedTime op = new GetGraphCreatedTime();
+
+        assertThat(op).isInstanceOf(GetGraphCreatedTime.class);
+    }
+
+    protected GetGraphCreatedTime getTestObject() {
+        return new GetGraphCreatedTime();
+    }
+
+    @Test
+    public void shouldShallowCloneOperation() {
+        // Given
+        final GetGraphCreatedTime getGraphCreatedTime = new GetGraphCreatedTime();
+
+        // When
+        final Operation clone = getGraphCreatedTime.shallowClone();
+
+        // Then
+        assertThat(clone).isNotSameAs(getGraphCreatedTime);
+    }
+}
diff --git a/core/store/src/test/java/uk/gov/gchq/gaffer/store/StoreTest.java b/core/store/src/test/java/uk/gov/gchq/gaffer/store/StoreTest.java
index 76dd423bfc..c01e7241d8 100644
--- a/core/store/src/test/java/uk/gov/gchq/gaffer/store/StoreTest.java
+++ b/core/store/src/test/java/uk/gov/gchq/gaffer/store/StoreTest.java
@@ -92,6 +92,7 @@ import uk.gov.gchq.gaffer.operation.impl.generate.GenerateObjects;
 import uk.gov.gchq.gaffer.operation.impl.get.GetAdjacentIds;
 import uk.gov.gchq.gaffer.operation.impl.get.GetAllElements;
 import uk.gov.gchq.gaffer.operation.impl.get.GetElements;
+import uk.gov.gchq.gaffer.operation.impl.get.GetGraphCreatedTime;
 import uk.gov.gchq.gaffer.operation.impl.job.CancelScheduledJob;
 import uk.gov.gchq.gaffer.operation.impl.job.GetAllJobDetails;
 import uk.gov.gchq.gaffer.operation.impl.job.GetJobDetails;
@@ -140,6 +141,7 @@ import uk.gov.gchq.koryphe.impl.binaryoperator.StringConcat;
 
 import java.lang.reflect.Field;
 import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -604,6 +606,7 @@ public class StoreTest {
                         ForEach.class,
                         Reduce.class,
                         CancelScheduledJob.class,
+                        GetGraphCreatedTime.class,
 
                         // Function
                         Filter.class,
@@ -712,6 +715,7 @@ public class StoreTest {
                         ToSingletonList.class,
                         ForEach.class,
                         Reduce.class,
+                        GetGraphCreatedTime.class,
 
                         // Function
                         Filter.class,
@@ -917,6 +921,19 @@ public class StoreTest {
         assertThat(result).isSameAs(graphLibrary);
     }
 
+    @Test
+    void shouldGetCreatedTime() {
+        // Given
+        final Store testStore = new StoreImpl();
+
+        // When
+        String storeTime = testStore.getCreatedTime();
+
+        // Then
+        assertThat(storeTime).isInstanceOf(String.class);
+        assertThat(LocalDateTime.parse(storeTime)).isInstanceOf(LocalDateTime.class);
+    }
+
     private Schema createSchemaMock() {
         final Schema schema = mock(Schema.class);
         given(schema.validate()).willReturn(new ValidationResult());
diff --git a/core/store/src/test/java/uk/gov/gchq/gaffer/store/operation/handler/GetGraphCreatedTimeHandlerTest.java b/core/store/src/test/java/uk/gov/gchq/gaffer/store/operation/handler/GetGraphCreatedTimeHandlerTest.java
new file mode 100644
index 0000000000..edd264026e
--- /dev/null
+++ b/core/store/src/test/java/uk/gov/gchq/gaffer/store/operation/handler/GetGraphCreatedTimeHandlerTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 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.
+ * 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 uk.gov.gchq.gaffer.store.operation.handler;
+
+import org.junit.jupiter.api.Test;
+import static org.mockito.Mockito.mock;
+
+import uk.gov.gchq.gaffer.operation.OperationException;
+import uk.gov.gchq.gaffer.operation.impl.get.GetGraphCreatedTime;
+import uk.gov.gchq.gaffer.store.Context;
+import uk.gov.gchq.gaffer.store.Store;
+import uk.gov.gchq.gaffer.user.User;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class GetGraphCreatedTimeHandlerTest {
+    private final Store store = mock(Store.class);
+    private final Context context = new Context(new User());
+    private final GetGraphCreatedTimeHandler handler = new GetGraphCreatedTimeHandler();
+
+    @Test
+    void shouldReturnGraphCreatedTimeMap() throws OperationException {
+        // Given
+        GetGraphCreatedTime op = new GetGraphCreatedTime();
+
+        // When
+        Map<String, String> result = handler.doOperation(op, context, store);
+
+        // Then
+        assertThat(result).isInstanceOf(Map.class);
+    }
+}
diff --git a/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/controller/GraphConfigurationControllerTest.java b/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/controller/GraphConfigurationControllerTest.java
index 3dca9ccd0f..96f6e95790 100644
--- a/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/controller/GraphConfigurationControllerTest.java
+++ b/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/controller/GraphConfigurationControllerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2023 Crown Copyright
+ * Copyright 2020-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 uk.gov.gchq.koryphe.impl.predicate.IsLessThan;
 import uk.gov.gchq.koryphe.impl.predicate.IsMoreThan;
 import uk.gov.gchq.koryphe.impl.predicate.Not;
 
+import java.time.LocalDateTime;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -53,8 +54,6 @@ import java.util.Set;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -68,13 +67,13 @@ public class GraphConfigurationControllerTest {
     private GraphFactory graphFactory;
 
     @BeforeEach
-    public void initialiseMocks() {
+    void initialiseMocks() {
         MockitoAnnotations.initMocks(this);
         Mockito.reset(graphFactory);
     }
 
     @Test
-    public void shouldReturnDescription() {
+    void shouldReturnDescription() {
         // Given
         when(graphFactory.getGraph()).thenReturn(new Graph.Builder()
                 .config(new GraphConfig("id"))
@@ -89,11 +88,11 @@ public class GraphConfigurationControllerTest {
         final String description = controller.getDescription();
 
         // Then
-        assertEquals("test graph", description);
+        assertThat(description).isEqualTo("test graph");
     }
 
     @Test
-    public void shouldReturnGraphId() {
+    void shouldReturnGraphId() {
         // Given
         when(graphFactory.getGraph()).thenReturn(new Graph.Builder()
                 .config(new GraphConfig("id"))
@@ -108,11 +107,11 @@ public class GraphConfigurationControllerTest {
         final String graphId = controller.getGraphId();
 
         // Then
-        assertEquals("id", graphId);
+        assertThat(graphId).isEqualTo("id");
     }
 
     @Test
-    public void shouldGetFilterFunctions() {
+    void shouldGetFilterFunctions() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -124,7 +123,7 @@ public class GraphConfigurationControllerTest {
     }
 
     @Test
-    public void shouldGetFilterFunctionsWithNullInput() {
+    void shouldGetFilterFunctionsWithNullInput() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -136,7 +135,7 @@ public class GraphConfigurationControllerTest {
     }
 
     @Test
-    public void shouldGetFilterFunctionsWithInputClass() {
+    void shouldGetFilterFunctionsWithInputClass() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -148,7 +147,7 @@ public class GraphConfigurationControllerTest {
     }
 
     @Test
-    public void shouldGetFilterFunctionsUsingShortClassName() {
+    void shouldGetFilterFunctionsUsingShortClassName() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -160,7 +159,7 @@ public class GraphConfigurationControllerTest {
     }
 
     @Test
-    public void shouldThrowExceptionWhenGetFilterFunctionsWithUnknownClassName() {
+    void shouldThrowExceptionWhenGetFilterFunctionsWithUnknownClassName() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -171,7 +170,7 @@ public class GraphConfigurationControllerTest {
     }
 
     @Test
-    public void shouldGetSerialisedFields() {
+    void shouldGetSerialisedFields() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -184,7 +183,7 @@ public class GraphConfigurationControllerTest {
     }
 
     @Test
-    public void shouldGetSerialisedFieldsForGetElementsClass() {
+    void shouldGetSerialisedFieldsForGetElementsClass() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -200,11 +199,11 @@ public class GraphConfigurationControllerTest {
         expectedFields.add("views");
 
         // Then
-        assertEquals(expectedFields, fields);
+        assertThat(fields).isEqualTo(expectedFields);
     }
 
     @Test
-    public void shouldGetCorrectSerialisedFieldsForEdgeClass() {
+    void shouldGetCorrectSerialisedFieldsForEdgeClass() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -222,11 +221,11 @@ public class GraphConfigurationControllerTest {
         expectedFields.put("directedType", String.class.getName());
 
         // Then
-        assertEquals(expectedFields, fields);
+        assertThat(fields).isEqualTo(expectedFields);
     }
 
     @Test
-    public void shouldGetCorrectSerialisedFieldsForGetWalksClass() {
+    void shouldGetCorrectSerialisedFieldsForGetWalksClass() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -242,11 +241,11 @@ public class GraphConfigurationControllerTest {
         expectedFields.put("conditional", "uk.gov.gchq.gaffer.operation.util.Conditional");
 
         // Then
-        assertEquals(expectedFields, fields);
+        assertThat(fields).isEqualTo(expectedFields);
     }
 
     @Test
-    public void shouldThrowExceptionWhenGetSerialisedFieldsWithUnknownClassName() {
+    void shouldThrowExceptionWhenGetSerialisedFieldsWithUnknownClassName() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -257,7 +256,7 @@ public class GraphConfigurationControllerTest {
     }
 
     @Test
-    public void shouldReturnStoreType() {
+    void shouldReturnStoreType() {
         // Given
         when(graphFactory.getGraph()).thenReturn(new Graph.Builder()
             .config(new GraphConfig("id"))
@@ -275,7 +274,7 @@ public class GraphConfigurationControllerTest {
     }
 
     @Test
-    public void shouldGetStoreTraits() throws OperationException {
+    void shouldGetStoreTraits() throws OperationException {
         // Given
         Store store = mock(Store.class);
         Schema schema = new Schema();
@@ -299,11 +298,11 @@ public class GraphConfigurationControllerTest {
         final Set<StoreTrait> traits = controller.getStoreTraits();
 
         // Then
-        assertEquals(MapStore.TRAITS, traits);
+        assertThat(traits).isEqualTo(MapStore.TRAITS);
     }
 
     @Test
-    public void shouldGetTransformFunctions() {
+    void shouldGetTransformFunctions() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -311,11 +310,11 @@ public class GraphConfigurationControllerTest {
         final Set<Class> classes = controller.getTransformFunctions();
 
         // Then
-        assertFalse(classes.isEmpty());
+        assertThat(classes).isNotEmpty();
     }
 
     @Test
-    public void shouldGetAggregationFunctions() {
+    void shouldGetAggregationFunctions() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -323,11 +322,11 @@ public class GraphConfigurationControllerTest {
         final Set<Class> classes = controller.getAggregationFunctions();
 
         // Then
-        assertFalse(classes.isEmpty());
+        assertThat(classes).isNotEmpty();
     }
 
     @Test
-    public void shouldGetElementGenerators() {
+    void shouldGetElementGenerators() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -335,11 +334,11 @@ public class GraphConfigurationControllerTest {
         final Set<Class> classes = controller.getElementGenerators();
 
         // Then
-        assertFalse(classes.isEmpty());
+        assertThat(classes).isNotEmpty();
     }
 
     @Test
-    public void shouldGetObjectGenerators() {
+    void shouldGetObjectGenerators() {
         // Given
         GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
 
@@ -347,11 +346,11 @@ public class GraphConfigurationControllerTest {
         final Set<Class> classes = controller.getObjectGenerators();
 
         // Then
-        assertFalse(classes.isEmpty());
+        assertThat(classes).isNotEmpty();
     }
 
     @Test
-    public void shouldSerialiseAndDeserialiseGetStoreTraits() throws SerialisationException, OperationException {
+    void shouldSerialiseAndDeserialiseGetStoreTraits() throws SerialisationException, OperationException {
         // Given
         Store store = mock(Store.class);
 
@@ -382,12 +381,32 @@ public class GraphConfigurationControllerTest {
         final Set<String> traits = JSONSerialiser.deserialise(bytes, Set.class);
 
         // Then
-        assertEquals(Sets.newHashSet(
-                INGEST_AGGREGATION.name(),
-                PRE_AGGREGATION_FILTERING.name(),
-                POST_AGGREGATION_FILTERING.name()
-                ),
-                traits);
+        assertThat(traits).isEqualTo(Sets.newHashSet(
+            INGEST_AGGREGATION.name(),
+            PRE_AGGREGATION_FILTERING.name(),
+            POST_AGGREGATION_FILTERING.name()
+            ));
+    }
+    @Test
+    void shouldGetGraphCreatedTime() {
+        // Given
+        when(graphFactory.getGraph()).thenReturn(new Graph.Builder()
+                .config(new GraphConfig("id"))
+                .addSchema(new Schema())
+                .storeProperties(new MapStoreProperties())
+                .description("test graph")
+                .build());
+
+        // When
+        LocalDateTime time = LocalDateTime.now();
+        GraphConfigurationController controller = new GraphConfigurationController(graphFactory);
+
+        // When
+        final String graphCreatedTime = controller.getGraphCreatedTime();
+
+        // Then
+        assertThat(graphCreatedTime).isInstanceOf(String.class);
+        assertThat(LocalDateTime.parse(graphCreatedTime)).isBeforeOrEqualTo(time);
     }
 
 
diff --git a/store-implementation/accumulo-store/src/test/java/uk/gov/gchq/gaffer/accumulostore/AccumuloStoreTest.java b/store-implementation/accumulo-store/src/test/java/uk/gov/gchq/gaffer/accumulostore/AccumuloStoreTest.java
index 4b8d3c19a5..de77d4d34a 100644
--- a/store-implementation/accumulo-store/src/test/java/uk/gov/gchq/gaffer/accumulostore/AccumuloStoreTest.java
+++ b/store-implementation/accumulo-store/src/test/java/uk/gov/gchq/gaffer/accumulostore/AccumuloStoreTest.java
@@ -89,6 +89,7 @@ import uk.gov.gchq.koryphe.impl.binaryoperator.StringConcat;
 import uk.gov.gchq.koryphe.impl.binaryoperator.Sum;
 import uk.gov.gchq.koryphe.impl.predicate.IsMoreThan;
 
+import java.time.LocalDateTime;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -580,4 +581,45 @@ public class AccumuloStoreTest {
         }
      }
 
+     @Test
+     void shouldStoreTableCreatedTimeProperty() throws Exception {
+         // Given
+         final AccumuloProperties properties = PROPERTIES.clone();
+         String graphId = "graphId";
+
+         final AccumuloStore store = new MiniAccumuloStore();
+         // When
+
+         store.initialise(graphId, SCHEMA, properties);
+         LocalDateTime dateTime = LocalDateTime.now();
+         LocalDateTime storeDateTime = LocalDateTime.parse(store.getCreatedTime());
+         // Then
+         assertThat(storeDateTime).isBeforeOrEqualTo(dateTime);
+     }
+     @Test
+     void shouldThrowGafferRuntimeExceptionForGetCreatedTimeIfTableNotFound() throws Exception {
+        // Given
+        final AccumuloStore store = new MiniAccumuloStore();
+        store.initialise("graphId", SCHEMA, PROPERTIES);
+
+        // When
+        store.getConnection().tableOperations().delete("graphId");
+
+        // Then
+        assertThatExceptionOfType(GafferRuntimeException.class).isThrownBy(() -> store.getCreatedTime()).withMessage("Error getting timestamp.");
+     }
+
+     @Test
+     void shouldThrowGafferRuntimeExceptionIfTableCreatedTimePropertyNotFound() throws Exception {
+        // Given
+        final AccumuloStore store = new MiniAccumuloStore();
+        store.initialise("graphId", SCHEMA, PROPERTIES);
+
+        // When
+        store.getConnection().tableOperations().removeProperty("graphId", AccumuloProperties.TABLE_CREATED_TIME);
+
+        // Then
+        assertThatExceptionOfType(GafferRuntimeException.class).isThrownBy(() -> store.getCreatedTime()).withMessage("Timestamp not found on table");
+
+     }
 }
diff --git a/store-implementation/federated-store/src/test/java/uk/gov/gchq/gaffer/federatedstore/FederatedStoreTest.java b/store-implementation/federated-store/src/test/java/uk/gov/gchq/gaffer/federatedstore/FederatedStoreTest.java
index ad12c9de15..5964a611a2 100644
--- a/store-implementation/federated-store/src/test/java/uk/gov/gchq/gaffer/federatedstore/FederatedStoreTest.java
+++ b/store-implementation/federated-store/src/test/java/uk/gov/gchq/gaffer/federatedstore/FederatedStoreTest.java
@@ -748,7 +748,6 @@ public class FederatedStoreTest {
 
         // When
         final Collection<GraphSerialisable> returnedGraphs = store.getGraphs(blankUser, getCleanStrings("mockGraphId1,mockGraphId2,mockGraphId4"), new GetAllGraphIds());
-
         // Then
         assertThat(returnedGraphs)
                 .hasSize(3)