package ee.shy.storage;

import ee.shy.io.Json;
import ee.shy.io.Jsonable;
import ee.shy.map.UnkeyableSimpleMap;
import org.apache.commons.io.IOUtils;

import java.io.*;
import java.security.DigestInputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * Map-like abstract storage class for binary data.
 * Access data by hash.
 *
 * @see Hash
 * @see MapStorage
 * @see FileStorage
 */
public abstract class DataStorage implements UnkeyableSimpleMap<Hash, InputStream> {
    /**
     * Adds binary data from input stream to storage.
     * @param source input stream to get data from
     * @return hash of stored data
     */
    @Override
    public final Hash put(InputStream source) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            DigestInputStream dis = new DigestInputStream(source, md);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            IOUtils.copy(dis, baos);

            Hash hash = new Hash(md.digest());
            put(hash, new ByteArrayInputStream(baos.toByteArray()));
            return hash;
        }
        catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Adds object as JSON to storage.
     * @param object object to store
     * @return hash of stored data
     */
    public final Hash put(Jsonable object) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            DigestOutputStream dos = new DigestOutputStream(baos, md);

            object.write(dos);

            Hash hash = new Hash(md.digest());
            put(hash, new ByteArrayInputStream(baos.toByteArray()));
            return hash;
        }
        catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Adds binary data from input stream with precomputed hash.
     * @param hash hash of data from input stream
     * @param source input stream to get data from
     * @throws IOException if there was a problem reading the input stream or writing to some output
     */
    protected abstract void put(Hash hash, InputStream source) throws IOException;

    /**
     * Gets binary data as input stream by hash.
     * Input data is checked against given hash to ensure correctness.
     * @param hash hash of data to get
     * @return input stream of data
     */
    @Override
    public final InputStream get(Hash hash) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");

            try (DigestInputStream dis = new DigestInputStream(getUnchecked(hash), md);
                 ByteArrayOutputStream baos = new ByteArrayOutputStream();) {

                IOUtils.copy(dis, baos);

                Hash hashComputed = new Hash(md.digest());
                if (!hash.equals(hashComputed))
                    throw new RuntimeException("stored file content does not match hash");

                return new ByteArrayInputStream(baos.toByteArray());
            }
            catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Gets object from JSON by hash.
     * @param hash hash of object to get
     * @param classofT class of object to get
     * @param <T> type of obect to get
     * @return object from JSON
     * @throws IOException if JSON deserialization from underlying stream fails
     */
    public final <T> T get(Hash hash, Class<T> classofT) throws IOException {
        return Json.read(get(hash), classofT);
    }

    /**
     * Gets binary data as input stream by hash.
     * Input data is <b>not</b> checked against given hash to ensure correctness.
     * @param hash hash of data to get
     * @return input stream of data
     * @throws IOException if there was a problem reading from some input
     */
    protected abstract InputStream getUnchecked(Hash hash) throws IOException;
}

@@ -160,17 +160,130 @@
 java.io.
-*
+ByteArrayInputStream;%0Aimport java.io.ByteArrayOutputStream;%0Aimport java.io.IOException;%0Aimport java.io.InputStream
 ;%0Aimport
@@ -2969,24 +2969,131 @@
 (%22SHA-1%22);%0A%0A
+            InputStream is = getUnchecked(hash);%0A            if (is == null)%0A                return null;%0A%0A
             
@@ -3143,34 +3143,18 @@
 tStream(
-getUnchecked(hash)
+is
 , md);%0A 
@@ -3555,24 +3555,30 @@
       %7D%0A    
+    %7D%0A
         catc
@@ -3572,34 +3572,47 @@
 %0A        catch (
-IO
+NoSuchAlgorithm
 Exception e) %7B%0A 
@@ -3602,36 +3602,32 @@
 mException e) %7B%0A
-    
             thro
@@ -3653,30 +3653,16 @@
 ion(e);%0A
-            %7D%0A
         
@@ -3674,39 +3674,26 @@
      catch (
-NoSuchAlgorithm
+IO
 Exception e)

Fix DataStorage#get not returning null for non-existent keys