[WAYANG-32] Base structure for Wayang Experiments Storage functionalities
diff --git a/wayang-commons/wayang-utils/pom.xml b/wayang-commons/wayang-utils/pom.xml
new file mode 100644
index 0000000..7768f19
--- /dev/null
+++ b/wayang-commons/wayang-utils/pom.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>wayang-commons</artifactId>
+        <groupId>org.apache.wayang</groupId>
+        <version>0.6.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>wayang-utils</artifactId>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>wayang-profile-db</module>
+    </modules>
+
+
+</project>
\ No newline at end of file
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/pom.xml b/wayang-commons/wayang-utils/wayang-profile-db/pom.xml
new file mode 100644
index 0000000..45bfaf9
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/pom.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>wayang-utils</artifactId>
+        <groupId>org.apache.wayang</groupId>
+        <version>0.6.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>wayang-profile-db</artifactId>
+
+    <dependencies>
+        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+        </dependency>
+
+    </dependencies>
+
+
+</project>
\ No newline at end of file
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/readme.md b/wayang-commons/wayang-utils/wayang-profile-db/readme.md
new file mode 100644
index 0000000..8c14afc
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/readme.md
@@ -0,0 +1,3 @@
+Base on
+
+https://github.com/sekruse/profiledb-java.git
\ No newline at end of file
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/ProfileDB.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/ProfileDB.java
new file mode 100644
index 0000000..cd90d40
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/ProfileDB.java
@@ -0,0 +1,105 @@
+package profiledb;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import profiledb.json.MeasurementDeserializer;
+import profiledb.json.MeasurementSerializer;
+import profiledb.model.Experiment;
+import profiledb.model.Measurement;
+import profiledb.storage.Storage;
+
+import java.io.*;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * This class provides facilities to save and load {@link Experiment}s.
+ */
+public class ProfileDB {
+
+    /**
+     * Maintains the full list of {@link Class}es for {@link Measurement}s. Which are required for deserialization.
+     */
+    private List<Class<? extends Measurement>> measurementClasses = new LinkedList<>();
+
+    /**
+     * Controls how conducted experiments will be persisted and loaded
+     */
+    private Storage storage;
+
+    /**
+     * Maintains actions to preparate {@link Gson}.
+     */
+    private List<Consumer<GsonBuilder>> gsonPreparationSteps = new LinkedList<>();
+
+    /**
+     * Maintains a {@link Gson} object for efficiency. It will be dropped on changes, though.
+     */
+    private Gson gson;
+
+    /**
+     * Creates a new instance.
+     */
+    public ProfileDB(Storage storage) {
+
+        this.storage = storage;
+        this.storage.setContext(this);
+        //this.measurementClasses.add(TimeMeasurement.class);
+    }
+
+    /**
+     * To work with storage object provided to persist or load experiments
+     *
+     * @return Storage object proportioned for this instance
+     */
+    public Storage getStorage() {
+        return storage;
+    }
+
+    /**
+     * Register a {@link Measurement} type. This is required before being able to load that type.
+     *
+     * @param measurementClass the {@link Measurement} {@link Class}
+     * @return this instance
+     */
+    public ProfileDB registerMeasurementClass(Class<? extends Measurement> measurementClass) {
+        this.measurementClasses.add(measurementClass);
+        this.gson = null;
+        return this;
+    }
+
+    /**
+     * Apply any changes necessary to {@link Gson} so that it can be used for de/serialization of custom objects.
+     *
+     * @param preparation a preparatory step performed on a {@link GsonBuilder}
+     * @return this instance
+     */
+    public ProfileDB withGsonPreparation(Consumer<GsonBuilder> preparation) {
+        this.gsonPreparationSteps.add(preparation);
+        this.gson = null;
+        return this;
+    }
+
+    /**
+     * Provide a {@link Gson} object.
+     *
+     * @return the {@link Gson} object
+     */
+    public Gson getGson() {
+        if (this.gson == null) {
+            MeasurementSerializer measurementSerializer = new MeasurementSerializer();
+            MeasurementDeserializer measurementDeserializer = new MeasurementDeserializer();
+            this.measurementClasses.forEach(measurementDeserializer::register);
+            final GsonBuilder gsonBuilder = new GsonBuilder()
+                    .registerTypeAdapter(Measurement.class, measurementDeserializer)
+                    .registerTypeAdapter(Measurement.class, measurementSerializer);
+            this.gsonPreparationSteps.forEach(step -> step.accept(gsonBuilder));
+            this.gson = gsonBuilder.create();
+        }
+        return this.gson;
+    }
+
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementDeserializer.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementDeserializer.java
new file mode 100644
index 0000000..cedecd8
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementDeserializer.java
@@ -0,0 +1,40 @@
+package profiledb.json;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import profiledb.model.Measurement;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Custom deserializer for {@link Measurement}s
+ * Detects actual subclass of serialized instances and then delegates the deserialization to that subtype.
+ */
+public class MeasurementDeserializer implements JsonDeserializer<Measurement> {
+
+    private final Map<String, Class<? extends Measurement>> measurementTypes = new HashMap<>();
+
+    public void register(Class<? extends Measurement> measurementClass) {
+        String typeName = Measurement.getTypeName(measurementClass);
+        this.measurementTypes.put(typeName, measurementClass);
+    }
+
+    @Override
+    public Measurement deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+        final JsonElement typeElement = jsonElement.getAsJsonObject().get("type");
+        if (typeElement == null) {
+            throw new IllegalArgumentException("Missing type in " + jsonElement);
+        }
+        final String typeName = typeElement.getAsString();
+        final Class<? extends Measurement> measurementClass = this.measurementTypes.get(typeName);
+        if (measurementClass == null) {
+            throw new JsonParseException("Unknown measurement type: " + typeName);
+        }
+        return jsonDeserializationContext.deserialize(jsonElement, measurementClass);
+    }
+
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementSerializer.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementSerializer.java
new file mode 100644
index 0000000..4f820e8
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementSerializer.java
@@ -0,0 +1,23 @@
+package profiledb.json;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import profiledb.model.Measurement;
+
+import java.lang.reflect.Type;
+
+/**
+ * Custom serializer for {@link Measurement}s
+ * Detects actual subclass of given instances, encodes this class membership, and then delegates serialization to that subtype.
+ */
+public class MeasurementSerializer implements JsonSerializer<Measurement> {
+
+    @Override
+    public JsonElement serialize(Measurement measurement, Type type, JsonSerializationContext jsonSerializationContext) {
+        final JsonObject jsonObject = (JsonObject) jsonSerializationContext.serialize(measurement);
+        jsonObject.addProperty("type", measurement.getType());
+        return jsonObject;
+    }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Experiment.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Experiment.java
new file mode 100644
index 0000000..1222a75
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Experiment.java
@@ -0,0 +1,163 @@
+package profiledb.model;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Objects;
+
+/**
+ * An experiment comprises {@link Measurement}s from one specific {@link Subject} execution.
+ */
+public class Experiment {
+
+    /**
+     * Identifier for this instance.
+     */
+    private String id;
+
+    /**
+     * Description for this instance. (Optional)
+     */
+    private String description;
+
+    /**
+     * When this experiment has been started.
+     */
+    private long startTime;
+
+    /**
+     * Tags to group multiple Experiment instances. (Optional)
+     */
+    private Collection<String> tags;
+
+    /**
+     * {@link Measurement}s captured for this instance.
+     */
+    private Collection<Measurement> measurements;
+
+    /**
+     * The {@link Subject} being experimented with.
+     */
+    private Subject subject;
+
+    /**
+     * For deserialization.
+     */
+    private Experiment() {
+    }
+
+    /**
+     * Create a new instance that is starting right now.
+     *
+     * @param id      Identifier for the new instance
+     * @param subject the {@link Subject}
+     * @param tags    tags to group several experiments
+     */
+    public Experiment(String id, Subject subject, String... tags) {
+        this(id, subject, System.currentTimeMillis(), tags);
+    }
+
+    /**
+     * Create a new instance.
+     *
+     * @param id        Identifier for the new instance
+     * @param subject   the {@link Subject} of this experiment
+     * @param startTime start timestamp of this experiment
+     * @param tags      tags to group several experiments
+     */
+    public Experiment(String id, Subject subject, long startTime, String... tags) {
+        this.id = id;
+        this.subject = subject;
+        this.startTime = startTime;
+        this.tags = Arrays.asList(tags);
+        this.measurements = new LinkedList<>();
+    }
+
+    /**
+     * Adds a description for this instance.
+     *
+     * @param description the description
+     * @return this instance
+     */
+    public Experiment withDescription(String description) {
+        this.description = description;
+        return this;
+    }
+
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public long getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(long startTime) {
+        this.startTime = startTime;
+    }
+
+    public Collection<String> getTags() {
+        return tags;
+    }
+
+    public void setTags(Collection<String> tags) {
+        this.tags = tags;
+    }
+
+    public void addMeasurement(Measurement measurement) {
+        this.measurements.add(measurement);
+    }
+
+    public Collection<Measurement> getMeasurements() {
+        return measurements;
+    }
+
+    public Subject getSubject() {
+        return this.subject;
+    }
+
+    public void setSubject(Subject subject) {
+        this.subject = subject;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Experiment that = (Experiment) o;
+        return startTime == that.startTime &&
+                Objects.equals(id, that.id) &&
+                Objects.equals(description, that.description) &&
+                Objects.equals(tags, that.tags) &&
+                Objects.equals(subject, that.subject);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, startTime);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "%s[%s, %d tags, %d measurements]",
+                this.getClass().getSimpleName(),
+                this.id,
+                this.tags.size(),
+                this.measurements.size()
+        );
+    }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Measurement.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Measurement.java
new file mode 100644
index 0000000..23567a1
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Measurement.java
@@ -0,0 +1,54 @@
+package profiledb.model;
+
+import java.util.Objects;
+
+/**
+ *
+ * Measurement captures the value of a metric at a specific time
+ */
+public abstract class Measurement {
+
+    private String id;
+
+    /**
+     * Returns implementation Class of this Measurement
+     */
+    public static String getTypeName(Class<? extends Measurement> measurementClass) {
+        return measurementClass.getDeclaredAnnotation(Type.class).value();
+    }
+
+    /**
+     * Deserialization constructor.
+     */
+    protected Measurement() {
+    }
+
+    public Measurement(String id) {
+        this.id = id;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getType() {
+        return getTypeName(this.getClass());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Measurement that = (Measurement) o;
+        return Objects.equals(id, that.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Subject.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Subject.java
new file mode 100644
index 0000000..cfb6710
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Subject.java
@@ -0,0 +1,69 @@
+package profiledb.model;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * The subject of an {@link Experiment}, e.g., an application or algorithm.
+ */
+public class Subject {
+
+    /**
+     * Identifier for the subject.
+     */
+    private String id;
+
+    /**
+     * Version of the subject.
+     */
+    private String version;
+
+    /**
+     * Configuration of this object.
+     */
+    private Map<String, Object> configuration = new HashMap<>();
+
+    /**
+     * Creates a new instance.
+     *
+     * @param id      Identifier for the subject
+     * @param version To distinguish different versions among instances with the same {@code id}
+     */
+    public Subject(String id, String version) {
+        this.id = id;
+        this.version = version;
+    }
+
+    /**
+     * Adds a configuration.
+     *
+     * @param key   Key of the configuration entry
+     * @param value Value for the new configuration entry; must be JSON-compatible, e.g. {@link Integer} or {@link String}
+     * @return this instance
+     */
+    public Subject addConfiguration(String key, Object value) {
+        this.configuration.put(key, value);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s[%s:%s]", this.getClass().getSimpleName(), this.id, this.version);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Subject subject = (Subject) o;
+        return Objects.equals(id, subject.id) &&
+                Objects.equals(version, subject.version) &&
+                Objects.equals(configuration, subject.configuration);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, version, configuration);
+    }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Type.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Type.java
new file mode 100644
index 0000000..f96e5f2
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Type.java
@@ -0,0 +1,13 @@
+package profiledb.model;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Type {
+
+    String value();
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/measurement/TimeMeasurement.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/measurement/TimeMeasurement.java
new file mode 100644
index 0000000..e73caa9
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/measurement/TimeMeasurement.java
@@ -0,0 +1,199 @@
+package profiledb.model.measurement;
+
+import profiledb.model.Measurement;
+import profiledb.model.Type;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+/**
+ * A {@link Measurement} that captures a certain amount of time in milliseconds. Instances can be nested within
+ * each other.
+ * <p>Besides storing those data, it also provides utility functionality to obtain measurements.</p>
+ */
+@Type("time")
+public class TimeMeasurement extends Measurement {
+
+    /**
+     * The measured time in milliseconds.
+     */
+    private long millis = 0L;
+
+    /**
+     * Keeps track on measurement starts.
+     */
+    private transient long startTime = -1L;
+
+    /**
+     * Sub-{@link TimeMeasurement}s of this instance.
+     */
+    private Collection<TimeMeasurement> rounds = new LinkedList<>();
+
+    /**
+     * Serialization constructor.
+     */
+    @SuppressWarnings("unused")
+    private TimeMeasurement() {
+        super();
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param id the ID of the new instance
+     */
+    public TimeMeasurement(String id) {
+        super(id);
+    }
+
+    /**
+     * Start measuring time for this instance.
+     */
+    public void start() {
+        this.startTime = System.currentTimeMillis();
+    }
+
+    /**
+     * Ensure that this instance has its timer started.
+     */
+    private void ensureStarted() {
+        if (this.startTime == -1L) {
+            this.startTime = System.currentTimeMillis();
+        }
+    }
+
+    /**
+     * Start a (potentially new) sub-{@link TimeMeasurement}.
+     *
+     * @param identifiers identifies the target {@link TimeMeasurement} as a path of IDs
+     * @return the started instance
+     */
+    public TimeMeasurement start(String... identifiers) {
+        return this.start(identifiers, 0);
+    }
+
+    /**
+     * Start a (potentially new) sub-{@link TimeMeasurement}.
+     *
+     * @param identifiers identifies the target {@link TimeMeasurement} as a path of IDs
+     * @param index       the index of this instance within {@code identifiers}
+     * @return the started instance
+     */
+    private TimeMeasurement start(String[] identifiers, int index) {
+        if (index >= identifiers.length) {
+            this.start();
+            return this;
+        } else {
+            this.ensureStarted();
+            TimeMeasurement round = this.getOrCreateRound(identifiers[index]);
+            return round.start(identifiers, index + 1);
+        }
+    }
+
+    /**
+     * Retrieves an existing {@link TimeMeasurement} from {@link #rounds} with the given {@code id} or creates and stores a new one.
+     *
+     * @param id the ID of the {@link TimeMeasurement}
+     * @return the {@link TimeMeasurement}
+     */
+    public TimeMeasurement getOrCreateRound(String id) {
+        TimeMeasurement round = this.getRound(id);
+        if (round != null) return round;
+
+        round = new TimeMeasurement(id);
+        this.rounds.add(round);
+        return round;
+    }
+
+    /**
+     * Retrieves an existing {@link TimeMeasurement} from {@link #rounds} with the given {@code id}.
+     *
+     * @param id the ID of the {@link TimeMeasurement}
+     * @return the {@link TimeMeasurement} or {@code null} if it does not exist
+     */
+    private TimeMeasurement getRound(String id) {
+        for (TimeMeasurement round : this.rounds) {
+            if (id.equals(round.getId())) {
+                return round;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Stop a measurement that has been started via {@link #start()} or derivatives.
+     */
+    public void stop() {
+        this.stop(System.currentTimeMillis());
+    }
+
+
+    /**
+     * Stop a measurement that has been started via {@link #start()} or derivatives.
+     *
+     * @param stopTime at which the measurement has been stopped
+     */
+    private void stop(long stopTime) {
+        if (this.startTime != -1L) {
+            this.millis += (stopTime - this.startTime);
+            this.startTime = -1L;
+        }
+        for (TimeMeasurement round : this.rounds) {
+            round.stop(stopTime);
+        }
+    }
+
+    /**
+     * Stop a measurement that has been started via {@link #start(String...)} or related.
+     *
+     * @param identfiers identify the target {@link TimeMeasurement} as a path of IDs
+     */
+    public void stop(String... identfiers) {
+        long stopTime = System.currentTimeMillis();
+        TimeMeasurement round = this;
+        for (String identfier : identfiers) {
+            round = round.getRound(identfier);
+            if (round == null) return;
+        }
+        round.stop(stopTime);
+    }
+
+    public long getMillis() {
+        return millis;
+    }
+
+    public void setMillis(long millis) {
+        this.millis = millis;
+    }
+
+    public Collection<TimeMeasurement> getRounds() {
+        return rounds;
+    }
+
+    public void addRounds(TimeMeasurement round) {
+        this.rounds.add(round);
+    }
+
+    /**
+     * Formats the given milliseconds as {@code h:MM:ss.mmm}.
+     *
+     * @param millis the milliseconds to format
+     * @return the formatted milliseconds
+     */
+    public static String formatDuration(long millis) {
+        if (millis < 0) return "-" + formatDuration(-millis);
+        long ms = millis % 1000;
+        millis /= 1000;
+        long s = millis % 60;
+        millis /= 60;
+        long m = millis % 60;
+        millis /= 60;
+        long h = millis % 60;
+        return String.format("%d:%02d:%02d.%03d", h, m, s, ms);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s[%s, %s, %d subs]", this.getClass().getSimpleName(), this.getId(), formatDuration(this.millis), this.rounds.size());
+    }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/FileStorage.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/FileStorage.java
new file mode 100644
index 0000000..4182dc5
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/FileStorage.java
@@ -0,0 +1,101 @@
+package profiledb.storage;
+
+import profiledb.model.Experiment;
+
+import java.io.*;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+
+public class FileStorage extends Storage {
+
+    /**
+     * File where {@link Experiment}s will be written
+     */
+    private File file;
+
+    /**
+     * Assigns File where {@link Experiment}s will be written regarding to given URI
+     *
+     * @param uri URI where experiments are persisted
+     */
+    public FileStorage(URI uri) {
+
+        super(uri);
+        this.file = new File(uri);
+    }
+
+    /**
+     * To change target URI during execution
+     *
+     * @param uri determines new URI where {@link Experiment}s will be persisted
+     */
+    @Override
+    public void changeLocation(URI uri){
+
+        super.changeLocation(uri);
+        this.file = new File(uri);
+    }
+
+    /**
+     * Write {@link Experiment}s to a {@link File}. Existing file contents will be overwritten.
+     *
+     * @param experiments the {@link Experiment}s
+     * @throws IOException if the writing fails
+     */
+    @Override
+    public void save(Collection<Experiment> experiments) throws IOException {
+        this.file.getAbsoluteFile().getParentFile().mkdirs();
+        try (FileOutputStream fos = new FileOutputStream(this.file, false)) {
+            this.save(experiments, fos);
+        }
+    }
+
+    /**
+     * Write {@link Experiment}s to a {@link File}. Existing file contents will be overwritten.
+     *
+     * @param experiments the {@link Experiment}s
+     * @throws IOException if the writing fails
+     */
+    @Override
+    public void save(Experiment... experiments) throws IOException {
+        this.save(Arrays.asList(experiments));
+    }
+
+    /**
+     * Load {@link Experiment}s from a {@link File}.
+     *
+     * @return the {@link Experiment}s
+     */
+    @Override
+    public Collection<Experiment> load() throws IOException {
+        return load(new FileInputStream(this.file));
+    }
+
+    /**
+     * Append {@link Experiment}s to a {@link File}. Existing file contents will be preserved.
+     *
+     * @param experiments the {@link Experiment}s
+     * @throws IOException if the writing fails
+     */
+    @Override
+    public void append(Collection<Experiment> experiments) throws IOException {
+        this.file.getAbsoluteFile().getParentFile().mkdirs();
+        try (FileOutputStream fos = new FileOutputStream(this.file, true)) {
+            this.save(experiments, fos);
+        }
+    }
+
+    /**
+     * Append {@link Experiment}s to a {@link File}. Existing file contents will be preserved.
+     *
+     * @param experiments the {@link Experiment}s
+     * @throws IOException if the writing fails
+     */
+    @Override
+    public void append(Experiment... experiments) throws IOException {
+        this.append(Arrays.asList(experiments));
+    }
+
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/JDBCStorage.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/JDBCStorage.java
new file mode 100644
index 0000000..ba847c1
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/JDBCStorage.java
@@ -0,0 +1,90 @@
+package profiledb.storage;
+
+import profiledb.model.Experiment;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class JDBCStorage extends Storage {
+
+    //TODO: Implement JDBC connection
+
+    private File file;
+
+    public JDBCStorage(URI uri) {
+        super(uri);
+        this.file = new File(uri);
+    }
+
+    @Override
+    public void changeLocation(URI uri){
+
+        super.changeLocation(uri);
+        this.file = new File(uri);
+    }
+
+    /**
+     * Write {@link Experiment}s to a {@link File}. Existing file contents will be overwritten.
+     *
+     * @param experiments the {@link Experiment}s
+     * @throws IOException if the writing fails
+     */
+    @Override
+    public void save(Collection<Experiment> experiments) throws IOException {
+        this.file.getAbsoluteFile().getParentFile().mkdirs();
+        try (FileOutputStream fos = new FileOutputStream(this.file, false)) {
+            this.save(experiments, fos);
+        }
+    }
+
+    /**
+     * Write {@link Experiment}s to a {@link File}. Existing file contents will be overwritten.
+     *
+     * @param experiments the {@link Experiment}s
+     * @throws IOException if the writing fails
+     */
+    @Override
+    public void save(Experiment... experiments) throws IOException {
+        this.save(Arrays.asList(experiments));
+    }
+
+    /**
+     * Load {@link Experiment}s from a {@link File}.
+     *
+     * @return the {@link Experiment}s
+     */
+    @Override
+    public Collection<Experiment> load() throws IOException {
+        return load(new FileInputStream(this.file));
+    }
+
+    /**
+     * Append {@link Experiment}s to a {@link File}. Existing file contents will be preserved.
+     *
+     * @param experiments the {@link Experiment}s
+     * @throws IOException if the writing fails
+     */
+    @Override
+    public void append(Collection<Experiment> experiments) throws IOException {
+        this.file.getAbsoluteFile().getParentFile().mkdirs();
+        try (FileOutputStream fos = new FileOutputStream(this.file, true)) {
+            this.save(experiments, fos);
+        }
+    }
+
+    /**
+     * Append {@link Experiment}s to a {@link File}. Existing file contents will be preserved.
+     *
+     * @param experiments the {@link Experiment}s
+     * @throws IOException if the writing fails
+     */
+    @Override
+    public void append(Experiment... experiments) throws IOException {
+        this.append(Arrays.asList(experiments));
+    }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/Storage.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/Storage.java
new file mode 100644
index 0000000..cdda15b
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/Storage.java
@@ -0,0 +1,122 @@
+package profiledb.storage;
+
+import com.google.gson.Gson;
+import profiledb.ProfileDB;
+import profiledb.model.Experiment;
+
+import java.io.*;
+import java.net.URI;
+import java.util.Collection;
+import java.util.LinkedList;
+
+/**
+ * Controls how conducted experiments will be persisted and loaded
+ */
+public abstract class Storage {
+
+    /**
+     * Object or URI where experiments are persisted
+     */
+    private URI storageFile;
+
+    /**
+     * To access profileDB general serialization functions
+     */
+    private ProfileDB context;
+
+    /**
+     * Creates a new instance.
+     * @param uri Object or URI where experiments are persisted
+     */
+    public Storage(URI uri){
+        this.storageFile = uri;
+    }
+
+    /**
+     * Sets the ProfileDB for this instance that manages all the Measurement subclasses
+     * */
+    public void setContext(ProfileDB context) {
+        this.context = context;
+    }
+
+    /**
+     * Allows to change where future experiments will be persisted and loaded
+     * @param uri
+     */
+    public void changeLocation(URI uri){
+        this.storageFile = uri;
+    }
+
+    public void save(Experiment... experiments) throws IOException {}
+
+    public void save(Collection<Experiment> experiments) throws IOException {}
+
+    public void append(Experiment... experiments) throws IOException {}
+
+    public void append(Collection<Experiment> experiments) throws IOException {}
+
+    public Collection<Experiment> load() throws IOException { return null; }
+
+    /**
+     * Write {@link Experiment}s to an {@link OutputStream}.
+     *
+     * @param outputStream the {@link OutputStream}
+     */
+    public void save(Collection<Experiment> experiments, OutputStream outputStream) throws IOException {
+        try {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));
+            this.save(experiments, writer);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpectedly, UTF-8 is not supported.");
+        }
+    }
+
+    /**
+     * Write {@link Experiment}s to a {@link Writer}.
+     *
+     * @param writer the {@link Writer}
+     */
+    public void save(Collection<Experiment> experiments, Writer writer) throws IOException {
+        try {
+            Gson gson = context.getGson();
+            for (Experiment experiment : experiments) {
+                gson.toJson(experiment, writer);
+                writer.append('\n');
+            }
+            writer.flush();
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpectedly, UTF-8 is not supported.");
+        }
+    }
+
+    /**
+     * Load {@link Experiment}s from an {@link InputStream}.
+     *
+     * @param inputStream the {@link InputStream}
+     * @return the {@link Experiment}s
+     */
+    public Collection<Experiment> load(InputStream inputStream) throws IOException {
+        try {
+            return load(new BufferedReader(new InputStreamReader(inputStream, "UTF-8")));
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpectedly, UTF-8 is not supported.");
+        }
+    }
+
+    /**
+     * Load {@link Experiment}s from an {@link Reader}.
+     *
+     * @param reader the {@link Reader}
+     * @return the {@link Experiment}s
+     */
+    public Collection<Experiment> load(BufferedReader reader) throws IOException {
+        Collection<Experiment> experiments = new LinkedList<>();
+        Gson gson = context.getGson();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            Experiment experiment = gson.fromJson(line, Experiment.class);
+            experiments.add(experiment);
+        }
+        return experiments;
+    }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/ProfileDBTest.java b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/ProfileDBTest.java
new file mode 100644
index 0000000..da05470
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/ProfileDBTest.java
@@ -0,0 +1,199 @@
+package profiledb;
+
+import org.junit.Assert;
+import org.junit.Test;
+import profiledb.measurement.TestMemoryMeasurement;
+import profiledb.measurement.TestTimeMeasurement;
+import profiledb.model.Experiment;
+import profiledb.model.Measurement;
+import profiledb.model.Subject;
+import profiledb.storage.FileStorage;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.util.*;
+
+public class ProfileDBTest {
+
+    @Test
+    public void testPolymorphSaveAndLoad() throws IOException {
+
+        try {
+            URI uri = new URI("file:///Users/rodrigopardomeza/Desktop/random/myfile.txt");
+            FileStorage store = new FileStorage(uri);
+
+            ProfileDB profileDB = new ProfileDB(store)
+                    .registerMeasurementClass(TestMemoryMeasurement.class)
+                    .registerMeasurementClass(TestTimeMeasurement.class);
+
+            /**
+             * Esto es lo que se espera del codigo del cliente
+             * Tiene que usar la API para registrar medidas
+             */
+            // crea un experimento falso
+            final Experiment experiment = new Experiment("test-xp", new Subject("PageRank", "1.0"), "test experiment");
+
+            // Agrega medidas falsas hardcoded
+            Measurement timeMeasurement = new TestTimeMeasurement("exec-time", 12345L);
+            Measurement memoryMeasurement = new TestMemoryMeasurement("exec-time", System.currentTimeMillis(), 54321L);
+
+            /*Agrega las medidas al experimento*/
+            experiment.addMeasurement(timeMeasurement);
+            experiment.addMeasurement(memoryMeasurement);
+
+            // Save the experiment.
+            /**
+             * Guarda el experimento en memoria
+             */
+            byte[] buffer;
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            profileDB.getStorage().save(Collections.singleton(experiment), bos);
+            bos.close();
+            buffer = bos.toByteArray();
+            System.out.println("Buffer contents: " + new String(buffer, "UTF-8"));
+
+            // Load the experiment.
+            /**
+             * Lee el experimento desde el buffer en memoria
+             */
+            ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
+            Collection<Experiment> loadedExperiments = profileDB.getStorage().load(bis);
+
+            // Compare the experiments.
+            Assert.assertEquals(1, loadedExperiments.size());
+            Experiment loadedExperiment = loadedExperiments.iterator().next();
+            Assert.assertEquals(experiment, loadedExperiment);
+
+            // Compare the measurements.
+            Assert.assertEquals(2, loadedExperiment.getMeasurements().size());
+            Set<Measurement> expectedMeasurements = new HashSet<>(2);
+            expectedMeasurements.add(timeMeasurement);
+            expectedMeasurements.add(memoryMeasurement);
+            Set<Measurement> loadedMeasurements = new HashSet<>(loadedExperiment.getMeasurements());
+            Assert.assertEquals(expectedMeasurements, loadedMeasurements);
+
+        } catch (URISyntaxException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void testRecursiveSaveAndLoad() throws IOException {
+        try {
+            URI uri = new URI("file:///Users/rodrigopardomeza/Desktop/random/myfile.txt");
+            FileStorage store = new FileStorage(uri);
+
+            ProfileDB profileDB = new ProfileDB(store)
+                    .registerMeasurementClass(TestMemoryMeasurement.class)
+                    .registerMeasurementClass(TestTimeMeasurement.class);
+
+            // Create an example experiment.
+            final Experiment experiment = new Experiment("test-xp", new Subject("PageRank", "1.0"), "test experiment");
+            TestTimeMeasurement topLevelMeasurement = new TestTimeMeasurement("exec-time", 12345L);
+            TestTimeMeasurement childMeasurement = new TestTimeMeasurement("sub-exec-time", 2345L);
+            topLevelMeasurement.addSubmeasurements(childMeasurement);
+            experiment.addMeasurement(topLevelMeasurement);
+
+            // Save the experiment.
+            byte[] buffer;
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            profileDB.getStorage().save(Collections.singleton(experiment), bos);
+            bos.close();
+            buffer = bos.toByteArray();
+            System.out.println("Buffer contents: " + new String(buffer, "UTF-8"));
+
+            // Load the experiment.
+            ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
+            Collection<Experiment> loadedExperiments = profileDB.getStorage().load(bis);
+
+            // Compare the experiments.
+            Assert.assertEquals(1, loadedExperiments.size());
+            Experiment loadedExperiment = loadedExperiments.iterator().next();
+            Assert.assertEquals(experiment, loadedExperiment);
+
+            // Compare the measurements.
+            Assert.assertEquals(1, loadedExperiment.getMeasurements().size());
+            final Measurement loadedMeasurement = loadedExperiment.getMeasurements().iterator().next();
+            Assert.assertEquals(topLevelMeasurement, loadedMeasurement);
+        } catch (URISyntaxException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void testFileOperations() throws IOException {
+
+        try {
+            URI uri = new URI("file:///Users/rodrigopardomeza/Desktop/random/myfile.txt");
+            FileStorage store = new FileStorage(uri);
+
+            ProfileDB profileDB = new ProfileDB(store)
+                    .registerMeasurementClass(TestMemoryMeasurement.class)
+                    .registerMeasurementClass(TestTimeMeasurement.class);
+
+            // Create example experiments.
+            final Experiment experiment1 = new Experiment("xp1", new Subject("PageRank", "1.0"), "test experiment 1");
+            experiment1.addMeasurement(new TestTimeMeasurement("exec-time", 1L));
+            final Experiment experiment2 = new Experiment("xp2", new Subject("KMeans", "1.1"), "test experiment 2");
+            experiment2.addMeasurement(new TestTimeMeasurement("exec-time", 2L));
+            final Experiment experiment3 = new Experiment("xp3", new Subject("Apriori", "2.0"), "test experiment 3");
+            experiment3.addMeasurement(new TestMemoryMeasurement("ram", System.currentTimeMillis(), 3L));
+
+            // Save the experiments.
+            File tempDir = Files.createTempDirectory("profiledb").toFile();
+            File file = new File(tempDir, "profiledb.json");
+            profileDB.getStorage().save(experiment1);
+            profileDB.getStorage().append(experiment2, experiment3);
+
+            Files.lines(file.toPath()).forEach(System.out::println);
+
+            // Load and compare.
+            final Set<Experiment> loadedExperiments = new HashSet<>(profileDB.getStorage().load());
+            final List<Experiment> expectedExperiments = Arrays.asList(experiment1, experiment2, experiment3);
+            Assert.assertEquals(expectedExperiments.size(), loadedExperiments.size());
+            Assert.assertEquals(new HashSet<>(expectedExperiments), new HashSet<>(loadedExperiments));
+        } catch (URISyntaxException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void testAppendOnNonExistentFile() throws IOException {
+
+        try {
+            URI uri = new URI("file:///Users/rodrigopardomeza/Desktop/random/myfile.txt");
+            FileStorage store = new FileStorage(uri);
+
+            // This seems to be an issue on Linux.
+            ProfileDB profileDB = new ProfileDB(store)
+                    .registerMeasurementClass(TestMemoryMeasurement.class)
+                    .registerMeasurementClass(TestTimeMeasurement.class);
+
+            // Create example experiments.
+            final Experiment experiment1 = new Experiment("xp1", new Subject("PageRank", "1.0"), "test experiment 1");
+            experiment1.addMeasurement(new TestTimeMeasurement("exec-time", 1L));
+
+            // Save the experiments.
+            File tempDir = Files.createTempDirectory("profiledb").toFile();
+            File file = new File(tempDir, "new-profiledb.json");
+            Assert.assertTrue(!file.exists() || file.delete());
+            profileDB.getStorage().append(experiment1);
+
+            Files.lines(file.toPath()).forEach(System.out::println);
+
+            // Load and compare.
+            final Set<Experiment> loadedExperiments = new HashSet<>(profileDB.getStorage().load());
+            final List<Experiment> expectedExperiments = Collections.singletonList(experiment1);
+            Assert.assertEquals(expectedExperiments.size(), loadedExperiments.size());
+            Assert.assertEquals(new HashSet<>(expectedExperiments), new HashSet<>(loadedExperiments));
+        } catch (URISyntaxException e) {
+            e.printStackTrace();
+        }
+    }
+
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestMemoryMeasurement.java b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestMemoryMeasurement.java
new file mode 100644
index 0000000..200a661
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestMemoryMeasurement.java
@@ -0,0 +1,62 @@
+package profiledb.measurement;
+
+import profiledb.model.Measurement;
+import profiledb.model.Type;
+
+import java.util.Objects;
+
+/**
+ * {@link Measurement} implementation for test purposes.
+ */
+@Type("test-mem")
+public class TestMemoryMeasurement extends Measurement {
+
+    private long timestamp;
+
+    private long usedMb;
+
+    public TestMemoryMeasurement(String id, long timestamp, long usedMb) {
+        super(id);
+        this.timestamp = timestamp;
+        this.usedMb = usedMb;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public long getUsedMb() {
+        return usedMb;
+    }
+
+    public void setUsedMb(long usedMb) {
+        this.usedMb = usedMb;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+        TestMemoryMeasurement that = (TestMemoryMeasurement) o;
+        return timestamp == that.timestamp &&
+                usedMb == that.usedMb;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), timestamp, usedMb);
+    }
+
+    @Override
+    public String toString() {
+        return "TestMemoryMeasurement{" +
+                "timestamp=" + timestamp +
+                ", usedMb=" + usedMb +
+                '}';
+    }
+}
\ No newline at end of file
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestTimeMeasurement.java b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestTimeMeasurement.java
new file mode 100644
index 0000000..2f2b037
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestTimeMeasurement.java
@@ -0,0 +1,56 @@
+package profiledb.measurement;
+
+import profiledb.model.Measurement;
+import profiledb.model.Type;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Objects;
+
+/**
+ * {@link Measurement} implementation for test purposes.
+ */
+@Type("test-time")
+public class TestTimeMeasurement extends Measurement {
+
+    private long millis;
+
+    private Collection<Measurement> submeasurements;
+
+    public TestTimeMeasurement(String id, long millis) {
+        super(id);
+        this.millis = millis;
+        this.submeasurements = new LinkedList<>();
+    }
+
+    public long getMillis() {
+        return millis;
+    }
+
+    public void setMillis(long millis) {
+        this.millis = millis;
+    }
+
+    public Collection<Measurement> getSubmeasurements() {
+        return submeasurements;
+    }
+
+    public void addSubmeasurements(TestTimeMeasurement submeasurements) {
+        this.submeasurements.add(submeasurements);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+        TestTimeMeasurement that = (TestTimeMeasurement) o;
+        return millis == that.millis &&
+                Objects.equals(submeasurements, that.submeasurements);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), millis, submeasurements);
+    }
+}
\ No newline at end of file