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

dev.dsf.fhir.dao.jdbc.AbstractResourceDaoJdbc Maven / Gradle / Ivy

package dev.dsf.fhir.dao.jdbc;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.sql.DataSource;

import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.annotation.ResourceDef;
import dev.dsf.common.auth.conf.Identity;
import dev.dsf.fhir.dao.ResourceDao;
import dev.dsf.fhir.dao.exception.ResourceDeletedException;
import dev.dsf.fhir.dao.exception.ResourceNotFoundException;
import dev.dsf.fhir.dao.exception.ResourceNotMarkedDeletedException;
import dev.dsf.fhir.dao.exception.ResourceVersionNoMatchException;
import dev.dsf.fhir.search.DbSearchQuery;
import dev.dsf.fhir.search.PageAndCount;
import dev.dsf.fhir.search.PartialResult;
import dev.dsf.fhir.search.SearchQuery;
import dev.dsf.fhir.search.SearchQuery.SearchQueryBuilder;
import dev.dsf.fhir.search.SearchQueryIdentityFilter;
import dev.dsf.fhir.search.SearchQueryIncludeParameter;
import dev.dsf.fhir.search.SearchQueryParameter;
import dev.dsf.fhir.search.SearchQueryParameterFactory;
import dev.dsf.fhir.search.SearchQueryRevIncludeParameter;
import dev.dsf.fhir.search.SearchQueryRevIncludeParameterFactory;
import dev.dsf.fhir.search.parameters.ResourceId;
import dev.dsf.fhir.search.parameters.ResourceLastUpdated;
import dev.dsf.fhir.search.parameters.ResourceProfile;

abstract class AbstractResourceDaoJdbc implements ResourceDao, InitializingBean
{
	private static final Logger logger = LoggerFactory.getLogger(AbstractResourceDaoJdbc.class);

	protected static  SearchQueryParameterFactory factory(String parameterName,
			Supplier> supplier)
	{
		Objects.requireNonNull(parameterName, "parameterName");
		Objects.requireNonNull(supplier, "supplier");

		return new SearchQueryParameterFactory<>(parameterName, supplier, null, null, null);
	}

	private static final class ResourceDistinctById
	{
		final IdType id;
		final Resource resource;

		ResourceDistinctById(IdType id, Resource resource)
		{
			this.id = id;
			this.resource = resource;
		}

		@Override
		public int hashCode()
		{
			return Objects.hash(id);
		}

		@Override
		public boolean equals(Object obj)
		{
			if (this == obj)
				return true;
			if (obj == null || getClass() != obj.getClass())
				return false;
			ResourceDistinctById other = (ResourceDistinctById) obj;
			return Objects.equals(id, other.id);
		}

		public Resource getResource()
		{
			return resource;
		}
	}

	private final DataSource dataSource;
	private final DataSource permanentDeleteDataSource;
	private final Class resourceType;
	private final String resourceTypeName;

	private final String resourceTable;
	private final String resourceColumn;
	private final String resourceIdColumn;

	private final PreparedStatementFactory preparedStatementFactory;
	private final Function identityFilter;
	private final List> searchParameterFactories = new ArrayList<>();
	private final List searchRevIncludeParameterFactories = new ArrayList<>();

	private final SearchQueryParameterFactory resourceIdFactory;
	private final SearchQueryParameterFactory resourceLastUpdatedFactory;
	private final SearchQueryParameterFactory resourceProfileFactory;

	protected static SearchQueryRevIncludeParameterFactory factory(
			Supplier revIncludeSupplier, List revIncludeParameterValues)
	{
		Objects.requireNonNull(revIncludeSupplier, "revIncludeSupplier");
		Objects.requireNonNull(revIncludeParameterValues, "revIncludeParameterValues");

		return new SearchQueryRevIncludeParameterFactory(revIncludeSupplier, revIncludeParameterValues);
	}

	protected static  SearchQueryParameterFactory factory(String parameterName,
			Supplier> supplier, List nameModifiers,
			Supplier> includeSupplier, List includeParameterValues)
	{
		Objects.requireNonNull(parameterName, "parameterName");
		Objects.requireNonNull(supplier, "supplier");
		Objects.requireNonNull(nameModifiers, "nameModifiers");
		Objects.requireNonNull(includeSupplier, "includeSupplier");
		Objects.requireNonNull(includeParameterValues, "includeParameterValues");

		return new SearchQueryParameterFactory<>(parameterName, supplier, nameModifiers, includeSupplier,
				includeParameterValues);
	}

	protected static  SearchQueryParameterFactory factory(String parameterName,
			Supplier> supplier, List nameModifiers)
	{
		Objects.requireNonNull(parameterName, "parameterName");
		Objects.requireNonNull(supplier, "supplier");
		Objects.requireNonNull(nameModifiers, "nameModifiers");

		return new SearchQueryParameterFactory<>(parameterName, supplier, nameModifiers, null, null);
	}

	AbstractResourceDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext,
			Class resourceType, String resourceTable, String resourceColumn, String resourceIdColumn,
			Function userFilter,
			List> searchParameterFactories,
			List searchRevIncludeParameterFactories)
	{
		this(dataSource, permanentDeleteDataSource, resourceType, resourceTable, resourceColumn, resourceIdColumn,
				new PreparedStatementFactoryDefault<>(fhirContext, resourceType, resourceTable, resourceIdColumn,
						resourceColumn),
				userFilter, searchParameterFactories, searchRevIncludeParameterFactories);
	}

	AbstractResourceDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, Class resourceType,
			String resourceTable, String resourceColumn, String resourceIdColumn,
			PreparedStatementFactory preparedStatementFactory,
			Function userFilter,
			List> searchParameterFactories,
			List searchRevIncludeParameterFactories)
	{
		this.dataSource = dataSource;
		this.permanentDeleteDataSource = permanentDeleteDataSource;
		this.resourceType = resourceType;
		resourceTypeName = Objects.requireNonNull(resourceType, "resourceType").getAnnotation(ResourceDef.class).name();

		this.resourceTable = resourceTable;
		this.resourceColumn = resourceColumn;
		this.resourceIdColumn = resourceIdColumn;

		this.preparedStatementFactory = preparedStatementFactory;

		this.identityFilter = userFilter;

		if (searchParameterFactories != null)
			this.searchParameterFactories.addAll(searchParameterFactories);
		if (searchRevIncludeParameterFactories != null)
			this.searchRevIncludeParameterFactories.addAll(searchRevIncludeParameterFactories);

		resourceIdFactory = new SearchQueryParameterFactory<>(ResourceId.PARAMETER_NAME,
				() -> new ResourceId<>(resourceType, resourceIdColumn));
		resourceLastUpdatedFactory = new SearchQueryParameterFactory<>(ResourceLastUpdated.PARAMETER_NAME,
				() -> new ResourceLastUpdated<>(resourceType, resourceColumn));
		resourceProfileFactory = new SearchQueryParameterFactory<>(ResourceProfile.PARAMETER_NAME,
				() -> new ResourceProfile<>(resourceType, resourceColumn), ResourceProfile.getNameModifiers());
	}

	@Override
	public void afterPropertiesSet() throws Exception
	{
		Objects.requireNonNull(dataSource, "dataSource");
		Objects.requireNonNull(permanentDeleteDataSource, "permanentDeleteDataSource");
		Objects.requireNonNull(resourceType, "resourceType");
		Objects.requireNonNull(resourceTable, "resourceTable");
		Objects.requireNonNull(resourceColumn, "resourceColumn");
		Objects.requireNonNull(resourceIdColumn, "resourceIdColumn");
		Objects.requireNonNull(preparedStatementFactory, "preparedStatementFactory");
		Objects.requireNonNull(identityFilter, "userFilter");
	}

	protected DataSource getDataSource()
	{
		return dataSource;
	}

	protected String getResourceTable()
	{
		return resourceTable;
	}

	protected String getResourceIdColumn()
	{
		return resourceIdColumn;
	}

	protected String getResourceColumn()
	{
		return resourceColumn;
	}

	protected PreparedStatementFactory getPreparedStatementFactory()
	{
		return preparedStatementFactory;
	}

	@Override
	public String getResourceTypeName()
	{
		return resourceTypeName;
	}

	@Override
	public Class getResourceType()
	{
		return resourceType;
	}

	@Override
	public Connection newReadWriteTransaction() throws SQLException
	{
		Connection connection = dataSource.getConnection();
		connection.setReadOnly(false);
		connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
		connection.setAutoCommit(false);

		return connection;
	}

	@Override
	public final R create(R resource) throws SQLException
	{
		Objects.requireNonNull(resource, "resource");

		return createWithId(resource, UUID.randomUUID());
	}

	@Override
	public R createWithId(R resource, UUID uuid) throws SQLException
	{
		Objects.requireNonNull(resource, "resource");
		Objects.requireNonNull(uuid, "uuid");

		try (Connection connection = dataSource.getConnection())
		{
			connection.setReadOnly(false);
			return createWithTransactionAndId(connection, resource, uuid);
		}
	}

	@Override
	public R createWithTransactionAndId(Connection connection, R resource, UUID uuid) throws SQLException
	{
		Objects.requireNonNull(connection, "connection");
		Objects.requireNonNull(resource, "resource");
		Objects.requireNonNull(uuid, "uuid");
		if (connection.isReadOnly())
			throw new IllegalArgumentException("Connection is read-only");

		R inserted = create(connection, resource, uuid);

		logger.debug("{} with ID {} created", resourceTypeName, inserted.getId());
		return inserted;
	}

	private R create(Connection connection, R resource, UUID uuid) throws SQLException
	{
		resource = copy(resource); // XXX defensive copy, might want to remove this call
		resource.setIdElement(new IdType(resourceTypeName, uuid.toString(), FIRST_VERSION_STRING));
		resource.getMeta().setVersionId(FIRST_VERSION_STRING);
		resource.getMeta().setLastUpdated(new Date());

		try (PreparedStatement statement = connection.prepareStatement(preparedStatementFactory.getCreateSql()))
		{
			preparedStatementFactory.configureCreateStatement(statement, resource, uuid);

			statement.execute();
		}

		return resource;
	}

	protected abstract R copy(R resource);

	protected R getResource(ResultSet result, int index) throws SQLException
	{
		String json = result.getString(index);
		return preparedStatementFactory.getJsonParser().parseResource(resourceType, json);
	}

	@Override
	public final Optional read(UUID uuid) throws SQLException, ResourceDeletedException
	{
		if (uuid == null)
			return Optional.empty();

		try (Connection connection = dataSource.getConnection())
		{
			return readWithTransaction(connection, uuid);
		}
	}

	@Override
	public Optional readWithTransaction(Connection connection, UUID uuid)
			throws SQLException, ResourceDeletedException
	{
		Objects.requireNonNull(connection, "connection");
		if (uuid == null)
			return Optional.empty();

		try (PreparedStatement statement = connection.prepareStatement(preparedStatementFactory.getReadByIdSql()))
		{
			preparedStatementFactory.configureReadByIdStatement(statement, uuid);

			try (ResultSet result = statement.executeQuery())
			{
				if (result.next())
				{
					LocalDateTime deleted = preparedStatementFactory.getReadByIdDeleted(result);
					if (deleted != null)
					{
						long version = preparedStatementFactory.getReadByIdVersion(result);
						logger.debug("{} with IdPart {} found, but marked as deleted", resourceTypeName, uuid);
						throw newResourceDeletedException(uuid, deleted, version);
					}
					else
					{
						logger.debug("{} with IdPart {} found", resourceTypeName, uuid);
						return Optional.of(preparedStatementFactory.getReadByIdResource(result));
					}
				}
				else
				{
					logger.debug("{} with IdPart {} not found", resourceTypeName, uuid);
					return Optional.empty();
				}
			}
		}
	}

	private ResourceDeletedException newResourceDeletedException(UUID uuid, LocalDateTime deleted, long version)
	{
		return new ResourceDeletedException(new IdType(resourceTypeName, uuid.toString(), String.valueOf(version + 1)),
				deleted);
	}

	@Override
	public final Optional readVersion(UUID uuid, long version) throws SQLException, ResourceDeletedException
	{
		if (uuid == null || version < FIRST_VERSION)
			return Optional.empty();

		try (Connection connection = dataSource.getConnection())
		{
			return readVersionWithTransaction(connection, uuid, version);
		}
	}

	@Override
	public Optional readVersionWithTransaction(Connection connection, UUID uuid, long version)
			throws SQLException, ResourceDeletedException
	{
		Objects.requireNonNull(connection, "connection");
		if (uuid == null || version < FIRST_VERSION)
			return Optional.empty();

		try (PreparedStatement statement = connection
				.prepareStatement(preparedStatementFactory.getReadByIdAndVersionSql()))
		{
			preparedStatementFactory.configureReadByIdAndVersionStatement(statement, uuid, version);

			try (ResultSet result = statement.executeQuery())
			{
				if (result.next())
				{
					LocalDateTime deleted = preparedStatementFactory.getReadByIdVersionDeleted(result);
					long readVersion = preparedStatementFactory.getReadByIdVersionVersion(result);
					if (deleted == null && readVersion != version)
					{
						logger.debug("{} with IdPart {} and Version {} not found", resourceTypeName, uuid, version);
						return Optional.empty();
					}
					else if (deleted != null && readVersion + 1 == version)
					{
						logger.debug(
								"{} with IdPart {} and Version {} found, but marked as deleted (delete history entry)",
								resourceTypeName, uuid, version);
						throw newResourceDeletedException(uuid, deleted, readVersion);
					}
					else
					{
						logger.debug("{} with IdPart {} and Version {} found", resourceTypeName, uuid, version);
						return Optional.of(preparedStatementFactory.getReadByIdAndVersionResource(result));
					}
				}
				else
				{
					logger.debug("{} with IdPart {} and Version {} not found", resourceTypeName, uuid, version);
					return Optional.empty();
				}
			}
		}
	}

	@Override
	public Optional readIncludingDeleted(UUID uuid) throws SQLException
	{
		if (uuid == null)
			return Optional.empty();

		try (Connection connection = dataSource.getConnection())
		{
			return readIncludingDeletedWithTransaction(connection, uuid);
		}
	}

	@Override
	public Optional readIncludingDeletedWithTransaction(Connection connection, UUID uuid) throws SQLException
	{
		Objects.requireNonNull(connection, "connection");
		if (uuid == null)
			return Optional.empty();

		try (PreparedStatement statement = connection.prepareStatement(preparedStatementFactory.getReadByIdSql()))
		{
			preparedStatementFactory.configureReadByIdStatement(statement, uuid);

			try (ResultSet result = statement.executeQuery())
			{
				if (result.next())
				{
					if (preparedStatementFactory.getReadByIdDeleted(result) != null)
						logger.warn("{} with IdPart {} found, but marked as deleted", resourceTypeName, uuid);
					else
						logger.debug("{} with IdPart {} found", resourceTypeName, uuid);

					return Optional.of(preparedStatementFactory.getReadByIdResource(result));
				}
				else
				{
					logger.debug("{} with IdPart {} not found", resourceTypeName, uuid);
					return Optional.empty();
				}
			}
		}
	}

	@Override
	public List readAll() throws SQLException
	{
		try (Connection connection = dataSource.getConnection())
		{
			return readAllWithTransaction(connection);
		}
	}

	@Override
	public List readAllWithTransaction(Connection connection) throws SQLException
	{
		Objects.requireNonNull(connection, "connection");

		try (PreparedStatement statement = connection
				.prepareStatement("SELECT " + getResourceColumn() + " FROM current_" + getResourceTable()))
		{
			try (ResultSet result = statement.executeQuery())
			{
				List all = new ArrayList<>();

				while (result.next())
					all.add(getResource(result, 1));

				return all;
			}
		}
	}

	@Override
	public boolean existsNotDeleted(String idString, String versionString) throws SQLException
	{
		try (Connection connection = dataSource.getConnection())
		{
			return existsNotDeletedWithTransaction(connection, idString, versionString);
		}
	}

	@Override
	public boolean existsNotDeletedWithTransaction(Connection connection, String idString, String versionString)
			throws SQLException
	{
		Objects.requireNonNull(connection, "connection");

		UUID uuid = toUuid(idString);

		if (uuid == null)
			return false;

		if (versionString == null || versionString.isBlank())
		{
			try (PreparedStatement statement = connection.prepareStatement("SELECT deleted IS NOT NULL FROM "
					+ resourceTable + " WHERE " + resourceIdColumn + " = ? ORDER BY version DESC LIMIT 1"))
			{
				statement.setObject(1, preparedStatementFactory.uuidToPgObject(uuid));

				try (ResultSet result = statement.executeQuery())
				{
					return result.next() && !result.getBoolean(1);
				}
			}
		}
		else
		{
			Long version = toLong(versionString);
			if (version == null || version < FIRST_VERSION)
				return false;

			try (PreparedStatement statement = connection.prepareStatement("SELECT deleted IS NOT NULL FROM "
					+ resourceTable + " WHERE " + resourceIdColumn + " = ? AND version = ?"))
			{
				statement.setObject(1, preparedStatementFactory.uuidToPgObject(uuid));
				statement.setLong(2, version);

				try (ResultSet result = statement.executeQuery())
				{
					return result.next() && !result.getBoolean(1);
				}
			}
		}
	}

	@Override
	public final R update(R resource, Long expectedVersion)
			throws SQLException, ResourceNotFoundException, ResourceVersionNoMatchException
	{
		Objects.requireNonNull(resource, "resource");
		// expectedVersion may be null

		try (Connection connection = dataSource.getConnection())
		{
			connection.setReadOnly(false);
			connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
			connection.setAutoCommit(false);

			try
			{
				R updatedResource = updateWithTransaction(connection, resource, expectedVersion);

				connection.commit();

				return updatedResource;
			}
			catch (Exception e)
			{
				connection.rollback();
				throw e;
			}
		}
	}

	@Override
	public R updateWithTransaction(Connection connection, R resource, Long expectedVersion)
			throws SQLException, ResourceNotFoundException, ResourceVersionNoMatchException
	{
		Objects.requireNonNull(connection, "connection");
		Objects.requireNonNull(resource, "resource");
		// expectedVersion may be null
		if (connection.isReadOnly())
			throw new IllegalArgumentException("Connection is read-only");
		if (connection.getTransactionIsolation() != Connection.TRANSACTION_REPEATABLE_READ
				&& connection.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE)
			throw new IllegalArgumentException("Connection transaction isolation not REPEATABLE_READ or SERIALIZABLE");
		if (connection.getAutoCommit())
			throw new IllegalArgumentException("Connection transaction is in auto commit mode");

		resource = copy(resource); // XXX defensive copy, might want to remove this call

		LatestVersion latestVersion = getLatestVersion(resource, connection);

		if (expectedVersion != null && expectedVersion != latestVersion.version)
		{
			logger.info("Expected version {} does not match latest version {}", expectedVersion, latestVersion.version);
			throw new ResourceVersionNoMatchException(resource.getIdElement().getIdPart(), expectedVersion,
					latestVersion.version);
		}

		// latestVersion gives stored latest version +1 if resource is deleted
		long newVersion = latestVersion.version + 1;

		R updated = update(connection, resource, newVersion);

		logger.debug("{} with IdPart {} updated, new version {}", resourceTypeName, updated.getIdElement().getIdPart(),
				newVersion);
		return updated;
	}

	protected final UUID toUuid(String id)
	{
		if (id == null)
			return null;

		// TODO control flow by exception
		try
		{
			return UUID.fromString(id);
		}
		catch (IllegalArgumentException e)
		{
			return null;
		}
	}

	protected final Long toLong(String version)
	{
		if (version == null)
			return null;

		// TODO control flow by exception
		try
		{
			return Long.parseLong(version);
		}
		catch (NumberFormatException e)
		{
			return null;
		}
	}

	private R update(Connection connection, R resource, long version) throws SQLException
	{
		UUID uuid = toUuid(resource.getIdElement().getIdPart());
		if (uuid == null)
			throw new IllegalArgumentException("resource.id is not a UUID");

		resource = copy(resource);
		String versionAsString = String.valueOf(version);
		resource.setIdElement(new IdType(resourceTypeName, resource.getIdElement().getIdPart(), versionAsString));
		resource.getMeta().setVersionId(versionAsString);
		resource.getMeta().setLastUpdated(new Date());

		try (PreparedStatement statement = connection.prepareStatement(preparedStatementFactory.getUpdateNewRowSql()))
		{
			preparedStatementFactory.configureUpdateNewRowSqlStatement(statement, uuid, version, resource);

			statement.execute();
		}

		return resource;
	}

	protected static class LatestVersion
	{
		final long version;
		final boolean deleted;

		LatestVersion(long version, boolean deleted)
		{
			this.version = version + (deleted ? 1 : 0);
			this.deleted = deleted;
		}
	}

	protected final LatestVersion getLatestVersion(R resource, Connection connection)
			throws SQLException, ResourceNotFoundException
	{
		UUID uuid = toUuid(resource.getIdElement().getIdPart());
		if (uuid == null)
			throw new ResourceNotFoundException(resource.getId() != null ? resource.getId() : "'null'");

		return getLatestVersion(uuid, connection);
	}

	protected final LatestVersion getLatestVersion(UUID uuid, Connection connection)
			throws SQLException, ResourceNotFoundException
	{
		if (uuid == null)
			throw new ResourceNotFoundException("'null'");

		return getLatestVersionIfExists(uuid, connection)
				.orElseThrow(() -> new ResourceNotFoundException(uuid.toString()));
	}

	protected final Optional getLatestVersionIfExists(UUID uuid, Connection connection)
			throws SQLException, ResourceNotFoundException
	{
		if (uuid == null)
			return Optional.empty();

		try (PreparedStatement statement = connection.prepareStatement("SELECT version, deleted IS NOT NULL FROM "
				+ resourceTable + " WHERE " + resourceIdColumn + " = ? ORDER BY version DESC LIMIT 1"))
		{
			statement.setObject(1, preparedStatementFactory.uuidToPgObject(uuid));

			try (ResultSet result = statement.executeQuery())
			{
				if (result.next())
				{
					long version = result.getLong(1);
					boolean deleted = result.getBoolean(2);

					logger.debug("Latest version for {} with IdPart {} is {}{}", resourceTypeName, uuid.toString(),
							version, deleted ? ", resource marked as deleted" : "");
					return Optional.of(new LatestVersion(version, deleted));
				}
				else
				{
					logger.debug("{} with IdPart {} not found", resourceTypeName, uuid.toString());
					return Optional.empty();
				}
			}
		}
	}

	@Override
	public final boolean delete(UUID uuid) throws SQLException, ResourceNotFoundException
	{
		try (Connection connection = dataSource.getConnection())
		{
			connection.setReadOnly(false);

			return deleteWithTransaction(connection, uuid);
		}
	}

	@Override
	public boolean deleteWithTransaction(Connection connection, UUID uuid)
			throws SQLException, ResourceNotFoundException
	{
		return delete(connection, uuid);
	}

	protected final boolean delete(Connection connection, UUID uuid) throws SQLException, ResourceNotFoundException
	{
		Objects.requireNonNull(connection, "connection");
		if (connection.isReadOnly())
			throw new IllegalStateException("Connection is read-only");
		if (uuid == null)
			throw new ResourceNotFoundException("'null'");

		LatestVersion latestVersion = getLatestVersion(uuid, connection);

		if (latestVersion.deleted)
			return false;

		try (PreparedStatement statement = connection.prepareStatement("UPDATE " + resourceTable
				+ " SET deleted = ? WHERE " + resourceIdColumn + " = ? AND version = (SELECT MAX(version) FROM "
				+ resourceTable + " WHERE " + resourceIdColumn + " = ?)"))
		{
			statement.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now()));
			statement.setObject(2, preparedStatementFactory.uuidToPgObject(uuid));
			statement.setObject(3, preparedStatementFactory.uuidToPgObject(uuid));

			statement.execute();

			logger.debug("{} with ID {} marked as deleted", resourceTypeName, uuid);
			return true;
		}
	}

	@Override
	public final PartialResult search(DbSearchQuery query) throws SQLException
	{
		Objects.requireNonNull(query, "query");

		try (Connection connection = dataSource.getConnection())
		{
			return searchWithTransaction(connection, query);
		}
	}

	@Override
	public PartialResult searchWithTransaction(Connection connection, DbSearchQuery query) throws SQLException
	{
		Objects.requireNonNull(connection, "connection");
		Objects.requireNonNull(query, "query");

		int total = 0;
		try (PreparedStatement statement = connection.prepareStatement(query.getCountSql()))
		{
			query.modifyStatement(statement, connection::createArrayOf);

			try (ResultSet result = statement.executeQuery())
			{
				if (result.next())
					total = result.getInt(1);
			}
		}

		List partialResult = new ArrayList<>();
		List includes = new ArrayList<>();

		if (!query.getPageAndCount().isCountOnly(total))
		{
			try (PreparedStatement statement = connection.prepareStatement(query.getSearchSql()))
			{
				query.modifyStatement(statement, connection::createArrayOf);

				try (ResultSet result = statement.executeQuery())
				{
					ResultSetMetaData metaData = result.getMetaData();
					while (result.next())
					{
						R resource = getResource(result, 1);
						modifySearchResultResource(resource, connection);
						partialResult.add(resource);

						for (int columnIndex = 2; columnIndex <= metaData.getColumnCount(); columnIndex++)
							getResources(result, columnIndex, includes, connection, query);
					}
				}
			}
		}

		// needs to be filtered by read rules, before returning to user, see rest access layer
		includes = includes.stream().map(r -> new ResourceDistinctById(r.getIdElement(), r)).distinct()
				.map(ResourceDistinctById::getResource).collect(Collectors.toList());

		return new PartialResult<>(total, query.getPageAndCount(), partialResult, includes);
	}

	/**
	 * Override this method to modify resources retrieved by search queries before returning to the user. This method
	 * can be used, if the resource returned by the search is not complete and additional content needs to be retrieved.
	 * For example the content of a {@link Binary} resource might not be stored in the json column.
	 *
	 * @param resource
	 *            not null
	 * @param connection
	 *            not null
	 * @throws SQLException
	 *             if database access errors occur
	 */
	protected void modifySearchResultResource(R resource, Connection connection) throws SQLException
	{
	}

	private void getResources(ResultSet result, int columnIndex, List includeResources,
			Connection connection, DbSearchQuery query) throws SQLException
	{
		String json = result.getString(columnIndex);

		if (json == null)
			return;

		JsonArray array = (JsonArray) JsonParser.parseString(json);
		for (JsonElement jsonElement : array)
		{
			IBaseResource resource = preparedStatementFactory.getJsonParser().parseResource(jsonElement.toString());
			if (resource instanceof Resource r)
			{
				query.modifyIncludeResource(r, columnIndex, connection);
				includeResources.add(r);
			}
			else
				logger.warn("parsed resouce of type {} not instance of {}, ignoring include resource",
						resource.getClass().getName(), Resource.class.getName());
		}
	}

	@Override
	public final SearchQuery createSearchQuery(Identity identity, PageAndCount pageAndCount)
	{
		Objects.requireNonNull(identity, "identity");

		return doCreateSearchQuery(identity, pageAndCount);
	}

	@Override
	public SearchQuery createSearchQueryWithoutUserFilter(PageAndCount pageAndCount)
	{
		return doCreateSearchQuery(null, pageAndCount);
	}

	private SearchQuery doCreateSearchQuery(Identity identity, PageAndCount pageAndCount)
	{
		Objects.requireNonNull(pageAndCount, "pageAndCount");

		var builder = SearchQueryBuilder.create(resourceType, getResourceTable(), getResourceColumn(), pageAndCount);

		if (identity != null)
			builder = builder.with(identityFilter.apply(identity));

		return builder.with(resourceIdFactory).with(resourceLastUpdatedFactory).with(resourceProfileFactory)
				.with(searchParameterFactories).withRevInclude(searchRevIncludeParameterFactories).build();
	}

	@Override
	public void deletePermanently(UUID uuid)
			throws SQLException, ResourceNotFoundException, ResourceNotMarkedDeletedException
	{
		try (Connection connection = permanentDeleteDataSource.getConnection())
		{
			connection.setReadOnly(false);

			deletePermanentlyWithTransaction(connection, uuid);
		}
	}

	@Override
	public void deletePermanentlyWithTransaction(Connection connection, UUID uuid)
			throws SQLException, ResourceNotFoundException, ResourceNotMarkedDeletedException
	{
		Objects.requireNonNull(connection, "connection");
		if (connection.isReadOnly())
			throw new IllegalStateException("Connection is read-only");
		if (uuid == null)
			throw new ResourceNotFoundException("'null'");

		LatestVersion latestVersion = getLatestVersion(uuid, connection);

		if (!latestVersion.deleted)
			throw new ResourceNotMarkedDeletedException(uuid.toString());

		try (PreparedStatement statement = connection
				.prepareStatement("DELETE FROM " + resourceTable + " WHERE " + resourceIdColumn + "= ?"))
		{
			statement.setObject(1, preparedStatementFactory.uuidToPgObject(uuid));

			statement.execute();

			logger.debug("{} with ID {} deleted permanently", resourceTypeName, uuid);
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy