Wiki Spaces

Documentation
Projects
Resources

Get Help from Others

Q&A: Ask OpenMRS
Discussion: OpenMRS Talk
Real-Time: IRC Chat | Slack

Projects

Page tree
Skip to end of metadata
Go to start of metadata

This was the project page used while designing the Webservices.rest Module. See that module's documentation for the most up-to-date information

Background

OpenMRS has an explicit API which has not been cleanly or explicitly exposed through web services.  We have a REST module and SOAP-based module(s) where folks have done work in the area.  We need a clear and explicit strategy for supporting web services.

Requirements

  • A single OpenMRS module to support the core web service needs. 
  • This module will follow a RESTful approach
  • The framework should allow new web service methods to be added with minimal effort
  • At this point, we can leave the Java objects with Hibernate magic (all object properties assumed to exist) and convert the data to DTOs from our module.  These DTOs may inform how we transform our API in the future though.
  • Requirements for Version 1 of this module

API Service and Object Design

Action Plan

  1. Web services will be developed as an augmentation to the existing API... that is, we will not rewrite the current API and then expose them as web services... we will write the web services as an independent activity, hoping that they inform the further maintenance and redevelopment of the existing API.
  2. Support OAuth or an equivalent authentication scheme
  3. An upcoming web services sprint is scheduled. See Development Sprints and the ws sprint page: 2011-05-16 Development Sprint

Interested Parties and Mentors

Ben Wolfe (mentor)

Burke Mamlin

Darius Jazayeri

Saptarshi Purkayastha

See Also

Result / Output

See the REST Module for the documentation and final output.

9 Comments

  1. We need to find out what the convention is for REST-based service when creating objects.  Is a handle for the new object returned in the response?  Or is a redirect used?  We're assuming errors are handled through HTTP error codes +/- response data about the error.

    1. From what I've seen, most RESTful creates and edits return the created/edited object, so that if new objects are created, their IDs and history data and unsent required-with-default data will be created on the host side and returned as part of the object.

  2. Thought examples:

    1. Creating an encounter with observations

    REST 1:
    * POST to "patient/PATIENT_ID/encounters" with encounter metadata as parameters,
    * then repeatedly POST to "patient/PATIENT_ID/encounters/ENCOUNTER_ID/obs"

    REST 2: POST to "patient/PATIENT_ID/encounters" with xml the includes encounter metadata and obs

    SOAP 1: web method call to createEncounter(EncounterDTO)

    SOAP 2:
    * web method call to createEncounter(patient, location, date, type)
    * then repeated web method calls to createObs(encounter, obsDetails...)

    2. Enrolling a patient in a program

    REST: POST to "programs/PROGRAM_ID/enrollPatient" with patient=PATIENT_ID&date=DATE
    SOAP: web method call to enrollPatientInProgram(patient, program, date)

  3. So, Saptarshi... sitting here in a design conversation with Burke, Ben, and Darius... here are some questions/suggestions for you:

    1. There are two ConceptDTO boxes... what do you mean by this?
    2. BaseOpenmrsMetadataExtObjectDTO... what's the rationale here?
    3. Instead of using ConceptDTO.ConceptClassName, create a new ref object that has a UUID and name attribute... consider calling it MetadataRef or something like this...
    4. DTOs for concept_numeric and concept_complex?
    5. PersonAddress should extend OpenmrsData
    6. ConceptDTO should use OpenmrsMetadata.Name instead of ConceptName
    7. PersonAddress and Location should have updated column names consistent with our general move of generalizing them (address 1-6 or whatever it was decided)
  4. Questions for experts:

    • Is the model of ws.getPerson(uuid, extraAttributes) a good idea?
    1. e.g. getPatient(123, [includeProperties: ["preferredName", "birthdate", "gender"]])

      My fear is that this ties the implementation to property names too closely.  If we decide to change the model, these method calls fail.  (or fail silently I suppose)

  5. Iterating on Ben's last comment (notes here):

    Random thoughts and comments

    • Ideally we can name the class Concept on the client-side, even though it will probably be ConceptDTO on the server side. (Is this possible?)
    • Consider putting the entire web service API under a namespace like "omrs1" for future growth.
    • We want to let clients flexibly refer to metadata (e.g. concept datatype) by uuid or name. To support this:
      1. have api methods like getConceptDatatypes(), getConceptNameTypes(), ...
      2. define a flexible "Ref" class with String properties for className, uuid, and a displayable value. You might create a concept by passing datatype="Numeric", but if you fetch a concept, its datatype property is one of these refs:
        Ref example
        concept.datatype ->
        {
            className: "org.openmrs.ConceptDatatype",
            value: "Numeric",
            uuid: "abc123..."
        }
        

    Domain Objects

    Domain objects have:

    • required properties vs. optional properties
    • properties that are always fetched vs. properties that are only fetched when requested

    CRUD method templates for domain object Xyz

    • C: createXyz(requredProp1, requiredProp2, ..., Map optionalProperties)
      • returns persisted Xyz
      • usage: createXyz("req1", "req2", "req3", {"opt1":val1, "opt2":val2});
      • usage: createObs(personUuid, datetime, questionUuid, value, {"location": "Rwinkwavu"});
      • optionalProperties is a Map from (String) property name to property value
    • R: getXyz("uuid or unique name", List extraProperties)
      • returns Xyz
      • metadata may also be fetched by its unique name
      • data can only be fetched by its uuid
      • extraProperties is a List of (String) property names which would otherwise be null on the returned object
      • other retrieval methods depend on the specific domain object
    • U: updateXyz("uuid or unique name", Map propertiesToValues)
      • returns updated Xyz
      • only the specified properties of the object are updated to the new values
      • does not work for collection properties
    • D: retireXyz("uuid or unique name", "reason for retiring")
      • returns retired xyz
      • for metadata like concept, location, encounterType
      • marks the object as inactive
    • D: deleteXyz("uuid", "reason for deleting")
      • returns deleted xyz
      • for data like patient, obs, encounter
      • marks the object as voided (but I think "deleted" is clearer for the client)
    • X: purgeXyz("uuid")
      • returns nothing
      • maybe this should require a secret token as a second argument
      • deletes the object from the database
      • fails if the object is in use

    Concept, as a fully-worked example

    Concept Properties

    Concept object
    // required for creation
    Ref datatype
    Ref conceptClass
    String name // primary name
    String locale // locale of primary name
    
    // always returned (in addition to above)
    String uuid
    boolean retired
    
    // optional (null unless requested)
    String description
    AuditInfo auditInfo
    List<ConceptName> names
    List<ConceptName> shortNames
    List<ConceptDescription> descriptions
    List<Concept> setMembers
    List<ConceptAnswer> answers
    
    // intentionally not exposed: conceptId, isSet, version
    
    AuditInfo
    Ref creator
    Date dateCreated
    Ref changedBy
    Date dateChanged
    Ref retiredBy
    Date dateRetired
    String retireReason
    Ref voidedBy
    Date dateVoided
    String voidReason
    
    ConceptAnswer
    //properties
    String answerType // "concept", "drug", "conceptSet", or "conceptClass"
    Ref answer
    
    // currently answers are only allowed to be concept or drug, but we want to be future-proof
    // example usage
    if (question.answers[0].answerType == "set") {
        Concept[] members = api.getConceptSetMembers(question.answers[0].answer.uuid);
        // do something with each
    }
    

    Standard CRUD, applied to Concept

    • C: c = api.createConcept("Numeric", "Test", "HEMOGLOBIN", "en", {description: ""Stuff in the blood..."});
    • R: c = api.getConcept("HEMOGLOBIN", null);
    • U: c = api.updateConcept(c.uuid, {"conceptClass": "Symptom"});
    • D: c = api.retireConcept(c.uuid, "because");
    • X: api.purgeConcept(c.uuid)

    Other retrieval methods:

    • Concept[] findConcepts("partial name", "locale", [extraProperties]) // fuzzy/partial name search
    • Concept[] findConcepts("partial name", datatype, conceptClass, [extraProperties])
      • Ben will say we should name these two getConceptsByName
    • Concept getConceptByFullySpecifiedName("fully specified name", "locale", [extraProperties])
    • Concept[] getConceptSetMembers(setUuid, [extraProperties])
    • ConceptAnswer[] getConceptAnswers(questionUuid, [extraProperties])
    • Concept[] getConceptsByMapping(source, code, [extraProperties])

    Collection properties of Concept

    Concept names

    • addNameToConcept(c.uuid, "a name", "en_US", preferred, type="fully-specified", {optionalNameProperties})
      • On the topic of "addConceptName" vs. "addNameToConcept" choice for method names: since we are already grouping by function (i.e., "add"), then the "addNameToConcept" approach further groups by function groupings AND reads better.
    • removeNameFromConcept(conceptNameUuid)
    • removeNameFromConcept(conceptUuid, "a name", "en_US")
      • both these methods void the name. They fail if it's the only name, removing fully specified name, etc.
    • addShortNameToConcept(...)
    • removeShortNameFromConcept(...) x2
      • same options as addNameToConcept/removeNameFromConcept. The point is that short name is a separate property, not an alternate name
        Edit an existing name
        ConceptName cn = something ; // somehow get this from the API
        api.updateConceptName(cn, {name: "Cough", "localePreferred", true});
        

    Concept set members needs work

    • addMemberToSet(setUuid, memberUuid, beforeMemberUuid)
      • assigns isSet=true on the set if not already specified
      • beforeMemberUuid is nullable.  if null, memberUuid is at the end of the list
    • setMembersOnSet(setUuid, [memberUuids])
      • Replaces all with the given order? 
      • Rename this method?
    • removeMemberFromSet(setUuid, memberUuid)

    Concept subclasses (ConceptNumeric, ConceptComplex)

    Question for the experts: which path to take for Concept subclasses?

    1. ConceptNumeric extends Concept (and includes range, units, and precision properties)
    2. ConceptNumeric includes a Concept property (and also range, units, and precision properties)
    3. just have Concept, which has range, units, and precision as optional properties
    1. Do we want to consider addNamesToConcept() in addition or instead of addNameToConcept()?

    2. Darius, thanks for the documentation.  FYI – I moved your notes to this page and did a little reformatting/tweaking.