org.phenotips.remote.common.internal.api.DefaultPatientToJSONConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.phenotips.remote.common.internal.api.DefaultPatientToJSONConverter.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.phenotips.remote.common.internal.api;

import org.phenotips.remote.api.tojson.PatientToJSONConverter;
import org.phenotips.data.Disorder;
import org.phenotips.data.Feature;
import org.phenotips.data.FeatureMetadatum;
import org.phenotips.data.Patient;
import org.phenotips.data.PatientData;
import org.phenotips.ontology.internal.solr.SolrOntologyTerm;
import org.phenotips.data.similarity.internal.PatientGenotype;
import org.phenotips.remote.api.ApiConfiguration;
import org.slf4j.Logger;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

public class DefaultPatientToJSONConverter implements PatientToJSONConverter {
    private final static String PATIENTMATCHING_JSON_FEATUREMATCHES = "featureMatches";
    private final static String PATIENTMATCHING_JSON_CATEGORY = "category";
    private final static String PATIENTMATCHING_JSON_CATEGORY_ID = "id";
    private final static String PATIENTMATCHING_JSON_MATCH = "match";

    //private final static String ERROR_MESSAGE_UNSUPPORTED_JSON_FORMAT = "Unsupported JSON representation of matched features";

    private Logger logger;

    private final String apiVersion;

    public DefaultPatientToJSONConverter(String apiVersion, Logger logger) {
        this.apiVersion = apiVersion;
        this.logger = logger;
    }

    public JSONObject convert(Patient patient, boolean removePrivateData) {
        return this.convert(patient, removePrivateData, 0);
    }

    public JSONObject convert(Patient patient, boolean removePrivateData, int includedTopGenes) {
        JSONObject json = new JSONObject();

        try {
            json.put(ApiConfiguration.JSON_GENDER, DefaultPatientToJSONConverter.gender(patient));
            json.putAll(DefaultPatientToJSONConverter.globalQualifiers(patient));
        } catch (Exception ex) {
            // Do nothing. These are optional.
        }
        JSONArray disorders = DefaultPatientToJSONConverter.disorders(patient);
        if (!disorders.isEmpty()) {
            json.put(ApiConfiguration.JSON_DISORDERS, disorders);
        }
        if (removePrivateData) {
            json.put(ApiConfiguration.JSON_FEATURES, DefaultPatientToJSONConverter.nonPersonalFeatures(patient));
        } else {
            json.put(ApiConfiguration.JSON_FEATURES, DefaultPatientToJSONConverter.features(patient));
        }
        JSONArray genes = DefaultPatientToJSONConverter.genes(patient, includedTopGenes, logger);
        if (!genes.isEmpty()) {
            json.put(ApiConfiguration.JSON_GENES, genes);
        }
        return json;
    }

    private static JSONArray features(Patient patient) {
        JSONArray features = new JSONArray();
        for (Feature patientFeature : patient.getFeatures()) {
            Map<String, ? extends FeatureMetadatum> metadata = patientFeature.getMetadata();
            FeatureMetadatum ageOfOnset = metadata.get(ApiConfiguration.FEATURE_AGE_OF_ONSET);

            JSONObject featureJson = new JSONObject();
            featureJson.put(ApiConfiguration.REPLY_JSON_FEATURE_ID, patientFeature.getId());
            featureJson.put(ApiConfiguration.REPLY_JSON_FEATURE_OBSERVED,
                    observedStatusToJSONString(patientFeature));

            if (ageOfOnset != null) {
                featureJson.put(ApiConfiguration.REPLY_JSON_FEATURE_AGE_OF_ONSET, ageOfOnset.getId());
            }
            features.add(featureJson);
        }
        return features;
    }

    private static String observedStatusToJSONString(Feature feature) {
        if (feature.isPresent()) {
            return ApiConfiguration.REPLY_JSON_FEATURE_OBSERVED_YES;
        }
        return ApiConfiguration.REPLY_JSON_FEATURE_OBSERVED_NO;
    }

    private static JSONArray nonPersonalFeatures(Patient patient) {
        /* Example of a expected reply which should be parsed for features:
         *
         * QUERY: "features": [ {"id": "HP:0000316", "observed": "yes"},
         *                      {"id": "HP:0004325", "observed": "yes"},
         *                      {"id": "HP:0001999", "observed": "yes"} ]
         *
         * REPLY:
         *  1) "matchabe" patient with the same set of symptoms {"HP:0000316", "HP:0004325", "HP:0001999"}
         *     and two other unmatched feature:
         *
         *   "features": [{"score":0.46,"category":{"id":"HP:0100886",...},"reference":["HP:0000316"],"match":[""]},
         *                {"score":0.44,"category":{"id":"HP:0004323",...},"reference":["HP:0004325"],"match":[""]},
         *                {"score":0.40,"category":{"id":"HP:0000271",...},"reference":["HP:0001999"],"match":[""]},
         *                {"score":0,"category":{"id":"","name":"Unmatched"},"match":["",""]}]
         *
         *  2) "public" patient with {"HP:0004325", "HP:0001999", "HP:0000479"} and two other unmatched features:
         *
         *   "features": [{"score":0.44,"category":{"id":"HP:0004325",...},"reference":["HP:0004325"],"match":["HP:0004325"]},
         *                {"score":0.40,"category":{"id":"HP:0001999",...},"reference":["HP:0001999"],"match":["HP:0001999"]},
         *                {"score":0.22,"category":{"id":"HP:0000478",...},"reference":["HP:0000316"],"match":["HP:0000479"]},
         *                {"score":0,"category":{"id":"","name":"Unmatched"},"match":["HP:0011276","HP:0000505"]}]
         *
         *  For now feature info returned by the patient-network component will be used, in order
         *  not to reinvent the "privacy" wheel. All non-matched features will be returned as
         *  HP:0000118 ("Phenotypic abnormality"), and (given how patient-netowrk component works)
         *  all non-observed features will be ignored.
         */

        Map<String, Integer> featureCounts = new HashMap<String, Integer>();
        Set<String> obfuscatedFeatures = new HashSet<String>();
        Set<String> notMatchedFeatures = new HashSet<String>();

        JSONArray similarityFeaturesJson = patient.toJSON().getJSONArray(PATIENTMATCHING_JSON_FEATUREMATCHES);

        for (Object featureMatchUC : similarityFeaturesJson) {
            JSONObject featureMatch = (JSONObject) featureMatchUC;

            JSONObject featureCategory = featureMatch.optJSONObject(PATIENTMATCHING_JSON_CATEGORY);
            if (featureCategory == null) {
                //FIXME: throw new Exception(ERROR_MESSAGE_UNSUPPORTED_JSON_FORMAT);
                continue;
            }

            String catId = featureCategory.optString(PATIENTMATCHING_JSON_CATEGORY_ID, "");

            // an unmatched feature
            JSONArray featureMatches = featureMatch.optJSONArray(PATIENTMATCHING_JSON_MATCH);
            if (featureMatches == null) {
                // FIXME: need to throw to indicate unsuported format:throw new Exception(ERROR_MESSAGE_UNSUPPORTED_JSON_FORMAT);
                continue;
            }
            for (int i = 0; i < featureMatches.size(); i++) {
                String matchFeature = featureMatches.getString(i);

                // if feature id is obfuscated use category Id instead as the best available substitute
                String featureId = matchFeature.isEmpty() ? catId : matchFeature;

                // replace empty features by the most generic generic term,
                // and (possibly) re-format feature ID to the expected output format
                featureId = processFeatureID(featureId);

                if (catId.isEmpty()) {
                    notMatchedFeatures.add(featureId);
                }
                if (matchFeature.isEmpty()) {
                    obfuscatedFeatures.add(featureId);
                }

                Integer count = featureCounts.containsKey(featureId) ? featureCounts.get(featureId) : 0;
                featureCounts.put(featureId, count + 1);
            }
        }

        // convert featuresWithCounts to features
        // note: for now only observed features are supported, so "observed" is hardcoded to "yes" for now
        JSONArray features = new JSONArray();

        for (String featureId : featureCounts.keySet()) {

            JSONObject featureJson = new JSONObject();
            featureJson.put(ApiConfiguration.REPLY_JSON_FEATURE_ID, featureId);
            featureJson.put(ApiConfiguration.REPLY_JSON_FEATURE_OBSERVED,
                    ApiConfiguration.REPLY_JSON_FEATURE_OBSERVED_YES);
            featureJson.put(ApiConfiguration.REPLY_JSON_FEATURE_MATCHED, !notMatchedFeatures.contains(featureId));
            featureJson.put(ApiConfiguration.REPLY_JSON_FEATURE_OBFUSCATED, obfuscatedFeatures.contains(featureId));
            int count = featureCounts.get(featureId);
            if (count > 1) {
                featureJson.put(ApiConfiguration.REPLY_JSON_FEATURE_COUNT, count);
            }
            features.add(featureJson);
        }

        return features;
    }

    private static String processFeatureID(String featureIdFromMatching) {
        if (featureIdFromMatching.isEmpty()) {
            return ApiConfiguration.REPLY_JSON_FEATURE_HPO_MOST_GENERIC_TERM;
        }
        return featureIdFromMatching;
    }

    private static JSONArray disorders(Patient patient) {
        JSONArray disorders = new JSONArray();
        for (Disorder disease : patient.getDisorders()) {
            disorders.add(disease.getId());
        }
        return disorders;
    }

    private static JSONArray genes(Patient patient, int includedTopGenes, Logger logger) {
        PatientGenotype genotype = new PatientGenotype(patient);

        JSONArray genes = new JSONArray();
        try {
            Collection<String> candidateGeneNames;
            //Collection<String> candidateGeneNames = getPatientCandidateGeneNames(patient);
            if (includedTopGenes <= 0) {
                candidateGeneNames = genotype.getCandidateGenes();
            } else {
                final Map<String, Double> genesWithScore = new HashMap<String, Double>();
                Collection<String> allGenes = genotype.getGenes();
                for (String gene : allGenes) {
                    genesWithScore.put(gene, genotype.getGeneScore(gene));
                }
                Set<String> set = genesWithScore.keySet();
                List<String> keys = new ArrayList<String>(set);
                Collections.sort(keys, new Comparator<String>() {
                    public int compare(String s1, String s2) {
                        // Sort by score, descending
                        return Double.compare(genesWithScore.get(s2), genesWithScore.get(s1));
                    }
                });
                List<String> topGenes = keys.subList(0, Math.min(keys.size(), includedTopGenes));
                candidateGeneNames = new HashSet<String>();
                for (String topGene : topGenes) {
                    candidateGeneNames.add(topGene);
                }
            }

            for (String geneName : candidateGeneNames) {
                JSONObject nextGene = new JSONObject();
                nextGene.put(ApiConfiguration.JSON_GENES_GENENAME, geneName);
                nextGene.put(ApiConfiguration.JSON_GENES_ASSEMBLY, "GRCh37"); // TODO: pull from candidate genes/patient/vcf?
                genes.add(nextGene);
            }
        } catch (Exception ex) {
            logger.error("Error getting candidate genes for patient [{}]: [{}]", patient.getId(), ex);
            return new JSONArray();
        }
        return genes;
    }

    private static String gender(Patient patient) {
        return patient.<ImmutablePair<String, String>>getData("sex").get(0).getRight();
    }

    private static Map<String, String> globalQualifiers(Patient patient) {
        Map<String, String> globalQualifiers = new HashMap<String, String>();
        Map<String, String> remappedGlobalQualifierStrings = new HashMap<String, String>();
        remappedGlobalQualifierStrings.put("global_age_of_onset", "age_of_onset");
        remappedGlobalQualifierStrings.put("global_mode_of_inheritance", "mode_of_inheritance");

        // These are the actual qualifiers, that are remapped to have the keys compliant with the remote JSON standard.
        PatientData<ImmutablePair<String, SolrOntologyTerm>> existingQualifiers = patient
                .<ImmutablePair<String, SolrOntologyTerm>>getData("global-qualifiers");
        if (globalQualifiers != null) {
            for (ImmutablePair<String, SolrOntologyTerm> qualifierPair : existingQualifiers) {
                for (String key : remappedGlobalQualifierStrings.keySet()) {
                    // Could do contains, but is it safe?
                    if (StringUtils.equalsIgnoreCase(qualifierPair.getLeft(), key)) {
                        globalQualifiers.put(remappedGlobalQualifierStrings.get(key),
                                qualifierPair.getRight().getId());
                        break;
                    }
                }
            }
        }
        return globalQualifiers;
    }
}