org.obiba.onyx.jade.instrument.holologic.APEXScanDataExtractor.java Source code

Java tutorial

Introduction

Here is the source code for org.obiba.onyx.jade.instrument.holologic.APEXScanDataExtractor.java

Source

/*******************************************************************************
 * Copyright (c) 2011 OBiBa. All rights reserved.
 *
 * This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package org.obiba.onyx.jade.instrument.holologic;

import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.dcm4che2.data.DicomElement;
import org.dcm4che2.data.DicomObject;
import org.dcm4che2.data.Tag;
import org.dcm4che2.tool.dcmrcv.ApexTag;
import org.dcm4che2.tool.dcmrcv.DicomServer;
import org.dcm4che2.tool.dcmrcv.DicomServer.StoredDicomFile;
import org.dcm4che2.util.StringUtils;
import org.obiba.onyx.jade.instrument.holologic.APEXInstrumentRunner.Side;
import org.obiba.onyx.util.data.Data;
import org.obiba.onyx.util.data.DataBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.ResultSetExtractor;

public abstract class APEXScanDataExtractor {

    /**
     * Static ivar needed for computing T- and Z-scores. Map distinct BMD variable name(s) (eg., HTOT_BMD) for a given
     * PatScanDb table (eg., Hip) and the corresponding bonerange code in the RefScanDb ReferenceCurve table (eg., 123.).
     * Additional BMD variables and codes should be added here for other tables (ie., Spine).
     */
    protected static final Map<String, String> ranges = new HashMap<String, String>();
    static {
        // forearm
        ranges.put("RU13TOT_BMD", "1..");
        ranges.put("RUMIDTOT_BMD", ".2.");
        ranges.put("RUUDTOT_BMD", "..3");
        ranges.put("RUTOT_BMD", "123");
        ranges.put("R_13_BMD", "R..");
        ranges.put("R_MID_BMD", ".R.");
        ranges.put("R_UD_BMD", "..R");
        ranges.put("RTOT_BMD", "RRR");
        ranges.put("U_13_BMD", "U..");
        ranges.put("U_MID_BMD", ".U.");
        ranges.put("U_UD_BMD", "..U");
        ranges.put("UTOT_BMD", "UUU");

        // whole body
        ranges.put("WBTOT_BMD", "NULL");

        // hip
        ranges.put("NECK_BMD", "1...");
        ranges.put("TROCH_BMD", ".2..");
        ranges.put("INTER_BMD", "..3.");
        ranges.put("WARDS_BMD", "...4");
        ranges.put("HTOT_BMD", "123.");

        // ap lumbar spine
        ranges.put("L1_BMD", "1...");
        ranges.put("L2_BMD", ".2..");
        ranges.put("L3_BMD", "..3.");
        ranges.put("L4_BMD", "...4");
        ranges.put("TOT_BMD", "1234");
        ranges.put("TOT_L1_BMD", "1...");
        ranges.put("TOT_L2_BMD", ".2..");
        ranges.put("TOT_L3_BMD", "..3.");
        ranges.put("TOT_L4_BMD", "...4");
        ranges.put("TOT_L1L2_BMD", "12..");
        ranges.put("TOT_L1L3_BMD", "1.3.");
        ranges.put("TOT_L1L4_BMD", "1..4");
        ranges.put("TOT_L2L3_BMD", ".23.");
        ranges.put("TOT_L2L4_BMD", ".2.4");
        ranges.put("TOT_L3L4_BMD", "..34");
        ranges.put("TOT_L1L2L3_BMD", "123.");
        ranges.put("TOT_L1L2L4_BMD", "12.4");
        ranges.put("TOT_L1L3L4_BMD", "1.34");
        ranges.put("TOT_L2L3L4_BMD", ".234");
    }

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

    private JdbcTemplate patScanDb;

    private JdbcTemplate refCurveDb;

    private String scanID;

    private String scanDate;

    private String scanMode;

    private Map<String, String> participantData;

    private DicomServer server;

    private ApexReceiver apexReceiver;

    //
    // Abstract methods.
    //

    public abstract String getName();

    public abstract String getBodyPartName();

    public abstract Side getSide();

    protected abstract long getScanType();

    public abstract String getRefType();

    public abstract String getRefSource();

    protected List<ApexDicomData> apexDicomList = new ArrayList<ApexDicomData>();

    /**
     * Constructor.
     * @param patScanDb
     * @param refCurveDb
     * @param participantData
     * @param server
     * @param apexReceiver
     */
    protected APEXScanDataExtractor(JdbcTemplate patScanDb, JdbcTemplate refCurveDb,
            Map<String, String> participantData, DicomServer server, ApexReceiver apexReceiver) {
        super();
        this.patScanDb = patScanDb;
        this.refCurveDb = refCurveDb;
        this.participantData = participantData;
        this.server = server;
        this.apexReceiver = apexReceiver;
    }

    /**
     * Called by APEXInstrumentRunner extractScanData(). Get scan information from Apex PatScan db, get the data values,
     * compute T- and Z-scores for BMD values.
     */
    public Map<String, Data> extractData() {

        /** get the dicom file(s) for a given type of scan */
        Map<String, Data> data = extractScanAnalysisData();

        log.info("start data with " + Integer.toString(data.size()) + " entries");

        /** get the variables associated with an analysis of the scan */

        if (!data.isEmpty()) {
            log.info("getting data from concrete impl of extractDataImp");
            extractDataImpl(data);
            try {
                if (!"LSPINE".equals(getBodyPartName())) {
                    log.info("getting TZscores... ");
                    computeTZScore(data);
                }
            } catch (ParseException e) {
                log.info("failed to parse dates");
            }
        }

        log.info("returning data with " + Integer.toString(data.size()) + " entries");
        return data;
    }

    /**
     * Called by extractData(). Query the Apex PatScan db for the scan ID, raw data file name, scan mode and scan type
     * based on the patient key and scan type. Scan type is provided by child classes (eg., AP Spine = 1).
     */
    private Map<String, Data> extractScanAnalysisData() {
        log.info("extractscananalysisdata: " + getParticipantKey() + ", " + Long.toString(getScanType()));
        return patScanDb.query(
                "SELECT SCANID, SCAN_MODE, SCAN_DATE FROM ScanAnalysis WHERE PATIENT_KEY = ? AND SCAN_TYPE = ?",
                new PreparedStatementSetter() {
                    public void setValues(PreparedStatement ps) throws SQLException {
                        ps.setString(1, getParticipantKey());
                        ps.setString(2, Long.toString(getScanType()));
                    }
                }, new ScanAnalysisResultSetExtractor());
    }

    /**
     * Called by extractData().
     *
     * @param data
     */
    protected abstract void extractDataImpl(Map<String, Data> data);

    /**
     * Called by extractData(). Computes T- and Z-score and adds to data collection.
     *
     * @param data
     */
    protected void computeTZScore(Map<String, Data> data)
            throws DataAccessException, IllegalArgumentException, ParseException {

        if (null == data || data.isEmpty())
            return;

        Map<String, Double> bmdData = new HashMap<String, Double>();
        String prefix = getResultPrefix() + "_";
        String type = getRefType();
        String source = getRefSource();

        // AP lumbar spine:
        // - identify the included vertebral levels
        // - sum the area and sum the bmc of the included vertebral levels
        // - compute the revised total bmd from summed bmc / summed area
        // - provide the proper bone range code for total bmd
        //
        if (type.equals("S")) {
            boolean[] included_array = { false, false, false, false };
            double[] area_array = { 0.0, 0.0, 0.0, 0.0 };
            double[] bmc_array = { 0.0, 0.0, 0.0, 0.0 };
            double tot_bmd = 0.0;
            for (Map.Entry<String, Data> entry : data.entrySet()) {
                String key = entry.getKey();
                int index = -1;
                if (key.startsWith("L1")) {
                    index = 0;
                } else if (key.startsWith("L2")) {
                    index = 1;
                } else if (key.startsWith("L3")) {
                    index = 2;
                } else if (key.startsWith("L4")) {
                    index = 3;
                }

                if (-1 != index) {
                    if (key.endsWith("_INCLUDED")) {
                        included_array[index] = entry.getValue().getValue();
                    } else if (key.endsWith("_AREA")) {
                        area_array[index] = entry.getValue().getValue();
                    } else if (key.endsWith("_BMC")) {
                        bmc_array[index] = entry.getValue().getValue();
                    }
                }

                if (key.endsWith("_BMD")) {
                    log.info("key pre: " + key + ", new key: " + key.replace(prefix, ""));
                    key = key.replace(prefix, "");
                    if (key.equals("TOT_BMD")) {
                        tot_bmd = entry.getValue().getValue();
                    } else {
                        if (ranges.containsKey(key)) {
                            bmdData.put(key, (Double) entry.getValue().getValue());
                            log.info("ranges contains key: " + key);
                        }
                    }
                }
            }
            double tot_area = 0.0;
            double tot_bmc = 0.0;
            for (int i = 0; i < 4; i++) {
                if (included_array[i]) {
                    tot_area += area_array[i];
                    tot_bmc += bmc_array[i];
                }
            }
            if (0. != tot_area) {
                double last_tot_bmd = tot_bmd;
                tot_bmd = tot_bmc / tot_area;
                log.info("updating ap lumbar spine total bmd from " + ((Double) last_tot_bmd).toString() + " to "
                        + ((Double) tot_bmd).toString());
            }
            String tot_key = "TOT_BMD";
            if (included_array[0] && !(included_array[1] || included_array[2] || included_array[3])) {
                //_bonerange="1..."
                tot_key = "TOT_L1_BMD";
            } else if (included_array[1] && !(included_array[0] || included_array[2] || included_array[3])) {
                // bonerange=".2.."
                tot_key = "TOT_L2_BMD";
            } else if (included_array[2] && !(included_array[0] || included_array[1] || included_array[3])) {
                // bonerange="..3."
                tot_key = "TOT_L3_BMD";
            } else if (included_array[3] && !(included_array[0] || included_array[1] || included_array[2])) {
                // bonerange="...4"
                tot_key = "TOT_L4_BMD";
            } else if (included_array[0] && included_array[1] && !(included_array[2] || included_array[3])) {
                // bonerange="12.."
                tot_key = "TOT_L1L2_BMD";
            } else if (included_array[0] && included_array[2] && !(included_array[1] || included_array[3])) {
                // bonerange="1.3."
                tot_key = "TOT_L1L3_BMD";
            } else if (included_array[0] && included_array[3] && !(included_array[1] || included_array[2])) {
                // bonerange="1..4"
                tot_key = "TOT_L1L4_BMD";
            } else if (included_array[1] && included_array[2] && !(included_array[0] || included_array[3])) {
                // bonerange=".23."
                tot_key = "TOT_L2L3_BMD";
            } else if (included_array[1] && included_array[3] && !(included_array[0] || included_array[2])) {
                // bonerange=".2.4"
                tot_key = "TOT_L2L4_BMD";
            } else if (included_array[2] && included_array[3] && !(included_array[0] || included_array[1])) {
                // bonerange="..34"
                tot_key = "TOT_L3L4_BMD";
            } else if (included_array[0] && included_array[1] && included_array[2] && !included_array[3]) {
                // bonerange="123."
                tot_key = "TOT_L1L2L3_BMD";
            } else if (included_array[0] && included_array[1] && included_array[3] && !included_array[2]) {
                // bonerange="12.4"
                tot_key = "TOT_L1L2L4_BMD";
            } else if (included_array[0] && included_array[2] && included_array[3] && !included_array[1]) {
                // bonerange="1.34"
                tot_key = "TOT_L1L3L4_BMD";
            } else if (included_array[1] && included_array[2] && included_array[3] && !included_array[0]) {
                // bonerange=".234"
                tot_key = "TOT_L2L3L4_BMD";
            } else {
                // bonerange="1234"
                tot_key = "TOT_BMD";
            }

            if (ranges.containsKey(tot_key)) {
                bmdData.put(tot_key, (Double) tot_bmd);
                log.info("ranges contains key: " + tot_key);
            }
        } else {
            for (Map.Entry<String, Data> entry : data.entrySet()) {
                String key = entry.getKey();
                if (key.endsWith("_BMD")) {
                    log.info("key pre: " + key + ", new key: " + key.replace(prefix, ""));
                    key = key.replace(prefix, "");
                    if (ranges.containsKey(key)) {
                        bmdData.put(key, (Double) entry.getValue().getValue());
                        log.info("ranges contains key: " + key);
                    }
                }
            }
        }

        log.info(prefix + " data contains: " + Integer.toString(data.size())
                + " possible entries to get bmd values from");
        log.info(prefix + " bmddata contains: " + Integer.toString(bmdData.size()) + " entries to get tz");

        DecimalFormat format = new DecimalFormat("#.0");
        ageBracket bracket = new ageBracket();

        // Determine the participant's age (at the time of the scan).
        //
        Double age = null;
        try {
            age = computeYearsDifference(getScanDate(), getParticipantDOB());
        } catch (ParseException e) {
            throw e;
        }

        log.info("computed age from scandate and dob: " + age.toString());

        for (Map.Entry<String, Double> entry : bmdData.entrySet()) {
            String bmdBoneRangeKey = entry.getKey();
            Double bmdValue = entry.getValue();

            log.info("working on range key:" + bmdBoneRangeKey + " with value: " + bmdValue.toString());

            // T- and Z-scores are interpolated from X, Y reference curve data.
            // A curve depends on the type of scan, gender, ethnicity, and
            // the coded anatomic region that bmd was measured in.
            // Determine the unique curve ID along with the age at which
            // peak bmd occurs. Implementation of T-score assumes ethnicity is always Caucasian
            // and gender is always female in accordance with WHO and
            // Osteoporosis Canada guidelines.
            //
            String method = " AND METHOD IS NULL";
            if (type.equals("S") && (bmdBoneRangeKey.contains("L1_") || bmdBoneRangeKey.contains("L4_"))) {
                method = " AND METHOD = 'APEX'";
            }

            String sql = "SELECT UNIQUE_ID, AGE_YOUNG FROM ReferenceCurve";
            sql += " WHERE REFTYPE = '" + type + "'";
            sql += " AND IF_CURRENT = 1 AND SEX = 'F' AND ETHNIC IS NULL";
            sql += method;
            sql += " AND SOURCE LIKE '%" + source + "%'";
            sql += " AND Y_LABEL = 'IDS_REF_LBL_BMD'";
            sql += " AND BONERANGE ";
            sql += (ranges.get(bmdBoneRangeKey).equals("NULL") ? ("IS NULL")
                    : ("= '" + ranges.get(bmdBoneRangeKey) + "'"));

            log.info("first query (T score): " + sql);
            Map<String, Object> mapResult;
            try {
                mapResult = refCurveDb.queryForMap(sql);
            } catch (DataAccessException e) {
                throw e;
            }
            String curveId = mapResult.get("UNIQUE_ID").toString();
            Double ageYoung = new Double(mapResult.get("AGE_YOUNG").toString());

            // Determine the bmd, skewness factor and standard deviation
            // at the peak bmd age value.
            //
            sql = "SELECT Y_VALUE, L_VALUE, STD FROM Points WHERE UNIQUE_ID = " + curveId;
            sql += " AND X_VALUE = " + ageYoung;

            log.info("second query (T score): " + sql);

            mapResult.clear();
            try {
                mapResult = refCurveDb.queryForMap(sql);
            } catch (DataAccessException e) {
                throw e;
            }

            List<Double> bmdValues = new ArrayList<Double>();
            bmdValues.add(new Double(mapResult.get("Y_VALUE").toString()));
            bmdValues.add(new Double(mapResult.get("L_VALUE").toString()));
            bmdValues.add(new Double(mapResult.get("STD").toString()));

            Double X_value = bmdValue;
            Double M_value = bmdValues.get(0);
            Double L_value = bmdValues.get(1);
            Double sigma = bmdValues.get(2);

            Double T_score = M_value * (Math.pow(X_value / M_value, L_value) - 1.) / (L_value * sigma);
            T_score = Double.valueOf(format.format(T_score));
            if (0. == Math.abs(T_score))
                T_score = 0.;

            String varName = getResultPrefix() + "_";
            if (type.equals("S") && bmdBoneRangeKey.startsWith("TOT_")) {
                varName += "TOT_T";
            } else {
                varName += bmdBoneRangeKey.replace("_BMD", "_T");
            }
            if (data.keySet().contains(varName)) {
                throw new IllegalArgumentException("Instrument variable name already defined: " + varName);
            }
            data.put(varName, DataBuilder.buildDecimal(T_score));
            log.info(varName + " = " + T_score.toString());

            Double Z_score = null;
            varName = getResultPrefix() + "_";
            if (type.equals("S") && bmdBoneRangeKey.startsWith("TOT_")) {
                varName += "TOT_Z";
            } else {
                varName += bmdBoneRangeKey.replace("_BMD", "_Z");
            }
            if (data.keySet().contains(varName)) {
                throw new IllegalArgumentException("Instrument variable name already defined: " + varName);
            }

            // APEX reference curve db has no ultra distal ulna data for males
            //
            String gender = getParticipantGender().toUpperCase();
            if (0 == gender.length() || gender.startsWith("F"))
                gender = " AND SEX = 'F'";
            else if (gender.startsWith("M")) {
                if (bmdBoneRangeKey.equals("U_UD_BMD")) {
                    data.put(varName, DataBuilder.buildDecimal((Double) null));
                    continue;
                }
                gender = " AND SEX = 'M'";
            }

            // APEX reference curve db has no forearm data for black or hispanic ethnicity
            //
            String ethnicity = getParticipantEthnicity();
            if (null == ethnicity)
                ethnicity = "";
            ethnicity.toUpperCase();
            if (0 == ethnicity.length() || ethnicity.equals("W") || ethnicity.equals("O") || ethnicity.equals("P")
                    || ethnicity.equals("I")
                    || (type.equals("R") && (ethnicity.equals("H") || ethnicity.equals("B")))) {
                ethnicity = " AND ETHNIC IS NULL";
            } else {
                ethnicity = " AND ETHNIC = '" + ethnicity + "'";
            }

            sql = "SELECT UNIQUE_ID, AGE_YOUNG FROM ReferenceCurve";
            sql += " WHERE REFTYPE = '" + getRefType() + "'";
            sql += " AND IF_CURRENT = 1";
            sql += gender;
            sql += ethnicity;
            sql += method;
            sql += " AND SOURCE LIKE '%" + getRefSource() + "%'";
            sql += " AND Y_LABEL = 'IDS_REF_LBL_BMD'";
            sql += " AND BONERANGE ";
            sql += (ranges.get(bmdBoneRangeKey).equals("NULL") ? ("IS NULL")
                    : ("= '" + ranges.get(bmdBoneRangeKey) + "'"));

            log.info("first query (Z score): " + sql);

            try {
                mapResult = refCurveDb.queryForMap(sql);
            } catch (DataAccessException e) {
                throw e;
            }
            curveId = mapResult.get("UNIQUE_ID").toString();

            // Determine the age values (X axis variable) of the curve
            //
            sql = "SELECT X_VALUE FROM Points WHERE UNIQUE_ID = " + curveId;

            log.info("second query (Z score): " + sql);

            List<Map<String, Object>> listResult;
            try {
                listResult = refCurveDb.queryForList(sql);
            } catch (DataAccessException e) {
                throw e;
            }
            List<Double> ageTable = new ArrayList<Double>();
            for (Map<String, Object> row : listResult) {
                ageTable.add(new Double(row.get("X_VALUE").toString()));
            }

            bracket.compute(age, ageTable);
            if (0. != bracket.ageSpan) {

                // Determine the bmd, skewness factor and standard deviation
                // at the bracketing and peak bmd age values.
                //
                sql = "SELECT Y_VALUE, L_VALUE, STD FROM Points WHERE UNIQUE_ID = " + curveId;
                sql += " AND X_VALUE = ";

                Double[] x_value_array = { bracket.ageMin, bracket.ageMax };
                bmdValues.clear();
                for (int i = 0; i < x_value_array.length; i++) {
                    log.info("third query (Z score) iter " + ((Integer) i).toString() + " : " + sql
                            + x_value_array[i].toString());

                    mapResult.clear();
                    try {
                        mapResult = refCurveDb.queryForMap(sql + x_value_array[i].toString());
                    } catch (DataAccessException e) {
                        throw e;
                    }

                    bmdValues.add(new Double(mapResult.get("Y_VALUE").toString()));
                    bmdValues.add(new Double(mapResult.get("L_VALUE").toString()));
                    bmdValues.add(new Double(mapResult.get("STD").toString()));
                }

                Double u = (age - bracket.ageMin) / bracket.ageSpan;
                List<Double> interpValues = new ArrayList<Double>();
                for (int i = 0; i < bmdValues.size() / 2; i++)
                    interpValues.add((1. - u) * bmdValues.get(i) + u * bmdValues.get(i + 3));

                M_value = interpValues.get(0);
                L_value = interpValues.get(1);
                sigma = interpValues.get(2);

                Z_score = M_value * (Math.pow(X_value / M_value, L_value) - 1.) / (L_value * sigma);
                Z_score = Double.valueOf(format.format(Z_score));
                if (0. == Math.abs(Z_score))
                    Z_score = 0.;
            }

            data.put(varName, DataBuilder.buildDecimal(Z_score));

            if (null != Z_score) {
                log.info(varName + " = " + Z_score.toString());
            } else {
                log.info(varName + " = null");
            }

            log.info("finished current key: " + bmdBoneRangeKey);
        }
    }

    /**
     * Called by extractScanAnalysisData(). Implementation of ResultSetExtractor. Process the query that recovers raw DEXA
     * scan P & R data file names and dicom files from Apex receiver. P and R data files are embedded in dicom files.
     * Note: the Enterprise Data Management install option must be activated in Apex with a license key for the dicom
     * export of embedded P and R data.
     */
    private final class ScanAnalysisResultSetExtractor implements ResultSetExtractor<Map<String, Data>> {
        @Override
        public Map<String, Data> extractData(ResultSet rs) throws SQLException, DataAccessException {
            Map<String, Data> data = new HashMap<String, Data>();

            log.info("starting result set processing");

            while (rs.next()) {
                scanID = rs.getString("SCANID");
                scanMode = rs.getString("SCAN_MODE");
                scanDate = rs.getString("SCAN_DATE");
                log.info("Visiting scan: {}, mode: {}, date: {}", scanID, scanMode, scanDate);
            }

            if (null != scanID && null != scanMode) {

                List<StoredDicomFile> listSelected = new ArrayList<StoredDicomFile>();
                List<StoredDicomFile> listDicomFiles = server.listSortedDicomFiles();

                // there must be at least one dicom file with a body part examined key
                // body part name depends on the data extractor class
                // LSPINE = lateral iva spine, expects 3 files
                // SPINE = AP lumbar spine, expects 1 file
                // null = whole body, expects 2 files
                // HIP = hip, expects 1 to 2 files
                // ARM = forearm, expects 1 to 2 files
                // the study instance UID is used to further group files together
                //
                String bodyPartName = getBodyPartName();
                log.info("body part name: " + bodyPartName);

                boolean first = true;
                int fileCount = 0;
                String dcmStudyInstanceUID = "";
                for (StoredDicomFile sdf : listDicomFiles) {
                    try {
                        DicomObject dicomObject = sdf.getDicomObject();
                        // only retain images from the same study
                        if (first) {
                            dcmStudyInstanceUID = dicomObject.getString(Tag.StudyInstanceUID);
                            first = false;
                        }
                        if (!dcmStudyInstanceUID.equals(dicomObject.getString(Tag.StudyInstanceUID))) {
                            continue;
                        }
                        // only retain images with the correct set of dicom tags for the current body part exam
                        for (ApexDicomData dicomData : apexDicomList) {
                            if (dicomData.validate(sdf)) {
                                dicomData.file = sdf;
                                fileCount++;
                            }
                        }
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }

                if (fileCount >= apexDicomList.size()) {
                    switch (bodyPartName) {
                    case "WBODY":
                        log.info("processing whole body dicom");
                        break;
                    case "ARM":
                        log.info("processing forearm dicom side: " + getSide().toString());
                        break;
                    case "LSPINE":
                        log.info("processing lateral iva spine dicom");
                        break;
                    case "SPINE":
                        log.info("processing ap lumbar spine dicom");
                        break;
                    case "HIP":
                        log.info("processing hip dicom side: " + getSide().toString());
                        break;
                    }
                    data.put(getResultPrefix() + "_SCANID", DataBuilder.buildText(scanID));
                    data.put(getResultPrefix() + "_SCAN_MODE", DataBuilder.buildText(scanMode));
                    processFilesExtraction(data);
                }
                log.info("stored dicom files: {}, selected for processing {} ", listDicomFiles.size(), fileCount);
            }

            log.info("finished processing files");
            return data;
        }
    }

    /**
     * Called by ScanAnalysisResultSetExtractor extractData(). Adds dicom files to data collection.
     *
     * @param data
     */
    private void processFilesExtraction(Map<String, Data> data) {
        for (ApexDicomData dicomData : apexDicomList) {
            StoredDicomFile sdf = dicomData.file;
            // if the file failed validation, it is not the correct body part being requested
            if (null == sdf)
                continue;
            boolean completeDicom = isCompleteDicom(sdf);
            apexReceiver.updatePandRDicomFileState(completeDicom);
            boolean correctDicom = isCorrectDicom(sdf);
            apexReceiver.updateParticipantDicomFileState(correctDicom);
            if (completeDicom && correctDicom) {
                try {
                    log.info("putting dicom file with patient ID: {}",
                            sdf.getDicomObject().getString(Tag.PatientID));
                    putDicom(data, dicomData.name, sdf);
                } catch (IOException e) {
                }
            } else {
                // flag this file as being of no use
                server.cacheDirtyFile(sdf);
                dicomData.file = null;
            }
        }
    }

    /**
     * Called by processFilesExtraction method. Add a dicom file exported from Apex via DICOM send transfer to the data
     * collection.
     *
     * @param data
     * @param name
     * @param storedDicomFile
     */
    public void putDicom(Map<String, Data> data, String name, StoredDicomFile storedDicomFile) {
        Data binary = DataBuilder.buildBinary(storedDicomFile.getFile());
        data.put(name, binary);
    }

    /**
     * Called by putDicom(). Return true if dicom contains raw P & R data, false otherwise.
     *
     * @return
     */
    private boolean isCompleteDicom(StoredDicomFile storedDicomFile) {
        for (ApexTag tag : ApexTag.PandRTagSet) {
            try {
                DicomObject dicomObject = storedDicomFile.getDicomObject();
                if (dicomObject.contains(tag.getValue())) {
                    if (false == dicomObject.containsValue(tag.getValue())) {
                        log.info("Missing P and/or R data in DICOM file: " + tag.name());
                        return false;
                    }
                }
            } catch (IOException e) {
            }
        }
        return true;
    }

    /**
     * Called by putDicom(). Return true if dicom contains correct participant identifier, false otherwise.
     *
     * @return
     */
    private boolean isCorrectDicom(StoredDicomFile storedDicomFile) {
        String participantID = getParticipantID();
        try {
            DicomObject dicomObject = storedDicomFile.getDicomObject();
            String patientID = dicomObject.getString(Tag.PatientID);
            if (!participantID.equals(patientID)) {
                log.info("Expecting file for participant with ID {} but received one with ID {}", participantID,
                        patientID);
                return false;
            }
        } catch (IOException e) {
        }
        return true;
    }

    /**
     * Called by extractDataImpl(). Implementation is specific to child classes which define Apex PatScan db table names
     * corresponding to the type of scan. Adds all analysis variables to data collection.
     *
     * @param table
     * @param data
     * @param rsExtractor
     * @return
     */
    protected Map<String, Data> extractScanData(String table, Map<String, Data> data,
            ResultSetExtractor<Map<String, Data>> rsExtractor) {
        return getPatScanDb().query("SELECT * FROM " + table + " WHERE PATIENT_KEY = ? AND SCANID = ?",
                new PreparedStatementSetter() {
                    public void setValues(PreparedStatement ps) throws SQLException {
                        ps.setString(1, getParticipantKey());
                        ps.setString(2, getScanID());
                    }
                }, rsExtractor);
    }

    /**
     * Used during extractDataImpl(). Implementation of ResultSetExtractor. Processes the query that recovers all scan
     * analysis variables from Apex PatScan db.
     */
    protected abstract class ResultSetDataExtractor implements ResultSetExtractor<Map<String, Data>> {

        protected Map<String, Data> data;

        protected ResultSet rs;

        public ResultSetDataExtractor(Map<String, Data> data) {
            super();
            this.data = data;
        }

        @Override
        public Map<String, Data> extractData(ResultSet rs) throws SQLException, DataAccessException {
            this.rs = rs;
            if (rs.next()) {
                putData();
            }
            return data;
        }

        protected void putBoolean(String name) throws SQLException {
            put(name, DataBuilder.buildBoolean(rs.getBoolean(name)));
        }

        protected void putString(String name) throws SQLException {
            put(name, DataBuilder.buildText(rs.getString(name)));
        }

        protected void putNString(String name) throws SQLException {
            put(name, DataBuilder.buildText(rs.getNString(name)));
        }

        protected void putInt(String name) throws SQLException {
            put(name, DataBuilder.buildInteger(rs.getInt(name)));
        }

        protected void putLong(String name) throws SQLException {
            put(name, DataBuilder.buildInteger(rs.getLong(name)));
        }

        protected void putDouble(String name) throws SQLException {
            put(name, DataBuilder.buildDecimal(rs.getDouble(name)));
        }

        protected void put(String name, Data value) {
            String varName = getVariableName(name);
            if (data.keySet().contains(varName)) {
                throw new IllegalArgumentException("Instrument variable name already defined: " + varName);
            }
            data.put(varName, value);
        }

        protected String getVariableName(String name) {
            return getResultPrefix() + "_" + name;
        }

        protected abstract void putData() throws SQLException, DataAccessException;
    }

    /**
     * Called by computeTZScore().
     *
     * @param s1
     * @param s2
     * @return
     * @throws ParseException
     */
    public static Double computeYearsDifference(String s1, String s2) throws ParseException {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        Date d1;
        try {
            d1 = format.parse(s1);
        } catch (ParseException e) {
            throw e;
        }
        Date d2;
        try {
            d2 = format.parse(s2);
        } catch (ParseException e) {
            throw e;
        }

        Calendar c1 = Calendar.getInstance();
        c1.setTime(d1);
        Calendar c2 = Calendar.getInstance();
        c2.setTime(d2);

        Double diff = (c1.getTimeInMillis() - c2.getTimeInMillis()) / (1000. * 60. * 60. * 24. * 365.25);
        if (diff < 0.)
            diff *= -1.;

        return diff;
    }

    /**
     *  Helper class for storing a validator and a unique variable name
     *  for a given dicom image
     */
    protected class ApexDicomData {
        public Map<Integer, TagEntry> validator = new HashMap<Integer, TagEntry>();

        public String name;

        public StoredDicomFile file;

        public ApexDicomData() {
            this.name = null;
            this.file = null;
        }

        boolean validate(StoredDicomFile sdf) {
            try {
                DicomObject dicomObject = sdf.getDicomObject();
                int failCount = 0;
                for (Map.Entry<Integer, TagEntry> entry : validator.entrySet()) {
                    Integer tag = entry.getKey();
                    TagEntry te = entry.getValue();

                    boolean hasTag = dicomObject.contains(tag);
                    boolean hasValue = dicomObject.containsValue(tag);
                    String dicomValue = hasValue ? dicomObject.getString(tag).trim() : null;
                    String tagName = dicomObject.nameOf(tag);

                    if ((te.expected && !hasTag) || (!te.expected && hasTag)) {
                        // fail condition expected or unexpected tag
                        failCount++;
                        continue;
                    }
                    if (!te.expected && !hasTag) {
                        // pass condition tag is not expected and tag is not present
                        continue;
                    }
                    if (te.matching) {
                        if (null == te.value) {
                            if (hasValue) {
                                // fail condition expected matching null tag value
                                failCount++;
                                continue;
                            }
                        } else {
                            if (!te.value.equals(dicomValue)) {
                                // fail condition expected matching tag value
                                failCount++;
                                continue;
                            }
                        }
                    } else {
                        if (!hasValue) {
                            // fail condition expected non-matching non-empty tag value
                            failCount++;
                            continue;
                        }
                    }
                } // end for loop

                if (0 == failCount) {
                    // found a conditionally valid file
                    return true;
                }
            } catch (IOException e) {
            }

            return false;
        }

    }

    /**
     * Helper class for identifying candidate dicom files based on their tags
     */
    protected class TagEntry {
        public boolean expected;

        public boolean matching;

        public String value;

        public TagEntry(boolean expected, boolean matching, String value) {
            this.expected = expected;
            this.matching = matching;
            this.value = value;
        }

        public TagEntry() {
            this.expected = false;
            this.matching = false;
            this.value = null;
        }
    }

    /**
     * Helper class for computeTZScore().
     */
    protected final class ageBracket {
        public Double ageMin;

        public Double ageMax;

        public Double ageSpan;

        public ageBracket() {
            ageMin = Double.MIN_VALUE;
            ageMax = Double.MAX_VALUE;
            ageSpan = 0.;
        }

        public void compute(Double age, List<Double> ageTable) {
            ageMin = Double.MIN_VALUE;
            ageMax = Double.MAX_VALUE;
            for (int i = 0; i < ageTable.size() - 1; i++) {
                double min = ageTable.get(i);
                double max = ageTable.get(i + 1);
                if (age >= min && age <= max) {
                    ageMin = min;
                    ageMax = age == min ? min : max;
                } else if (age > max) {
                    ageMin = max;
                    ageMax = max;
                }
            }
            if (Double.MIN_VALUE == ageMin)
                ageMin = age;
            if (Double.MAX_VALUE == ageMax)
                ageMax = age;
            ageSpan = ageMax - ageMin;
        }
    }

    protected JdbcTemplate getPatScanDb() {
        return patScanDb;
    }

    protected String getParticipantKey() {
        return participantData.get("participantKey");
    }

    protected String getParticipantDOB() {
        return participantData.get("participantDOB");
    }

    protected String getParticipantGender() {
        return participantData.get("participantGender");
    }

    protected String getParticipantEthnicity() {
        return participantData.get("participantEthnicity");
    }

    protected String getParticipantID() {
        return participantData.get("participantID");
    }

    protected String getResultPrefix() {
        return getName();
    }

    protected String getScanID() {
        return scanID;
    }

    protected String getScanDate() {
        return scanDate;
    }

}