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

io.continual.services.model.impl.awsS3.S3Model Maven / Gradle / Ivy

There is a newer version: 0.3.23
Show newest version
/*
 *	Copyright 2019, Continual.io
 *
 *	Licensed under the Apache License, Version 2.0 (the "License");
 *	you may not use this file except in compliance with the License.
 *	You may obtain a copy of the License at
 *	
 *	http://www.apache.org/licenses/LICENSE-2.0
 *	
 *	Unless required by applicable law or agreed to in writing, software
 *	distributed under the License is distributed on an "AS IS" BASIS,
 *	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *	See the License for the specific language governing permissions and
 *	limitations under the License.
 */

package io.continual.services.model.impl.awsS3;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;

import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.SdkClientException;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.ListObjectsV2Request;
import com.amazonaws.services.s3.model.ListObjectsV2Result;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;

import io.continual.builder.Builder.BuildFailure;
import io.continual.metrics.MetricsCatalog;
import io.continual.metrics.MetricsService;
import io.continual.metrics.MetricsSupplier;
import io.continual.metrics.impl.noop.NoopMeter;
import io.continual.metrics.impl.noop.NoopTimer;
import io.continual.metrics.metricTypes.Meter;
import io.continual.metrics.metricTypes.Timer;
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.ModelQuery;
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.ShardedExpiringCache;
import io.continual.util.data.exprEval.ExpressionEvaluator;
import io.continual.util.data.json.CommentedJsonTokener;
import io.continual.util.naming.Name;
import io.continual.util.naming.Path;

public class S3Model extends CommonJsonDbModel implements MetricsSupplier
{
	public static void initModel ( String acctId, String modelId, String accessKey, String secretKey, Regions region, String bucketId, String prefix ) throws BuildFailure
	{
		final AmazonS3 fS3 = AmazonS3ClientBuilder
			.standard ()
			.withRegion ( region )
			.withCredentials ( new S3Creds ( accessKey, secretKey ) )
			.build ()
		;
		final String fBucketId = bucketId;
		final String fPrefix = prefix == null ? "" : prefix;

		// setup metadata using our Path tools
		Path metadataPath = Path.getRootPath ();
		if ( fPrefix.length () > 0 )
		{
			metadataPath = metadataPath.makeChildItem ( Name.fromString ( fPrefix ) );
		}
		metadataPath = metadataPath
			.makeChildItem ( Name.fromString ( acctId ) )
			.makeChildItem ( Name.fromString ( modelId  ) )
			.makeChildItem ( Name.fromString ( "metadata.json" ) )
		;
		final String metdataStr = metadataPath.toString ().substring ( 1 );

		// if the object exists, something's not right
		if ( fS3.doesObjectExist ( fBucketId, metdataStr ) )
		{
			throw new BuildFailure ( "This model is already initialized." );
		}

		// write the metadata object
		final JSONObject metadata = new JSONObject ()
			.put ( "version", Version.V2.toString () )
		;

		try (
			final ByteArrayInputStream bais = new ByteArrayInputStream ( metadata.toString ( 4 ).getBytes ( kUtf8 ) )
		)
		{
			fS3.putObject ( fBucketId, metdataStr, bais, new ObjectMetadata () );
		}
		catch ( IOException x )
		{
			throw new BuildFailure ( x );
		}
	}

	public S3Model ( String acctId, String modelId, String accessKey, String secretKey, String bucketId, String prefix ) throws BuildFailure
	{
		super ( modelId );

		fAcctId = acctId;

		fS3 = AmazonS3ClientBuilder
			.standard ()
			.withRegion ( Regions.DEFAULT_REGION )
			.withCredentials ( new S3Creds ( accessKey, secretKey ) )
			.build ()
		;
		fBucketId = bucketId;
		fPrefix = prefix == null ? "" : prefix;

		fCache = new ShardedExpiringCache.Builder ()
			.named ( "object cache" )
			.build ()
		;
		fNotFoundCache = new ShardedExpiringCache.Builder ()
			.named ( "not found cache" )
			.build ()
		;

		fVersion = determineVersion ( );
		fRelnMgr = new S3SysRelnMgr ( fS3, fBucketId, getRelationsPath () );

		fFoldersAsObjects = false;
	}

	public S3Model ( ServiceContainer sc, JSONObject config ) throws BuildFailure
	{
		super ( sc, config );

		try
		{
			final ExpressionEvaluator evaluator = sc.getExprEval ( config );
			final JSONObject evaledConfig = evaluator.evaluateJsonObject ( config );

			final String accessKey = evaledConfig.getString ( "accessKey" );
			final String secretKey = evaledConfig.getString ( "secretKey" );
			final Regions region = Regions.fromName ( evaledConfig.optString ( "region", Regions.US_WEST_2.getName () ) );

			fS3 = AmazonS3ClientBuilder
				.standard ()
				.withRegion ( region )
				.withCredentials ( new S3Creds ( accessKey, secretKey ) )
				.build ()
			;
			fBucketId = evaledConfig.getString ( "bucket" );
			fPrefix = evaledConfig.optString ( "prefix", "" );

			fCache = new ShardedExpiringCache.Builder ()
				.named ( "object cache" )
				.build ()
			;
			fNotFoundCache = new ShardedExpiringCache.Builder ()
				.named ( "not found cache" )
				.build ()
			;

			fAcctId = evaledConfig.getString ( "acctId" );

			Version vv = determineVersion ();
			if ( config.optBoolean ( "initOk", false ) && vv == Version.V1_IMPLIED )
			{
				initModel ( fAcctId, super.getId (), accessKey, secretKey, region, fBucketId, fPrefix );
				vv = Version.V2;
			}
			fVersion = vv;

			fRelnMgr = new S3SysRelnMgr ( fS3, fBucketId, getRelationsPath () );

			fFoldersAsObjects = evaledConfig.optBoolean ( "foldersAsObjects", false );

			// optionally report metrics
			final MetricsService ms = sc.get ( "metrics", MetricsService.class );
			if ( ms != null )
			{
				populateMetrics ( ms.getCatalog ( "S3Model " + config.optString ( "name", "anonymous" ) ) );
			}
		}
		catch ( JSONException e )
		{
			throw new BuildFailure ( e );
		}
	}

	@Override
	public void populateMetrics ( MetricsCatalog metrics )
	{
		fCacheHitCounter = metrics.meter ( "cacheHits" );
		fCacheMissCounter = metrics.meter ( "cacheMisses" );
		fReadTimer = metrics.timer ( "readTimer" );
		fS3ReadTimer = metrics.timer ( "s3ReadTimer" );
		fWriteTimer = metrics.timer ( "writeTimer" );
		fRemoveTimer = metrics.timer ( "removeTimer" );
	}

	@Override
	public long getMaxSerializedObjectLength ()
	{
		// S3 has various limits; for now we'll use the allowed in a PUT, 5 GB
		return 5L * 1024L * 1024L * 1024L;
	}

	@Override
	public long getMaxPathLength ()
	{
		return 1024L;
	}

	@Override
	public long getMaxRelnNameLength ()
	{
		// this is more arbitrary depending on how we store it
		return 1024L;
	}

	private List loadNextS3Set ( ListObjectsV2Request req, Path prefix, TreeSet seen, AtomicBoolean lastResultTruncated, PageRequest pr )
	{
		final LinkedList pending = new LinkedList<> ();

		final ListObjectsV2Result result = fS3.listObjectsV2 ( req );
		for ( S3ObjectSummary objectSummary : result.getObjectSummaries () )
		{
			final String key = objectSummary.getKey ();
			final Path asPath = s3KeyToPath ( key );
			if ( !asPath.equals ( prefix ) && !seen.contains ( asPath ) )
			{
				pending.add ( asPath );
				seen.add ( asPath );
			}
		}
		for ( String commonPrefix : result.getCommonPrefixes () )
		{
			final Path asPath = s3KeyToPath ( commonPrefix );
			if ( !asPath.equals ( prefix ) && !seen.contains ( asPath ) )
			{
				pending.add ( asPath );
				seen.add ( asPath );
			}
		}
		final String token = result.getNextContinuationToken ();
		req.setContinuationToken ( token );

		lastResultTruncated.set ( result.isTruncated () );

		return pending;
	}
	
	@Override
	public ModelPathListPage listChildrenOfPath ( ModelRequestContext context, Path prefix, PageRequest pr ) throws ModelServiceException, ModelRequestException
	{
		final LinkedList pending = new LinkedList<> ();
		final TreeSet seen = new TreeSet<> ();

		final ListObjectsV2Request req = new ListObjectsV2Request ()
			.withBucketName ( fBucketId )
			.withPrefix ( pathToS3Path ( prefix ) + "/" )
			.withDelimiter ( Path.getPathSeparatorString () )
		;

		final AtomicBoolean lastResultTruncated = new AtomicBoolean ( true );

		return new ModelPathListPage ()
		{
			final Iterator iter = new Iterator ()
			{
				@Override
				public boolean hasNext ()
				{
					if ( pending.size () > 0 ) return true;

					// load data for this page
					long skipsLeft = pr.getRequestedPage () * pr.getRequestedPageSize ();
					long itemsLeft = pr.getRequestedPageSize ();

					while ( lastResultTruncated.get () && itemsLeft > 0 )
					{
						final List nextS3Set = loadNextS3Set ( req, prefix, seen, lastResultTruncated, pr );
						for ( Path p : nextS3Set )
						{
							if ( skipsLeft-- > 0 ) continue;
							if ( itemsLeft-- <= 0 ) break;

							pending.add ( p );
						}
					}

					return pending.size () > 0;
				}

				@Override
				public Path next ()
				{
					return pending.removeFirst ();
				}
			};

			@Override
			public Iterator iterator () { return iter; }

			@Override
			public PageRequest getPageRequest () { return pr; }

			@Override
			public long getTotalItemCount () { return ModelPathListPage.UNKNOWN; }

			@Override
			public long getTotalPageCount () { return ModelPathListPage.UNKNOWN; }

			@Override
			public long getItemCountOnPage ()
			{
				// trigger a fetch
				return iter.hasNext () ? pending.size () : 0;
			}
		};
	}

	private class S3ModelQuery extends SimpleModelQuery
	{
		@Override
		public  ModelObjectList execute ( ModelRequestContext context, ModelObjectFactory factory, DataAccessor accessor, K userContext ) throws ModelRequestException, ModelServiceException
		{
			Comparator orderBy = getOrdering ();
			if ( orderBy != null )
			{
				return fullLoad ( context, factory, accessor, userContext );
			}
			else
			{
				return streamLoad ( context, factory, accessor, userContext );
			}
		}

		private  ModelObjectList fullLoad ( ModelRequestContext context, ModelObjectFactory factory, DataAccessor accessor, K userContext ) throws ModelRequestException, ModelServiceException
		{
			final LinkedList> result = new LinkedList<> ();

			final ModelPathListPage objectPaths = listChildrenOfPath ( context, getPathPrefix () );
			for ( Path objectPath : objectPaths )
			{
				final T mo = load ( context, objectPath, factory, userContext );
				boolean match = true;
				for ( Filter f : getFilters() )
				{
					match = f.matches ( accessor.getDataFrom ( mo ) );
					if ( !match )
					{
						break;
					}
				}
				if ( match )
				{
					result.add ( ModelObjectAndPath.from ( objectPath, mo ) );
				}
			}

			// now sort our list
			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 () )
						);
					}
				} );
			}

			return new ModelObjectList ()
			{
				@Override
				public Iterator> iterator ()
				{
					return result.iterator ();
				}
			};
		}

		private  ModelObjectList streamLoad ( ModelRequestContext context, ModelObjectFactory factory, DataAccessor accessor, K userContext ) throws ModelRequestException, ModelServiceException
		{
			final ModelPathListPage objectPaths = listChildrenOfPath ( context, getPathPrefix () );
			final Iterator paths = objectPaths.iterator ();

			final LinkedList> pending = new LinkedList<> ();

			return new ModelObjectList ()
			{
				@Override
				public Iterator> iterator ()
				{
					return new Iterator> ()
					{
						@Override
						public boolean hasNext ()
						{
							if ( pending.size () > 0 ) return true;

							// otherwise, we're empty... are there more available from the path list?
							if ( !paths.hasNext () ) return false;

							// paths has more
							while ( pending.size () == 0 )
							{
								final Path p = paths.next ();
								try
								{
									final T mo = load ( context, p, factory, userContext );
									boolean match = true;
									for ( Filter f : getFilters() )
									{
										match = f.matches ( accessor.getDataFrom ( mo ) );
										if ( !match )
										{
											break;
										}
									}
									if ( match )
									{
										pending.add ( ModelObjectAndPath.from ( p, mo ) );
									}
								}
								catch ( ModelServiceException | ModelRequestException x )
								{
									log.warn ( "Exception retrieving next object: " + x.getMessage () );
									return false;
								}
							}
							return pending.size () > 0;
						}

						@Override
						public ModelObjectAndPath next ()
						{
							return pending.removeFirst ();
						}
					};
				}
			};
		}
	}
	
	@Override
	public ModelQuery startQuery ()
	{
		return new S3ModelQuery ();
	}

	private static final String kObjects = "objects";
	private static final String kRelationships = "relationships";

	private Path s3KeyToPath ( String s3Key )
	{
		if ( !s3Key.startsWith ( fPrefix  ) )
		{
			throw new IllegalArgumentException ( "The key [ " + s3Key + "] is not from this bucket." );
		}

		String cleanedUp = s3Key.substring ( fPrefix.length () );
		if ( cleanedUp.endsWith ( Path.getPathSeparatorString () ) )
		{
			cleanedUp = cleanedUp.substring ( 0, cleanedUp.length () - 1 );
		}

		if ( !cleanedUp.startsWith ( Path.getPathSeparatorString () ) )
		{
			cleanedUp = Path.getPathSeparatorString () + cleanedUp;
		}
		
		Path asPath = Path.fromString ( cleanedUp );

		asPath = asPath.makePathWithinParent ( Path.fromString ( "/" + fAcctId ) );
		asPath = asPath.makePathWithinParent ( Path.fromString ( "/" + getId() ) );

		if ( Version.isV2OrLater ( fVersion ) )
		{
			asPath = asPath.makePathWithinParent ( Path.fromString ( "/" + kObjects ) );
		}

		return asPath;
	}

	private String pathToS3Path ( Path path )
	{
		Path p = Path.getRootPath ();
		if ( fPrefix != null && fPrefix.length () > 0 )
		{
			p = p.makeChildItem ( Name.fromString ( fPrefix ) );
		}

		p = p
			.makeChildItem ( Name.fromString ( fAcctId ) )
			.makeChildItem ( Name.fromString ( getId () ) )
		;

		if ( Version.isV2OrLater ( fVersion ) )
		{
			p = p.makeChildItem ( Name.fromString ( kObjects ) );
		}

		p = p.makeChildPath ( path );

		return p.toString ().substring ( 1 );
	}

	private Path getRelationsPath ()
	{
		Path p = Path.getRootPath ();
		if ( fPrefix != null && fPrefix.length () > 0 )
		{
			p = p.makeChildItem ( Name.fromString ( fPrefix ) );
		}

		return p
			.makeChildItem ( Name.fromString ( fAcctId ) )
			.makeChildItem ( Name.fromString ( getId () ) )
			.makeChildItem ( Name.fromString ( kRelationships ) )
		;
	}

	private String metadataS3Path ()
	{
		Path p = Path.getRootPath ();
		if ( fPrefix != null && fPrefix.length () > 0 )
		{
			p = p.makeChildItem ( Name.fromString ( fPrefix ) );
		}

		p = p
			.makeChildItem ( Name.fromString ( fAcctId ) )
			.makeChildItem ( Name.fromString ( getId () ) )
		;

		return p.toString ().substring ( 1 ) + "/metadata.json";
	}

	@Override
	protected boolean objectExists ( ModelRequestContext context, Path objectPath ) throws ModelRequestException
	{
		try
		{
			final String s3Path = pathToS3Path ( objectPath );
			final ModelDataTransfer mo = fCache.read ( s3Path );
			if ( mo != null ) return true;

			final Boolean notFound = fNotFoundCache.read ( s3Path );
			if ( notFound != null ) return false;

			final boolean asObj = fS3.doesObjectExist ( fBucketId, s3Path );
			if ( asObj ) return true;

			if ( fFoldersAsObjects )
			{
				final ObjectListing ol = fS3.listObjects ( fBucketId, s3Path + "/" );
				return ( ol.getObjectSummaries ().size () > 0 );
			}
			
			return false;
		}
		catch ( SdkClientException x )
		{
			throw new ModelRequestException ( x );
		}
	}

	@Override
	protected ModelDataTransfer loadObject ( ModelRequestContext context, final Path objectPath ) throws ModelServiceException, ModelRequestException
	{
		final String s3Path = pathToS3Path ( objectPath );

		try ( Timer.Context timingContext = fReadTimer.time () )
		{
			ModelDataTransfer result = fCache.read ( s3Path );
			if ( result != null )
			{
				fCacheHitCounter.mark ();
				return result;
			}

			// is it known not-found?
			final Boolean notFound = fNotFoundCache.read ( s3Path );
			if ( notFound != null ) return null;
			
			// otherwise, load from S3

			fCacheMissCounter.mark ();

			try ( Timer.Context s3TimingContext = fS3ReadTimer.time () )
			{
				final S3Object o = fS3.getObject ( fBucketId, s3Path );
	
				final JSONObject rawData;
				try ( InputStream is = o.getObjectContent () )
				{
					 rawData = new JSONObject ( new CommentedJsonTokener ( is ) );
				}
				catch ( JSONException x )
				{
					throw new ModelRequestException ( "The object data is corrupt." );
				}
				catch ( IOException x )
				{
					throw new ModelServiceException ( x );
				}

				result = new CommonDataTransfer ( objectPath, rawData );
				fCache.write ( s3Path, result );

				return result;
			}
			finally
			{
				// 
			}
		}
		catch ( AmazonS3Exception x ) 
		{
			if ( x.getErrorCode ().equals ( "NoSuchKey" ) )
			{
				if ( fFoldersAsObjects )
				{
					final ObjectListing ol = fS3.listObjects ( fBucketId, s3Path + "/" );
					if ( ol.getObjectSummaries ().size () > 0 )
					{
						// it's a "folder"; return an empty object
						final CommonDataTransfer result = new CommonDataTransfer ( objectPath, new JSONObject () );
						fCache.write ( s3Path, result );
						return result;
					}
				}

				// else: we're not reporting folders or we are but this isn't one
				fNotFoundCache.write ( s3Path, true );
				throw new ModelItemDoesNotExistException ( objectPath );
			}
			throw new ModelRequestException ( x );
		}
		catch ( SdkClientException x )
		{
			throw new ModelRequestException ( x );
		}
	}

	@Override
	protected void internalStore ( ModelRequestContext context, Path objectPath, ModelDataTransfer o ) throws ModelRequestException, ModelServiceException
	{
		try (
			final Timer.Context timingContext = fWriteTimer.time ();
			final ByteArrayInputStream bais = new ByteArrayInputStream ( CommonDataTransfer.toDataObject ( o ).toString ( 4 ).getBytes ( kUtf8 ) )
		)
		{
			final String s3Path = pathToS3Path ( objectPath );
			fS3.putObject ( fBucketId, s3Path, bais, new ObjectMetadata () );
			fCache.write ( s3Path, o );
			fNotFoundCache.remove ( s3Path );
		}
		catch ( JSONException x )
		{
			throw new ModelRequestException ( "The object data is corrupt." );
		}
		catch ( IOException x )
		{
			throw new ModelServiceException ( x );
		}
	}

	@Override
	protected boolean internalRemove ( ModelRequestContext context, Path objectPath ) throws ModelRequestException, ModelServiceException
	{
		try ( final Timer.Context timingContext = fRemoveTimer.time () )
		{
			final boolean existed = exists ( context, objectPath );
			final String s3Path = pathToS3Path ( objectPath );

			fS3.deleteObject ( fBucketId, s3Path );
			fRelnMgr.removeAllRelations ( objectPath );

			fCache.remove ( s3Path );
			fNotFoundCache.write ( s3Path, true );

			return existed;
		}
	}

	private final String fAcctId;	// because we've been using this for awhile...
	private final AmazonS3 fS3;
	private final String fBucketId;
	private final String fPrefix;
	private final S3SysRelnMgr fRelnMgr;
	private final boolean fFoldersAsObjects;

	private final ShardedExpiringCache fCache;
	private final ShardedExpiringCache fNotFoundCache;	// because null means not-found :-(

	private enum Version
	{
		V1_IMPLIED,
		V1,
		V2;

		public static Version fromText ( String s )
		{
			if ( s == null ) return V1_IMPLIED;
			try
			{
				return Version.valueOf ( s.toUpperCase ().trim () );
			}
			catch ( IllegalArgumentException x )
			{
				// ignore
			}
			return V1_IMPLIED;
		}

		public static boolean isV2OrLater ( Version v )
		{
			return v != V1 && v != V1_IMPLIED;
		}
	};
	private final Version fVersion;
	
	private Meter fCacheHitCounter = new NoopMeter ();
	private Meter fCacheMissCounter = new NoopMeter ();
	private Timer fReadTimer = new NoopTimer ();
	private Timer fS3ReadTimer = new NoopTimer ();
	private Timer fWriteTimer = new NoopTimer ();
	private Timer fRemoveTimer = new NoopTimer ();

	static final Charset kUtf8 = Charset.forName ( "UTF8" );

	private static class S3Creds implements AWSCredentialsProvider
	{
		public S3Creds ( String key, String secret )
		{
			fAccessKey = key;
			fPrivateKey = secret;
		}

		@Override
		public AWSCredentials getCredentials ()
		{
			return new AWSCredentials ()
			{
				@Override
				public String getAWSAccessKeyId () { return fAccessKey; }

				@Override
				public String getAWSSecretKey () { return fPrivateKey; }
			};
		}

		@Override
		public void refresh ()
		{
			// ignore
		}

		private final String fAccessKey;
		private final String fPrivateKey;
	}

	// we store a JSON object in the bucket/prefix folder with model metadata.
	private JSONObject readModelMetadata () throws BuildFailure
	{
		final String loc = metadataS3Path ();

		try ( Timer.Context s3TimingContext = fS3ReadTimer.time () )
		{
			final S3Object o = fS3.getObject ( fBucketId, loc );

			final JSONObject rawData;
			try ( InputStream is = o.getObjectContent () )
			{
				rawData = new JSONObject ( new CommentedJsonTokener ( is ) );
			}
			catch ( JSONException x )
			{
				throw new BuildFailure ( "The model metadata is corrupt." );
			}
			catch ( IOException x )
			{
				throw new BuildFailure ( x );
			}

			return rawData;
		}
		catch ( AmazonS3Exception x )
		{
			if ( x.getErrorCode ().equals ( "NoSuchKey" ) )
			{
				return new JSONObject ();
			}
			else
			{
				throw new BuildFailure ( x );
			}
		}
		catch ( SdkClientException x )
		{
			throw new BuildFailure ( x );
		}
	}
	
	private Version determineVersion () throws BuildFailure
	{
		final JSONObject metadata = readModelMetadata ();
		return Version.fromText ( metadata.optString ( "version", Version.V1_IMPLIED.toString () ) );
	}

	@Override
	public Model setRelationType ( ModelRequestContext context, String relnName, RelationType rt ) throws ModelServiceException, ModelRequestException
	{
		if ( !Version.isV2OrLater ( fVersion ) ) throw new ModelServiceException ( "not implemented" );

		checkReadOnly ();

		fRelnMgr.setRelationType ( context, relnName, rt );

		return this;
	}

	@Override
	public ModelRelationInstance relate ( ModelRequestContext context, ModelRelation mr ) throws ModelServiceException, ModelRequestException
	{
		if ( !Version.isV2OrLater ( fVersion ) ) throw new ModelServiceException ( "not implemented" );

		checkReadOnly ();

		fRelnMgr.relate ( mr );

		return ModelRelationInstance.from ( mr );
	}

	@Override
	public boolean unrelate ( ModelRequestContext context, ModelRelation reln ) throws ModelServiceException, ModelRequestException
	{
		if ( !Version.isV2OrLater ( fVersion ) ) throw new ModelServiceException ( "not implemented" );

		checkReadOnly ();

		final boolean exists = fRelnMgr.doesRelationExist ( reln );
		fRelnMgr.unrelate ( reln );
		return exists;
	}

	@Override
	public boolean unrelate ( ModelRequestContext context, String relnId ) throws ModelServiceException, ModelRequestException
	{
		try
		{
			checkReadOnly ();

			final ModelRelationInstance mr = ModelRelationInstance.from ( relnId );
			return unrelate ( context, mr );
		}
		catch ( IllegalArgumentException x )
		{
			throw new ModelRequestException ( x );
		}
	}

	@Override
	public List getInboundRelationsNamed ( ModelRequestContext context, Path forObject, String named ) throws ModelServiceException, ModelRequestException
	{
		if ( !Version.isV2OrLater ( fVersion ) )
		{
			// these models didn't support relations
			return new LinkedList<> ();
		}
		return fRelnMgr.getInboundRelationsNamed ( forObject, named );
	}

	@Override
	public List getOutboundRelationsNamed ( ModelRequestContext context, Path forObject, String named ) throws ModelServiceException, ModelRequestException
	{
		if ( !Version.isV2OrLater ( fVersion ) )
		{
			// these models didn't support relations
			return new LinkedList<> ();
		}
		return fRelnMgr.getOutboundRelationsNamed ( forObject, named );
	}

	private static final Logger log = LoggerFactory.getLogger ( S3Model.class );
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy