cache

Details

diff --git a/src/main/java/br/ufrgs/inf/prosoft/cache/Cache.java b/src/main/java/br/ufrgs/inf/prosoft/cache/Cache.java
index f71673a..ea8de42 100644
--- a/src/main/java/br/ufrgs/inf/prosoft/cache/Cache.java
+++ b/src/main/java/br/ufrgs/inf/prosoft/cache/Cache.java
@@ -9,42 +9,41 @@ import java.util.Optional;
 import java.util.function.Supplier;
 
 /**
- *
- * @author romulo
  * @param <K>
  * @param <V>
+ * @author romulo
  */
 public interface Cache<K, V> {
 
-    public void put(K key, V value, long timeToLive);
+  void put(K key, V value, long timeToLive);
 
-    public void put(K key, V value);
+  void put(K key, V value);
 
-    public V get(K key) throws KeyNotFoundException;
+  V get(K key) throws KeyNotFoundException;
 
-    public void invalidate(K key);
+  void invalidate(K key);
 
-    public default V computeIfAbsent(K key, Supplier<V> supplier, long timeToLive) {
-        synchronized (Optional.ofNullable(key)) {
-            try {
-                return get(key);
-            } catch (KeyNotFoundException ex) {
-                V get = supplier.get();
-                put(key, get, timeToLive);
-                return get;
-            }
-        }
+  default V computeIfAbsent(K key, Supplier<V> supplier, long timeToLive) {
+    synchronized (Optional.ofNullable(key)) {
+      try {
+        return get(key);
+      } catch (KeyNotFoundException ex) {
+        V get = supplier.get();
+        put(key, get, timeToLive);
+        return get;
+      }
     }
-
-    public default V computeIfAbsent(K key, Supplier<V> supplier) {
-        synchronized (Optional.ofNullable(key)) {
-            try {
-                return get(key);
-            } catch (KeyNotFoundException ex) {
-                V get = supplier.get();
-                put(key, get);
-                return get;
-            }
-        }
+  }
+
+  default V computeIfAbsent(K key, Supplier<V> supplier) {
+    synchronized (Optional.ofNullable(key)) {
+      try {
+        return get(key);
+      } catch (KeyNotFoundException ex) {
+        V get = supplier.get();
+        put(key, get);
+        return get;
+      }
     }
+  }
 }
diff --git a/src/main/java/br/ufrgs/inf/prosoft/cache/Caffeine.java b/src/main/java/br/ufrgs/inf/prosoft/cache/Caffeine.java
index 974e524..2612978 100644
--- a/src/main/java/br/ufrgs/inf/prosoft/cache/Caffeine.java
+++ b/src/main/java/br/ufrgs/inf/prosoft/cache/Caffeine.java
@@ -1,212 +1,200 @@
-/*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
- */
 package br.ufrgs.inf.prosoft.cache;
 
+import br.ufrgs.inf.prosoft.cache.tools.CSV;
 import com.github.benmanes.caffeine.cache.Expiry;
-import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
+
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.function.BiConsumer;
+import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
-/**
- *
- * @author root
- * @param <K>
- * @param <V>
- */
 public class Caffeine<K, V> implements Cache<K, V>, AutoCloseable {
 
-    private static final boolean CACHE_EVALUATE_PERFORMANCE = System.getenv("CACHE_EVENTS") == null || !System.getenv("CACHE_EVENTS").equals("false");
-    private static final boolean CACHE_REGISTER_SIZE = System.getenv("CACHE_REGISTER_SIZE") != null && System.getenv("CACHE_REGISTER_SIZE").equals("true");
-    private static final boolean CACHE_REFRESH_TTL = System.getenv("CACHE_REFRESH_TTL") != null && System.getenv("CACHE_REFRESH_TTL").equals("true");
-    private final com.github.benmanes.caffeine.cache.Cache<Optional<K>, Optional<V>> cache;
-    private final ConcurrentHashMap<Optional<K>, Long> keyHasTTL;
-    private final CachePerformance cachePerformance;
-    private final ScheduledExecutorService sizeMonitor;
-
-    public Caffeine() {
-        this(new CachePerformance());
-    }
-
-    public Caffeine(long ttl) {
-        this(new CachePerformance(), ttl);
-    }
-
-    public Caffeine(String name) {
-        this(new CachePerformance(name));
-    }
-
-    public Caffeine(String name, long ttl) {
-        this(new CachePerformance(name), ttl);
-    }
-
-    public Caffeine(String name, Long ttl, long size) {
-        this(new CachePerformance(name), ttl, size);
-    }
-
-    public Caffeine(CachePerformance cachingPerformance) {
-        this(cachingPerformance, null);
-    }
-
-    public Caffeine(CachePerformance cachingPerformance, Long defaultTTL) {
-        this(cachingPerformance, defaultTTL, null);
-    }
-
-    public Caffeine(CachePerformance cachingPerformance, Long defaultTTL, Long size) {
-        this.cachePerformance = cachingPerformance;
-        this.keyHasTTL = new ConcurrentHashMap<>();
-        com.github.benmanes.caffeine.cache.Caffeine<Optional<K>, Optional<V>> builder = com.github.benmanes.caffeine.cache.Caffeine.newBuilder()
-                .expireAfter(new Expiry<Optional<K>, Optional<V>>() {
-                    @Override
-                    public long expireAfterCreate(Optional<K> key, Optional<V> value, long currentTime) {
-                        Long customTTL = keyHasTTL.remove(key);
-                        if (customTTL != null) {
-                            return customTTL * 1000000;
-                        }
-                        if (defaultTTL == null) {
-                            return Long.MAX_VALUE;
-                        }
-                        long ttlNanoseconds = defaultTTL * 1000000;
-                        if (ttlNanoseconds < 0) {
-                            return Long.MAX_VALUE;
-                        }
-                        return ttlNanoseconds;
-                    }
-
-                    @Override
-                    public long expireAfterUpdate(Optional<K> key, Optional<V> value, long currentTime, long currentDuration) {
-                        return expireAfterCreate(key, value, currentTime);
-                    }
-
-                    @Override
-                    public long expireAfterRead(Optional<K> key, Optional<V> value, long currentTime, long currentDuration) {
-                        if (CACHE_REFRESH_TTL) {
-                            return expireAfterCreate(key, value, currentTime);
-                        }
-                        return currentDuration;
-                    }
-                });
-        if (size != null) {
-            builder.maximumSize(size);
-        }
-        builder.removalListener((k, v, cause) -> {
-            registerEvent(EventType.INVALIDATION, v.orElse(null));
-        });
-        this.cache = builder.build();
-
-        this.sizeMonitor = Executors.newScheduledThreadPool(0);
-        if (Caffeine.CACHE_EVALUATE_PERFORMANCE) {
-            this.sizeMonitor.scheduleAtFixedRate(() -> {
-                this.cachePerformance.registerEvent(EventType.POPULATION, String.valueOf(size()));
-            }, 0, 5, TimeUnit.SECONDS);
-        }
-    }
-
-    @Override
-    public void close() throws IOException {
-        try {
-            Thread.sleep(1000);
-        } catch (InterruptedException ex) {
-        }
-        this.sizeMonitor.shutdown();
-    }
-
-    public CachePerformance getCachePerformance() {
-        return this.cachePerformance;
-    }
-
-    @Override
-    public void put(K key, V value, long timeToLive) {
-        this.keyHasTTL.put(Optional.ofNullable(key), timeToLive);
-        put(key, value);
-    }
-
-    @Override
-    public void put(K key, V value) {
-        this.cache.put(Optional.ofNullable(key), Optional.ofNullable(value));
-        registerEvent(EventType.ADDITION, value);
-    }
-
-    @Override
-    public V get(K key) throws KeyNotFoundException {
-        Optional<V> get = this.cache.getIfPresent(Optional.ofNullable(key));
-        if (get == null) {
-            registerEvent(EventType.MISS, key);
-            throw new KeyNotFoundException();
+  private static final Logger LOGGER = Logger.getLogger(Caffeine.class.getName());
+  private static final boolean CACHE_EVALUATE_PERFORMANCE = System.getenv("CACHE_EVENTS") == null || !System.getenv("CACHE_EVENTS").equals("false");
+  private static final boolean CACHE_REGISTER_SIZE = System.getenv("CACHE_REGISTER_SIZE") != null && System.getenv("CACHE_REGISTER_SIZE").equals("true");
+  private static final boolean CACHE_REFRESH_TTL = System.getenv("CACHE_REFRESH_TTL") != null && System.getenv("CACHE_REFRESH_TTL").equals("true");
+  private static final String CACHE_METHODS_TTL = System.getenv("CACHE_METHODS_TTL") != null && !System.getenv("CACHE_METHODS_TTL").isEmpty() && !System.getenv("CACHE_METHODS_TTL").equals("null")
+    ? System.getenv("CACHE_METHODS_TTL") : null;
+  private static final Map<String, Long> METHOD_HAS_TTL = new HashMap<>();
+  private final com.github.benmanes.caffeine.cache.Cache<Optional<K>, Optional<V>> cache;
+  private final ConcurrentHashMap<Optional<K>, Long> keyHasTTL;
+  private final CachePerformance cachePerformance;
+  private final ScheduledExecutorService sizeMonitor;
+
+  public Caffeine() {
+    this(new CachePerformance());
+  }
+
+  public Caffeine(long ttl) {
+    this(new CachePerformance(), ttl);
+  }
+
+  public Caffeine(String name) {
+    this(new CachePerformance(name));
+  }
+
+  public Caffeine(String name, long ttl) {
+    this(new CachePerformance(name), ttl);
+  }
+
+  public Caffeine(String name, Long ttl, long size) {
+    this(new CachePerformance(name), ttl, size);
+  }
+
+  public Caffeine(CachePerformance cachingPerformance) {
+    this(cachingPerformance, null);
+  }
+
+  public Caffeine(CachePerformance cachingPerformance, Long defaultTTL) {
+    this(cachingPerformance, defaultTTL, null);
+  }
+
+  public Caffeine(CachePerformance cachingPerformance, Long defaultTTL, Long size) {
+    this.cachePerformance = cachingPerformance;
+    this.keyHasTTL = new ConcurrentHashMap<>();
+    com.github.benmanes.caffeine.cache.Caffeine<Optional<K>, Optional<V>> builder = com.github.benmanes.caffeine.cache.Caffeine.newBuilder()
+      .expireAfter(new Expiry<Optional<K>, Optional<V>>() {
+        @Override
+        public long expireAfterCreate(Optional<K> key, Optional<V> value, long currentTime) {
+          Long customTTL = keyHasTTL.remove(key);
+          if (customTTL != null) return customTTL * 1000000;
+          if (defaultTTL == null) return Long.MAX_VALUE;
+          long ttlNanoseconds = defaultTTL * 1000000;
+          if (ttlNanoseconds < 0) return Long.MAX_VALUE;
+          return ttlNanoseconds;
         }
-        registerEvent(EventType.HIT, get);
-        return get.orElse(null);
-    }
 
-    @Override
-    public void invalidate(K key) {
-        this.cache.invalidate(Optional.ofNullable(key));
-        registerEvent(EventType.INVALIDATION, null);
-    }
-
-    public boolean containsKey(K key) {
-        Optional<V> get = this.cache.getIfPresent(Optional.ofNullable(key));
-        return get != null;
-    }
-
-    public Set<Map.Entry<K, V>> entrySet() {
-        Set<Map.Entry<K, V>> entrySet = this.cache.asMap().entrySet().stream().collect(
-                Collectors.toMap(entry -> entry.getKey().orElse(null), entry -> entry.getValue().orElse(null)))
-                .entrySet();
-        if (CACHE_EVALUATE_PERFORMANCE) {
-            entrySet.forEach(entry -> {
-                registerEvent(EventType.HIT, entry.getValue());
-            });
+        @Override
+        public long expireAfterUpdate(Optional<K> key, Optional<V> value, long currentTime, long currentDuration) {
+          return expireAfterCreate(key, value, currentTime);
         }
-        return entrySet;
-    }
-
-    public void forEach(BiConsumer<K, V> consumer) {
-        entrySet().forEach(entry -> {
-            consumer.accept(entry.getKey(), entry.getValue());
-        });
-    }
-
-    public int size() {
-        return this.cache.asMap().size();
-    }
-
-    public boolean isEmpty() {
-        return this.cache.asMap().isEmpty();
-    }
-
-    private String getIdentifier(Object object) {
-        return object != null ? String.valueOf(object.hashCode()) : "null";
-    }
 
-    private void registerEvent(EventType eventType, Object object) {
-        if (!CACHE_EVALUATE_PERFORMANCE) {
-            return;
-        }
-        String identifier = getIdentifier(object);
-        if (CACHE_REGISTER_SIZE) {
-            this.cachePerformance.registerEvent(eventType, identifier, CachePerformance.calculateObjectSize(object));
-        } else {
-            this.cachePerformance.registerEvent(eventType, identifier);
-        }
-        if (eventType.equals(EventType.ADDITION)) {
-            this.cachePerformance.registerSize(size());
+        @Override
+        public long expireAfterRead(Optional<K> key, Optional<V> value, long currentTime, long currentDuration) {
+          if (CACHE_REFRESH_TTL) return expireAfterCreate(key, value, currentTime);
+          return currentDuration;
         }
-    }
-
-    public List<V> values() {
-        return this.cache.asMap().values().stream()
-                .map(value -> value.orElse(null))
-                .collect(Collectors.toList());
-    }
+      });
+    if (size != null) builder.maximumSize(size);
+    builder.removalListener((k, v, cause) -> registerEvent(EventType.INVALIDATION, v.orElse(null)));
+    this.cache = builder.build();
+
+    this.sizeMonitor = Executors.newScheduledThreadPool(0);
+    if (Caffeine.CACHE_EVALUATE_PERFORMANCE) this.sizeMonitor.scheduleAtFixedRate(() ->
+      this.cachePerformance.registerEvent(EventType.POPULATION, String.valueOf(size())), 0, 5, TimeUnit.SECONDS);
+  }
+
+  private synchronized static void loadCacheableParameters() {
+    if (CACHE_METHODS_TTL == null) return;
+    if (METHOD_HAS_TTL.isEmpty()) return;
+    CSV methodsHasTTL = CSV.read(CACHE_METHODS_TTL);
+    methodsHasTTL.stream().forEach(it -> METHOD_HAS_TTL.put(it.get("method"), Long.valueOf(it.get("ttl"))));
+  }
+
+  @Override
+  public void close() {
+    try {
+      Thread.sleep(1000);
+    } catch (InterruptedException ignored) {
+    }
+    this.sizeMonitor.shutdown();
+  }
+
+  public CachePerformance getCachePerformance() {
+    return this.cachePerformance;
+  }
+
+  private Long getTTLForMethod() {
+    loadCacheableParameters();
+    if (METHOD_HAS_TTL.isEmpty()) return null;
+    StackTraceElement[] traceElements = Thread.currentThread().getStackTrace();
+    for (int i = 3; i < traceElements.length; i++) {
+      String methodName = traceElements[i].getClassName() + "." + traceElements[i].getMethodName();
+      if (METHOD_HAS_TTL.containsKey(methodName)) return METHOD_HAS_TTL.get(methodName);
+    }
+    return null;
+  }
+
+  @Override
+  public void put(K key, V value, long timeToLive) {
+    Long ttl = getTTLForMethod();
+    if (ttl != null) timeToLive = ttl;
+    this.keyHasTTL.put(Optional.ofNullable(key), timeToLive);
+    put(key, value);
+  }
+
+  @Override
+  public void put(K key, V value) {
+    if (!this.keyHasTTL.containsKey(Optional.ofNullable(key))) {
+      Long ttl = getTTLForMethod();
+      if (ttl != null) this.keyHasTTL.put(Optional.ofNullable(key), ttl);
+    }
+    this.cache.put(Optional.ofNullable(key), Optional.ofNullable(value));
+    registerEvent(EventType.ADDITION, value);
+  }
+
+  @Override
+  public V get(K key) throws KeyNotFoundException {
+    Optional<V> get = this.cache.getIfPresent(Optional.ofNullable(key));
+    if (get == null) {
+      registerEvent(EventType.MISS, key);
+      throw new KeyNotFoundException();
+    }
+    registerEvent(EventType.HIT, get);
+    return get.orElse(null);
+  }
+
+  @Override
+  public void invalidate(K key) {
+    this.cache.invalidate(Optional.ofNullable(key));
+    registerEvent(EventType.INVALIDATION, null);
+  }
+
+  public boolean containsKey(K key) {
+    Optional<V> get = this.cache.getIfPresent(Optional.ofNullable(key));
+    return get != null;
+  }
+
+  public Set<Map.Entry<K, V>> entrySet() {
+    Set<Map.Entry<K, V>> entrySet = this.cache.asMap().entrySet().stream().collect(
+      Collectors.toMap(entry -> entry.getKey().orElse(null), entry -> entry.getValue().orElse(null))
+    ).entrySet();
+    if (CACHE_EVALUATE_PERFORMANCE) entrySet.forEach(entry -> registerEvent(EventType.HIT, entry.getValue()));
+    return entrySet;
+  }
+
+  public void forEach(BiConsumer<K, V> consumer) {
+    entrySet().forEach(entry -> consumer.accept(entry.getKey(), entry.getValue()));
+  }
+
+  public int size() {
+    return this.cache.asMap().size();
+  }
+
+  public boolean isEmpty() {
+    return this.cache.asMap().isEmpty();
+  }
+
+  private String getIdentifier(Object object) {
+    return object != null ? String.valueOf(object.hashCode()) : "null";
+  }
+
+  private void registerEvent(EventType eventType, Object object) {
+    if (!CACHE_EVALUATE_PERFORMANCE) return;
+    String identifier = getIdentifier(object);
+    if (CACHE_REGISTER_SIZE) this.cachePerformance.registerEvent(eventType, identifier, CachePerformance.calculateObjectSize(object));
+    else this.cachePerformance.registerEvent(eventType, identifier);
+    if (eventType.equals(EventType.ADDITION)) this.cachePerformance.registerSize(size());
+  }
+
+  public List<V> values() {
+    return this.cache.asMap().values().stream()
+      .map(value -> value.orElse(null))
+      .collect(Collectors.toList());
+  }
 }
diff --git a/src/main/java/br/ufrgs/inf/prosoft/cache/tools/CSV.java b/src/main/java/br/ufrgs/inf/prosoft/cache/tools/CSV.java
index 1aee4fd..6910a16 100644
--- a/src/main/java/br/ufrgs/inf/prosoft/cache/tools/CSV.java
+++ b/src/main/java/br/ufrgs/inf/prosoft/cache/tools/CSV.java
@@ -1,281 +1,242 @@
 package br.ufrgs.inf.prosoft.cache.tools;
 
 import br.ufrgs.inf.prosoft.cache.tools.CSV.Row;
+
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
+import java.util.*;
+import java.util.stream.Stream;
 
 public class CSV implements Iterable<Row> {
 
-    private class Reference {
-
-        String value;
-
-        public Reference() {
-        }
-
-        public Reference(String value) {
-            this.value = value;
-        }
-
-        @Override
-        public int hashCode() {
-            int hash = 7;
-            hash = 11 * hash + Objects.hashCode(this.value);
-            return hash;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this.value == null) {
-                return obj == null;
-            }
-            if (obj instanceof Reference) {
-                return this.value.equals(((Reference) obj).value);
-            }
-            if (obj instanceof String) {
-                return this.value.equals((String) obj);
-            }
-            return false;
-        }
-
-        @Override
-        public String toString() {
-            return value;
-        }
+  private final List<Header> headers;
+  private final List<Row> rows;
+  private final List<Column> columns;
+  private final Map<Header, Map<Reference, Value>> indexes;
+
+  public CSV(String... headers) {
+    this(headers, new String[]{});
+  }
+
+  public CSV(String[] headers, String[] indexes) {
+    this.headers = new ArrayList<>();
+    this.indexes = new HashMap<>();
+    this.columns = new ArrayList<>();
+    this.rows = new ArrayList<>();
+
+    ArrayList<String> indexesList = new ArrayList<>(Arrays.asList(indexes));
+
+    for (String strHeader : headers) {
+      Header header = new Header(strHeader);
+      this.headers.add(header);
+      if (indexesList.contains(header.toString())) {
+        indexesList.remove(header.toString());
+        this.indexes.put(header, new HashMap<>());
+      }
+      this.columns.add(new Column());
     }
-
-    private class Header extends Reference {
-
-        public Header(String value) {
-            super(value);
-        }
-
+    if (!indexesList.isEmpty()) throw new RuntimeException("indexes not present on headers: " + indexesList);
+  }
+
+  private static String[] process(String line) {
+    if (line.contains("'") || line.contains("\"")) throw new UnsupportedOperationException("Scape values is not supported yet");
+    return line.split(",");
+  }
+
+  public static CSV read(String path) {
+    return read(path, new String[]{});
+  }
+
+  public static CSV read(String path, String... indexes) {
+    try {
+      List<String> lines = Files.readAllLines(Paths.get(path));
+      if (lines.isEmpty()) throw new RuntimeException("Empty file");
+      String headers = lines.remove(0);
+      CSV csv = new CSV(process(headers), indexes);
+      lines.forEach(line -> csv.append(process(line)));
+      return csv;
+    } catch (IOException ex) {
+      throw new RuntimeException("Cannot read file");
+    }
+  }
+
+  public CSV append(String... values) {
+    if (values.length != this.headers.size()) throw new RuntimeException("Values not matching headers length");
+    List<Value> list = new ArrayList<>();
+    Row row = new Row(list);
+    Iterator<Column> columnsIterator = this.columns.iterator();
+    Iterator<Header> headersIterator = this.headers.iterator();
+    for (String strValue : values) {
+      Header header = headersIterator.next();
+      Reference reference = new Reference(strValue);
+      Value value = new Value(header, reference, row, columnsIterator.next());
+      list.add(value);
+      if (this.indexes.containsKey(header)) {
+        if (this.indexes.get(header).get(reference) != null) throw new RuntimeException("duplicate index " + reference + " for " + header);
+        this.indexes.get(header).put(reference, value);
+      }
+    }
+    this.rows.add(row);
+    return this;
+  }
+
+  public Optional<Row> selectFirst(String property, String value) {
+    Header mockProperty = new Header(property);
+    Reference mockValue = new Reference(value);
+    if (!this.headers.contains(mockProperty)) throw new RuntimeException("header not present");
+    Map<Reference, Value> values = this.indexes.get(mockProperty);
+    if (values != null) return Optional.ofNullable(values.get(mockValue).getRow());
+
+    for (Row row : this) if (row.get(property).equals(value)) return Optional.ofNullable(row);
+    return Optional.empty();
+  }
+
+  public List<Row> select(String property, String value) {
+    Header mockProperty = new Header(property);
+    Reference mockValue = new Reference(value);
+    if (!this.headers.contains(mockProperty)) throw new RuntimeException("header not present");
+    List<Row> select = new ArrayList<>();
+
+    Map<Reference, Value> values = this.indexes.get(mockProperty);
+    if (values != null) {
+      select.add(values.get(mockValue).getRow());
+      return select;
     }
 
-    private class Value {
-
-        private final Header header;
-        private Reference reference;
-        private final Row row;
-        private final Column column;
-
-        public Value(Header header, Row row, Column column) {
-            this(header, null, row, column);
-        }
+    for (Row row : this) if (row.get(property).equals(value)) select.add(row);
+    return select;
+  }
 
-        public Value(Header header, Reference reference, Row row, Column column) {
-            this.header = header;
-            this.reference = reference;
-            this.row = row;
-            this.column = column;
-        }
+  @Override
+  public Iterator<Row> iterator() {
+    return this.rows.iterator();
+  }
 
-        public String getProperty() {
-            return header.value;
-        }
+  public Stream<Row> stream() {
+    return this.rows.stream();
+  }
 
-        public String getValue() {
-            return reference.value;
-        }
+  private static class Reference {
 
-        private void setValue(Reference reference) {
-            this.reference = reference;
-        }
+    String value;
 
-        public Row getRow() {
-            return row;
-        }
+    public Reference() {
+    }
 
-        public Column getColumn() {
-            return column;
-        }
+    public Reference(String value) {
+      this.value = value;
+    }
 
-        @Override
-        public String toString() {
-            return getProperty() + ": " + getValue();
-        }
+    @Override
+    public int hashCode() {
+      int hash = 7;
+      hash = 11 * hash + Objects.hashCode(this.value);
+      return hash;
+    }
 
+    @Override
+    public boolean equals(Object obj) {
+      if (this.value == null) return obj == null;
+      if (obj instanceof Reference) return this.value.equals(((Reference) obj).value);
+      if (obj instanceof String) return this.value.equals(obj);
+      return false;
     }
 
-    protected class Row implements Iterable<Value> {
+    @Override
+    public String toString() {
+      return value;
+    }
+  }
 
-        private final List<Value> values;
+  private static class Header extends Reference {
 
-        public Row(List<Value> values) {
-            if (values == null) {
-                throw new RuntimeException("Tried to create a null row");
-            }
-            this.values = values;
-        }
+    public Header(String value) {
+      super(value);
+    }
 
-        public String get(String property) {
-            for (Value value : this) {
-                if (value.getProperty().equals(property)) {
-                    return value.getValue();
-                }
-            }
-            throw new RuntimeException("data does not contain property " + property);
-        }
+  }
 
-        @Override
-        public Iterator<Value> iterator() {
-            return this.values.iterator();
-        }
+  private class Value {
 
-        @Override
-        public String toString() {
-            return values.toString();
-        }
+    private final Header header;
+    private final Row row;
+    private final Column column;
+    private Reference reference;
 
+    public Value(Header header, Row row, Column column) {
+      this(header, null, row, column);
     }
 
-    private class Column extends Row {
+    public Value(Header header, Reference reference, Row row, Column column) {
+      this.header = header;
+      this.reference = reference;
+      this.row = row;
+      this.column = column;
+    }
 
-        public Column() {
-            this(new ArrayList<>());
-        }
+    public String getProperty() {
+      return header.value;
+    }
 
-        public Column(List<Value> values) {
-            super(values);
-        }
+    public String getValue() {
+      return reference.value;
     }
 
-    private List<Header> headers;
-    private final List<Row> rows;
-    private final List<Column> columns;
-    private final Map<Header, Map<Reference, Value>> indexes;
+    private void setValue(Reference reference) {
+      this.reference = reference;
+    }
 
-    public CSV(String... headers) {
-        this(headers, new String[]{});
+    public Row getRow() {
+      return row;
     }
 
-    public CSV(String[] headers, String[] indexes) {
-        this.headers = new ArrayList<>();
-        this.indexes = new HashMap<>();
-        this.columns = new ArrayList<>();
-        this.rows = new ArrayList<>();
-
-        ArrayList<String> indexesList = new ArrayList(Arrays.asList(indexes));
-
-        for (String strHeader : headers) {
-            Header header = new Header(strHeader);
-            this.headers.add(header);
-            if (indexesList.contains(header.toString())) {
-                indexesList.remove(header.toString());
-                this.indexes.put(header, new HashMap<>());
-            }
-            this.columns.add(new Column());
-        }
-        if (!indexesList.isEmpty()) {
-            throw new RuntimeException("indexes not present on headers: " + indexesList);
-        }
+    public Column getColumn() {
+      return column;
     }
 
-    public CSV append(String... values) {
-        if (values.length != this.headers.size()) {
-            throw new RuntimeException("Values not matching headers length");
-        }
-        List<Value> list = new ArrayList<>();
-        Row row = new Row(list);
-        Iterator<Column> columnsIterator = this.columns.iterator();
-        Iterator<Header> headersIterator = this.headers.iterator();
-        for (String strValue : values) {
-            Header header = headersIterator.next();
-            Reference reference = new Reference(strValue);
-            Value value = new Value(header, reference, row, columnsIterator.next());
-            list.add(value);
-            if (this.indexes.containsKey(header)) {
-                if (this.indexes.get(header).get(reference) != null) {
-                    throw new RuntimeException("duplicate index " + reference + " for " + header);
-                }
-                this.indexes.get(header).put(reference, value);
-            }
-        }
-        this.rows.add(row);
-        return this;
+    @Override
+    public String toString() {
+      return getProperty() + ": " + getValue();
     }
 
-    private static String[] process(String line) {
-        if (line.contains("'") || line.contains("\"")) {
-            throw new UnsupportedOperationException("Scaped values not supported yet");
-        }
-        return line.split(",");
+  }
+
+  public class Row implements Iterable<Value> {
+
+    private final List<Value> values;
+
+    public Row(List<Value> values) {
+      if (values == null) throw new RuntimeException("Tried to create a null row");
+      this.values = values;
     }
 
-    public static CSV read(String path) {
-        return read(path, new String[]{});
+    public String get(String property) {
+      for (Value value : this) if (value.getProperty().equals(property)) return value.getValue();
+      throw new RuntimeException("data does not contain property " + property);
     }
 
-    public static CSV read(String path, String... indexes) {
-        try {
-            List<String> lines = Files.readAllLines(Paths.get(path));
-            if (lines.isEmpty()) {
-                throw new RuntimeException("Empty file");
-            }
-            String headers = lines.remove(0);
-            CSV csv = new CSV(process(headers), indexes);
-            lines.forEach(line -> {
-                csv.append(process(line));
-            });
-            return csv;
-        } catch (IOException ex) {
-            throw new RuntimeException("Cannot read file");
-        }
+    @Override
+    public Iterator<Value> iterator() {
+      return this.values.iterator();
     }
 
-    public Optional<Row> selectFirst(String property, String value) {
-        Header mockProperty = new Header(property);
-        Reference mockValue = new Reference(value);
-        if (!this.headers.contains(mockProperty)) {
-            throw new RuntimeException("header not present");
-        }
-        Map<Reference, Value> values = this.indexes.get(mockProperty);
-        if (values != null) {
-            return Optional.ofNullable(values.get(mockValue).getRow());
-        }
-
-        for (Row row : this) {
-            if (row.get(property).equals(value)) {
-                return Optional.ofNullable(row);
-            }
-        }
-        return Optional.empty();
+    @Override
+    public String toString() {
+      return values.toString();
     }
 
-    public List<Row> select(String property, String value) {
-        Header mockProperty = new Header(property);
-        Reference mockValue = new Reference(value);
-        if (!this.headers.contains(mockProperty)) {
-            throw new RuntimeException("header not present");
-        }
-        List<Row> select = new ArrayList<>();
-
-        Map<Reference, Value> values = this.indexes.get(mockProperty);
-        if (values != null) {
-            select.add(values.get(mockValue).getRow());
-            return select;
-        }
-
-        for (Row row : this) {
-            if (row.get(property).equals(value)) {
-                select.add(row);
-            }
-        }
-        return select;
+  }
+
+  private class Column extends Row {
+
+    public Column() {
+      this(new ArrayList<>());
     }
 
-    @Override
-    public Iterator<Row> iterator() {
-        return this.rows.iterator();
+    public Column(List<Value> values) {
+      super(values);
     }
+  }
 
 }