DirectoryContentBasedMap.java

package eu.javaexperience.io;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import eu.javaexperience.reflect.Mirror;

public abstract class DirectoryContentBasedMap<T> implements Map<String, T>, Closeable
{
	protected File dir;
	
	public DirectoryContentBasedMap(File directory) throws FileNotFoundException
	{
		if(!directory.isDirectory())
		{
			throw new RuntimeException("file: \""+directory+"\" must be directory");
		}
		
		this.dir = directory;
		
		populateKeys();
		
		File kf = getKeyFile();
		if(kf.exists())
		{
			keyFd = new FileOutputStream(getKeyFile(), true);
		}
		else
		{
			keyFd = new FileOutputStream(getKeyFile());
		}
	}
	

	protected abstract T readValue(File f, int i);
	
	protected OutputStream keyFd;
	
	protected Map<String, Integer> keyIndex = new HashMap<>();
	
	protected File getKeyFile()
	{
		return new File(dir+"/keys");
	}
	
	protected void populateKeys()
	{
		File keys = getKeyFile();
		if(keys.exists())
		{
			try
			(
				FileInputStream fis = new FileInputStream(keys);
				InputStreamReader isr = new InputStreamReader(fis);
				BufferedReader br = new BufferedReader(isr);
			)
			{
				String line = null;
				int i=0;
				while(null != (line = br.readLine()))
				{
					line = URLDecoder.decode(line, "UTF-8");
					keyStore.add(line);
					keyIndex.put(line, i++);
				}
			}
			catch (Exception e)
			{
				throw new RuntimeException(e);
			}
		}
	}
	
	protected int assertKey(String key)
	{
		if(null == key)
		{
			return -1;
		}
		
		int index = getKeyIndex(key);
		if(index > -1)
		{
			return index;
		}
		
		try
		{
			keyFd.write((URLEncoder.encode(key, "UTF-8")+"\n").getBytes());
			keyFd.flush();
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
		
		keyStore.add(key);
		index = keyStore.size()-1;
		keyIndex.put(key, index);
		return index;
	}
	
	protected void clearKeys()
	{
		keyStore.clear();
		keyIndex.clear();
		try
		{
			keyFd.close();
			File keyFile = getKeyFile(); 
			keyFile.delete();
			keyFd = new FileOutputStream(keyFile);
		}
		catch (IOException e)
		{
			throw new RuntimeException(e);
		}
	}
	
	protected ArrayList<String> keyStore = new ArrayList<>();
	
	protected File getDestinationFile(int index)
	{
		return new File(dir+"/"+index);		
	}
	
	protected void dropValue(int index)
	{
		getDestinationFile(index).delete();
	}
	
	protected boolean isValueSet(int index)
	{
		return getDestinationFile(index).exists();
	}
	
	protected int getKeyIndex(Object Tey)
	{
		Integer ret = keyIndex.get(Tey);
		if(null == ret)
		{
			return -1;
		}
		return ret;
	}
	
	@Override
	public int size()
	{
		return keyIndex.size();
	}
	
	@Override
	public boolean isEmpty()
	{
		return keyIndex.isEmpty();
	}
	
	@Override
	public boolean containsKey(Object Tey)
	{
		int index = getKeyIndex(Tey);
		if(index < 0)
		{
			return false;
		}
		return isValueSet(index);
	}
	
	@Override
	public boolean containsValue(Object value)
	{
		return false;
	}
	
	@Override
	public synchronized T get(Object Tey)
	{
		int index = getKeyIndex(Tey);
		if(index > -1)
		{
			return readValue(getDestinationFile(index), index);
		}
		return null;
	}
	
	protected abstract void saveValue(File f, int i, T value);
	
	@Override
	public synchronized T put(String Tey, T value)
	{
		int index = assertKey(Tey);
		if(index > -1)
		{
			saveValue(getDestinationFile(index), index, value);
		}
		
		return null;
	}
	
	@Override
	public synchronized T remove(Object Tey)
	{
		int index = getKeyIndex(Tey);
		if(index > -1)
		{
			dropValue(index);
		}
		return null;
	}
	
	@Override
	public synchronized void putAll(Map<? extends String, ? extends T> m)
	{
		for(java.util.Map.Entry<? extends String, ? extends T> Tv:m.entrySet())
		{
			put(Tv.getKey(), Tv.getValue());
		}
	}
	
	@Override
	public synchronized void clear()
	{
		for(int i=0;i<keyStore.size();++i)
		{
			dropValue(i);
		}
		clearKeys();
	}
	
	@Override
	public synchronized Set<String> keySet()
	{
		Set<String> ret = new HashSet<>();
		//TODO that's wrong! we return the keys that we dropped.
		ret.addAll(keyStore);
		return ret;
	}
	
	@Override
	public synchronized Collection<T> values()
	{
		ArrayList<T> ret = new ArrayList<>();
		for(int i=0;i<keyStore.size();++i)
		{
			T add = readValue(getDestinationFile(i), i);
			if(null != add)
			{
				ret.add(add);
			}
		}
		
		return ret;
	}
	
	public class LazyKV implements Entry<String, T>
	{
		int index;
		
		protected LazyKV(int index)
		{
			this.index = index;
		}
		
		@Override
		public String getKey()
		{
			return keyStore.get(index);
		}
	
		@Override
		public T getValue()
		{
			return readValue(getDestinationFile(index), index);
		}
	
		@Override
		public T setValue(T value)
		{
			saveValue(getDestinationFile(index), index, value);
			return null;
		}
	
		public int getEntryIndex()
		{
			return index;
		}
	}
	
	@Override
	public synchronized Set<java.util.Map.Entry<String, T>> entrySet()
	{
		Set<java.util.Map.Entry<String, T>> ret = new HashSet<>();
		for(int i=0;i<keyStore.size();++i)
		{
			if(isValueSet(i))
			{
				ret.add(new LazyKV(i));
			}
		}
		
		return ret;
	}
	
	public static <T extends Serializable> FileContentMapper<T> openWithCreate(String file) throws FileNotFoundException
	{
		File dir = new File(file);
		if(!dir.exists())
		{
			dir.mkdirs();
		}
		
		return new FileContentMapper<T>(dir);
	}
	
	public static <T extends Serializable> FileContentMapper<T> openWithCreateRuntimeException(String file)
	{
		try
		{
			return openWithCreate(file);
		}
		catch(Exception e)
		{
			Mirror.propagateAnyway(e);
			return null;
		}
	}
	
	@Override
	public synchronized void close() throws IOException
	{
		keyFd.close();
	}
	
	@Override
	protected void finalize() throws Throwable
	{
		IOTools.silentClose(this);
		super.finalize();
	}
	
	public synchronized long getValueLastModify(String key)
	{
		int index = getKeyIndex(key);
		if(index < 0)
		{
			return 0;
		}
		
		File dst = getDestinationFile(index);
		if(null == dst || !dst.exists())
		{
			return 0;
		}
		
		return dst.lastModified();
	}
}