org.phenotips.data.internal.PhenoTipsPatient.java Source code

Java tutorial

Introduction

Here is the source code for org.phenotips.data.internal.PhenoTipsPatient.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/
 */
package org.phenotips.data.internal;

import org.phenotips.Constants;
import org.phenotips.components.ComponentManagerRegistry;
import org.phenotips.data.Disorder;
import org.phenotips.data.Feature;
import org.phenotips.data.Patient;
import org.phenotips.data.PatientData;
import org.phenotips.data.PatientDataController;

import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.component.manager.ComponentLookupException;
import org.xwiki.context.Execution;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReference;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.BaseProperty;
import com.xpn.xwiki.objects.ListProperty;

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

/**
 * Implementation of patient data based on the XWiki data model, where patient data is represented by properties in
 * objects of type {@code PhenoTips.PatientClass}.
 *
 * @version $Id: c867825f17be5feb2e5b9e93a772977088e9af1a $
 * @since 1.0M8
 */
public class PhenoTipsPatient implements Patient {
    /** The default template for creating a new patient. */
    public static final EntityReference TEMPLATE_REFERENCE = new EntityReference("PatientTemplate",
            EntityType.DOCUMENT, Constants.CODE_SPACE_REFERENCE);

    /** used for generating JSON and reading from JSON. */
    protected static final String JSON_KEY_FEATURES = "features";

    protected static final String JSON_KEY_NON_STANDARD_FEATURES = "nonstandard_features";

    protected static final String JSON_KEY_DISORDERS = "disorders";

    protected static final String JSON_KEY_ID = "id";

    protected static final String JSON_KEY_REPORTER = "reporter";

    /** Known phenotype properties. */
    private static final String PHENOTYPE_POSITIVE_PROPERTY = "phenotype";

    private static final String PHENOTYPE_NEGATIVE_PROPERTY = "negative_phenotype";

    private static final String[] PHENOTYPE_PROPERTIES = new String[] { PHENOTYPE_POSITIVE_PROPERTY,
            PHENOTYPE_NEGATIVE_PROPERTY };

    private static final String DISORDER_PROPERTIES_OMIMID = "omim_id";

    private static final String[] DISORDER_PROPERTIES = new String[] { DISORDER_PROPERTIES_OMIMID };

    /** Logging helper object. */
    private Logger logger = LoggerFactory.getLogger(PhenoTipsPatient.class);

    /** @see #getDocument() */
    private DocumentReference document;

    /** @see #getReporter() */
    private DocumentReference reporter;

    /** @see #getFeatures() */
    private Set<Feature> features = new TreeSet<Feature>();

    /** @see #getDisorders() */
    private Set<Disorder> disorders = new TreeSet<Disorder>();

    /** The list of all the initialized data holders (PatientDataSerializer). */
    private List<PatientDataController<?>> serializers;

    /** Extra data that can be plugged into the patient record. */
    private Map<String, PatientData<?>> extraData = new HashMap<String, PatientData<?>>();

    /**
     * Constructor that copies the data from an XDocument.
     *
     * @param doc the XDocument representing this patient in XWiki
     */
    public PhenoTipsPatient(XWikiDocument doc) {
        this.document = doc.getDocumentReference();
        this.reporter = doc.getCreatorReference();

        BaseObject data = doc.getXObject(CLASS_REFERENCE);
        if (data == null) {
            return;
        }

        try {
            loadFeatures(doc, data);
            loadDisorders(doc, data);
        } catch (XWikiException ex) {
            this.logger.warn("Failed to access patient data for [{}]: {}", doc.getDocumentReference(),
                    ex.getMessage());
        }

        // Read-only from now on
        this.features = Collections.unmodifiableSet(this.features);
        this.disorders = Collections.unmodifiableSet(this.disorders);

        loadSerializers();
        readPatientData();
    }

    private void loadFeatures(XWikiDocument doc, BaseObject data) {
        @SuppressWarnings("unchecked")
        Collection<BaseProperty<EntityReference>> fields = data.getFieldList();
        for (BaseProperty<EntityReference> field : fields) {
            if (field == null || !field.getName().matches("(?!extended_)(.*_)?phenotype")
                    || !ListProperty.class.isInstance(field)) {
                continue;
            }
            ListProperty values = (ListProperty) field;
            for (String value : values.getList()) {
                if (StringUtils.isNotBlank(value)) {
                    this.features.add(new PhenoTipsFeature(doc, values, value));
                }
            }
        }
    }

    private void loadDisorders(XWikiDocument doc, BaseObject data) throws XWikiException {
        for (String property : DISORDER_PROPERTIES) {
            ListProperty values = (ListProperty) data.get(property);
            if (values != null) {
                for (String value : values.getList()) {
                    if (StringUtils.isNotBlank(value)) {
                        this.disorders.add(new PhenoTipsDisorder(values, value));
                    }
                }
            }
        }
    }

    private void loadSerializers() {
        try {
            this.serializers = ComponentManagerRegistry.getContextComponentManager()
                    .getInstanceList(PatientDataController.class);
        } catch (ComponentLookupException e) {
            this.logger.error("Failed to find component", e);
        }
    }

    /**
     * Loops through all the available serializers and passes each a document reference.
     */
    private void readPatientData() {
        for (PatientDataController<?> serializer : this.serializers) {
            PatientData<?> data = serializer.load(this);
            if (data != null) {
                this.extraData.put(data.getName(), data);
            }
        }
    }

    private boolean isFieldIncluded(Collection<String> includedFieldNames, String fieldName) {
        return (includedFieldNames == null || includedFieldNames.contains(fieldName));
    }

    private boolean isFieldIncluded(Collection<String> includedFieldNames, String[] fieldNames) {
        if (includedFieldNames == null) {
            return true;
        }
        for (String fieldName : fieldNames) {
            if (isFieldIncluded(includedFieldNames, fieldName)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public String getId() {
        return this.document.getName();
    }

    @Override
    public String getExternalId() {
        try {
            return this.<String>getData("identifiers").get("external_id");
        } catch (Exception ex) {
            return null;
        }
    }

    @Override
    public DocumentReference getDocument() {
        return this.document;
    }

    @Override
    public DocumentReference getReporter() {
        return this.reporter;
    }

    @Override
    public Set<Feature> getFeatures() {
        return this.features;
    }

    @Override
    public Set<Disorder> getDisorders() {
        return this.disorders;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> PatientData<T> getData(String name) {
        return (PatientData<T>) this.extraData.get(name);
    }

    @Override
    public JSONObject toJSON() {
        return toJSON(null);
    }

    /** creates & returns a new JSON array of all patient features (as JSON objects). */
    private JSONArray featuresToJSON() {
        JSONArray featuresJSON = new JSONArray();
        for (Feature phenotype : this.features) {
            if (StringUtils.isBlank(phenotype.getId())) {
                continue;
            }
            featuresJSON.add(phenotype.toJSON());
        }
        return featuresJSON;
    }

    private JSONArray nonStandardFeaturesToJSON() {
        JSONArray featuresJSON = new JSONArray();
        for (Feature phenotype : this.features) {
            if (StringUtils.isNotBlank(phenotype.getId())) {
                continue;
            }
            featuresJSON.add(phenotype.toJSON());
        }
        return featuresJSON;
    }

    /** creates & returns a new JSON array of all patient diseases (as JSON objects). */
    private JSONArray diseasesToJSON() {
        JSONArray diseasesJSON = new JSONArray();
        for (Disorder disease : this.disorders) {
            diseasesJSON.add(disease.toJSON());
        }
        return diseasesJSON;
    }

    @Override
    public JSONObject toJSON(Collection<String> onlyFieldNames) {
        JSONObject result = new JSONObject();

        if (isFieldIncluded(onlyFieldNames, JSON_KEY_ID)) {
            result.element(JSON_KEY_ID, getDocument().getName());
        }

        if (getReporter() != null && isFieldIncluded(onlyFieldNames, JSON_KEY_REPORTER)) {
            result.element(JSON_KEY_REPORTER, getReporter().getName());
        }

        if (!this.features.isEmpty() && isFieldIncluded(onlyFieldNames, PHENOTYPE_PROPERTIES)) {
            result.element(JSON_KEY_FEATURES, featuresToJSON());
            result.element(JSON_KEY_NON_STANDARD_FEATURES, nonStandardFeaturesToJSON());
        }

        if (!this.disorders.isEmpty() && isFieldIncluded(onlyFieldNames, DISORDER_PROPERTIES)) {
            result.element(JSON_KEY_DISORDERS, diseasesToJSON());
        }

        for (PatientDataController<?> serializer : this.serializers) {
            serializer.writeJSON(this, result, onlyFieldNames);
        }

        return result;
    }

    private void updateFeaturesFromJSON(XWikiDocument doc, BaseObject data, XWikiContext context, JSONObject json) {
        try {
            JSONArray inputFeatures = json.optJSONArray(JSON_KEY_FEATURES);
            if (inputFeatures != null) {
                // keep this instance of PhenotipsPatient in sync with the document: reset features
                this.features = new TreeSet<Feature>();

                // new feature lists (for setting values in the Wiki document)
                List<String> positiveValues = new LinkedList<String>();
                List<String> negativeValues = new LinkedList<String>();

                for (int i = 0; i < inputFeatures.size(); i++) {
                    JSONObject featureInJSON = inputFeatures.optJSONObject(i);
                    if (featureInJSON == null) {
                        continue;
                    }

                    Feature phenotipsFeature = new PhenoTipsFeature(featureInJSON);
                    this.features.add(phenotipsFeature);

                    if (phenotipsFeature.isPresent()) {
                        positiveValues.add(phenotipsFeature.getValue());
                    } else {
                        negativeValues.add(phenotipsFeature.getValue());
                    }
                }

                // as in constructor: make unmodifiable
                this.features = Collections.unmodifiableSet(this.features);

                // update the values in the document (overwriting the old list, if any)
                data.set(PHENOTYPE_POSITIVE_PROPERTY, positiveValues, context);
                data.set(PHENOTYPE_NEGATIVE_PROPERTY, negativeValues, context);
                context.getWiki().saveDocument(doc, "Updated features from JSON", true, context);
            }
        } catch (Exception ex) {
            this.logger.warn("Failed to update patient features from JSON [{}]: {}", ex.getMessage(), ex);
        }
    }

    private void updateDisordersFromJSON(XWikiDocument doc, BaseObject data, XWikiContext context,
            JSONObject json) {
        try {
            JSONArray inputDisorders = json.optJSONArray(JSON_KEY_DISORDERS);
            if (inputDisorders != null) {
                // keep this instance of PhenotipsPatient in sync with the document: reset disorders
                this.disorders = new TreeSet<Disorder>();

                // new disorders list (for setting values in the Wiki document)
                List<String> disorderValues = new LinkedList<String>();

                for (int i = 0; i < inputDisorders.size(); i++) {
                    JSONObject disorderJSON = inputDisorders.optJSONObject(i);
                    if (disorderJSON == null) {
                        continue;
                    }

                    Disorder phenotipsDisorder = new PhenoTipsDisorder(disorderJSON);
                    this.disorders.add(phenotipsDisorder);

                    disorderValues.add(phenotipsDisorder.getValue());
                }

                // as in constructor: make unmofidiable
                this.disorders = Collections.unmodifiableSet(this.disorders);

                // update the values in the document (overwriting the old list, if any)
                data.set(DISORDER_PROPERTIES_OMIMID, disorderValues, context);
                context.getWiki().saveDocument(doc, "Updated disorders from JSON", true, context);
            }
        } catch (Exception ex) {
            this.logger.warn("Failed to update patient disorders from JSON [{}]: {}", ex.getMessage(), ex);
        }
    }

    @Override
    public void updateFromJSON(JSONObject json) {
        try {
            // TODO: Check versions and throw if versions mismatch if necessary
            // TODO: Separate updateFromJSON and saveToDB? Move to PatientRepository?

            Execution execution = ComponentManagerRegistry.getContextComponentManager()
                    .getInstance(Execution.class);
            XWikiContext context = (XWikiContext) execution.getContext().getProperty("xwikicontext");

            DocumentAccessBridge documentAccessBridge = ComponentManagerRegistry.getContextComponentManager()
                    .getInstance(DocumentAccessBridge.class);
            XWikiDocument doc = (XWikiDocument) documentAccessBridge.getDocument(getDocument());

            BaseObject data = doc.getXObject(CLASS_REFERENCE);
            if (data == null) {
                return;
            }

            updateFeaturesFromJSON(doc, data, context, json);

            updateDisordersFromJSON(doc, data, context, json);

            for (PatientDataController<?> serializer : this.serializers) {
                try {
                    PatientData<?> patientData = serializer.readJSON(json);
                    if (patientData != null) {
                        this.extraData.put(patientData.getName(), patientData);
                        serializer.save(this);
                        this.logger.info("Successfully updated patient form JSON using serializer [{}]",
                                serializer.getName());
                    }
                } catch (UnsupportedOperationException ex) {
                    this.logger.info("Unable to update patient from JSON using serializer [{}]: not supported",
                            serializer.getName());
                } catch (Exception ex) {
                    this.logger.warn("Failed to update patient data from JSON using serializer [{}]: {}",
                            serializer.getName(), ex.getMessage(), ex);
                }
            }
        } catch (Exception ex) {
            this.logger.warn("Failed to update patient data from JSON [{}]: {}", ex.getMessage(), ex);
        }
    }

    @Override
    public String toString() {
        return toJSON().toString(2);
    }
}