Nova postagem

Pesquisar

Artigo
· Out. 16, 2023 10min de leitura

Using FHIR Adapter to offer FHIR services over legacy systems - Reading a Resouce

We resume our series of articles on the FHIR Adapter tool available to HealthShare HealthConnect and InterSystems IRIS users.

In the previous articles we have presented the small application on which we set up our workshop and showed the architecture deployed in our IRIS instance after installing the FHIR Adapter. In today's article we will see an example of how we can perform one of the most common CRUD (Create - Read - Update - Delete) operations, the reading operation, and we will do it by recovering a Resource.

What is a Resource?

A Resource in FHIR corresponds to a type of clinical information that is relevant, this information can be a patient (Patient), a request to a laboratory (ServiceRequest) or a diagnosis (Condition), etc. Each resource defines the type of data that comprises it, as well as the restrictions on the data and the relationships with other types of resources. Each resource allows the extension of the information it contains, thus allowing needs to be covered that are outside the FHIR 80% (covering the needs used by more than the 80% of the users).

For the example in this article we are going to use the most common resource, the Patient. Let's look at its definition:

{
  "resourceType" : "Patient",
  // from Resource: id, meta, implicitRules, and language
  // from DomainResource: text, contained, extension, and modifierExtension
  "identifier" : [{ Identifier }], // An identifier for this patient
  "active" : <boolean>, // Whether this patient's record is in active use
  "name" : [{ HumanName }], // A name associated with the patient
  "telecom" : [{ ContactPoint }], // A contact detail for the individual
  "gender" : "<code>", // male | female | other | unknown
  "birthDate" : "<date>", // The date of birth for the individual
  // deceased[x]: Indicates if the individual is deceased or not. One of these 2:
  "deceasedBoolean" : <boolean>,
  "deceasedDateTime" : "<dateTime>",
  "address" : [{ Address }], // An address for the individual
  "maritalStatus" : { CodeableConcept }, // Marital (civil) status of a patient
  // multipleBirth[x]: Whether patient is part of a multiple birth. One of these 2:
  "multipleBirthBoolean" : <boolean>,
  "multipleBirthInteger" : <integer>,
  "photo" : [{ Attachment }], // Image of the patient
  "contact" : [{ // A contact party (e.g. guardian, partner, friend) for the patient
    "relationship" : [{ CodeableConcept }], // The kind of relationship
    "name" : { HumanName }, // I A name associated with the contact person
    "telecom" : [{ ContactPoint }], // I A contact detail for the person
    "address" : { Address }, // I Address for the contact person
    "gender" : "<code>", // male | female | other | unknown
    "organization" : { Reference(Organization) }, // I Organization that is associated with the contact
    "period" : { Period } // The period during which this contact person or organization is valid to be contacted relating to this patient
  }],
  "communication" : [{ // A language which may be used to communicate with the patient about his or her health
    "language" : { CodeableConcept }, // R!  The language which can be used to communicate with the patient about his or her health
    "preferred" : <boolean> // Language preference indicator
  }],
  "generalPractitioner" : [{ Reference(Organization|Practitioner|
   PractitionerRole) }], // Patient's nominated primary care provider
  "managingOrganization" : { Reference(Organization) }, // Organization that is the custodian of the patient record
  "link" : [{ // Link to a Patient or RelatedPerson resource that concerns the same actual individual
    "other" : { Reference(Patient|RelatedPerson) }, // R!  The other patient or related person resource that the link refers to
    "type" : "<code>" // R!  replaced-by | replaces | refer | seealso
  }]
}

As you can see, it covers practically all the administrative information needs of a patient.

Recovering a patient from our HIS

If you remember from previous articles we have deployed a PostgreSQL database that simulates the database of an HIS system, let's take a look at the example tables that we have in our particular HIS.

There are not many, but they will be enough for our example. Let's look at our patient table in a little more detail.

Here we have our 3 example patients, as you can see each one has a unique identifier (ID) as well as a series of administrative data relevant to the health organization. Our first objective will be to obtain the FHIR resource for one of our patients.

Patient consultation

How can we request patient data from our server? According to the implementation specification made by FHIR, we must perform a GET via REST to a URL with the address of our server, the name of the resource and the identifier. We must invoke:

http://SERVER_PATH/Patient/{id}

For our example we will perform a search for Juan López Hurtado, with his id = 1, so we must invoke the following URL:

http://localhost:52774/Adapter/r4/Patient/1

For testing we will use Postman as a client. Let's see what the server's response is:

{
    "resourceType": "Patient",
    "address": [
        {
            "city": "TERUEL",
            "line": [
                "CALLE SUSPIROS 39 2ºA"
            ],
            "postalCode": "98345"
        }
    ],
    "birthDate": "1966-11-23",
    "gender": "M",
    "id": "1",
    "identifier": [
        {
            "type": {
                "text": "ID"
            },
            "value": "1"
        },
        {
            "type": {
                "text": "NHC"
            },
            "value": "588392"
        },
        {
            "type": {
                "text": "DNI"
            },
            "value": "12345678X"
        }
    ],
    "name": [
        {
            "family": "LÓPEZ HURTADO",
            "given": [
                "JUAN"
            ]
        }
    ],
    "telecom": [
        {
            "system": "phone",
            "value": "844324239"
        },
        {
            "system": "email",
            "value": "juanitomaravilla@terra.es"
        }
    ]
}

Now let's analyze the path that our request has taken within our production:

Here we have the path:

  1. Arrival of the request to our BS InteropService.
  2. Forwarding the request to the BP that we have configured as the destination of our BS where the patient identifier of the call received will be recovered.
  3. Query from our BO FromAdapterToHIS to our HIS database.
  4. Forwarding the patient data to our BP and transforming it to an FHIR Patient resource.
  5. Forwarding the response to the BS.

Let's take a look at the type of message we receive in our BP ProcessFHIRBP:

Let's look at three attributes that will be key to identifying what type of operation has been requested from the client:

  • Request.RequestMethod: which tells us what type of operation we are going to perform. In this example, the search for a patient will be a GET.
  • Request.RequestPath: this attribute contains the path of the request that arrived at our server, this attribute will indicate the resource on which we will work and in this case it will include the specific identifier for its recovery.
  • Quick.StreamId: FHIR Adapter will transform every FHIR message received into a Stream and assign it an identifier that will be saved in this attribute. For this example we will not need it since we are performing a GET and we are not sending any FHIR object.

Let's continue with the journey of our message by analyzing in depth the GLP responsible for the processing.

ProcessFHIRBP:

We have implemented a BPL in our production that will manage the FHIR messaging that we receive from our Business Service. Let's see how it is implemented:

Let's see the operations that we will perform in each step:

Manage FHIR object:

We will invoke the BO FromAdapterToHIS responsible for the connection to the HIS database and which will be in charge of the database query.

Method ManageFHIR(requestData As HS.FHIRServer.Interop.Request, response As Adapter.Message.FHIRResponse) As %Status
{
  set sc = $$$OK
  set response = ##class(Adapter.Message.FHIRResponse).%New()

  if (requestData.Request.RequestPath = "Bundle")
  {
    If requestData.QuickStreamId '= "" {
        Set quickStreamIn = ##class(HS.SDA3.QuickStream).%OpenId(requestData.QuickStreamId,, .tSC)
        
        set dynamicBundle = ##class(%DynamicAbstractObject).%FromJSON(quickStreamIn)

        set sc = ..GetBundle(dynamicBundle, .response)
      }
    
  }
  elseif (requestData.Request.RequestPath [ "Patient")
  {
    if (requestData.Request.RequestMethod = "POST")
    {
      If requestData.QuickStreamId '= "" {
        Set quickStreamIn = ##class(HS.SDA3.QuickStream).%OpenId(requestData.QuickStreamId,, .tSC)
        
        set dynamicPatient = ##class(%DynamicAbstractObject).%FromJSON(quickStreamIn)

        set sc = ..InsertPatient(dynamicPatient, .response)
      }      
    }
    elseif (requestData.Request.RequestMethod = "GET")
    {
      set patientId = $Piece(requestData.Request.RequestPath,"/",2)
      set sc = ..GetPatient(patientId, .response)
    }

  }
  Return sc
}

Our BO will check the message received of type HS.FHIRServer.Interop.Request, in this case by setting a GET and indicating in the path that corresponds to the Patient resource the GetPatient Method will be invoked, which we will see below:

Method GetPatient(patientId As %String, Output patient As Adapter.Message.FHIRResponse) As %Status
{
  Set tSC = $$$OK
  set sql="SELECT id, name, lastname, phone, address, city, email, nhc, postal_code, birth_date, dni, gender FROM his.patient WHERE id = ?"
  //perform the Select
  set tSC = ..Adapter.ExecuteQuery(.resultSet, sql, patientId)
  
  If resultSet.Next() {
     set personResult = {"id":(resultSet.GetData(1)), "name": (resultSet.GetData(2)), 
        "lastname": (resultSet.GetData(3)), "phone": (resultSet.GetData(4)), 
        "address": (resultSet.GetData(5)), "city": (resultSet.GetData(6)),
        "email": (resultSet.GetData(7)), "nhc": (resultSet.GetData(8)),
        "postalCode": (resultSet.GetData(9)), "birthDate": (resultSet.GetData(10)),
        "dni": (resultSet.GetData(11)), "gender": (resultSet.GetData(12)), "type": ("Patient")}

   } else {
     set personResult = {}
   }
  
  //create the response message
  do patient.Resource.Insert(personResult.%ToJSON())
 
	Return tSC
}

As you can see, this method only launches a query on the database of our HIS and recovers all the patient information, then generates a DynamicObject that is subsequently transformed into a String and stored in a variable of the type Adapter.Message.FHIRResponse. We have defined the Resource property as a String list to be able to display the response later in the trace. You could define it directly as DynamicObjects, saving subsequent transformations.

Check if Bundle:

With the response from our BO we check if it is a Bundle type (we will explain it in a future article) or if it is just a Resource.

Create Dynamic Object:

We transform the BO response into a DynamicObject and assign it to a temporary context variable (context.temporalDO). The function used for the transformation is the following:

##class(%DynamicAbstractObject).%FromJSON(context.FHIRObject.Resource.GetAt(1))

FHIR transform:

With our temporary variable of type DynamicObject we launch a transformation of it to an object of class HS.FHIR.DTL.vR4.Model.Resource.Patient. If we want to look for other types of resources we would have to define particular transformations for each type. Let's see our transformation:

This transformation allows us to have an object that our BS InteropService can interpret. We will store the result in the variable context.PatientResponse.

Assign resource to Stream:

We convert the variable context.PatientResponse obtained in FHIR transform to Stream.

Transform to QuickStream:

We assign to the response variable all the data that we must return to our client:

 set qs=##class(HS.SDA3.QuickStream).%New()
 set response.QuickStreamId = qs.%Id()
 set copyStatus = qs.CopyFrom(context.JSONPayloadStream)
 set response.Response.ResponseFormatCode="JSON"
 set response.Response.Status=200
 set response.ContentType="application/fhir+json"
 set response.CharSet = "utf8"
  

In this case we are always returning a 200 response. In a production environment we should check if we have correctly recovered the searched resource, and if not, modify the status of the response from 200 to a 404 corresponding to a "Not found". As you can see in this code fragment, the object HS.FHIR.DTL.vR4.Model.Resource.Patient
is transformed into a Stream and stored as a HS.SDA3.QuickStream, adding the identifier of said object to the QuickStreamID attribute, subsequently our InteropService service will return the result correctly as a JSON.

Conclusion:

Let's summarize what we have done:

  1. We have sent a GET type request to search for a Patient resource with a defined ID.
  2. The BS InteropService has forwarded the request to the configured BP.
  3. The BP has invoked the BO responsible for interacting with the HIS database.
  4. The configured BO has retrieved the patient data from the HIS database.
  5. The BP has transformed the result into an object understandable by the BS created by default InteropService.
  6. The BS has received the response and has forwarded it to the client.

As you can see, the operation is relatively simple, if we want to add more types of resources to our server we will only have to add in our BO the query to the tables of our database that correspond to the new resource to be recovered and include in our BP the transformation of the result of our BO to an object of type HS.FHIR.DTL.vR4.Model.Resource.* that corresponds.

In our next article we will review how we can add new FHIR resources of the Patient type to our HIS database.

Thank you all for your attention!

3 Comments
Discussão (3)1
Entre ou crie uma conta para continuar
Pergunta
· Out. 13, 2023

Python Integration Issues on Remote IRIS Server

Hi everyone, 

I'm attempting to compile a basic Python code on a remote server, but it appears that the compiler doesn't recognize the language.

The remote server is running a virtual machine with Oracle Linux Server 7.9 (64-bit), and it has IRIS for UNIX (Red Hat Enterprise Linux for x86-64) 2021.1 (Build 215U) [HealthConnect:3.3.0] installed.

When I try to compile a script that includes a Python ClassMethod, such as this "testpy.cls":

ClassMethod python() As %Status [ Language = python ]
{
    # prova python
    print("hello world")
}

This error returns (Error #5486: Invalid method language / Error #5030 An error occurred while compiling class '<className>'): 

Compilation started on 10/13/2023 17:01:41 with qualifiers 'cuk'
Compiling class <className>
ERROR #5486: Invalid method language: <className>:MethodName:python
  > ERROR #5030: An error occurred while compiling class '<className>'
Detected 1 errors during compilation in 0.090s.

I've attempted to follow the instructions provided on this page: Python Prerequisites, but I couldn't resolve this problem. The problem persisted, even after executing "sudo su -" command to obtain root privileges before trying to install python3 or a python package, like numpy, through the command "yum install python3".

I also attempted to write some ObjectScript code, recalling ##class(%SYS.Python).Builtins() or ##calls(%SYS.Python).Shell(), such as: 

ClassMethod HelloWorld() As %Status
{
    set pythonBuiltins = ##class(%SYS.Python).Builtins()
    do pythonBuiltins.print("hello world")
}

However, I encountered an error indicating that these methods do not exist.

The same Python codes work fine on my local instance of IRIS and I can't figure out why they doesn't work on the remote server instance.

I've identified the path of the Python packages folder on my computer (C:\InterSystems\IRISHealth\mgr\python) and noticed the only big difference between the local and remote instances. While the local folder includes several subfolders (such as numpy, pandas, etc.), the corresponding folder on the remote server (u01/<instanceName>/mgr/python) is empty, despite multiple attempts to install python3 and packages like numpy or pandas, even if executed directly from the mgr/python folder. 

It is like Python and its packages are installed but invisible. In fact, if I try to run the command yum install python3 again, it returns this message, even if the folder is still empty:

Loaded plugins: langpacks, ulninfo
Package python3-3.6.8-19.0.1.el7_9.x86_64 already installed and latest version
Nothing to do

I've tried to clean some space on the remote server and re-tried the installation with 2.5 Gb of free space available, but nothing changed. 

Does anybody know how to handle this?

4 Comments
Discussão (4)1
Entre ou crie uma conta para continuar
Artigo
· Out. 10, 2023 6min de leitura

IRISのライセンス使用量の推移を調査する方法

弊社FAQサイトや開発者コミュニティには、ライセンスに関する記事を数多く掲載しています。

こちらの記事では、上記記事でご紹介している様々な機能をTipsとして使い、実際にライセンス使用量の推移を調査する方法をご紹介します。

1.ライセンス使用状況をスポットで確認する方法(今現在の使用状況)

2.ある一定期間のライセンス使用状況を継続して監視する方法
 

1.ライセンス使用状況をスポットで確認する方法(今現在の使用状況)

現在のライセンス使用量は管理ポータルで確認できます。
[システムオペレーション] > [ライセンス使用量] 

 

※各項目の意味は以下の記事を参照してください。
管理ポータルのライセンス使用量のページに表示される値の意味
 

具体的にどのようなユーザがライセンスを消費しているのかは、「ユーザ毎の使用」で見られますが、こちらはローカルのサーバのインスタンスのみの情報となります。
リモート接続しているインスタンスへのユーザ毎の使用情報は、リモートのサーバ上のインスタンスで確認する必要があります。

また、現在のライセンス使用状況は ライセンスユーティリティ(%SYSTEM.License クラスのメソッド)を使用してコマンドでファイル出力することも可能です。

以下の記事の添付ファイルに、使用例を掲載しております。
使用中のライセンス情報を取得する方法
 

2.ある一定期間のライセンス使用状況を継続して監視する方法

ある一定期間ライセンスの使用状況を監視する方法として、履歴モニタを使用する方法があります。
こちらは、どの時間帯に最もライセンスが消費されるのかを調べる際に使用できます。
履歴モニタの使用方法は、ドキュメント と、パフォーマンス調査ガイド(P.14~)をご覧ください。

こちらの記事では、履歴モニタを使用して実際にライセンス情報を監視する方法とその計測結果(サンプル)をご案内します。

ライセンス使用状況(現在使用中のライセンス数:ローカル/リモート)は、アプリケーションモニタで提供されている %Monitor.System.HistorySys クラスを有効化することで情報収集できます。

こちらの設定をすると、既定で5分(900秒)ごとにライセンス使用状況を含むシステム使用情報を収集します。
データは永続クラスとして保存されるため、標準 SQL または永続オブジェクトアクセスを使用して利用できます。

SQLを使用する場合は、以下のようにして情報を確認することができます。
※Sys_LicUsedLocal:ローカルのライセンス使用量、Sys_LicUsedDist:リモートのライセンス使用量

select ID,DateTime,Sys_LicUsedLocal,Sys_LicUsedDist from SYS_History.SysData


出力結果は、以下のように 5分ごとのライセンスの使用状況が確認できます。
 
 

設定手順は以下のようになります。
%Monitor.System.HistorySys クラスを有効化したあと、システムモニタの再起動が必要となります。

 
^%SYSMONMGRユーティリティを使用した履歴モニタの設定方法


既定のインターバルは 5分(900秒)になりますが、こちらを変更することも可能です。
変更の方法は以下になります。
※変更後、システムモニタの再起動を行うようにしてください。

 
履歴モニタのインターバル設定方法


enlightened【ご参考】
使用中のライセンス情報の取得方法を教えてください。
管理ポータルのライセンス使用量のページに表示される値の意味
ライセンスサーバでライセンスを一元管理する方法
 

Discussão (0)0
Entre ou crie uma conta para continuar
Pergunta
· Out. 10, 2023

Connection Refused ao utilizar o IRIS Community no java com JDBC

Estou utilizando JDBC para conexão da aplicação JAVA com o IRIS versão Community, porém está ocorrendo o seguinte erro:

Caused by: java.sql.SQLException: [InterSystems IRIS JDBC] Communication link failure: Acesso Negado
    at com.intersystems.jdbc.IRISConnection.connect(IRISConnection.java:751)
    at com.intersystems.jdbc.IRISConnection.<init>(IRISConnection.java:165)
    at com.intersystems.jdbc.IRISDriver.connect(IRISDriver.java:58)
    ... 46 more

Ao verificar o relatório de auditoria do banco IRIS verifiquei o seguinte log de erro:

Mensagem de erro: ERRO #5915: Não foi possível alocar uma licença
Nome do serviço: %Service_Bindings
$I: |TCP|1972|37861
$P: |TCP|1972|37861

3 Comments
Discussão (3)3
Entre ou crie uma conta para continuar
Pergunta
· Out. 8, 2023

Automatic prettification of JSON in Visual Trace

Hello everyone,

Is there a way to prettify the JSON streams in the incoming HTTP messages so that they are more readable in Visual Trace?

The HTTP messages sent by many software aren't prettified and are displayed like this in the Visual Trace:

On the other hand, other software like Postman prettify the messages and they are displayed in this way:

I know there is a JSON Formatter class, however what I would like is an automatic setting that automatically prettifies the JSON displayed in the Visual Trace without having to go through code.

Is there anything similar? 

3 Comments
Discussão (3)1
Entre ou crie uma conta para continuar