Salesforce Apex API 连接到 FHIR 服务器





0/5 (0投票)
如何使用 Salesforce Apex 语言(非常像 Java)连接到 FHIR(电子病历)服务器进行 API 调用(GET、POST)
引言
这提供了足够的 Salesforce 背景知识和代码,可以使用 Salesforce Apex 语言(非常像 Java 或 C#,并且这段代码可以非常轻松地修改以在 Java 或 C# 中工作,有些甚至无需修改)来对 FHIR(电子健康记录)服务器进行 GET
和 POST
API 调用。
Salesforce 目前是一个基于 Web 组件的 SAS 解决方案,它是一个流行的开箱即用的销售和 CRM 解决方案。但它被设计成非常可扩展的。我目前的评估是,它非常健壮,是一个好产品。看起来他们只是抓住了他们能找到的所有最好的技术并将其整合在一起。(我认出了很多部分。)这意味着你可以用它做很多事情,但为了避免烦恼,尽量弄清楚 Salesforce 的做事方式,以及他们使用了什么(很多 CSV)。
我会尝试添加一些介绍性信息,因为 Salesforce 在美国似乎相当新颖。Salesforce 提供了大量的学习资源,来自 YouTube 和其他网络来源。这个生态系统非常非常面向开发人员,拥有免费的开发区域,并附带强大的开发工具,包括 Visual Studio Code 的扩展(这意味着重构不是一件有趣的事情,所以请非常仔细地选择变量名)。顺便说一句,我是一名 .NET 专家,但被聘为合同工,因为他们找不到 Salesforce 开发人员。它可能在印度更受欢迎,并且大部分 YouTube 资源都是由印度人制作的,他们很聪明。Salesforce 是一个令人印象深刻的系统,但我对它除了名字和它很大之外一无所知。我写这篇帖子的目的是填补一些我不得不努力学习的内容。我希望它足够聚焦,能够有用,但我的工作范围相当广泛。
虽然该项目与 Lightning(Salesforce Web 组件实现)无关,但我还是会提到它,因为它很重要。它是 UI,并且由于是 Web 组件,它非常通用,内置了 AJAX。由于其健壮性,它的学习曲线很陡峭。它似乎是 Web 组件(大量的 JavaScript),下面有一些类似 Bootstrap、jQuery、jQuery validation 等东西。这让我想起了 ASP.NET 2.0 Web Parts。因此,它很强大,但学习曲线很艰难。
在深入研究之前,请先学会使用 Salesforce 开发人员控制台。如果您正在寻找这段代码,您可能已经知道了。
首先,Lightning 中的 UI。Lightning 非常庞大,我并没有真正学到多少。我确实使用了 ChatGPT 来制作其中一些。这里有足够的 Lightning 代码来调用和返回 API 值。“说了这么多”。关于 Lightning 的 YouTube 播放列表有很多。
API - 从 Postman 或类似的工具开始所有 API 项目。市面上有许多测试 API,其中许多是免费的(但许多不起作用),但它们倾向于通过一个需要信用卡的服务。StackExchange 有一个非常有用的 API,你可以免费测试。
FHIR 快速医疗保健互操作性资源(本文代码相关的 API)- 电子健康记录。不同的记录类型称为“资源”。它们可能是 Patient 资源。它们可能是 Observation 资源(所有患者的就诊记录)。它们可能是其他类型的,但每种都代表一个 JSON 记录类型。HL7 规范列出了所有不同的资源类型以及它们的数据类型/要求。对于任何从事医疗保健软件开发的人来说,了解这一点可能很有价值,并且非常适合测试 API 代码。http://hl7.org/fhir/index.html 是 HL7 标准。现在它看起来很光滑,但更光滑的是 Sidharth Ramesh 的 YouTube 视频播放列表,他是 FHIR 和电子病历方面的专家。这是学习所有电子健康记录和使用 Postman 进行 FHIR 开发的绝佳资源。
Using the Code
最后,代码... 但这不是你想要的代码。(开发者思维技巧)... 顺便说一下,我的代码往往很混乱,里面充斥着关于什么有效、什么失败以及注意事项的注释。我偶尔会为此受到感谢。我编写代码已经很久了,写过非常复杂的东西。它应该像一个故事一样阅读。它可能看起来与许多编码风格不同……但我取得了巨大的成功。请记住,软件的主要成本是维护。我的代码就是为此而写的。
Salesforce 代码分为两部分:类(*.cls 文件)和 Lightning Web Components(LWC)。.cls 文件包含 API 代码。LWC 是标准的 Web 组件类型,因此包括一个 HTML 文件、一个 JavaScript 文件、一个清单文件和一个可选的 CSS 文件,所有文件名称相同,但文件扩展名不同……所有文件都在同名的文件夹中。(查看与代码匹配的文件名。)当你使用 CLI 创建新的 LWC 时,它会为你生成模板。所有 4 个 LWC 文件都将位于 CLI 在创建新 LWC 对象时创建的 apiFhirPost 文件夹中。本文是关于 API 代码的,但你需要 Lightning UI 代码来调用它,所以这里是你需要的最少内容
apiFhirPost.html
你会注意到 Bootstrap 的外观...
<template>
<lightning-card title="Click button for FHIR API POST Test"
class="my-card" style="width: 100%;">
<div class="slds-m-around_medium">
<lightning-input type="text" label="Enter Text" class="clTextInput"
value={inputValue} onchange={handleInputChange}></lightning-input>
<div class="slds-m-top_medium">
<lightning-button variant="brand" label="FHIR API POST"
onclick={handleButtonClick}></lightning-button>
</div>
<div class="slds-m-top_medium">
<lightning-formatted-text class="clResponse" value={response}>
</lightning-formatted-text>
</div>
</div>
</lightning-card>
</template>
apiFhirPost.js
console.log
语句没有性能损失,并且在产品上线数月后,它们可能会为你节省很多麻烦。请明智地使用它们。
import { LightningElement, api, wire, track } from 'lwc';
import {getRecord, getFieldValue} from 'lightning/uiRecordApi';
import calloutForStack from
'@salesforce/apex/ApiFhirPost.calloutForStack' //importing method of apex class to js
export default class ApiFhirPost extends LightningElement {
@api recordId; /* Will have AccountId on Account page. */
@api sname; /* Will have Name on Account page. */
@api semail; /* Will have email on Account page. */
@api response;
@track inputValue;
// There are two ways of doing this.
//@wire(getRecord, {recordId: '$recordId', fields:[NAME_FIELD,EMAIL_FIELD]})
//@wire(getRecord, {recordId: '$recordId',
//fields:['Contact.Name','Contact.Email']})
//@wire(getRecord, {recordId: '$recordId', fields:FIELDS})
//record; // data and error
@wire(calloutForStack, { inputString: '$inputValue' }) //calling apex class method
wiredMethod({ error, data }) {
if (data) {
console.log("apex/ApiFhirPost(12) wiredMethod data: " + data + '...');
this.response = data;
console.log("apex/ApiFhirPost(14) wiredMethod responce: " +
this.response + '...');
} else if (error) {
console.error(error);
}
}
imAFunction(inptster) {
console.log('ImAFunction(53.js): ' + inptster + '...');
}
// This is used because the onChange event is coming from the input and can be read.
// When the button is read, it is the button causing the event, not the input.
// The problem is that this also seems to be triggering the
// Apex code (wiredMethod). Verified
handleInputChange(event) { ;
// this.inputValue= event.target.value;
// console.log('apex/ApiFhirPost.handleInputChange(1f.js):' +
// this.inputValue + '...');
}
handleButtonClick() {
console.log('apex/ApiFhirPost.handleButtonClick(1a.js)');
//var x = document.getElementById("textInput2").value; // Cannot use id
// as a selector. SF modifies them.
//document.getElementById("demo").innerHTML = x;
console.log('apex/ApiFhirPost.handleButtonClick(65)
Do calloutForStack()'); //: ' + this.inputValue + '...'); // [object promise]
//var responce = handleButtonClick();
///// responce = handleButtonClick(this.inputValue); // old method,
// nada comes back
// Very interesting article about sending parameters to Apex
// https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/
// controllers_server_apex_pass_data.htm
// @wire(handleButtonClick, { responce }); // Syntax
// Get a reference to the input element ... this works...
const inputEl = this.template.querySelector('lightning-input');
// Get the value of the input element
const inputValue1 = inputEl.value;
// Perform action based on input value
console.log("ApiFhirPost.handleButtonClick(77) input: " + inputValue1 + '...');
//this.response = 'You entered: ' + inputValue1;
// Get a reference to the input element by Id... Fails,
// but using a class as a selector works.
// This does not work
// "The reason is lwc framework changes the id you define in the
// HTML template to some unique values and that
// is where your defined id becomes irrelevant.
// As per the documentation you should not use ID selectors to
// find element in the DOM."
//const myInput = this.template.querySelector('#idTextInput'); // This fails
// as expected. Id gets modified by SF
// If you need to select an element outside of the component's scope, you can use
// the standard document.querySelector() method instead...
//const myInput = document.querySelector('#idTextInput'); // This fails too.
//console.log("ApiFhirPost.handleButtonClick(51a) inputId: " +
//myInput.value); // Output: the value of the input element
const myInputCl = this.template.querySelector('.clTextInput'); // This works
console.log("ApiFhirPost.handleButtonClick(78a) inputCl: " +
myInputCl.value); // Output: the value of the input element
var strOutputter = '';
//strOutputter = calloutForStack(myInputCl.value); // This is a promise.
//That's a problem...
console.log("ApiFhirPost.handleButtonClick(81) inputCl: " +
strOutputter); // Output: the value of the input element
var apiOutput = ''; // Call Apex method with input value
calloutForStack({ inputString: this.inputValue })
.then(result => {
//console.log("MyDemo.handleButtonClick30) input: " +
//this.inputValue + '...');
console.log("ApiFhirPost.handleButtonClick(86) input: " +
myInputCl.value + '...');
// For now... "System.HttpResponse[Status=OK, StatusCode=200]"
// value of div ... Seems to have leading and
// trailing double quotes - artifact
this.response = result;
console.log("ApiFhirPost.handleButtonClick(89) input: " +
result + '...');
// Try to remove leading and trailing double quotes
// so div value dosn't show as literal. Don't
// this.response = result.substring(1,result.length-1);
//console.log("ApiFhirPost.handleButtonClick(91) input: " +
//result + '...');
console.log("ApiFhirPost.handleButtonClick(92) input: " +
JSON.stringify(result) + '...');
apiOutput = JSON.parse(result);
console.log("ApiFhirPost.handleButtonClick(94) data: " +
apiOutput.data + '...');
// imAFunction(apiOutput); // it would be nice if this worked
// but it isn't essential.
console.log("ApiFhirPost.handleButtonClick(97) Enough nonsense...");
})
.catch(error => {
console.error(error);
});
// The returned value is good and being displayed, but it's not reaching here.
console.log('apex/ApiFhirPost.handleButtonClick
(100.js): ' + result + '...');
console.log('apex/ApiFhirPost.handleButtonClick(101.js): ' +
apiOutput + '...');
}
}
apiFhirPost.css
这是一个可选文件。这段代码实际上不应该需要,但它确实需要。
.my-card {
display: block;
border: 0.2rem outset black;
}
apiFhirPost.js-meta.xml
只是其中一部分,你应该已经知道了原因(默认版本控制)。
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
Apex 代码
SendEmails.cls
免费赠品——要从 Salesforce 页面中获取数据或信息可能非常困难。你无法从屏幕页面复制粘贴。调试器只能做到这一点。还有其他问题,所以有时你只能通过电子邮件发送给自己。
// Usage
// SendEmails.sendEmail('bgates@microsquish.com', 'Subject 1', 'Body, what a Fox');
public class SendEmails{
public static void sendEmail(String sSendTo, String sSubject, String sBody)
{
//String strTestString = 'For whom the bell tolls.';
//Messaging.EmailFileAttachment csvAttachment =
// new Messaging.EmailFileAttachment();
//Blob csvBlob = blob.valueOf(generatedCSVFile);
//String csvName = 'testfile092022c.csv';
//csvAttachment.setFileName(csvName);
//csvAttachment.setBody(csvBlob);
System.debug('SendEmails.sendEmail().........................');
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
//String[] toAddresses = new String[]{'mbreeden@lakecountyil.gov'};
String[] toAddresses = new String[]{sSendTo};
//String subject = 'Test File testfile092022ac';
String subject = sSubject;
email.setSubject(subject);
email.setToAddresses(toAddresses);
//email.setPlainTextBody('Do not ask for whom the bell tolls.
//It tools for thee.');
email.setPlainTextBody(sBody);
//email.setFileAttachments(new Messaging.EmailFileAttachment[]{csvAttachment});
Messaging.SendEmailResult[] r = Messaging.sendEmail
(new Messaging.SingleEmailMessage[]{email});
}
public static void sendEmailStringList(String sSendTo,
String sSubject, List<string> lststr)
{
System.debug('SendEmails.sendEmailStringList().........................');
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setSubject(sSubject);
email.setToAddresses(new String[]{sSendTo});
String sBody = string.join(lststr,',');
email.setPlainTextBody(sBody);
Messaging.SendEmailResult[] r = Messaging.sendEmail
(new Messaging.SingleEmailMessage[]{email});
}
}
FHIRAPiClasses.cls
这些是将被序列化为 JSON 以在 REST 请求中发送到 FHIR 服务器的类,以及从服务器响应中反序列化的类。对于你想要使用的所有 API,你几乎都需要它们。请原谅长度,但这就是它或...你可以自己输入。
/*
FHIR - Fast Healthcare Interoperability Resources
This is based on the resources at "http://http://hl7.org/ - FHIR Documentation.
This file is to define FHIR resources for communication
with the test FHIR server there.
The objects defined here reflect the JSON returned by
a GET API call to that FHIR server.
Message Returned by Postman from GET call - http://hapi.fhir.org/baseR4/Patient/8127780
(1) One exercise is to instantiate and populate a [Patient]
resource object to make the JSON message to put in the body of
a POST call to pass the new [Patient] record to a FHIR server
(and return the FHIR id assigned to the [Patient]).
A question arises. If you instantiate a [Patient] resource object in Apex,
is it going to instantiate the member objects in it
such as the List<nameResource>. Some languages do, some don't.
When deserializing from JSON, the deserialization code has
to take care of that automatically, but if you
instantiate a [Patient] object to use for the body of a POST call, do you have
to instantiate those member objects yourself?
The Development Console should answer that
question. The following code will only work if nameResource.given is instantiated as a
List upon instantiation of nameResource
FHIRAPiClasses.nameResource name = new FHIRAPiClasses.nameResource();
name.use = '';
name.family = 'Zagwap';
name.given = new List<String>(); // This is required...
name.given.Add('Freddy')
System.debug('Name: ' + name.family + '...');
... Null Pointer Exception... It does not instantiate them...
and must be done manually in the class code.
(2) When making the POST call, it looked in the log like
the body might be getting truncated by JSON.serialize()
so add the code to manually serialize. It's that or the error message:
"diagnostics": "HAPI-0446: Incorrect Content-Type header value of
\"application/x-www-form-urlencoded\" was provided in the request.
A FHIR Content-Type is required for \"CREATE\" operation"
... Or both...
Let's add/replace the header Content-Type: application/xml+fhir" -X
POST -d ... NO, this is JSON.
*/
public with sharing class FHIRAPiClasses {
public FHIRAPiClasses() {}
public class patientResponseResource {
public patientResponseResource() {
this.meta = new metaResource();
this.text = new textResource();
this.name = new List<nameResource>();
this.telecom = new List<telecomResource>();
this.address = new List<addressResource>();
}
public String resourceType {get;set;}
public String id {get;set;}
public metaResource meta {get;set;}
public textResource text {get;set;}
public List<nameResource> name {get;set;}
public List<telecomResource> telecom {get;set;}
public String gender {get;set;}
public String birthdate {get;set;}
public List<addressResource> address {get;set;}
// This is just to make a useful data output for testing purposes.
public String strNameIdAddress()
{
String dname = ''; // like data name to avoid confusion.
if(this.name.size() > 0)
{
dname = this.name[0].family + ', '; // make it easy
if(this.name[0].given.size() > 0)
dname = dname + this.name[0].given[0];
}
String dId = this.id;
String dAddress = '';
if(this.address.size() > 0)
{
if(this.address[0].line.size() > 0)
{
for (string sline : this.address[0].line)
dAddress += sline + ', ';
}
dAddress += this.address[0].city + ', ';
dAddress += this.address[0].state;
}
return dname + ' ID:' + dId + ' - ' + dAddress;
}
} // End public class patientResponseResource {
public class metaResource {
public metaResource()
{
this.versionId = '';
this.lastUpdated = '';
this.source = '';
}
public metaResource(String VersionId, String LastUpdated, String Source)
{
this.versionId = VersionId;
this.lastUpdated = LastUpdated;
this.source = Source;
}
public String versionId {get;set;}
public String lastUpdated {get;set;}
public String source {get;set;}
}
public class textResource {
public textResource()
{
this.status = '';
this.div = '';
}
public textResource(String Status, String Div)
{
this.status = Status;
this.div = Div;
}
public String status {get;set;}
public String div {get;set;}
}
// FHIRAPiClasses.nameResource name =
// new FHIRAPiClasses.nameResource('official','Byrne','Billy');
public class nameResource {
public nameResource() { given = new List<String>(); }
public nameResource(String Use, String Family, String Given)
{ this.given = new List<String>();
this.use = Use;
this.family = Family;
this.given.Add(Given);
}
public void addNameGiven(String input) {this.given.Add(input);}
public String use {get;set;}
public String family {get;set;}
public List<String> given {get;set;}
}
// The JSON from FHIR contains a reserved word "system.
// In the Apex code, it needs to do a string replace on the json string
// likejsonString.replace('"system":', '"system_x":');
// Otherwise, you will need to use the JSON.createGenerator() method
// to build the JSON manually
// (https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/
// apexcode/apex_class_System_Json.htm#apex_System_Json_createGenerator).
// This will be incredibly more time consuming than using the
// standard serialization.
public class telecomResource {
public telecomResource(){}
public telecomResource(String System_x, String Value, String Use)
{
this.system_x = System_x;
this.value = Value;
this.use = Use;
}
//public String system {get;set;} // "system" "System" are reserved words
public String system_x {get;set;}
public String value {get;set;}
public String use {get;set;}
}
public class addressResource {
public addressResource()
{
line = new List<String>();
// So it doesn't send nulls
this.city = '';
this.state = '';
this.PostalCode = '';
}
public addressResource(String Line, String City,
String State, String postalCode)
{ this.line = new List<String>();
this.line.Add(Line);
this.city = City;
this.state = State;
this.PostalCode = postalCode;
}
public void addAddressLine(String input) {this.line.Add(input);}
public List<String> line {get;set;}
public String city {get;set;}
public String state {get;set;}
public String PostalCode {get;set;}
}
/*
This seems to be the standard error message format from FHIR.
Got it for different things.
{
"resourceType": "OperationOutcome",
"text": {
"status": "generated",
"div": "<div xmlns=\"https://gcc02.safelinks.protection.outlook.com/
?url=http%3A%2F%2Fwww.w3.org%2F1999%2Fxhtml%2F&data=05%7C01%7
Cmbreeden%40lakecountyil.gov%7C803972ae963443e090ff08db1e4d611
7%7Cdd536cf592fd42ffa754e98666cb7a96%7C0%7C0%7C638137089921009
656%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMz
IiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000%7C%7C%7C&sdata=lA9CQDY
kRynsC4IhDE46sBiDXXt40%2BfPAxI8Ei0H0%2Fk%3D&reserved=0">
<h1>Operation Outcome</h1><table border=\"0\"><tr>
<td style=\"font-weight: bold;\">
ERROR</td><td>[]</td><td>HAPI-0446: Incorrect Content-Type header value of
"application/x-www-form-urlencoded" was provided in the request.
A FHIR Content-Type is required for "CREATE" operation</td></tr></table>
</div>"
},
"issue": [ {
"severity": "error",
"code": "processing",
"diagnostics": "HAPI-0446: Incorrect Content-Type header value of
\"application/x-www-form-urlencoded\" was provided in the request.
A FHIR Content-Type is required for \"CREATE\" operation"
} ]
}
*/
public class errorResource {
public String resourceType {get;set;}
public textResource text {get;set;}
public List<issueResource> issue {get;set;}
public errorResource()
{
this.resourceType = '';
this.text = new textResource();
this.issue = new List<issueResource>();
}
}
public class issueResource {
public String severity {get;set;}
public String code {get;set;}
public String diagnostics {get;set;}
public issueResource()
{
this.severity = '';
this.code = '';
this.diagnostics = '';
}
}
} // End public with sharing class FHIRAPiClasses
ApiResponseClass1.cls
这用于将处理过的消息从 API 代码传输到 Apex
类,该类会使用它。它的主要目的和用途是能够有一个错误和
数据部分。响应将发送到 LWC 的 JavaScript 函数,因此它需要是
JSON 的 string
格式。... 因为它返回的是 string
而不是 JSON,所以这是必需的吗?你不能
将 Apex 对象返回到 JavaScript,所以这可能需要也可能不需要。... 考虑到
它在某个时候会需要。
是的,这段代码很糟糕,但它有效。
public with sharing class ApiResponseClass1 {
// statusCode and status in one string
@AuraEnabled public String sCompleteStatus{ get; set; }
// Http Status Code
@AuraEnabled public String sStatusCode{ get; set; }
@AuraEnabled public String sdata{ get; set; }
@AuraEnabled public String serror{ get; set; }
public ApiResponseClass1() { } // Default constructor
// Trivial constructor, for server-side Apex -> client-side JavaScript
// ... Really cannot be used as API code is implemented
public ApiResponseClass1(String psStatusCode, String psdata, String pserror) {
this.sStatusCode = psStatusCode;
this.sdata = psdata;
this.serror = pserror;
} // constructor
public void setValues(String psStatusCode, String psdata, String pserror) {
this.sStatusCode = psStatusCode;
this.sdata = psdata;
this.serror = pserror;
}
/*** The rest of this shouldn't be doing anything ***/
//public String sresponse{ get; set; }
public String getToJson(){
/* String sresponse = '{"data":"' +
String.isNotBlank(this.sdata) ? this.sdata : '' +
'","error":"' +
String.isNotBlank(this.serror) ? this.serror : '' +
'"}'; */
String ssStatusCode = '';
String ssdata = '';
String sserror = '';
if(String.isNotBlank(this.sdata) == true)
ssdata = this.sdata;
if(String.isNotBlank(this.serror) == true)
sserror = this.serror;
if(String.isNotBlank(this.sStatusCode) == true)
ssStatusCode = this.sStatusCode;
String sresponse = '{"Data":"' + ssdata + '","Status Code":"' +
ssStatusCode + '","CompleteStatus":"' + sCompleteStatus + '",
"Error":"' + sserror + '"}';
return sresponse;
}
}
apiFhirGet.cls
真是一团糟... 请注意,第二个方法
httpResponse callout(String httpMethod, String endpoint, String body
它对 GET
、POST
、PUT
... 来说是通用的。这段代码可以进行一些修改,但它有效...
// Ultimately based on Integrating Third-party APIs Using APEX RESTful Callouts
// https://www.youtube.com/watch?v=705SeyjpoFs// For now, this will be called by
// ApiFhirGet.cls
//
public with sharing class ApiFhirGet {
public ApiFhirGet() {
}
public static String BASE_URL = 'http://hapi.fhir.org/baseR4/';
public static String tellMeYourName()
{
String sMyName = 'My Name is tellMeYourName(3) in ApiFhirGet.';
system.debug(sMyName);
return sMyName;
}
@AuraEnabled(cacheable=true)
public static String calloutForStack(String sInputString) {
// Do some processing with the input string
ApiResponseClass1 apiResponse = new ApiResponseClass1();
String outputString = 'FhirGet Processed string: ' + sInputString;
String strInput = sInputString; // back to where we were
String httpMethod = 'GET';
system.debug('Method ApiFhirGet.calloutForStack(1a) input:' + strInput + '...');
///// This seems to be a code killer for some reason.
///// Hardcoded works. Using the input from LWC is a problem
///// and returns a 500 server error.
///// It does work with input in the Development Console
//String endPoint = BASE_URL + 'Patient/8271353'; // For some reason only
// this works.
// LWC input fails.
String endPoint = BASE_URL + 'Patient/8301013'; // For some reason only
// this works.
// LWC input fails.
//String endPoint = BASE_URL + 'Patient/' + sInputString.trim(); // Problem
//String endPoint = BASE_URL + 'Patient/' + strInput.trim(); // Problem
//String endPoint = BASE_URL + strInput; // Problem
system.debug('Method ApiFhirGet.calloutForStack(1ep) endpoint: ' +
endPoint + '...');
String strReponseBody = '';
HttpResponse res = callout('GET', endPoint, '');
System.debug('calloutForStack(1ab)..');
//System.debug('calloutForStack GetBody(1s): ' + strReponseBody + '...');
// Response values from code shown in youtube.com/watch?v705SeyjppFs
Integer statusCode=res.getStatusCode(); // ? success or error
System.debug('calloutForStack(1d) statusCode: ' + statusCode + '...');
Integer iStatus= res.getStatusCode(); //
String sStatus= iStatus.format(); //
apiResponse.sStatusCode = iStatus.format(); //
// statusCode and status in one string
// In any case, this will be added to the error if there is one.
//String strEntireStatus = res.toString();
apiResponse.sCompleteStatus = res.toString();
String sResponse = '';
// Looks like 'Status=OK, StatusCode=200'
if(iStatus == 200){
System.debug('calloutForStack(1em) entireStatus: ' +
apiResponse.sCompleteStatus + '...');
// The JSON from FHIR contains a reserved word "system.
// In the Apex code, it needs to do a string replace
// on the json string likejsonString.replace('"system":', '"system_x":');
strReponseBody = res.getBody(); // This is everything received.
strReponseBody = strReponseBody.replace('"system":', '"system_x":');
System.debug('calloutForStack(1ea) body: ' + strReponseBody + '...');
List<String> headerKeys = res.getHeaderkeys();
System.debug('calloutForStack(1fa) Header Size: ' +
String.valueOf(headerKeys.size()) + '...');
// Works good. It shows 14 headers, so to display them
for (String key : headerKeys)
{
// Get a specific header value by key
String svalue = res.getHeader(key);
System.debug('calloutForStack(1fb) Header key=' +
key + '- value=' + svalue + '...');
}
// Working with JSON in Apex: A Primer -
// Shows how to deserialize an unknown JSON string
// https://ktema.org/articles/json-apex-intro/
// https://developer.salesforce.com/docs/
// atlas.en-us.apexref.meta/apexref/apex_methods_system_map.htm
Map<String, Object> result =
(Map<String, Object>)JSON.deserializeUntyped(strReponseBody);
// A set is an unordered collection of elements that do not contain
// any duplicates.
// Set elements can be of any data type—primitive types,
// collections, sObjects, user-defined types, and built-in Apex types.
Set <String> resSet = new Set<String>();
resSet = result.keySet(); // Should be the keys for all objects
List<String> lststr = new List<String>(resSet);
String sq = lststr[0];
System.debug('calloutForStack(1hp) Key: ' +
sq + '...'); // returns 'items' ... makes sense
//system.debug('calloutForStack(1j): ' + Result + '...');
sResponse = '{"Status":"' + sStatus + '"}';
System.debug('calloutForStack(1epm) entireStatus: ' + sResponse + '...');
// This needs replacing. It's probably the best to use.
FHIRAPiClasses.patientResponseResource RR =
(FHIRAPiClasses.patientResponseResource)JSON.deserialize
(res.getBody(),FHIRAPiClasses.patientResponseResource.class);
String ssResponse = RR.text.div.trim();
string sDiv = '<div xmlns="http://www.w3.org/1999/xhtml">';
sResponse = ssResponse.replace(sDiv, '<div>');
// It still displays this as a literal,
// so use different data from the structure
// Parse response to structure here if needed
//strData = RR.strNameIdAddress();
apiResponse.sdata = RR.strNameIdAddress();
system.debug('calloutForStack(112): ' + apiResponse.sdata + '...');
} // End if(iStatus == 200)
else { // There was an error
apiResponse.serror = '(2)Unknown Error GETing FHIR data';
// This should parse the returned error. See ApiFhirPost.cls
}
// When an instance of an Apex class is returned from a server-side action,
// the instance is serialized to JSON by the framework.
// Only the values of public instance properties and
// methods annotated with @AuraEnabled are serialized and returned.
// getToJson(); is so annotated.
//return apiResponse.getToJson(); // This does return JSON as expected,
// but the method makes it.
//return new ApiResponseClass1(apiResponse.sCompleteStatus,
//strReponseBody, strError); // oops
//system.debug('calloutForStack(1p): Status=' + apiResponse.sdata);
//return new ApiResponseClass1(apiResponse.sCompleteStatus,
//strData, strError); // complains
// This is the token that will be returned. @AuraEnabled allows
//for automatic serialization
//ApiResponseClass1 apiResponse = new ApiResponseClass1
//(apiResponse.sCompleteStatus, strData, strError);
//return apiResponse;
sResponse = apiResponse.getToJson();
system.debug('Method ApiFhirGet.calloutForStack(9za) sResponse:' +
sResponse + '...');
//return outputString;
//return apiResponse.sCompleteStatus; // works -
//"System.HttpResponse[Status=OK, StatusCode=200]"...
return sResponse;
//return strInput;
}
// Method to perform a callout and return an httpResponse
// Notice that here, this call is generic because it takes any URL
// Sample for Development Console: ApiFhirGet.callout
// ('GET','https://v2.jokeapi.dev/joke/Any','');
// Warning: Has some nasty jokes unless told to be clean
public static httpResponse callout
(String httpMethod, String endpoint, String body){
//Instantiate an httpRequest and set the required attributes
httpRequest req = new httpRequest();
req.setMethod(httpMethod);
req.setEndpoint(endpoint);
if(string.isNotBlank(body)) {
req.setBody(body);
req.setHeader('Content-Length', string.valueOf(body.Length()));
}
//Optional attributes are often required to conform to the
//3rd Party Web Service Requirements
req.setHeader('Accept-Encoding','gzip, deflate');
// Might be needed for FHIR or else returns just html
req.setHeader('Content-Type','application/json');
// Absolutely needed for FHIR or else returns just html
req.setHeader('Accept','application/json');
//You can adjust the timeout duration (in milliseconds)
//to deal with slow servers or large payloads
req.setTimeout(120000);
req.setCompressed(true);
//Use the HTTP Class to send the httpRequest and receive an httpResposne
/*If you are not using an HttpCalloutMock:
if (!test.isRunningTest){
*/
httpResponse res = new http().send(req);
/*If you are not using an HttpCalloutMock:
}
*/
system.debug(res.toString());
system.debug(res.getBody());
// Response values from code shown in youtube.com/watch?v705SeyjppFs
// This is basically sample code because they can't be used for anything here.
/*
Integer statusCode=res.getStatusCode(); // ? success or error
String status= res.getStatusCode(); // error name
// statusCode and status in one string
String entireStatus = res.toString();
// Looks like 'Status=OK, StatusCode=200'
String body = res.getBody();
List<String> headerKeys = res.getHeaderkeys();
// Get a specific header value by key
String headerX = res.getHeader(KEY);
*/
return res;
}
apiFhirPost.cls
同样,请注意第二个方法可以放在自己的类中... 可能... 并被所有 REST 调用使用。
// For now, this will be called by ApiFhirPost.cls
//
public with sharing class ApiFhirPost {
public ApiFhirPost() {
}
public static String BASE_URL = 'http://hapi.fhir.org/baseR4/';
public static String tellMeYourName()
{
String sMyName = 'My Name is tellMeYourName(3) in ApiFhirPost.';
system.debug(sMyName);
return sMyName;
}
// POSTed Patient Id's: 8127780, 8186215,
// Seaton ID = 8271353 DuQuesne ID = 8271558
@AuraEnabled(cacheable=true)
// Eventually sInputString would be the Account.id and
// all the data to POST would come from that through SOQL
public static String calloutForStack(String sInputString) {
ApiResponseClass1 apiResponse = new ApiResponseClass1();
// Do some processing with the input string
String outputString = 'FhirGet Processed string: ' + sInputString;
String strInput = sInputString; // back to where we were
String httpMethod = 'POST';
system.debug('Method ApiFhirPost.calloutForStack(1a) input:' +
strInput + '...');
String endPoint = BASE_URL + 'Patient'; // from postman
//String endPoint = BASE_URL + 'Patient/8271558'; // To force an error
system.debug('Method ApiFhirPost.calloutForStack(1ep) endpoint:' +
endPoint + '...');
String strRequestBody = '';
FHIRAPiClasses.patientResponseResource patient =
new FHIRAPiClasses.patientResponseResource();
System.debug('calloutForStack(48jab): ' + strRequestBody + '...');
//public String resourceType {get;set;}
patient.resourceType = 'Patient';
//public String id {get;set;} - Set by FHIR Server and comes back in body
patient.id = '';
//public metaResource meta {get;set;} // not sent
//public textResource text {get;set;} // not sent
//public List<nameResource> name {get;set;}
patient.name.Add(new FHIRAPiClasses.nameResource('official','Woo','David'));
//public List<telecomResource> telecom {get;set;}
// public telecomResource(String System_x, String Value, String Use)
patient.telecom.Add(new FHIRAPiClasses.telecomResource
('phone', '8012121756', 'mobile'));
patient.telecom.Add(new FHIRAPiClasses.telecomResource
('email', 'dwoo@lmicrosquish.com', ''));
//public String gender {get;set;}
patient.gender = 'male';
//public String birthdate {get;set;}
patient.birthdate = '2508-06-03';
//public List<addressResource> address {get;set;}
//public addressResource(String Line, String City, String State, "PostalCode")
patient.address.Add(new FHIRAPiClasses.addressResource
('200 CCatskinner Dr.', 'Slash City', 'TY', '54545'));
// https://ktema.org/articles/json-apex-intro/ ... primer...
strRequestBody = JSON.serialize(patient); // This call may produce
// truncated the JSON...
System.debug('calloutForStack(1jab): ' + strRequestBody + '...');
// Sending an email here causes a CalloutExcpeption from the POST request
// This is an error I got, but it was because I tried to send an email
// before I sent the RST POST Call.
// System.CalloutException: You have uncommitted work pending.
// Please commit or rollback before calling out
// 1. Salesforce enforces a limit of one API call (i.e., callout) per Apex routine.
// This is the Salesforce
// error caused by making two or more API calls in the same Apex routine.
// 2. One of the framework requirements when developing on the Salesforce
// platform is that you cannot
// perform any DML prior to making a callout to any external service
// in the same transaction.
//SendEmails.sendEmail('bgates@microsquish.com',
//'Subject Post data 1', strRequestBody);
System.debug('calloutForStack(1jac)..');
String strReponseBody = '';
String strData = ''; // What is actually going to be returned.
//return callout(httpMethod, endpoint, body);
HttpResponse res = callout('POST', endPoint, strRequestBody);
String strRawReponse = res.getBody();
System.debug('calloutForStack(1ab)...' + strRawReponse);
// Now I've got an error here... hopefully sending it as an email
// after the Callout will not cause an exception.
// Nah, it never gets here... In the Debug log, it looks like
// the serialized JSON was truncated. (Not in POAS message though.)
// I may need to do it manually instead of using JSON.Serialize(object) - Nope
//SendEmails.sendEmail('bgates@microsquish.com',
//'Subject Post data 1', strRawReponse);
Integer iStatus= res.getStatusCode(); // Response Code
apiResponse.sStatusCode = iStatus.format(); //
System.debug('calloutForStack(1d) statusCode: ' +
apiResponse.sStatusCode + '...');
// statusCode and status in one string
// Looks something like "System.HttpResponse[Status=Created, StatusCode=201]"
// In any case, this will be added to the error if there is one.
apiResponse.sCompleteStatus = res.toString();
String sResponse = '';
if(iStatus == 201){
System.debug('calloutForStack(1em) entireStatus: ' +
apiResponse.sCompleteStatus + '...');
// The JSON from FHIR contains a reserved word "system.
// So in the Apex code it needs to do a string replace on the json string
// likejsonString.replace('"system":', '"system_x":');
strReponseBody = res.getBody(); // This is everything received.
strReponseBody = strReponseBody.replace('"system":', '"system_x":');
System.debug('calloutForStack(1ea) body: ' + strReponseBody + '...');
List<String> headerKeys = res.getHeaderkeys(); // Not really needed, but...
System.debug('calloutForStack(1fa) Header Size: ' +
String.valueOf(headerKeys.size()) + '...');
// Works good. It shows 14 headers, so to display them
for (String key : headerKeys)
{
// Get a specific header value by key
String svalue = res.getHeader(key);
System.debug('calloutForStack(1fb) Header key=' +
key + '- value=' + svalue + '...');
}
// Working with JSON in Apex: A Primer - Shows how to deserialize
// an unknown JSON string.
// This is completely unnecessary here because below the response is
// deserialized to a known object type.
// https://ktema.org/articles/json-apex-intro/
// https://developer.salesforce.com/docs/
// atlas.en-us.apexref.meta/apexref/apex_methods_system_map.htm
Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped
(strReponseBody);
// A set is an unordered collection of elements that do not
// contain any duplicates.
// Set elements can be of any data type—primitive types,
// collections, sObjects,
// user-defined types, and built-in Apex types.
////Set <String> resSet = new Set<String>();
////resSet = result.keySet(); // Should be the keys for all objects
////List<String> lststr = new List<String>(resSet);
////String sq = lststr[0];
////System.debug('calloutForStack(1hp)
////Key: ' + sq + '...'); // returns 'items' ... makes sense
//system.debug('calloutForStack(1j): ' + Result + '...');
sResponse = '{"Status":"' + apiResponse.sStatusCode + '"}';
System.debug('calloutForStack(1epm) entireStatus: ' + sResponse + '...');
// This needs replacing. It's probably the best to use.
FHIRAPiClasses.patientResponseResource RR =
(FHIRAPiClasses.patientResponseResource)JSON.deserialize
(res.getBody(),FHIRAPiClasses.patientResponseResource.class);
String ssResponse = RR.text.div.trim();
string sDiv = '<div xmlns="http://www.w3.org/1999/xhtml">';
sResponse = ssResponse.replace(sDiv, '<div>');
// It still displays this as a literal, so use different data
// from the structure
strData = RR.strNameIdAddress(); // Not all data in the response.
apiResponse.sdata = RR.strNameIdAddress(); // Not all data in the response.
system.debug('calloutForStack(112): ' + strData + '...');
} // End if(iStatus == 201)
else if (iStatus == 400) { // FHIR error
System.debug('calloutForStack(12em) entireStatus: ' +
apiResponse.sCompleteStatus + '...');
sResponse = '{"Status":"' + apiResponse.sStatusCode + '"}';
System.debug('calloutForStack(12epm) entireStatus: ' + sResponse + '...');
// The JSON from FHIR contains a reserved word "system.
// In the Apex code, it needs to do a string replace
// on the json string likejsonString.replace('"system":', '"system_x":');
strReponseBody = res.getBody(); // This is everything received.
System.debug('calloutForStack(12ea) body: ' + strReponseBody + '...');
FHIRAPiClasses.errorResource RR1 =
(FHIRAPiClasses.errorResource)JSON.deserialize(res.getBody(),
FHIRAPiClasses.errorResource.class);
if(RR1.issue.size() > 0)
apiResponse.serror = RR1.issue[0].diagnostics;
else
apiResponse.serror = '(1)Unknown Error POSTing FHIR data';
}
else { // There was an error
apiResponse.serror = '(2)Unknown Error POSTing FHIR data';
}
// When an instance of an Apex class is returned from a server-side action,
// the instance is serialized to JSON by the framework.
// Only the values of public instance properties and
// methods annotated with @AuraEnabled are serialized and returned.
// getToJson(); is so annotated.
system.debug('calloutForStack(1p): Status=' + apiResponse.getToJson());
//return new ApiResponseClass1(apiResponse.sCompleteStatus,
// apiResponse.sdata , strError); // complains
// This is the token that will be returned. @AuraEnabled allows for
// automatic serialization
//apiResponse.setValues(apiResponse.sCompleteStatus,
//apiResponse.sdata , strError);
//return apiResponse; // mAybe a good idea if needed
sResponse = apiResponse.getToJson();
system.debug('Method ApiFhirPost.calloutForStack(9za)
sResponse:' + sResponse + '...');
return sResponse;
}
// Method to perform a callout and return an httpResponse
// Notice that here, this call is generic because it takes any URL
// Sample for Development Console: ApiFhirPost.callout
// ('GET','https://v2.jokeapi.dev/joke/Any','');
// Warning: Has some really nasty jokes unless told to be clean
public static httpResponse callout
(String httpMethod, String endpoint, String body){
//Instantiate an httpRequest and set the required attributes
httpRequest req = new httpRequest();
req.setMethod(httpMethod);
req.setEndpoint(endpoint);
if(string.isNotBlank(body)) {
req.setBody(body);
req.setHeader('Content-Length', string.valueOf(body.Length()));
}
//Optional attributes are often required to conform to the
//3rd Party Web Service Requirements
req.setHeader('Accept-Encoding','gzip, deflate');
// Might be needed for FHIR GET or else returns just html
req.setHeader('Content-Type','application/json');
// Absolutely needed for FHIR GET or else returns just html
req.setHeader('Accept','application/json');
// At one point got error message that it needed header header
// like this for a create:
//?req.setHeader('Content-Type: application/json+fhir" -X POST -d');
//You can adjust the timeout duration (in milliseconds)
//to deal with slow servers or large payloads
req.setTimeout(120000);
req.setCompressed(true);
//Use the HTTP Class to send the httpRequest and receive an httpResposne
/*If you are not using an HttpCalloutMock:
if (!test.isRunningTest){
*/
httpResponse res = new http().send(req);
/*If you are not using an HttpCalloutMock:
}
*/
system.debug(res.toString());
system.debug(res.getBody());
// Response values from code shown in youtube.com/watch?v705SeyjppFs
// This is basically sample code because they can't be used for anything here.
/*
Integer statusCode=res.getStatusCode(); // ? success or error
String status= res.getStatusCode(); // error name
// statusCode and status in one string
String entireStatus = res.toString();
// Looks like 'Status=OK, StatusCode=200'
String body = res.getBody();
List<String> headerKeys = res.getHeaderkeys();
// Get a specific header value by key
String headerX = res.getHeader(KEY);
*/
return res;
}
//Method to deserialize the response body
//public static FHIRAPiClasses.patientResponseResource deserialize
//(httpResponse res){
// return (FHIRAPiClasses.patientResponseResource)JSON.deserialize
// (res.getBody(),FHIRAPiClasses.patientResponseResource.class);
//}
} // End public with sharing class ApiFhirPost
真麻烦...
关注点
请记住在 UI 代码中添加视觉标签,因为可能存在部署延迟。你需要知道你是否看到的是你刚刚部署的代码。可能不是。类代码似乎部署得更快。
我不知道,Salesforce 很有趣,但考虑到我花了多长时间才在 ASP 环境(Classic、Framework、Core)中学会了标准的 Web 开发,我已经付出了我的时间和精力。不过,Salesforce、Lightning、Apex 相当令人印象深刻。如果你职业生涯处于早期或中期,我会考虑学习它。它本身就具有良好的安全性,而且似乎很少有人了解它。一个非常光滑的系统。
下一步将是扩展到 DocuSign API,但那是一个独立的生态系统,而且它非常健壮!!!尽管如此,这仍然是使用 DocuSign 数据结构所需的基本代码。
我希望这段代码能帮助到一些人。我本可以自己用得上。而且,只需进行少量修改,这段代码就非常适合 Java 或 C# API。
历史
- 2023年3月13日:初始版本