/*
*
* * 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.patient.impl;
import com.apothesource.pillfill.datamodel.DrugAlertType;
import com.apothesource.pillfill.datamodel.PatientType;
import com.apothesource.pillfill.datamodel.PharmacyType;
import com.apothesource.pillfill.datamodel.PrescriberType;
import com.apothesource.pillfill.datamodel.PrescriptionType;
import com.apothesource.pillfill.datamodel.UserDataType;
import com.apothesource.pillfill.datamodel.android.AuthToken;
import com.apothesource.pillfill.datamodel.android.SecurePatientTypeWrapper;
import com.apothesource.pillfill.datamodel.android.exception.EncryptionException;
import com.apothesource.pillfill.exception.InvalidPrescriptionIdException;
import com.apothesource.pillfill.exception.PFMasterPasswordException;
import com.apothesource.pillfill.exception.PFBadTokenException;
import com.apothesource.pillfill.network.PFNetworkManager;
import com.apothesource.pillfill.service.drug.DrugAlertService;
import com.apothesource.pillfill.service.drug.impl.DefaultDrugAlertServiceImpl;
import com.apothesource.pillfill.service.patient.command.ExecutionResult;
import com.apothesource.pillfill.service.patient.command.ModifyPatientActions;
import com.apothesource.pillfill.service.patient.command.ModifyPatientActions.ModifyPatientCommand;
import com.apothesource.pillfill.service.patient.command.ModifyPatientActions.PatientCommand;
import com.apothesource.pillfill.service.patient.command.ModifyPatientActions.UpdatePatientOnServerAction;
import com.apothesource.pillfill.service.PFServiceEndpoints;
import static com.apothesource.pillfill.utilites.ReactiveUtils.subscribeIoObserveImmediate;
import com.google.common.base.Joiner;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import rx.Observable;
import timber.log.Timber;
/**
* Created by rammic on 5/28/15.
*/
public class DefaultPatientServiceImpl implements PatientServiceImpl {
private static final Type RX_LIST_TYPE = new TypeToken<List<PrescriptionType>>() {
}.getType();
private final Gson mGson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create();
private final Map<String, PrescriptionType> mRxMap = new HashMap<>();
private final Map<String, PrescriberType> mDrMap = new HashMap<>();
private final Map<String, PharmacyType> mPharmMap = new HashMap<>();
public static final String STATUS_NOT_INITIALIZED = "NOT_INIT";
private SecurePatientTypeWrapper mPatient;
private AuthToken mAuthToken;
private char[] mPassword;
private boolean mNeedsServerUpdate;
private final HashSet<ModifyPatientCommand> uncommittedCommands = new HashSet<>();
@Override
public Observable<PatientType> init(AuthToken at, String password) {
mPassword = password.toCharArray();
mAuthToken = at;
LoadPatientDataAction action = new LoadPatientDataAction()
.patientService(this);
return doAction(action).flatMap(executionResult -> {
if (executionResult == ExecutionResult.RESULT_SUCCESS) {
try {
openPatientDocument(mPassword);
} catch (PFMasterPasswordException e) {
throw new RuntimeException(e);
}
} else {
throw new RuntimeException("Error getting patient doc.");
}
return Observable.just(mPatient);
});
}
@Override
public Observable<PatientType> getPatient() {
return Observable.just(mPatient);
}
public void setPatient(SecurePatientTypeWrapper pt) {
this.mPatient = pt;
}
@Override
public Observable<PrescriptionType> getAllPrescriptions() {
return Observable.from(mRxMap.values());
}
@Override
public Observable<PrescriptionType> getActivePrescriptions() {
Date now = new Date();
return Observable.from(mRxMap.values()).filter(rx -> rx.getComputedInactiveAfterDate() == null || rx.getComputedInactiveAfterDate().after(now));
}
@Override
public Observable<PrescriptionType> getInactivePrescriptions() {
Date now = new Date();
return Observable.from(mRxMap.values()).filter(rx -> rx.getComputedInactiveAfterDate() != null && rx.getComputedInactiveAfterDate().before(now));
}
@Override
public Observable<DrugAlertType> getDrugAlerts() {
return getActivePrescriptions().flatMap(rx -> Observable.from(rx.getDrugAlerts()));
}
@Override
public Observable<PrescriberType> getPrescribers() {
return Observable.from(mDrMap.values());
}
@Override
public Observable<PharmacyType> getPharmacies() {
return Observable.from(mPharmMap.values());
}
@Override
public Observable<ExecutionResult> doAction(PatientCommand e) {
if(e instanceof ModifyPatientCommand){
uncommittedCommands.add((ModifyPatientCommand) e);
}
e.patientService(this);
return e.doAction();
}
@Override
public Observable<ExecutionResult> undoAction(ModifyPatientCommand e) {
e.patientService(this);
return e.undoAction();
}
@Override
public Observable<ExecutionResult> commitToServer() {
return doAction(new UpdatePatientOnServerAction()).doOnNext(executionResult -> {
if(executionResult == ExecutionResult.RESULT_SUCCESS){
for(ModifyPatientCommand cmd : uncommittedCommands){
cmd.setRevisionId(mPatient.get_rev());
cmd.setChangeState(ModifyPatientActions.ChangeState.CHANGE_COMMITTED);
}
uncommittedCommands.clear();
}
});
}
public void addPrescription(PrescriptionType rx) {
mRxMap.put(rx.getUuid(), rx);
}
@Override
public void addAllPrescriptions(List<PrescriptionType> rxList) {
Observable.from(rxList).forEach(rx -> mRxMap.put(rx.getUuid(), rx));
}
@Override
public void removePrescription(PrescriptionType rx) {
removePrescription(rx.getUuid());
}
@Override
public void removeAllPrescriptions(List<PrescriptionType> rxList) {
Observable.from(rxList).forEach(rx -> {
mRxMap.remove(rx.getUuid());
});
}
public void removePrescription(String rxId) {
mRxMap.remove(rxId);
}
@Override
public void addPrescriber(PrescriberType dr) {
mDrMap.put(dr.getNpi(), dr);
}
@Override
public void addAllPrescribers(List<PrescriberType> drList) {
Observable.from(drList).forEach(dr -> {
mDrMap.put(dr.getNpi(), dr);
});
}
@Override
public void removePrescriber(PrescriberType dr) {
removePrescriber(dr.getNpi());
}
@Override
public void removeAllPrescribers(List<PrescriberType> drList) {
Observable.from(drList).forEach(dr -> {
mDrMap.remove(dr.getNpi());
});
}
public void removePrescriber(String npi) {
mDrMap.remove(npi);
}
@Override
public void updateDrugAlerts() {
doAction(new UpdateDrugAlertsAction()).subscribe(executionResult -> {
Timber.d("Updated Drug Alerts. Result: %s", executionResult.name());
});
}
@Override
public void setPatientDocument(JsonElement closedPtDoc) {
assert(closedPtDoc != null);
mPatient = mGson.fromJson(closedPtDoc, SecurePatientTypeWrapper.class);
assert(mPatient != null);
}
@Override
public SecurePatientTypeWrapper getClosedPatientDocument() throws EncryptionException {
JsonElement patientDocCopy = mGson.toJsonTree(getPatientData());
SecurePatientTypeWrapper wrapper = mGson.fromJson(patientDocCopy, SecurePatientTypeWrapper.class);
wrapper.closePatientDocument(mPassword);
return wrapper;
}
@Override
public void openPatientDocument(char[] password) throws PFMasterPasswordException {
mPatient.openPatientDocument(mPassword);
}
@Override
public String getCurrentRevision() {
return mPatient == null ? STATUS_NOT_INITIALIZED : mPatient.get_rev();
}
@Override
public boolean getNeedsServerUpdate() {
return mNeedsServerUpdate;
}
@Override
public void setNeedsServerUpdate(boolean mNeedsServerUpdate) {
this.mNeedsServerUpdate = mNeedsServerUpdate;
}
@Override
public UserDataType getUserData() {
return mPatient.getUserData();
}
@Override
public void setUserData(UserDataType userData) {
mPatient.setUserData(userData);
}
@Override
public void removePharmacy(PharmacyType changePharm) {
mPharmMap.remove(changePharm.get_id());
}
@Override
public void removeAllPharmacies(List<PharmacyType> phList) {
Observable.from(phList).forEach(pharmacyType -> {
mPharmMap.remove(pharmacyType.getUuid());
});
}
@Override
public PrescriptionType getPrescription(String uuid) {
return mRxMap.get(uuid);
}
@Override
public void addPharmacy(PharmacyType newPharm) {
mPharmMap.put(newPharm.getUuid(), newPharm);
}
@Override
public void addAllPharmacies(List<PharmacyType> phList) {
Observable.from(phList).forEach(pharmacyType -> {
mPharmMap.put(pharmacyType.getUuid(), pharmacyType);
});
}
@Override
public AuthToken getAuthToken() {
return mAuthToken;
}
@Override
public PatientType getPatientData() {
return mPatient;
}
@Override
public List<String> getPrescriptionIds() {
List<String> rxIds = getPatientData().getRxList().getCurrentList();
rxIds.addAll(getPatientData().getRxList().getInactiveList());
return rxIds;
}
public static class LoadPatientDataAction extends PatientCommand<LoadPatientDataAction> {
private String mUpdateUrl;
@Override
public Observable<ExecutionResult> doAction() {
mUpdateUrl = String.format(PFServiceEndpoints.PATIENT_UPDATE_URL, mPatientSvc.getAuthToken().getEmail());
return subscribeIoObserveImmediate(Observable.create(subscriber -> {
Timber.d("Requesting url: %d", mUpdateUrl);
try {
OkHttpClient okClient = PFNetworkManager.getPinnedPFHttpClient();
Request.Builder reqBuilder = new Request.Builder()
.url(mUpdateUrl)
.addHeader("Bearer", mPatientSvc.getAuthToken().toAuthTokenHeaderString())
.addHeader("Cache-Control", "no-cache");
Response response = okClient.newCall(reqBuilder.build()).execute();
if(response.code() == 200){
JsonParser parser = new JsonParser();
JsonElement doc = parser.parse(new InputStreamReader(response.body().byteStream()));
mPatientSvc.setPatientDocument(doc);
subscriber.onNext(ExecutionResult.RESULT_SUCCESS);
subscriber.onCompleted();
}else{
throw new PFBadTokenException(response.code(), "Unexpected response code from server: " + response.code());
}
} catch (Exception e) {
Timber.e(e, "Error syncing profile to server: %s", e.getMessage());
subscriber.onError(e);
}
}));
}
}
public static class LoadRxDataAction extends PatientCommand<LoadRxDataAction> {
final Gson mGson;
private final OkHttpClient mHttpClient;
private List<String> mRxIdList;
public LoadRxDataAction() {
mGson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create();
mHttpClient = PFNetworkManager.getPinnedPFHttpClient();
}
public LoadRxDataAction prescriptionIds(List<String> rxIds) {
mRxIdList = rxIds;
return this;
}
public LoadRxDataAction prescriptionIds(String... rxIds) {
return prescriptionIds(Arrays.asList(rxIds));
}
private void getAllPrescriptionIdsForPatient() {
mRxIdList = mPatientSvc.getPrescriptionIds();
}
@Override
public Observable<ExecutionResult> doAction() {
//If we didn't get a specific load list, load all rxIds for this patient
if (mRxIdList == null) getAllPrescriptionIdsForPatient();
return subscribeIoObserveImmediate(Observable.create(subscriber -> {
//We need to load in increments of 100 so break up the request if it's larger
for (int i = 0; i < mRxIdList.size(); i += 100) {
List<String> rxIdSubset = mRxIdList.subList(i, (mRxIdList.size() >= i + 100) ? i + 100 : mRxIdList.size());
String rxIdsParams = Joiner.on(",").join(rxIdSubset);
String updateUrl = String.format(PFServiceEndpoints.PRESCRIPTIONS_URL, rxIdsParams);
try {
Request.Builder loadRequest = new Request.Builder().url(updateUrl);
Response loadResponse = mHttpClient.newCall(loadRequest.build()).execute();
String rxBody = loadResponse.body().string();
List<PrescriptionType> rxList = mGson.fromJson(rxBody, RX_LIST_TYPE);
mPatientSvc.addAllPrescriptions(rxList);
subscriber.onNext(ExecutionResult.RESULT_SUCCESS);
} catch (Exception e) {
Timber.e(e,"Error syncing profile to server.");
subscriber.onError(e);
return;
}
}
subscriber.onCompleted();
}));
}
}
public static class UpdateDrugAlertsAction extends PatientCommand<UpdateDrugAlertsAction> {
DrugAlertService mDrugAlertSvc;
private HashMap<String, PrescriptionType> mRxMap;
@Override
public Observable<ExecutionResult> doAction() {
if (mRxMap == null) {
setPrescriptions(mPatientSvc.getActivePrescriptions().toList().toBlocking().first());
}
return subscribeIoObserveImmediate(Observable.create(subscriber -> {
Observable<DrugAlertType> fdaAlerts = mDrugAlertSvc.checkForDrugAlerts(mRxMap.values());
Observable<DrugAlertType> mrtdAlerts = mDrugAlertSvc.checkForDrugDoseWarnings(mRxMap.values(), mPatientSvc.getUserData().getWeightInKgs());
Observable<DrugAlertType> interactionAlerts = mDrugAlertSvc.checkForRxNormInteractions(mRxMap.values());
for (PrescriptionType rx : mRxMap.values()) {
rx.getDrugAlerts().clear();
}
Observable.merge(fdaAlerts, mrtdAlerts, interactionAlerts).subscribe(drugAlert -> {
Observable.from(drugAlert.getRelatedRxIds()).forEach(rxId -> {
PrescriptionType rx = mPatientSvc.getPrescription(rxId);
if (rx != null) {
rx.getDrugAlerts().add(drugAlert);
} else {
Timber.w("Invalid RxId Included: %s", rxId);
subscriber.onError(new InvalidPrescriptionIdException(rxId));
}
subscriber.onNext(ExecutionResult.RESULT_SUCCESS);
});
}, subscriber::onError, subscriber::onCompleted);
}));
}
public UpdateDrugAlertsAction setPrescriptions(List<PrescriptionType> rxList) {
mRxMap = new HashMap<>();
Observable.from(rxList).forEach(rx -> {
mRxMap.put(rx.getUuid(), rx);
});
return this;
}
public UpdateDrugAlertsAction drugAlertService(DefaultDrugAlertServiceImpl drugAlertService) {
this.mDrugAlertSvc = drugAlertService;
return this;
}
}
public static class CheckServerVersionAction extends PatientCommand<CheckServerVersionAction> {
private String mUpdateUrl;
private String mCurRevision;
@Override
public Observable<ExecutionResult> doAction() {
mUpdateUrl = String.format(PFServiceEndpoints.PATIENT_UPDATE_URL, mPatientSvc.getAuthToken().getEmail());
mCurRevision = mPatientSvc.getPatientData().get_rev();
return subscribeIoObserveImmediate(Observable.create(subscriber -> {
Timber.d("Requesting url: %s", mUpdateUrl);
try {
HttpsURLConnection connection = PFNetworkManager.getPFHttpsURLConnection(mUpdateUrl);
connection.setRequestProperty("Bearer", mPatientSvc.getAuthToken().toAuthTokenHeaderString());
connection.setRequestMethod("HEAD");
connection.addRequestProperty("Cache-Control", "no-cache");
String serverRev = connection.getHeaderField("Etag");
if (serverRev != null && mCurRevision != null) {
boolean updateReq = !mCurRevision.equalsIgnoreCase(serverRev);
if (updateReq) {
Timber.i("Update required. Cached patient document is: %s / Server has: %s", Arrays.asList(mCurRevision, serverRev));
mPatientSvc.setNeedsServerUpdate(true);
} else {
Timber.d("Update not required. Cached patient document is: %s / Server has: %s", Arrays.asList(mCurRevision, serverRev));
mPatientSvc.setNeedsServerUpdate(false);
}
subscriber.onNext(ExecutionResult.RESULT_SUCCESS);
subscriber.onCompleted();
} else if (mCurRevision == null) {
Timber.i("Current patient doc does not have a revision.");
mPatientSvc.setNeedsServerUpdate(true);
subscriber.onNext(ExecutionResult.RESULT_SUCCESS);
subscriber.onCompleted();
} else {
Timber.e("No ETag headers provided.");
for (String header : connection.getHeaderFields().keySet()) {
for (String value : connection.getHeaderFields().get(header)) {
Timber.e("Header: " + header + "/" + value);
}
}
subscriber.onError(new RuntimeException("No etag provided from server."));
}
} catch (IOException e) {
Timber.e(e,"IO error checking for revision.");
subscriber.onError(e);
}
}));
}
}
}