REST: Cannot update complex obs because complex data is null

Hi all,

I would like to update an obs via REST:

var saved = Obs.save(obs);
saved.$promise.then(function(obs) {
  ...
});

But I am getting an error 500 because ObsServiceImpl.saveObs(Obs obs, String changeMessage) throws a NullPointerException right at the beginning:

public Obs saveObs(Obs obs, String changeMessage) throws APIException {
  if (null != obs && null != obs.getConcept() && obs.getConcept().isComplex()
        && null != obs.getComplexData().getData()) {
      ...
    }
  ...
}

And indeed my obs’s complexData member is null (and obs.getComplexData() returns null).

I dug a bit further and I believe that there is a bug in ObsResource1_8.getByUniqueId(String uniqueId):

@Override
public Obs getByUniqueId(String uniqueId) {
  return Context.getObsService().getObsByUuid(uniqueId);
}

I am guessing that the above method does not return a complex obs when it should.

Am I correct to assume that either ObsResource1_8.getByUniqueId(String uniqueId) or ObsServiceImpl.getObsByUuid(String uuid) should be modified to handle complex obs?

I believe the bug rather lies in Core’s ObsServiceImpl.saveObs(Obs obs, String changeMessage). I woud like to suggest the following change where changeMessage conveniently (and bit hackily) could be used to specify the complex obs’s ‘view’:

public Obs saveObs(Obs obs, String changeMessage) throws APIException {

  if (null != obs && null != obs.getConcept() && obs.getConcept().isComplex()) {     
    String view = changeMessage;
    Obs complexObs = getComplexObs(obs.getId(), view);
    if (null != complexObs && null != obs.getComplexData()) {
      // save or update complexData object on this obs
      // this is done before the database save so that the obs.valueComplex
      // can be filled in by the handler.
      ComplexObsHandler handler = getHandler(obs);
      if (null != handler) {
        handler.saveObs(obs);
      } else {
        throw new APIException("Unknown handler for " + obs.getConcept());
      }
    }
  }
  ...
}

I believe the NullPointerException mentioned in my previous post should definitely have been avoided in the first place. In the sense that we want to check that complexData is not null but not that its inner data isn’t. One could imagine that someone attempts to save a complex obs with a null inner data, and it is up to the handler to take care of this.

P.S. I believe this ancient ticket reported the same symptom: ERR-391.

I wouldn’t change the meaning of changeMessage, from the API client point of view will be confusing (even if the javadoc explains the hack). If there is a specific method to save a complex obs, I would change the logic in the resource class and invoke the appropriate method from there.

Yes you are right, the correct saving should be somehow handled somewhere else.

It feels really weird that the view must be provided to update a complex obs, whereas it was never necessary to create it in the first place.

Anyhow I believe that the check “null != obs.getComplexData().getData()” is a bug in ObsServiceImpl.

Or the original design did not allow complex data to be null, at least when invoked from the webapp.

This solved the problem:

1. Backend: I overloaded ObsResource1_11 in my module’s OMOD

@Resource(name = RestConstants.VERSION_1 + "/complexobs", order = 1, supportedClass = Obs.class, supportedOpenmrsVersions = {"1.11.*", "1.12.*"})
public class ComplexObsResource extends ObsResource1_11 {
   ...
  @Override
  public Obs save(Obs delegate) {
    if (isComplex(delegate) && null == delegate.getComplexData()) {
      delegate = Context.getObsService().getComplexObs(delegate.getId(), "view");
    }
    return super.save(delegate);
  }
   
  public static boolean isComplex(Obs obs) {
    return (null != obs && null != obs.getConcept() && obs.getConcept().isComplex());
  }
}

2. Frontend: I added a convenience Angular service: complexObsService.js

angular.module('complexObsService', ['ngResource', 'uicommons.common'])
  .factory('ComplexObs', function($resource) {
    return $resource("/" + OPENMRS_CONTEXT_PATH  + "/ws/rest/v1/complexobs/:uuid", {
      uuid: '@uuid'
    },{
      query: { method:'GET', isArray:false }
    });
  });

This was just copied from UI Commons’s obsService.js .

2 Likes

I get the same error when I try to add new obs to the encounter which has complex obs. When obs is loaded with encounter, complexData will be null and it throws NullPointerException at this line.

Below is the failing test:

