/*
*
* * The MIT License
* *
* * Copyright {$YEAR} Apothesource, Inc.
* *
* * Permission is hereby granted, free of charge, to any person obtaining a copy
* * of this software and associated documentation files (the "Software"), to deal
* * in the Software without restriction, including without limitation the rights
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* * copies of the Software, and to permit persons to whom the Software is
* * furnished to do so, subject to the following conditions:
* *
* * The above copyright notice and this permission notice shall be included in
* * all copies or substantial portions of the Software.
* *
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* * THE SOFTWARE.
*
*/
package com.apothesource.pillfill.service.drug.impl;
import com.apothesource.pillfill.datamodel.DrugInformation;
import com.apothesource.pillfill.datamodel.PrescriptionType;
import com.apothesource.pillfill.datamodel.SplSearchResultEntry;
import com.apothesource.pillfill.datamodel.ndfrt.Concept;
import com.apothesource.pillfill.datamodel.ndfrt.FullConcept;
import com.apothesource.pillfill.datamodel.ndfrt.Property;
import com.apothesource.pillfill.datamodel.ndfrt.Role;
import com.apothesource.pillfill.datamodel.spl.Ingredient;
import com.apothesource.pillfill.datamodel.spl.Pkg;
import com.apothesource.pillfill.datamodel.spl.Product;
import com.apothesource.pillfill.datamodel.spl.SplEntry;
import com.apothesource.pillfill.datamodel.spl.SplInformation;
import com.apothesource.pillfill.network.PFNetworkManager;
import com.apothesource.pillfill.service.drug.DrugService;
import com.apothesource.pillfill.service.PFServiceEndpoints;
import com.apothesource.pillfill.utilites.ResourceUtil;
import static com.apothesource.pillfill.utilites.ReactiveUtils.subscribeIoObserveImmediate;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import rx.Observable;
import timber.log.Timber;
public class DefaultDrugServiceImpl implements DrugService {
private static final String URL_RXNORM_BRAND_NAME_BY_NDC = "http://rxnav.nlm.nih.gov/REST/rxcui.json?idtype=NDC&id=%s";
private static Gson gson = new GsonBuilder().disableHtmlEscaping().setDateFormat("yyyy-MM-dd").create();
private final XPathExpression XPATH_GET_DRUG_BN;
private final Properties urlToTypeMapping;
public Comparator<SplSearchResultEntry> scoreComparator = (lhs, rhs) -> {
try {
float lhsScore = Float.parseFloat(lhs.getScore());
float rhsScore = Float.parseFloat(rhs.getScore());
if (lhsScore == rhsScore) return 0;
return (lhsScore - rhsScore) < 0 ? -1 : 1;
} catch (Exception e) {
return 0;
}
};
public DefaultDrugServiceImpl() {
XPath path = XPathFactory.newInstance().newXPath();
urlToTypeMapping = ResourceUtil.getInstance().getMappingResources();
try {
XPATH_GET_DRUG_BN = path.compile("//conceptGroup[tty[text()='SBD']]/conceptProperties/synonym/text()");
} catch (XPathExpressionException e) {
throw new RuntimeException("Could not compile BrandName xpath", e);
}
}
/**
*
*
*
*
*
*
*
*
* Observable Network Call-Dependent Methods
*
*
*
*
*
*
*
*
*/
/**
* <a href="http://rxnav.nlm.nih.gov/RxNormAPIs.html#">NIH Service</a>: Get avaliable RxNorm IDs associated with an 11 digit National Drug Code (NDC).
*
* @param ndc The 11-digit NDC without dashes or spaces
* @return A list of RxNorm IDs associated with this NDC (should normally return 0 or 1)
*/
@Override
public Observable<String> getRxNormIdForDrugNdc(String ndc) {
return subscribeIoObserveImmediate(subscriber -> {
String urlStr = String.format(URL_RXNORM_BRAND_NAME_BY_NDC, ndc);
try{
String responseStr = PFNetworkManager.doPinnedGetForUrl(urlStr);
JsonParser parser = new JsonParser();
JsonObject response = parser.parse(responseStr).getAsJsonObject().get("idGroup").getAsJsonObject();
if (response.has("rxnormId")) {
JsonArray array = response.getAsJsonArray("rxnormId");
for(JsonElement e : array){
subscriber.onNext(e.getAsString());
}
subscriber.onCompleted();
} else {
Timber.e("No rxnormIds found for NDC: %s", ndc);
subscriber.onError(new RuntimeException("Could not find NDC->RxNorm for NDC."));
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
/**
* Retrieve {@link FullConcept} for for the provided FDA <code><a href="http://www.fda.gov/ForIndustry/DataStandards/SubstanceRegistrationSystem-UniqueIngredientIdentifierUNII/">UNique Ingredient Identifier</a></code>.
* @param uniis A list of the Drug UNIIs to be retrived
* @return An observable that emits a {@link FullConcept} for each related concept found in the National Drug File (NDF)
*/
@Override
public Observable<FullConcept> getNdfrtConceptsByUnii(String... uniis) {
return getConceptsByIds(uniis);
}
public Observable<FullConcept> getConceptsByIds(String... ids) {
return getDrugInformation(ids)
.filter(drugInfo -> !drugInfo.concepts.isEmpty())
.flatMap(drugInfo -> Observable.from(drugInfo.concepts));
}
/**
* Retrieve {@link SplInformation} for the provided SPL (FDA's Standard Packaging Label) Drug IDs. Each {@link SplInformation} instance represents a specific product & package on the US Drug Market (both OTC & RX). The class includes basic patient-focused drug information, ingredients, strengths, etc.
* @param ids The list of SPL IDs to lookup
* @return An observable that emits a {@link SplInformation} instance for each drug
* @see SplEntry
*/
@Override
public Observable<SplEntry> getSplInformation(String... ids) {
return getDrugInformation(ids)
.filter(drugInfo -> !drugInfo.products.isEmpty())
.flatMap(drugInfo -> Observable.from(drugInfo.products));
}
/**
* Retrieve {@link FullConcept} for for the provided National Drug File (NDF) Concept IDs (NUIs). IDs should be in standard NDF Concept ID format of 'N' followed by 10 digits (i.e. (N[\d]{10}) - e.g. 'N0000155155')
* @param nuis A list of the NUIs to be retrived
* @return An observable that emits {@link FullConcept}s based on the provided NUI list
*/
@Override
public Observable<FullConcept> getNdfrtInformation(String... nuis) {
return getConceptsByIds(nuis);
}
/**
* An experimental method to enable a list of generic concepts to be retrieved without a method-specific implementation. Use with caution.
* @param type The {@link Type} of class to be returned. Must be mapped in the <code>/src/main/resources/PFResourceMapping.properties</code> file or a RuntimeException will occur.
* @param ids The list of IDs to retrieve
* @return An observable that emits instances of the provided type based on the requested IDs
*/
protected <T> Observable<T> getConceptList(Class<T> type, String... ids) {
if(ids == null || ids.length == 0) return Observable.empty();
String urlTemplate = getUrlForType(type);
String url = String.format(urlTemplate, Joiner.on(",").join(ids));
String typeName = type.getSimpleName();
Timber.d("Requesting %s list from URL: %s", typeName, url);
return subscribeIoObserveImmediate(subscriber -> {
try{
String listJson = PFNetworkManager.doPinnedGetForUrl(url);
JsonParser parser = new JsonParser();
JsonArray array = parser.parse(listJson).getAsJsonArray();
for(JsonElement elem : array){
subscriber.onNext(gson.fromJson(elem, type));
}
subscriber.onCompleted();
} catch (IOException e) {
Timber.e("Error retrieving %s list with ids %s - %s", typeName, ids, e.getMessage());
subscriber.onError(e);
}
});
}
private String getUrlForType(Class t){
String[] components = t.getSimpleName().split("[\\.]");
String simpleName = components[components.length - 1].replaceAll("[^\\w]", "");
return urlToTypeMapping.getProperty(simpleName);
}
/**
* Get a list of brand names associated with the provided drug identified by its RxNorm ID (CUI).
* @param rxnormId The RxNormID for the generic drug
* @return A list of brand names associated with the generic drug
*/
@Override
public Observable<String> getBrandNamesForDrug(String rxnormId) {
if(rxnormId == null) return Observable.empty();
return subscribeIoObserveImmediate(subscriber -> {
try {
List<String> brandNames = loadBrandNameDrug(rxnormId);
if (!brandNames.isEmpty()) Observable.from(brandNames).forEach(subscriber::onNext);
subscriber.onCompleted();
} catch (Exception e) {
Timber.e(e, "Error accessing brand name for rxNormId: %s", rxnormId);
subscriber.onError(e);
}
});
}
@Override
public Observable<DrugInformation> getDrugInformation(String... ids) {
if (ids== null || ids.length == 0) return Observable.empty();
String idList = Joiner.on(",").join(ids);
final String url = String.format(PFServiceEndpoints.DRUG_INFO_URL, idList);
return subscribeIoObserveImmediate(subscriber -> {
try{
String response = PFNetworkManager.doPinnedGetForUrl(url);
List<DrugInformation> drugInformation = gson.fromJson(response, DRUGINFO_LIST_TYPE);
if(!drugInformation.isEmpty()){
Observable.from(drugInformation).forEach(subscriber::onNext);
}
subscriber.onCompleted();
} catch (IOException e) {
Timber.e("Couldn't get DrugInformation concepts for IDs: %s", ids.toString());
subscriber.onError(e);
}
});
}
private List<String> loadBrandNameDrug(final String rxnormId) throws IOException {
final String rxNormUrlTemplate = "http://rxnav.nlm.nih.gov/REST/rxcui/%s/allrelated";
final String urlString = String.format(rxNormUrlTemplate, rxnormId);
String response = PFNetworkManager.doPinnedGetForUrl(urlString);
try {
NodeList nl = (NodeList) XPATH_GET_DRUG_BN.evaluate(new InputSource(new StringReader(response)), XPathConstants.NODESET);
ArrayList<String> retList = new ArrayList<>();
for (int i = 0; i < nl.getLength(); i++) {
retList.add(nl.item(i).getNodeValue());
}
return retList;
}catch(XPathExpressionException e){
Timber.e(e, "Error processing response: %s", response);
}
return Collections.emptyList();
}
/**
*
*
*
*
*
*
* Static Utility Methods.
*
*
*
*
*
*
**/
public static Optional<Product> getProductByNdc(SplEntry entry, String ndc) {
for (Product p : entry.getProducts().getProduct()) {
for (Pkg pkg : p.getPkg()) {
if (ndc != null && pkg.getNdc() != null && pkg.getNdc().startsWith(ndc)) return Optional.of(p);
}
}
return Optional.absent();
}
public static ArrayList<Concept> getIngredients(FullConcept fc) {
return getRolesWithName("has_Ingredient", fc);
}
public static ArrayList<Concept> getTreatedConditions(FullConcept fc) {
return getRolesWithName("may_treat", fc);
}
public static ArrayList<String> getUnii(FullConcept fc) {
return getPropertiesWithName("FDA_UNII", fc);
}
public static ArrayList<String> getProperty(String propertyName, FullConcept fc) {
return getPropertiesWithName(propertyName, fc);
}
public static String getDefinition(FullConcept fc) {
ArrayList<String> list = getPropertiesWithName("MeSH_Definition", fc);
if (list.isEmpty()) {
return null;
} else {
return list.get(0);
}
}
public static ArrayList<Concept> getContraindications(FullConcept fc) {
return getRolesWithName("CI_with", fc);
}
private static ArrayList<Concept> getRolesWithName(String name, FullConcept fc) {
ArrayList<Concept> retList = new ArrayList<>();
for (Role role : fc.getGroupRoles().getRole()) {
if (role.getRoleName().equalsIgnoreCase(name)) {
retList.add(role.getConcept());
}
}
return retList;
}
private static ArrayList<String> getPropertiesWithName(String name, FullConcept fc) {
ArrayList<String> retList = new ArrayList<>();
if (fc == null) return retList;
if (fc.getGroupProperties() == null) return retList;
if (fc.getGroupProperties().getProperty() == null) return retList;
for (Property property : fc.getGroupProperties().getProperty()) {
if (property.getPropertyName().equalsIgnoreCase(name)) {
retList.add(property.getPropertyValue());
}
}
return retList;
}
public static List<String> getUniisFromSpl(SplEntry splEntry) {
HashSet<String> uniis = new HashSet<>();
if (splEntry == null || splEntry.getProducts() == null || splEntry.getProducts().getProduct() == null)
return null;
for (Product p : splEntry.getProducts().getProduct()) {
if (p.getIngredient() == null) {
return null;
}
for (Ingredient i : p.getIngredient()) {
uniis.add(i.getUnii());
}
}
return new ArrayList<>(uniis);
}
public static String padWithZeros(String src, int size) {
if (src.length() >= size) {
return src;
} else {
return padWithZeros("0" + src, size);
}
}
public static String getNdc(SplEntry entry) {
ArrayList<String> ndcs = getNdcs(entry);
if (ndcs.isEmpty()) return null;
return ndcs.get(0);
}
public static ArrayList<String> getNdcs(SplEntry entry) {
ArrayList<String> ndcs = new ArrayList<>();
if (entry.getProducts() != null && entry.getProducts().getProduct() != null && !entry.getProducts().getProduct().isEmpty()) {
for (Product p : entry.getProducts().getProduct()) {
if (p.getPkg() != null && !p.getPkg().isEmpty()) {
for (Pkg pkg : p.getPkg()) {
if (pkg.getNdc() != null) ndcs.add(pkg.getNdc());
}
}
}
}
return ndcs;
}
public static List<String> getIdsForPrescriptions(ArrayList<PrescriptionType> rxList) {
ArrayList<String> retList = new ArrayList<>();
for (PrescriptionType rx : rxList) {
retList.add(rx.get_id());
}
return retList;
}
}