GenericStorage.java

package hu.ddsi.java.database;

import hu.ddsi.java.database.GenericStoreData.GenericStorageObjectState;
import hu.ddsi.java.database.JavaSQLImp.SqlStorage;
import hu.ddsi.java.database.fieldAnnotations.GenericStoreIgnore;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import eu.javaexperience.collection.CollectionTools;
import eu.javaexperience.collection.map.SmallMap;
import eu.javaexperience.query.F;
import eu.javaexperience.query.LogicalGroup;
import eu.javaexperience.reflect.Mirror;
import eu.javaexperience.semantic.references.MayNull;

/**
 * S: Nem lehetséges minden adatbázist egyformán megszólítani.
 * Dehogynem! Jól meghározott feltételekkel minden adatbázis megszólítható, az implementáció lényege pontosan ez (bár cinikusan mondhatnánk azt is hogy mindenhol ugyanolyan rosszul.)
 * Azaz ha például a SQL-ben nem használunk relációt, MongoDB-ben pedig a tipus és struktúrafüggetlen tárolást, akkor jutunk ehhez a megoldáshoz.
 * 
 * Eszerint célszerű objektumokban gondolkodnunk. Képzeljük azt hogy van egy nagy Map<Long,Object>-ünk.
 * 
 * Belső működés:
 * 		- Minden olyan osztálynak, amit tárolni szeretnénk implementálnia kell a {@link GenericStorable} interfacet-t.
 * 		- Az egyes mezőket - attól függően hogy hogyan szeretnénk tárolni - annotációkkal kell ellátni.
 * 		- Az objektumok és tömbök általános tárolási módja a sorosítás, így tartalmában nem kereshetünk.
 * 		- A GenericStorable egy keretet biztosít, a végrehajtást a {@link GenericStoreDatabase}, {@link GenericStoreDataReader} és {@link GenericStoreDataWriter} implementációi végzik.
 * 
 * Mezők tárolásának a lehetősége:
 * 			- A statikus mezők nem kerülnek tárolásra.
 * 			- Azok a mezők amelyeket nem jelöltünk semmilyen annotációval nem vesznek rész a tárolásban.
 * 			- Szülő osztály mezőit is lehet tárolni:
 * 
 * A tárolandó objektum implementálja a {@link GenericStorable} interface-t akkor az általa szolgáltatott {@link GenericStorable#getSelfDefinedMapping()} Map alapján történik az egyes mezők tárolása,
 * ha ez null akkor a természetes úton történik a mezők begyüjtése. (Mi van ha a felmenőkben két ugyanolyan nevű privát mező is volt (más-más felső osztályba)? Az Object osztályhoz közelebbi lesz figyelembe véve...)
 * 
 * Ezt a Map-et célszerű statikusan egyszer létrehozni és mindig ugyanazt adni vissza.
 * A benne lévő {@link GenericStoreData} azért szükséges hogy ne kelljen absztakt osztályt létrehozni, ezzel megfosztva a tervezendő osztályt az öröklés lehetőségétől. Azt a mezőt ami a tarolási adatokat tartalmazza jelöljük {@link GenericStoreIgnore}-ral
 * 
 * Általános adatbázisok:
 * Szükségünk lesz egy adatbázisra, ami lehet MySQL, PostgreSQL, SQLite, MongoDB vagy //TODO solr.
 * Ezeket a megfelelő implementációját már megírtam:
 * 		- Java JDBC motorra épülő SQL adatbázis kezelő: minden olyan Driver ami {@link java.sql.Connection}-nel tér vissza, tesztelve: MySQL, PostgreSQL, SQLite
 * 			Osztály neve: {@link SqlStorage}
 * 		- MongoDB, szükésg lesz a com.mongodb-ben lévő osztályokra, a {@link MongoDBStorageDatabase} egy DB objektumot vár amit a {@link Mongo#getDB(String)} ad vissza.
 * 		- solr //TODO
 * 		- ilyen implementációkat mi is készíthetünk {@link GenericDatabaseHowToImplement}.
 * 
 * Egy objektum útja az új adatbázisba:
 * 		//TODO
 *	
 * 		Ha létrehoztunk egy {@link GenericStorable} példányt és az tárolni szeretnénk adjuk
 * 
 * 
 * Az objekum azonosítója az adatbázisban "do" névvesz szerepel, mivel ez a java-ban kulcsszó nem fedünk el használható mezőnevet.  
 * 
 * 
 * //jó lenne ha lehetne Mapot is beállítani hogy az itteni java mezők milyen adatbázis mezőknek felelnek meg! 
 * // bár az inkább adatbázis specifikus beállítás 
 * */
public class GenericStorage
{
	public static final long ObjectUntrackedUnstored = -2;
	public static final long NewObjectNotSaved = -1;
	
	public static FieldData getFieldByName(FieldData[] data, String name)
	{
		for(FieldData d:data)
		{
			if(d.f.getName().equals(name))
			{
				return d;
			}
		}
		
		return null;
	}
	/*
	public static FieldData[] getOrCollectClassData(String strCls) throws InstantiationException, IllegalAccessException, ClassNotFoundException
	{
		Class<? extends GenericStorable> cls = (Class<? extends GenericStorable>) stringToClass.get(strCls);
		
		if(cls != null)
			return classesFieldDatas.get(cls);
		FieldData[] data = getOrCollectClassData((Class<? extends GenericStorable>)Class.forName(strCls));
		cls = (Class<? extends GenericStorable>) stringToClass.get(strCls);
		return data;
	}
	
	static Class<?> getOrCollectClass(String clsStr) throws InstantiationException, IllegalAccessException, ClassNotFoundException
	{
		Class<?> cls = stringToClass.get(clsStr); 
		if(cls != null)
			return cls;
		
		getOrCollectClassData(clsStr);
		
		return stringToClass.get(clsStr);
	}
	*/
	public static FieldData[] getOrCollectClassDataRt(Class<? extends GenericStorable> cls)
	{
		try
		{
			return getOrCollectClassData(cls);
		}
		catch(Exception e)
		{
			Mirror.propagateAnyway(e);
			return null;
		}
	}
	
	public static FieldData[] getOrCollectClassData(Class<? extends GenericStorable> cls) throws InstantiationException, IllegalAccessException
	{
		return GenericStorageMappingData.getOrCollectClassData(cls);
	}
	
	public static <T extends GenericStorable> T newInstance(Class<T> cls) throws InstantiationException, IllegalAccessException
	{
		//return UnsafeMirror.allocObject(cls);
		return cls.newInstance();
	}
	
	/*static FieldData[] getStorageForClass(GenericStorable obj,GenericStoreDatabase db) throws InstantiationException, IllegalAccessException, GenericStoreException
	{
		try
		{
			FieldData[] fds = classesFieldDatas.get(obj.getClass());
			
			if(fds == null)
				fds = GenericStorageMappingData.getOrCollectClassData(obj);
			
			db.createStorageForClassIfNeeded(obj.getClass(), fds);
			
			return fds;
		}
		catch (Exception e)
		{
			throw new GenericStoreException(e);
		}
	}*/

	/*public static void createStorageForClassIfNeeded(Class<? extends GenericStorable> cls, GenericStoreDatabase db) throws Exception
	{
		FieldData[] fds = classesFieldDatas.get(cls);
		
		if(fds == null)
			fds = getOrCollectClassData(cls);
		
		db.createStorageForClassIfNeeded(cls, fds);
	}*/
	
	@Deprecated
	public static GenericStorable getObjectByID(long id,GenericStoreDatabase gdb) throws GenericStoreException
	{
		return getObjectByIDDescendantOf(id, GenericStorable.class, gdb);
	}
	
	public static <T extends GenericStorable> T getObjectByIDDescendantOf(long id,Class<? extends T> cls,GenericStoreDatabase gdb) throws GenericStoreException
	{
		try
		{
			return (T) gdb.getSingleObjectByID(id, gdb.getDescendantClassesFor(cls));
		}
		catch (Exception e)
		{
			throw new GenericStoreException(e);
		}
	}
	
	public static <D extends GenericStoreDatabase,W> void storeObject(GenericStorable gs,D gdb) throws GenericStoreException
	{
		GenericStoreData data = gs.getGenericStoreData();
		if(data != null)
		{
			if(gs.getGenericStoreData().id != -1 && (GenericStorageObjectState.UNDER_SAVE == data.state || !gs.getGenericStoreData().isModified()))
			{
				return;
			}
		}
		try
		{
			gdb.store(gs);
		}
		catch (Exception e)
		{
			throw new GenericStoreException(e.getMessage()+" "+gs.toString(),e);
		}
	}
	
	public static GenericStoreData getOrCreateGenericStoreData(GenericStorable gs)
	{
		GenericStoreData data = gs.getGenericStoreData();
		if(data == null)
		{
			gs.setGenericStoreData(data = new GenericStoreData());
		}
		return data;
	}
	
	public static void storeAll(Collection<? extends GenericStorable> coll,GenericStoreDatabase gdb) throws GenericStoreException
	{
		try
		{
			gdb.storeAll(coll);
		}
		catch(Exception e)
		{
			throw new GenericStoreException(e);
		}
	}
	
	public static void storeAll(GenericStoreDatabase gdb, GenericStorable... gss) throws GenericStoreException
	{
		try
		{
			gdb.storeAll(CollectionTools.inlineAdd(new ArrayList(), gss));
		}
		catch(Exception e)
		{
			throw new GenericStoreException(e);
		}
	}
	
	public static <T extends GenericStorable> void getAllObjectsByQuery(Class<? extends T> cls, LogicalGroup lg,Collection<T> coll,GenericStoreDatabase gdb, GsdbExtraCaluse... extra) throws GenericStoreException
	{
		try
		{
			gdb.getAllObjectsByQuery(cls, lg, coll, extra);
		}
		catch (Exception e)
		{
			throw new GenericStoreException(e);
		}		
	}
	
	/*public static <T extends GenericStorable> GenericStoreDBCursor<T> getObjectsByQuery(Class<T> cls,LogicalGroup lg,GenericStoreDatabase gdb) throws GenericStoreException
	{
		try
		{
			if(!gdb.isStored(cls))
				return new GenericStoreDBCursor<>();
			return new GenericStoreDBCursor<T>(gdb.getIDListByQuery(cls, lg,true), gdb,new Class[]{cls});
		}
		catch (Exception e)
		{
			throw new GenericStoreException(e);
		}		
	}*/
	
	/*public static <T extends GenericStorable> GenericStoreDBCursor<T> getObjectsByNativeStringQuery(Class<T> cls,String query,GenericStoreDatabase gdb) throws GenericStoreException
	{
		try
		{
			if(!gdb.isStored(cls))
				return new GenericStoreDBCursor<>();
			return new GenericStoreDBCursor<T>(gdb.getIDListByNativeStringQuery(cls, query), gdb,new Class[]{cls});
		}
		catch (Exception e)
		{
			throw new GenericStoreException(e);
		}		
	}*/
	
	public static void removeObject(GenericStorable gd, GenericStoreDatabase gdb) throws GenericStoreException
	{
		GenericStoreData data = gd.getGenericStoreData();
		if(data == null)
			return;
		
		if(data.id < 0)
			return;
		
		try
		{
			gdb.deleteObjectByIDSByClass(new long[]{data.id}, new Class[]{gd.getClass()});
		}
		catch (Exception e)
		{
			throw new GenericStoreException(e);
		}
		untraceObject(gd);
	}
	
	public static <A extends Collection<B>,B extends GenericStorable> void bulkRemoveObject(A gs, GenericStoreDatabase gdb) throws GenericStoreException
	{
		HashMap<Class<? extends GenericStorable>, List<GenericStorable>> map = new HashMap<>();
		
		for(GenericStorable g:gs)
		{
			if(g == null)
				continue;
			
			GenericStoreData data = g.getGenericStoreData();
			if(data == null)
				continue;
			
			if(data.id < 0)
				continue;
			
			List<GenericStorable> c = map.get(g.getClass());
			if(c == null)
				map.put(g.getClass(), c = new ArrayList<>());
			
			c.add(g);
		}
		
		
		try
		{
			for(Entry<Class<? extends GenericStorable>, List<GenericStorable>> kv:map.entrySet())
			{
				List<GenericStorable> l = kv.getValue();
				long[] idz = new long[l.size()];
				for(int i=0;i<idz.length;++i)
					idz[i] = getID(l.get(i));
				
				gdb.deleteObjectByIDSByClass(idz, new Class[]{kv.getKey()});
			}
		}
		catch (Exception e)
		{
			throw new GenericStoreException(e);
		}
		
		for(GenericStorable g:gs)
			if(g != null)
				untraceObject(g);
	}
	
	/**
	 * visszaadja az adatbázisban való tárolásnál használt egyedi azonosítót,
	 * ha még nem lett eltárolva akkor -1 -et
	 * */
	public static long getID(GenericStorable gs)
	{
		return gs.getGenericStoreData() == null?-1:gs.getGenericStoreData().id;
	}
	
	public static boolean isStored(GenericStorable gs)
	{
		GenericStoreData data = gs.getGenericStoreData();
		return data != null && data.owner != null && data.id > 0;
	}
	
	public static GenericStoreDatabase getOwnerDatabase(GenericStorable gs)
	{
		return gs.getGenericStoreData() == null?null:gs.getGenericStoreData().owner;
	}
	
	public static void objectModified(GenericStorable st)
	{
		if(st.getGenericStoreData() != null)
			st.getGenericStoreData().state = GenericStorageObjectState.MODIFIED;
	}
	
	public static void untraceObject(GenericStorable gs)
	{
		gs.getGenericStoreData().id = ObjectUntrackedUnstored;
		gs.getGenericStoreData().owner = null;
		gs.getGenericStoreData().state = GenericStorageObjectState.DELETED;
	}
	
	

	public static void fillFieldData
	(
		Class<? extends GenericStorable> type,
		Collection<FieldData> dst
	)
		throws InstantiationException, IllegalAccessException
	{
		FieldData[] fds = getOrCollectClassData(type);
		if(null != fds)
		{
			for(FieldData f:fds)
			{
				dst.add(f);
			}
		}
	}
	
	public static <T extends GenericStorable> void post(GenericStoreDatabase gdb, T j) throws GenericStoreException
	{
		GenericStoreDatabase odb = getOwnerDatabase(j);
		if(null != odb)
		{
			if(!odb.equals(gdb))
			{
				throw new RuntimeException("Object from different database");
			}
		}
		
		if(isStored(j))
		{
			objectModified(j);
		}
		
		storeObject(j, gdb);
	}
	
	public static <C extends Collection<GenericStorable>> C getReferences(GenericStorable subject, C refs) throws Exception
	{
		Class scls = subject.getClass();
		GenericStoreDatabase gdb = GenericStorage.getOwnerDatabase(subject);
		
		List<Class<? extends GenericStorable>> classes = gdb.getDescendantClassesFor(GenericStorable.class);
		for(Class c:classes)
		{
			if(c == GenericStorable.class)
			{
				continue;
			}
			FieldData[] fs = GenericStoreDatabase.getOrCreateFieldData(c);
			for(FieldData f:fs)
			{
				if(scls.isAssignableFrom(f.f.getType()))
				{
					GenericStorage.getAllObjectsByQuery(c, F.eq.is(f.f.getName(), subject), refs, gdb);
				}
			}
		}
		
		return refs;
	}

	public static <T extends GenericStorable> List<T> getObjectsByQuery(Class<T> class1, LogicalGroup lg, GenericStoreDatabase gdb) throws GenericStoreException
	{
		ArrayList<T> ret = new ArrayList<T>();
		getAllObjectsByQuery(class1, lg, ret, gdb);
		return ret;
	}

	public static <T extends GenericStorable> long[] getIds(Collection<T> objects)
	{
		long[] ret = new long[objects.size()];
		int i = 0;
		for(T o:objects)
		{
			ret[i++] = getID(o);
		}
		return ret;
	}

	public static <T extends GenericStorable> void postAll(GenericStoreDatabase gdb, Collection<T> els) throws GenericStoreException
	{
		for(T e:els)
		{
			if(isStored(e))
			{
				objectModified(e);
			}
		}
		
		storeAll(els, gdb);
	}

	public static <T extends GenericStorable> void getObjectsByQuery(GenericStoreDatabase gdb, Collection<T> ents, Class<T> cls, LogicalGroup lg, GsdbExtraCaluse... extra) throws GenericStoreException
	{
		getAllObjectsByQuery(cls, lg, ents, gdb, extra);
	}
	
	public static void update(GenericStorable elem) throws GenericStoreException
	{
		GenericStoreDatabase gdb = getOwnerDatabase(elem);
		if(null == gdb)
		{
			throw new RuntimeException("Object not yet stored, can't update");
		}
		
		post(gdb, elem);
	}
	
	/*
	TODO move other package
	public static <T extends GenericStorable> DataArray exportDatabaseCollecion(DataCommon comm,GenericStoreDatabase gdb,Class<T> cls) throws GenericStoreException
	{
		DataArray ret = comm.newArrayInstance();
		try
		{
			List<T> lst = new ArrayList<>();
			getAllObjectsByQuery(cls, F.gt.is("do", -1), lst, gdb);
			FieldData[] fds = getOrCollectClassData(cls);
			
			for(T s:lst)
			{
				DataObject crnt = comm.newObjectInstance();
				for(FieldData fd:fds)
				{
					Object val = fd.getField().get(s);
					if(val == null)
						continue;
					
					if(fd.isArray())
					{
						DataArray arr = comm.newArrayInstance();
						for(Object o:((Object[])val))
							DataReprezTools.put(arr,o);

						crnt.putArray(fd.getField().getName(), arr);
					}
					else
						DataReprezTools.put(crnt,fd.getField().getName(),val);
				}
				ret.putObject(crnt);
			}	
		}
		catch(Exception e)
		{
			throw new GenericStoreException(e);
		}
		
		return ret;
	}

	public static <T extends GenericStorable> void copyFields(DataObject src, T dst) throws IllegalArgumentException, IllegalAccessException, InstantiationException
	{
		FieldData[] fs = GenericStorage.getOrCollectClassData(dst.getClass());
		for(FieldData f:fs)
		{
			CastTo ct = CastTo.getCasterForTargetClass(f.f.getType());
			Object set = src.get(f.f.getName());
			if(null != ct)
			{
				set = ct.cast(set);
			}
			f.f.set(dst, set);
		}
	}
	*/
}