@Test
@Verifies(value="should save encounter with complex obs", method="saveEncounter(Encounter)")
public void saveEncounter_shouldSaveEncounterWithComplexObs() throws Exception {
	executeDataSet(ENC_OBS_HIERARCHY_DATA_XML);
	EncounterService es = Context.getEncounterService();

	Encounter encounter = es.getEncounter(101);
	Obs observation = buildObs();
	observation.setLocation(encounter.getLocation());
	observation.setPerson(encounter.getPatient());
	encounter.addObs(observation);

	es.saveEncounter(encounter);
	Context.flushSession();
	Context.clearSession();

	encounter = es.getEncounter(101);
	assertEquals(2, encounter.getObsAtTopLevel(true).size());
}

And below is the test data for the same:

<concept concept_id="3000" retired="false" datatype_id="13" class_id="11" is_set="false" creator="1" date_created="2004-08-12 00:00:00.0" version="" uuid="19ee3170-68ab-4444-bd76-0810271c1b76"/>
<concept_name concept_id="3000" name="Consultation Image" locale="en_GB" creator="1" date_created="2015-01-16 13:52:53.0" concept_name_id="2139" concept_name_type="SHORT" locale_preferred="0" voided="false" uuid="ef7ac658-3136-443c-2924-4617aa535673"/>
<concept_complex concept_id="3000" handler="ImageHandler" />
<encounter encounter_id="101" encounter_type="1" form_id="1" encounter_datetime="2015-01-01 00:00:00.0" patient_id="3" location_id="1" creator="1" date_created="2015-01-01 00:00:00.0" voided="0" uuid="6bdec581-23aa-45fd-b578-413807aa6e8b"/>
<encounter_provider encounter_provider_id="100" encounter_id="101" provider_id="2" encounter_role_id="2" creator="1" date_created="2006-03-11 15:57:35.0" voided="false" uuid="1596c524-013f-40f2-aa49-c993f0acab4e" />
<obs obs_id="4000" person_id="3" encounter_id="101" concept_id="3000" value_complex="file1.jpg" obs_datetime="2015-01-01 00:00:00.0" location_id="1" creator="1" date_created="2015-01-01 00:00:00.0" voided="false" uuid="4bf4d002-65e3-4203-ba87-6b1add8c2a53"/>

I think we should add a check where we call getComplexData()

It makes sense to check if complexData is not null, though it makes me wonder why one is posting it when it’s null, the REST API currently is not designed to support posting of complex Obs so I would think this is technically not a bug

It makes sense to check that the complex data member is not null but not, IMHO, to check that the inner data itself is not null (that’s up to the complex data handlers to deal with that kind of situations). And that is what is happening in ObsServiceImpl and that causes troubles:

private void handleExistingObsWithComplexConcept(Obs obs) {
  if (null != obs.getConcept() && obs.getConcept().isComplex()
    && null != obs.getComplexData().getData()) {
    ...
  }
}

This should be enough:

private void handleExistingObsWithComplexConcept(Obs obs) {
  if (null != obs.getConcept() && obs.getConcept().isComplex()
    && null != obs.getComplexData()) {
    ...
  }
}

@mksd i do not see much value in trying to avoid a NPE at this point. If it is a complex obs but with getComplexData() returning null, then we should not have reached here in the very first place. Something like the validators should have prevented the call to saveObs() from happening. Otherwise, by preventing this NPE, you would be saving meaningless data. :smile:

Sounds right for the validator to reject a complex Obs with null complex data, checking if complexData is null is pointless in the API because it isn’t expected to be null

I’m not necessarily trying to avoid the NPE though, I just thought that whatever is inside the complex data is the business of the ad-hoc complex data handler, and not of ObsService itself. I would rather see a handler throwing an APIException when there is an attempt to save a complex obs with an invalid complex data (such as one with a null inner data for instance).

On my end I worked around this problem anyway, and for the rest I trust your opinions then :wink:

It’s the role of the API to ensure that we are saving clean data, why would you save a complex obs if the data is null? The handler’s job is to save and retrieve the complex data

In my case, I am not trying to save complex object without complex data. Infact, am not touching the obs at all. All I am doing is, adding a new obs to the encounter which contains complex obs. Since complex data is not loaded by default, when encounter is saved, it tries to save all the obs in it and it finds complexData as null for complex obs.

I guess in that case probably it makes sense to make the check

Wyclif, I don’t know how to interpret this statement. :slight_smile:

The OpenMRS platform needs to support all aspects of complex obs through its REST API. Whether we call this a defect or an enhancement doesn’t matter: we should treat all limitations of the REST API as high-priority issues for the community to address.

I suggest we get this fixed in Platform 2.0.1, that @preethi_s is actively working on now. So, please let’s create any relevant tickets and get them ready-for-work.

That was my earlier comment before I saw @preethi_s case, created TRUNK-4966

Apparently they had already created another ticket TRUNK-4965, So i have closed TRUNK-4966