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

io.continual.services.model.impl.mem.InMemoryModel Maven / Gradle / Ivy

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

import java.util.AbstractMap;
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.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;

public class InMemoryModel extends CommonJsonDbModel
{
	public InMemoryModel ( ServiceContainer sc, JSONObject config ) throws BuildFailure
	{
		this (
			sc.getExprEval ( config ).evaluateText ( config.getString ( "modelId" ) )
		);
	}

	public InMemoryModel ( String modelId ) throws BuildFailure
	{
		this ( modelId, new JSONObject () );
	}

	public InMemoryModel ( String modelId, JSONObject data ) throws BuildFailure
	{
		super ( modelId );

		fRoot = new JSONObject ();
		fRoot.put ( kObjectsNode, data );
		fRoot.put ( kRelnsNode, new JSONObject () );

		fReversals = new HashMap> ();
	}

	@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 ModelPathListPage.emptyList ( pr );
		}

		final LinkedList paths = new LinkedList<> ();
		for ( String key : current.keySet () )
		{
			if ( !key.equals ( kLocalDataNode ) &&  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 model always keeps relations in order
		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 LinkedList toList = new LinkedList<> ( JsonVisitor.arrayToList ( relnNode ) );
			final TreeSet toSet = new TreeSet<> ( toList );
			if ( !toSet.contains ( target ) )
			{
				toList.add ( target );
				fromNode.put ( reln.getName (), JsonVisitor.collectionToArray ( toList ) );
			}

			return ModelRelationInstance.from ( reln );
		}
		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
		{
			return removeReln ( reln );
		}
		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 CommonDataTransfer 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 );
		}
		
		final JSONObject objData = current.optJSONObject ( kLocalDataNode );
		if ( objData == null ) throw new ModelItemDoesNotExistException ( objectPath );

		return new CommonDataTransfer ( objectPath, objData );
	}

	@Override
	protected void internalStore ( ModelRequestContext context, Path objectPath, ModelDataTransfer o ) throws ModelRequestException, ModelServiceException
	{
		// make a backup...
		final JSONObject rootCopy = JsonUtil.clone ( fRoot );
		try
		{
			JSONObject container = getDataRoot ();
			for ( Name name : objectPath.getParentPath ().getSegments () )
			{
				container = container.optJSONObject ( name.toString () );
				if ( container == null )
				{
					throw new ModelRequestException ( objectPath.toString () + " parent path unavailable at " + name.toString () );
				}
			}

			// this container may have data below the current level, so just make sure it exists
			final String nodeName = objectPath.getItemName ().toString ();
			JSONObject obj = container.optJSONObject ( nodeName );
			if ( obj == null )
			{
				obj = new JSONObject ();
				container.put ( nodeName, obj );
			}

			// overwrite the data at this level
			obj.put ( kLocalDataNode, CommonDataTransfer.toDataObject ( o.getMetadata (), o.getObjectData () ) );
		}
		catch ( ModelRequestException 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;
				}
			}

			boolean result = false;

			final String itemName = objectPath.getItemName ().toString ();
			final JSONObject node = current.optJSONObject ( itemName );
			if ( node != null )
			{
				final JSONObject localData = node.optJSONObject ( kLocalDataNode );
				if ( localData != null )
				{
					node.remove ( kLocalDataNode );
					removeRelnsFor ( objectPath );
					result = true;
				}
			}

			// now work back upward pruning any empty objects
			prune ( objectPath );

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

	private void prune ( Path objectPath )
	{
		final LinkedList> components = new LinkedList<> ();
		
		// drill down to container
		JSONObject current = getDataRoot ();
		for ( Name name : objectPath.getSegments () )
		{
			current = current.optJSONObject ( name.toString () );
			if ( current == null )
			{
				break;
			}
			components.addFirst ( new AbstractMap.SimpleEntry ( name.toString (), current ) );
		}

		// test for empty objects and remove them
		while ( components.size () > 0 && components.getFirst ().getValue ().isEmpty () )
		{
			final AbstractMap.SimpleEntry e = components.remove ();
			components.getFirst ().getValue ().remove ( e.getKey () );
		}
	}

	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;
		}

		final LinkedList tos = new LinkedList<> ( JsonVisitor.arrayToList ( relnNode ) );
		boolean result = tos.remove ( reln.getTo ().toString () );
		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 JSONObject fRoot;
	private HashMap> fReversals;

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

	private static final String kLocalDataNode = "~~local~~";
	
	private JSONObject getDataRoot ()
	{
		return fRoot.getJSONObject ( kObjectsNode );
	}

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

	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 ( !(key.equals(kLocalDataNode)) && 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