All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.continual.services.model.impl.file.SingleFileModel Maven / Gradle / Ivy

There is a newer version: 0.3.23
Show newest version
package io.continual.services.model.impl.file;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import io.continual.builder.Builder.BuildFailure;
import io.continual.services.ServiceContainer;
import io.continual.services.model.core.Model;
import io.continual.services.model.core.ModelObjectAndPath;
import io.continual.services.model.core.ModelObjectFactory;
import io.continual.services.model.core.ModelObjectList;
import io.continual.services.model.core.ModelPathListPage;
import io.continual.services.model.core.ModelRelation;
import io.continual.services.model.core.ModelRelationInstance;
import io.continual.services.model.core.ModelRequestContext;
import io.continual.services.model.core.PageRequest;
import io.continual.services.model.core.data.ModelObject;
import io.continual.services.model.core.exceptions.ModelItemDoesNotExistException;
import io.continual.services.model.core.exceptions.ModelRequestException;
import io.continual.services.model.core.exceptions.ModelServiceException;
import io.continual.services.model.impl.common.SimpleModelQuery;
import io.continual.services.model.impl.json.CommonDataTransfer;
import io.continual.services.model.impl.json.CommonJsonDbModel;
import io.continual.util.collections.MultiMap;
import io.continual.util.data.json.CommentedJsonTokener;
import io.continual.util.data.json.JsonUtil;
import io.continual.util.data.json.JsonVisitor;
import io.continual.util.data.json.JsonVisitor.ArrayVisitor;
import io.continual.util.data.json.JsonVisitor.ObjectVisitor;
import io.continual.util.naming.Name;
import io.continual.util.naming.Path;

/**
 * A single file model in JSON
 * @author peter
 *
 */
public class SingleFileModel extends CommonJsonDbModel
{
	public SingleFileModel ( ServiceContainer sc, JSONObject config ) throws BuildFailure
	{
		this (
			sc.getExprEval ( config ).evaluateText ( config.getString ( "modelId" ) ),
			new File ( sc.getExprEval ( config ).evaluateText ( config.getString ( "file" ) ) )
		);
	}

	public SingleFileModel ( String modelId, File f ) throws BuildFailure
	{
		super ( modelId );

		fFile = f;
		if ( !fFile.exists () || fFile.length () == 0L )
		{
			fRoot = new JSONObject ();
		}
		else
		{
			try ( FileInputStream fis = new FileInputStream ( f ) )
			{
				fRoot = new JSONObject ( new CommentedJsonTokener ( fis ) );
			}
			catch ( IOException x )
			{
				throw new BuildFailure ( x );
			}
		}

		if ( null == fRoot.optJSONObject ( kObjectsNode ) )
		{
			fRoot.put ( kObjectsNode, new JSONObject () );
		}
		if ( null == fRoot.optJSONArray ( kRelnsNode ) )
		{
			fRoot.put ( kRelnsNode, new JSONObject () );
		}

		fReversals = new HashMap> ();

		rebuildReversals ();
	}

	@Override
	public ModelPathListPage listChildrenOfPath ( ModelRequestContext context, Path parentPath, PageRequest pr ) throws ModelServiceException, ModelRequestException
	{
		JSONObject current = getDataRoot ();
		for ( Name name : parentPath.getSegments () )
		{
			current = current.optJSONObject ( name.toString () );
			if ( current == null ) return null;
		}

		final LinkedList paths = new LinkedList<> ();
		for ( String key : current.keySet () )
		{
			if ( null != current.optJSONObject ( key ) )
			{
				paths.add ( parentPath.makeChildItem ( Name.fromString ( key ) ) );
			}
		}

		return ModelPathListPage.wrap ( paths, pr );
	}

	@Override
	public ModelQuery startQuery () throws ModelRequestException
	{
		return new ModelQuery ();
	}

	@Override
	public Model setRelationType ( ModelRequestContext context, String relnName, RelationType rt ) throws ModelServiceException, ModelRequestException
	{
		// this implementation orders all relations
		return this;
	}

	@Override
	public ModelRelationInstance relate ( ModelRequestContext context, ModelRelation reln ) throws ModelServiceException, ModelRequestException
	{
		// make a backup...
		final JSONObject rootCopy = JsonUtil.clone ( fRoot );
		try
		{
			final JSONObject relns = getRelnRoot ();

			JSONObject fromNode = relns.optJSONObject ( reln.getFrom ().toString () );
			if ( fromNode == null )
			{
				fromNode = new JSONObject ();
				relns.put ( reln.getFrom ().toString (), fromNode );
			}
	
			JSONArray relnNode = fromNode.optJSONArray ( reln.getName () );
			if ( relnNode == null )
			{
				relnNode = new JSONArray ();
				fromNode.put ( reln.getName (), relnNode );
			}
	
			final String target = reln.getTo ().toString ();
			final List toList = JsonVisitor.arrayToList ( relnNode );
			final TreeSet toSet = new TreeSet<> ( toList );
			if ( ! toSet.contains ( target ) )
			{
				toList.add ( target );
				fromNode.put ( reln.getName (), JsonVisitor.collectionToArray ( toList ) );

				flush ();
			}

			return ModelRelationInstance.from ( reln );
		}
		catch ( ModelServiceException x )
		{
			// restore...
			fRoot = rootCopy;
			throw x;
		}
		catch ( Exception x )
		{
			// restore...
			fRoot = rootCopy;
			throw new ModelServiceException ( x );
		}
	}

	@Override
	public boolean unrelate ( ModelRequestContext context, ModelRelation reln ) throws ModelServiceException, ModelRequestException
	{
		// make a backup...
		final JSONObject rootCopy = JsonUtil.clone ( fRoot );
		try
		{
			final boolean result = removeReln ( reln );
			flush ();
			return result;
		}
		catch ( ModelServiceException x )
		{
			// restore...
			fRoot = rootCopy;
			throw x;
		}
		catch ( Exception x )
		{
			// restore...
			fRoot = rootCopy;
			throw new ModelServiceException ( x );
		}
	}

	@Override
	public boolean unrelate ( ModelRequestContext context, String relnId ) throws ModelServiceException, ModelRequestException
	{
		try
		{
			final ModelRelationInstance mr = ModelRelationInstance.from ( relnId );
			return unrelate ( context, mr );
		}
		catch ( IllegalArgumentException x )
		{
			throw new ModelRequestException ( x );
		}
	}

	@Override
	public List getOutboundRelationsNamed ( ModelRequestContext context, Path fromObject, String named ) throws ModelServiceException, ModelRequestException
	{
		final LinkedList result = new LinkedList<> ();
		
		JsonVisitor.forEachElement ( getRelnRoot ().optJSONObject ( fromObject.toString () ), new ObjectVisitor ()
		{
			@Override
			public boolean visit ( String relnName, JSONArray toList ) throws JSONException
			{
				JsonVisitor.forEachElement ( toList, new ArrayVisitor ()
				{
					@Override
					public boolean visit ( String toPathText ) throws JSONException
					{
						result.add ( ModelRelationInstance.from ( fromObject, relnName, Path.fromString ( toPathText ) ) );
						return true;
					}
				} );
				return true;
			}
		} );

		return result;
	}

	@Override
	public List getInboundRelationsNamed ( ModelRequestContext context, Path toObject, String named ) throws ModelServiceException, ModelRequestException
	{
		final LinkedList result = new LinkedList<> ();

		final MultiMap revRelns = fReversals.get ( toObject );
		if ( revRelns != null )
		{
			for ( Path fromObj : revRelns.get ( named ) )
			{
				result.add ( ModelRelationInstance.from ( fromObj, named, toObject ) );
			}
		}

		return result;
	}

	@Override
	protected ModelDataTransfer loadObject ( ModelRequestContext context, Path objectPath ) throws ModelItemDoesNotExistException, ModelServiceException, ModelRequestException
	{
		JSONObject current = getDataRoot ();
		for ( Name name : objectPath.getSegments () )
		{
			current = current.optJSONObject ( name.toString () );
			if ( current == null ) throw new ModelItemDoesNotExistException ( objectPath );
		}
		return new CommonDataTransfer ( objectPath, current );
	}

	@Override
	protected void internalStore ( ModelRequestContext context, Path objectPath, ModelDataTransfer o ) throws ModelRequestException, ModelServiceException
	{
		// make a backup...
		final JSONObject rootCopy = JsonUtil.clone ( fRoot );
		try
		{
			JSONObject current = getDataRoot ();
			for ( Name name : objectPath.getParentPath ().getSegments () )
			{
				JSONObject next = current.optJSONObject ( name.toString () );
				if ( next == null )
				{
					next = new JSONObject ();
					current.put ( name.toString (), next );
				}
				current = next;
			}
			current.put ( objectPath.getItemName ().toString (), CommonDataTransfer.toDataObject ( o.getMetadata (), o.getObjectData () ) );

			flush ( );
		}
		catch ( ModelServiceException x )
		{
			// restore...
			fRoot = rootCopy;
			throw x;
		}
		catch ( Exception x )
		{
			// restore...
			fRoot = rootCopy;
			throw new ModelServiceException ( x );
		}
	}

	@Override
	protected boolean internalRemove ( ModelRequestContext context, Path objectPath ) throws ModelRequestException, ModelServiceException
	{
		// make a backup...
		final JSONObject rootCopy = JsonUtil.clone ( fRoot );
		try
		{
			JSONObject current = getDataRoot ();
			for ( Name name : objectPath.getParentPath ().getSegments () )
			{
				current = current.optJSONObject ( name.toString () );
				if ( current == null )
				{
					return false;
				}
			}

			final String itemName = objectPath.getItemName ().toString ();
			if ( current.has ( itemName ) && null != current.optJSONObject ( itemName ) )
			{
				current.remove ( itemName );

				removeRelnsFor ( objectPath );

				flush ();

				return true;
			}
		}
		catch ( ModelServiceException x )
		{
			// restore...
			fRoot = rootCopy;
			throw x;
		}
		catch ( Exception x )
		{
			// restore...
			fRoot = rootCopy;
			throw new ModelServiceException ( x );
		}
		return false;
	}

	private void removeRelnsFor ( Path objectPath )
	{
		// remove relations on the "from" side
		getRelnRoot().remove ( objectPath.toString () );

		// lookup relation on "to" side
		final MultiMap revRelns = fReversals.get ( objectPath );
		if ( revRelns != null )
		{
			for ( Map.Entry> entry : revRelns.getValues ().entrySet () )
			{
				for ( Path from : entry.getValue () )
				{
					removeReln ( ModelRelation.from ( from, entry.getKey (), objectPath ) );
				}
			}
		}
	}

	private boolean removeReln ( ModelRelation reln )
	{
		final JSONObject relns = getRelnRoot ();
	
		JSONObject fromNode = relns.optJSONObject ( reln.getFrom ().toString () );
		if ( fromNode == null )
		{
			return false;
		}
	
		JSONArray relnNode = fromNode.optJSONArray ( reln.getName () );
		if ( relnNode == null )
		{
			return false;
		}

		boolean result = false;

		final String target = reln.getTo ().toString ();
		final List toList = JsonVisitor.arrayToList ( relnNode );
		final TreeSet tos = new TreeSet<> ( toList );
		if ( tos.contains ( target ) )
		{
			result = toList.remove ( target );	// must return true
			fromNode.put ( reln.getName (), JsonVisitor.collectionToArray ( tos ) );

			// also remove from reversals
			final MultiMap revReln = fReversals.get ( reln.getTo () );
			if ( revReln != null )
			{
				revReln.remove ( reln.getName (), reln.getFrom () );
			}
		}

		return result;
	}
	
	/*
		{
		    "relations":
		    {
		        "/foo/bar/baz":
		        {
		            "friendlyTo": [ "/foo/bar/mop" ]
		        }
		    },
		    "objects":
		    {
		        "foo":
		        {
		            "bar":
		            {
		                "baz":
		                {
		                    "color": "rose",
		                    "count": 123
		                },
	
		                "mop":
		                {
		                    "color": "brown",
		                    "count": 456
		                }
		            }
		        }
		    }
		}
	*/

	private final File fFile;
	private JSONObject fRoot;
	private HashMap> fReversals;

	private static final String kObjectsNode = "objects";
	private static final String kRelnsNode = "relations";

	private JSONObject getDataRoot ()
	{
		return fRoot.getJSONObject ( kObjectsNode );
	}

	private JSONObject getRelnRoot ()
	{
		return fRoot.getJSONObject ( kRelnsNode );
	}

	private void rebuildReversals ()
	{
		fReversals.clear ();

		JsonVisitor.forEachElement ( getRelnRoot(), new ObjectVisitor ()
		{
			@Override
			public boolean visit ( String fromPathStr, JSONObject relnData ) throws JSONException
			{
				final Path fromPath = Path.fromString ( fromPathStr );
				JsonVisitor.forEachElement ( relnData, new ObjectVisitor ()
				{
					@Override
					public boolean visit ( String relnName, JSONArray toPathList ) throws JSONException
					{
						JsonVisitor.forEachElement ( toPathList, new ArrayVisitor ()
						{
							@Override
							public boolean visit ( String pathText ) throws JSONException
							{
								MultiMap mm = fReversals.get ( fromPath );
								if ( mm == null )
								{
									mm = new MultiMap<> ();
									fReversals.put ( fromPath, mm );
								}
								mm.put ( relnName, Path.fromString ( pathText ) );
								return true;
							}
							
						} );
						return true;
					}
				} );

				return true;
			}
		} );
	}
	
	private void flush () throws ModelServiceException
	{
		try ( FileWriter fw = new FileWriter ( fFile ) )
		{
			fw.write ( fRoot.toString ( 4 ) );
		}
		catch ( IOException x )
		{
			throw new ModelServiceException ( x );
		}
	}

	private class ModelQuery extends SimpleModelQuery
	{
		private List collectObjectsUnder ( Path pathPrefix )
		{
			final LinkedList result = new LinkedList<> ();

			JSONObject current = getDataRoot ();
			for ( Name name : pathPrefix.getSegments () )
			{
				current = current.optJSONObject ( name.toString () );
				if ( current == null ) return result;
			}

			for ( String key : current.keySet () )
			{
				if ( null != current.optJSONObject ( key ) )
				{
					final Path pathHere = pathPrefix.makeChildItem ( Name.fromString ( key ) );
					result.add ( pathHere );
					result.addAll ( collectObjectsUnder ( pathHere ) );
				}
			}

			return result;
		}
		
		@Override
		public  ModelObjectList execute ( ModelRequestContext context, ModelObjectFactory factory, DataAccessor accessor, K userContext ) throws ModelRequestException, ModelServiceException
		{
			final LinkedList> result = new LinkedList<> ();

			for ( Path p : collectObjectsUnder ( getPathPrefix () ) )
			{
				final T mo = load ( context, p, factory, userContext );
				if ( mo != null )
				{
					boolean match = true;
					for ( SimpleModelQuery.Filter filter : getFilters () )
					{
						if ( !filter.matches ( accessor.getDataFrom ( mo ) ) )
						{
							match = false;
							break;
						}
					}

					if ( match )
					{
						result.add ( ModelObjectAndPath.from ( p, mo ) );
					}
				}
			}

			// now sort our list
			final Comparator orderBy = getOrdering ();
			if ( orderBy != null )
			{
				Collections.sort ( result, new java.util.Comparator> ()
				{
					@Override
					public int compare ( ModelObjectAndPath o1, ModelObjectAndPath o2 )
					{
						return orderBy.compare (
							accessor.getDataFrom ( o1.getObject () ),
							accessor.getDataFrom ( o2.getObject () )
						);
					}
				} );
			}

			// just remove from both ends of our list
			final long startIndex = (long)getPageSize() * (long)getPageNumber();
			long toDump = startIndex;
			while ( toDump > 0L && result.size () > 0 )
			{
				result.removeFirst ();
			}
			while ( result.size () > getPageSize() )
			{
				result.removeLast ();
			}
			
			// wrap our result
			return new ModelObjectList ()
			{
				@Override
				public Iterator> iterator ()
				{
					return result.iterator ();
				}
			};
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy