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

com.ibm.fhir.persistence.jdbc.dao.impl.ParameterVisitorBatchDAO Maven / Gradle / Ivy

There is a newer version: 4.11.1
Show newest version
/*
 * (C) Copyright IBM Corp. 2019, 2021
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package com.ibm.fhir.persistence.jdbc.dao.impl;

import static com.ibm.fhir.persistence.jdbc.JDBCConstants.UTC;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.ibm.fhir.persistence.exception.FHIRPersistenceException;
import com.ibm.fhir.persistence.jdbc.JDBCConstants;
import com.ibm.fhir.persistence.jdbc.dao.api.IResourceReferenceDAO;
import com.ibm.fhir.persistence.jdbc.dao.api.JDBCIdentityCache;
import com.ibm.fhir.persistence.jdbc.dto.CompositeParmVal;
import com.ibm.fhir.persistence.jdbc.dto.DateParmVal;
import com.ibm.fhir.persistence.jdbc.dto.ExtractedParameterValue;
import com.ibm.fhir.persistence.jdbc.dto.ExtractedParameterValueVisitor;
import com.ibm.fhir.persistence.jdbc.dto.LocationParmVal;
import com.ibm.fhir.persistence.jdbc.dto.NumberParmVal;
import com.ibm.fhir.persistence.jdbc.dto.QuantityParmVal;
import com.ibm.fhir.persistence.jdbc.dto.ReferenceParmVal;
import com.ibm.fhir.persistence.jdbc.dto.StringParmVal;
import com.ibm.fhir.persistence.jdbc.dto.TokenParmVal;
import com.ibm.fhir.persistence.jdbc.exception.FHIRPersistenceDataAccessException;
import com.ibm.fhir.persistence.jdbc.impl.ParameterTransactionDataImpl;
import com.ibm.fhir.schema.control.FhirSchemaConstants;
import com.ibm.fhir.search.util.ReferenceValue;
import com.ibm.fhir.search.util.ReferenceValue.ReferenceType;
import com.ibm.fhir.search.util.SearchUtil;

/**
 * Batch insert into the parameter values tables. Avoids having to create one stored procedure
 * per resource type, because the row type array approach apparently won't work with dynamic
 * SQL (EXECUTE ... USING ...). Unfortunately this means we have more database round-trips, we
 * don't have a choice.
 */
public class ParameterVisitorBatchDAO implements ExtractedParameterValueVisitor, AutoCloseable {
    private static final Logger logger = Logger.getLogger(ParameterVisitorBatchDAO.class.getName());

    // the connection to use for the inserts
    private final Connection connection;

    // the max number of rows we accumulate for a given statement before we submit the batch
    private final int batchSize;

    // FK to the logical resource for the parameters being added
    private final long logicalResourceId;

    // Maintainers: remember to close all statements in AutoCloseable#close()
    private final String insertString;
    private final PreparedStatement strings;
    private int stringCount;

    private final String insertNumber;
    private final PreparedStatement numbers;
    private int numberCount;

    private final String insertDate;
    private final PreparedStatement dates;
    private int dateCount;

    private final String insertQuantity;
    private final PreparedStatement quantities;
    private int quantityCount;

    // rarely used so no need for {@code java.sql.PreparedStatement} or batching on this one
    // even on Location resources its only there once by default
    private final String insertLocation;

    // Searchable string attributes stored at the system level
    private final PreparedStatement systemStrings;
    private int systemStringCount;

    // Searchable date attributes stored at the system level
    private final PreparedStatement systemDates;
    private int systemDateCount;

    // DAO for handling parameters stored as token values (including system-level token search params)
    private final IResourceReferenceDAO resourceReferenceDAO;

    // Collect a list of token values to process in one go
    private final List tokenValueRecs = new ArrayList<>();

    // The table prefix (resourceType)
    private final String tablePrefix;

    // The common cache for all our identity lookup needs
    private final JDBCIdentityCache identityCache;

    // If not null, we stash certain parameter data here for insertion later
    private final ParameterTransactionDataImpl transactionData;

    // tracks the number of composites so we know what next composite_id to use
    int compositeIdCounter = 0;

    // when set, this value is added as the composite_id value for each parameter we store
    Integer currentCompositeId = null;

    // Supports slightly more useful error messages if we hit a nested composite
    String currentCompositeParameterName = null;

    /**
     * Public constructor
     * @param c
     * @param resourceId
     */
    public ParameterVisitorBatchDAO(Connection c, String adminSchemaName, String tablePrefix, boolean multitenant, long logicalResourceId, int batchSize,
            JDBCIdentityCache identityCache, IResourceReferenceDAO resourceReferenceDAO, ParameterTransactionDataImpl ptdi) throws SQLException {
        if (batchSize < 1) {
            throw new IllegalArgumentException("batchSize must be >= 1");
        }

        this.connection = c;
        this.logicalResourceId = logicalResourceId;
        this.batchSize = batchSize;
        this.identityCache = identityCache;
        this.resourceReferenceDAO = resourceReferenceDAO;
        this.tablePrefix = tablePrefix;
        this.transactionData = ptdi;

        insertString = multitenant ?
                "INSERT INTO " + tablePrefix + "_str_values (mt_id, parameter_name_id, str_value, str_value_lcase, logical_resource_id, composite_id) VALUES (" + adminSchemaName + ".sv_tenant_id,?,?,?,?,?)"
                :
                "INSERT INTO " + tablePrefix + "_str_values (parameter_name_id, str_value, str_value_lcase, logical_resource_id, composite_id) VALUES (?,?,?,?,?)";
        strings = c.prepareStatement(insertString);

        insertNumber = multitenant ?
                "INSERT INTO " + tablePrefix + "_number_values (mt_id, parameter_name_id, number_value, number_value_low, number_value_high, logical_resource_id, composite_id) VALUES (" + adminSchemaName + ".sv_tenant_id,?,?,?,?,?,?)"
                :
                "INSERT INTO " + tablePrefix + "_number_values (parameter_name_id, number_value, number_value_low, number_value_high, logical_resource_id, composite_id) VALUES (?,?,?,?,?,?)";
        numbers = c.prepareStatement(insertNumber);

        insertDate = multitenant ?
                "INSERT INTO " + tablePrefix + "_date_values (mt_id, parameter_name_id, date_start, date_end, logical_resource_id, composite_id) VALUES (" + adminSchemaName + ".sv_tenant_id,?,?,?,?,?)"
                :
                "INSERT INTO " + tablePrefix + "_date_values (parameter_name_id, date_start, date_end, logical_resource_id, composite_id) VALUES (?,?,?,?,?)";
        dates = c.prepareStatement(insertDate);

        insertQuantity = multitenant ?
                "INSERT INTO " + tablePrefix + "_quantity_values (mt_id, parameter_name_id, code_system_id, code, quantity_value, quantity_value_low, quantity_value_high, logical_resource_id, composite_id) VALUES (" + adminSchemaName + ".sv_tenant_id,?,?,?,?,?,?,?,?)"
                :
                "INSERT INTO " + tablePrefix + "_quantity_values (parameter_name_id, code_system_id, code, quantity_value, quantity_value_low, quantity_value_high, logical_resource_id, composite_id) VALUES (?,?,?,?,?,?,?,?)";
        quantities = c.prepareStatement(insertQuantity);

        insertLocation = multitenant ? "INSERT INTO " + tablePrefix + "_latlng_values (mt_id, parameter_name_id, latitude_value, longitude_value, logical_resource_id, composite_id) VALUES (" + adminSchemaName + ".sv_tenant_id,?,?,?,?,?)"
                : "INSERT INTO " + tablePrefix + "_latlng_values (parameter_name_id, latitude_value, longitude_value, logical_resource_id, composite_id) VALUES (?,?,?,?,?)";

        // System level string attributes
        String insertSystemString = multitenant ?
                "INSERT INTO str_values (mt_id, parameter_name_id, str_value, str_value_lcase, logical_resource_id) VALUES (" + adminSchemaName + ".sv_tenant_id,?,?,?,?)"
                :
                "INSERT INTO str_values (parameter_name_id, str_value, str_value_lcase, logical_resource_id) VALUES (?,?,?,?)";
        systemStrings = c.prepareStatement(insertSystemString);

        // System level date attributes
        String insertSystemDate = multitenant ?
                "INSERT INTO date_values (mt_id, parameter_name_id, date_start, date_end, logical_resource_id) VALUES (" + adminSchemaName + ".sv_tenant_id,?,?,?,?)"
                :
                "INSERT INTO date_values (parameter_name_id, date_start, date_end, logical_resource_id) VALUES (?,?,?,?)";
        systemDates = c.prepareStatement(insertSystemDate);
    }

    /**
     * Look up the normalized id for the parameter, adding it to the parameter_names table if it doesn't yet exist
     * @param parameterName
     * @return
     */
    protected int getParameterNameId(String parameterName) throws FHIRPersistenceException {
        return identityCache.getParameterNameId(parameterName);
    }

    /**
     * Looks up the code system. If it doesn't exist, adds it to the database
     * @param codeSystem
     * @return
     */
    protected int getCodeSystemId(String codeSystem) throws FHIRPersistenceException {
        return identityCache.getCodeSystemId(codeSystem);
    }

    @Override
    public void visit(StringParmVal param) throws FHIRPersistenceException {
        String parameterName = param.getName();
        String value = param.getValueString();

        while (value != null && value.getBytes().length > FhirSchemaConstants.MAX_SEARCH_STRING_BYTES) {
            // keep chopping the string in half until its byte representation fits inside
            // the VARCHAR
            value = value.substring(0, value.length() / 2);
        }

        try {
            int parameterNameId = getParameterNameId(parameterName);
            if (isBase(param)) {
                if (logger.isLoggable(Level.FINE)) {
                    logger.fine("systemStringValue: " + parameterName + "[" + parameterNameId + "], " + value);
                }

                systemStrings.setInt(1, parameterNameId);
                if (value != null) {
                    systemStrings.setString(2, value);
                    systemStrings.setString(3, SearchUtil.normalizeForSearch(value));
                }
                else {
                    systemStrings.setNull(2, Types.VARCHAR);
                    systemStrings.setNull(3, Types.VARCHAR);
                }
                systemStrings.setLong(4, logicalResourceId);
                systemStrings.addBatch();

                if (++systemStringCount == this.batchSize) {
                    systemStrings.executeBatch();
                    systemStringCount = 0;
                }
            } else {
                // standard resource property
                if (logger.isLoggable(Level.FINE)) {
                    logger.fine("stringValue: " + parameterName + "[" + parameterNameId + "], " + value);
                }

                setStringParms(strings, parameterNameId, value);
                strings.addBatch();

                if (++stringCount == this.batchSize) {
                    strings.executeBatch();
                    stringCount = 0;
                }
            }
        } catch (SQLException x) {
            throw new FHIRPersistenceDataAccessException(parameterName + "=" + value, x);
        }
    }

    private void setStringParms(PreparedStatement insert, int parameterNameId, String value) throws SQLException {
        insert.setInt(1, parameterNameId);
        if (value != null) {
            insert.setString(2, value);
            insert.setString(3, SearchUtil.normalizeForSearch(value));
        } else {
            insert.setNull(2, Types.VARCHAR);
            insert.setNull(3, Types.VARCHAR);
        }
        insert.setLong(4, logicalResourceId);
        setCompositeId(insert, 5);
    }

    /**
     * Set the composite_id column value or null if required
     * @param ps the statement
     * @param idx the column index
     * @throws SQLException
     */
    private void setCompositeId(PreparedStatement ps, int idx) throws SQLException {
        if (this.currentCompositeId != null) {
            ps.setInt(idx, this.currentCompositeId);
        } else {
            ps.setNull(idx, Types.INTEGER);
        }
    }

    @Override
    public void visit(NumberParmVal param) throws FHIRPersistenceException {
        String parameterName = param.getName();
        BigDecimal value = param.getValueNumber();
        BigDecimal valueLow = param.getValueNumberLow();
        BigDecimal valueHigh = param.getValueNumberHigh();

        try {
            int parameterNameId = getParameterNameId(parameterName);

            if (logger.isLoggable(Level.FINE)) {
                logger.fine("numberValue: " + parameterName + "[" + parameterNameId + "], "
                        + value + " [" + valueLow + ", " + valueHigh + "]");
            }

            setNumberParms(numbers, parameterNameId, value, valueLow, valueHigh);
            numbers.addBatch();

            if (++numberCount == this.batchSize) {
                numbers.executeBatch();
                numberCount = 0;
            }
        }
        catch (SQLException x) {
            throw new FHIRPersistenceDataAccessException(parameterName + "={" + value + " ["+ valueLow + "," + valueHigh + "}", x);
        }
    }

    private void setNumberParms(PreparedStatement insert, int parameterNameId, BigDecimal value, BigDecimal valueLow, BigDecimal valueHigh) throws SQLException {
        insert.setInt(1, parameterNameId);
        insert.setBigDecimal(2, value);
        insert.setBigDecimal(3, valueLow);
        insert.setBigDecimal(4, valueHigh);
        insert.setLong(5, logicalResourceId);
        setCompositeId(insert, 6);
    }

    @Override
    public void visit(DateParmVal param) throws FHIRPersistenceException {
        String parameterName = param.getName();
        Timestamp dateStart = param.getValueDateStart();
        Timestamp dateEnd = param.getValueDateEnd();
        try {
            int parameterNameId = getParameterNameId(parameterName);

            if (isBase(param)) {
                // store as a system level search param
                if (logger.isLoggable(Level.FINE)) {
                    logger.fine("systemDateValue: " + parameterName + "[" + parameterNameId + "], "
                             + "[" + dateStart + ", " + dateEnd + "]");
                }

                // Insert record into the base level date attribute table
                setDateParms(systemDates, parameterNameId, dateStart, dateEnd);
                systemDates.addBatch();

                if (++systemDateCount == this.batchSize) {
                    systemDates.executeBatch();
                    systemDateCount = 0;
                }
            } else {
                if (logger.isLoggable(Level.FINE)) {
                    logger.fine("dateValue: " + parameterName + "[" + parameterNameId + "], "
                            + "period: [" + dateStart + ", " + dateEnd + "]");
                }

                setDateParms(dates, parameterNameId, dateStart, dateEnd);
                dates.addBatch();

                if (++dateCount == this.batchSize) {
                    dates.executeBatch();
                    dateCount = 0;
                }
            }
        } catch (SQLException x) {
            throw new FHIRPersistenceDataAccessException(parameterName + "={" + dateStart + ", " + dateEnd + "}", x);
        }

    }

    private void setDateParms(PreparedStatement insert, int parameterNameId, Timestamp dateStart, Timestamp dateEnd) throws SQLException {
        insert.setInt(1, parameterNameId);
        insert.setTimestamp(2, dateStart, UTC);
        insert.setTimestamp(3, dateEnd, UTC);
        insert.setLong(4, logicalResourceId);
        setCompositeId(insert, 5);
    }

    @Override
    public void visit(TokenParmVal param) throws FHIRPersistenceException {
        String parameterName = param.getName();
        String codeSystem = param.getValueSystem();
        String tokenValue = param.getValueCode();
        try {
            int parameterNameId = getParameterNameId(parameterName);

            boolean isSystemParam = isBase(param);
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("tokenValue: " + parameterName + "[" + parameterNameId + "], "
                        + codeSystem + ", " + tokenValue);
            }

            // Add the new token value to the collection we're building...what's the resourceTypeId?
            final int resourceTypeId = identityCache.getResourceTypeId(param.getResourceType());
            if (tokenValue == null) {
                logger.info("tokenValue is NULL for: " + parameterName + "[" + parameterNameId + "], " + codeSystem);
            }

            // Issue 1683, for composites we now also record the current composite id (can be null)
            ResourceTokenValueRec rec = new ResourceTokenValueRec(parameterNameId, param.getResourceType(), resourceTypeId, logicalResourceId, codeSystem, tokenValue, this.currentCompositeId, isSystemParam);
            if (this.transactionData != null) {
                this.transactionData.addValue(rec);
            } else {
                this.tokenValueRecs.add(rec);
            }
        } catch (FHIRPersistenceDataAccessException x) {
            throw new FHIRPersistenceDataAccessException(parameterName + "=" + codeSystem + ":" + tokenValue, x);
        }
    }

    @Override
    public void visit(QuantityParmVal param) throws FHIRPersistenceException {
        String parameterName = param.getName();
        String code = param.getValueCode();
        String codeSystem = param.getValueSystem();
        BigDecimal quantityValue = param.getValueNumber();
        BigDecimal quantityLow = param.getValueNumberLow();
        BigDecimal quantityHigh = param.getValueNumberHigh();

        // XXX why no check for isBase on this one?

        // Skip anything with a null code
        if (code == null || code.isEmpty()) {
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("CODELESS QUANTITY (skipped): " + parameterName + "=" + code + ":" + codeSystem + "{" + quantityValue + ", " + quantityLow + ", " + quantityHigh + "}");
            }
        } else {
            try {
                int parameterNameId = getParameterNameId(parameterName);

                if (logger.isLoggable(Level.FINE)) {
                    logger.fine("quantityValue: " + parameterName + "[" + parameterNameId + "], "
                            + quantityValue + " [" + quantityLow + ", " + quantityHigh + "]");
                }

                setQuantityParms(quantities, parameterNameId, codeSystem, code, quantityValue, quantityLow, quantityHigh);
                quantities.addBatch();

                if (++quantityCount == batchSize) {
                    quantities.executeBatch();
                    quantityCount = 0;
                }
            } catch (FHIRPersistenceDataAccessException x) {
                // wrap the exception so we have more context about the parameter causing the problem
                throw new FHIRPersistenceDataAccessException(parameterName + "=" + code + ":" + codeSystem + "{" + quantityValue + ", " + quantityLow + ", " + quantityHigh + "}", x);
            } catch (SQLException x) {
                throw new FHIRPersistenceDataAccessException(parameterName + "=" + code + ":" + codeSystem + "{" + quantityValue + ", " + quantityLow + ", " + quantityHigh + "}", x);
            }
        }

    }

    private void setQuantityParms(PreparedStatement insert, int parameterNameId, String codeSystem, String code, BigDecimal quantityValue, BigDecimal quantityLow, BigDecimal quantityHigh)
            throws SQLException, FHIRPersistenceException {
        insert.setInt(1, parameterNameId);
        insert.setInt(2, getCodeSystemId(codeSystem));
        insert.setString(3, code);
        insert.setBigDecimal(4, quantityValue);
        insert.setBigDecimal(5, quantityLow);
        insert.setBigDecimal(6, quantityHigh);
        insert.setLong(7, logicalResourceId);
        setCompositeId(insert, 8);
    }

    @Override
    public void visit(LocationParmVal param) throws FHIRPersistenceException {
        String parameterName = param.getName();
        double lat = param.getValueLatitude();
        double lng = param.getValueLongitude();

        try {
            PreparedStatement insert = connection.prepareStatement(insertLocation);
            setLocationParms(insert, getParameterNameId(parameterName), lat, lng);
            insert.executeUpdate();
        } catch (SQLException x) {
            throw new FHIRPersistenceDataAccessException(parameterName + "={" + lat + ", " + lng + "}", x);
        }
    }

    private void setLocationParms(PreparedStatement insert, int parameterNameId, double lat, double lng) throws SQLException, FHIRPersistenceException {
        insert.setInt(1, parameterNameId);
        insert.setDouble(2, lat);
        insert.setDouble(3, lng);
        insert.setLong(4, logicalResourceId);
        setCompositeId(insert, 5);

    }

    @Override
    public void visit(CompositeParmVal compositeParameter) throws FHIRPersistenceException {
        if (this.currentCompositeId != null) {
            // no soup for you
            logger.warning("A compositeParameter '" + currentCompositeParameterName + "' cannot itself contain a composite '" + compositeParameter.getName());
            throw new FHIRPersistenceException("composite parameters cannot themselves contain composites");
        }

        // This composite is a collection of multiple parameters.
        List component = compositeParameter.getComponent();
        this.currentCompositeId = this.compositeIdCounter++;
        this.currentCompositeParameterName = compositeParameter.getName();
        for (ExtractedParameterValue val : component) {
            val.accept(this);
        }

        // Clear the currentCompositeId value so we no longer associate it with other parameters
        this.currentCompositeId = null;
        this.currentCompositeParameterName = null;
    }

    @Override
    public void close() throws Exception {
        // flush any stragglers, remembering to reset each count because
        // close() should be idempotent.
        try {
            if (stringCount > 0) {
                strings.executeBatch();
                stringCount = 0;
            }

            if (numberCount > 0) {
                numbers.executeBatch();
                numberCount = 0;
            }

            if (dateCount > 0) {
                dates.executeBatch();
                dateCount = 0;
            }


            if (quantityCount > 0) {
                quantities.executeBatch();
                quantityCount = 0;
            }

            if (systemStringCount > 0) {
                systemStrings.executeBatch();
                systemStringCount = 0;
            }

            if (systemDateCount > 0) {
                systemDates.executeBatch();
                systemDateCount = 0;
            }
        } catch (SQLException x) {
            SQLException batchException = x.getNextException();
            if (batchException != null) {
                // We're really interested in the underlying cause here
                throw batchException;
            }
            else {
                throw x;
            }
        }

        // Process any tokens and references we've collected along the way
        if (!tokenValueRecs.isEmpty()) {
            this.resourceReferenceDAO.addCommonTokenValues(this.tablePrefix, tokenValueRecs);
        }

        closeStatement(strings);
        closeStatement(numbers);
        closeStatement(dates);
        closeStatement(quantities);
        closeStatement(systemStrings);
        closeStatement(systemDates);
    }

    /**
     * Quietly close the given statement
     * @param ps
     */
    private void closeStatement(PreparedStatement ps) {
        try {
            ps.close();
        } catch (SQLException x) {
            logger.warning("failed to close statement");
        }
    }

    private boolean isBase(ExtractedParameterValue param) {
        return "Resource".equals(param.getBase());
    }

    @Override
    public void visit(ReferenceParmVal rpv) throws FHIRPersistenceException {
        if (rpv.getRefValue() == null) {
            return;
        }

        final String resourceType = this.tablePrefix;
        int parameterNameId = getParameterNameId(rpv.getName());
        Integer resourceTypeId = identityCache.getResourceTypeId(resourceType);
        if (resourceTypeId == null) {
            // resourceType is not sensitive, so it's OK to include in the exception message
            throw new FHIRPersistenceException("Resource type not found in cache: '" + resourceType + "'");
        }


        // The ReferenceValue has already been processed to convert the reference to
        // the required standard form, ready for insertion as a token value.
        ReferenceValue refValue = rpv.getRefValue();

        // Ignore references containing only a "display" element (apparently supported by the spec,
        // but contains nothing useful to store because there's no searchable value).
        String refResourceType = refValue.getTargetResourceType();
        String refLogicalId = refValue.getValue();
        Integer refVersion = refValue.getVersion();
        ResourceTokenValueRec rec;

        if (refValue.getType() == ReferenceType.DISPLAY_ONLY || refValue.getType() == ReferenceType.INVALID) {
            // protect against code regression. Invalid/improper references should be
            // filtered out already.
            logger.warning("Invalid reference parameter type: '" + resourceType + "." + rpv.getName() + "' type=" + refValue.getType().name());
            throw new IllegalArgumentException("Invalid reference parameter value. See server log for details.");
        }

        // reference params are never system-level
        final boolean isSystemParam = false;
        if (refResourceType != null) {
            // Store a token value configured as a reference to another resource
            rec = new ResourceTokenValueRec(parameterNameId, resourceType, resourceTypeId, logicalResourceId, refResourceType, refLogicalId, refVersion, this.currentCompositeId, isSystemParam);
        } else {
            // stored as a token with the default system
            rec = new ResourceTokenValueRec(parameterNameId, resourceType, resourceTypeId, logicalResourceId, JDBCConstants.DEFAULT_TOKEN_SYSTEM, refLogicalId, this.currentCompositeId, isSystemParam);
        }

        if (this.transactionData != null) {
            this.transactionData.addValue(rec);
        } else {
            this.tokenValueRecs.add(rec);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy