How to convert XML response to temp-table in OpenEdge Progress4GL? - web-services

Hello Progress4GL Developers,
I am trying to use progress to consume a SOAP API. At the moment, I am just using a opensource sample SOAP service called: http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL.
The following program works a dream (i.e user inputs country code such as "ESP" and details about Spain are showed to user). The result is stored in temp-table1 and temp-table2:
/******* Sample Application to show Progress4GL Consuming a SOAP API *************/
/******* VARIABLES ***************************************************************/
DEFINE VARIABLE lReturn AS LOGICAL NO-UNDO.
DEFINE VARIABLE hServer AS HANDLE NO-UNDO.
DEFINE VARIABLE hPortType AS HANDLE NO-UNDO.
DEFINE VARIABLE capitalCity as LONGCHAR NO-UNDO.
DEFINE VARIABLE iCntryCode as char no-undo label "Country Code".
DEFINE VARIABLE oResponse as LONGCHAR no-undo.
/******* CONNECTION SETTINGS TO SOAP SERVICE *************************************/
CREATE SERVER hServer.
lReturn = hServer:CONNECT("-WSDL http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL").
IF lReturn = NO THEN DO:
MESSAGE
"Could not connect to WebService server".
END.
/******* SETTING PORT TYPE *******************************************************/
RUN CountryInfoServiceSoapType SET hPortType ON SERVER hServer.
IF NOT VALID-HANDLE(hPortType) THEN DO:
MESSAGE
"Could not establish portType".
END.
/****** USER UPDATE FORM**********************************************************/
UPDATE iCntryCode.
/****** NAME OF DATA FUNCTION TO RUN *********************************************/
RUN FullCountryInfo IN hPortType (INPUT iCntryCode, OUTPUT oResponse) NO-ERROR.
/****** TEMP TABLE TO HOLD ALL CAPITAL CITY FIELDS *******************************/
DEFINE TEMP-TABLE temptable1
SERIALIZE-NAME "sCapitalCity"
FIELD capitalCity AS CHAR XML-NODE-TYPE "text".
/****** DATASET WHICH RUNS CONVERSION ********************************************/
DEFINE DATASET dsa SERIALIZE-NAME "FullCountryInfoResult" FOR temptable1.
/***** TEMP TABLE TO HOLD ALL CURRENCY FIELDS ************************************/
DEFINE TEMP-TABLE temptable2
SERIALIZE-NAME "sCurrencyISOCode"
FIELD currencyCode AS CHAR XML-NODE-TYPE "text".
/****** DATASET WHICH RUNS CONVERSION ********************************************/
DEFINE DATASET dsb SERIALIZE-NAME "FullCountryInfoResult" FOR temptable2.
/****** POPULATE DATASET AND TEMP-TABLES FROM SOAP RESPONSE **********************/
DATASET dsa:READ-XML( "longchar", oResponse, ?, ?, ? ).
DATASET dsb:READ-XML( "longchar", oResponse, ?, ?, ? ).
FOR EACH temptable1 no-lock, each temptable2 no-lock:
DISPLAY temptable1.capitalcity temptable2.currencyCode.
END.
PAUSE 100.
/****** STOP CONNECTION TO SERVER ************************************************/
DELETE PROCEDURE hPortType.
hServer:DISCONNECT().
DELETE OBJECT hServer.
However, this sample web-service also has a procedure called FullCountryInfoAllCountries. This will give response for all countries. I have tried to re-factor the code above so that all temp-table1 and temp-table2 will be populated with all countries however I am having difficulty parsing the XML data.
I have tried experimenting with the NAMESPACE-URI, SERIALIZATION-NAME, and XML-NODE-NAME statements, but can't get it to work. The problem I have is that the XML response from FullCountryInfoAllCountries is structured differently to FullCountryInfo, and I always recieve an error along the lines of 'Namespace not found', or it will return nothing in the temp-tables.
Many Thanks in Advance!

You can peek at the output by messaging the response (commented out), this can then be mapped to dataset, temp-table and field names:
define variable hs as handle no-undo.
define variable hp as handle no-undo.
define variable lcresponse as longchar no-undo.
define temp-table tt serialize-name "tCountryInfo"
field sISOCode as character
field sName as character
.
define dataset ds serialize-name "FullCountryInfoAllCountriesResult" for tt.
create server hs.
hs:connect( "-WSDL http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL" ).
run CountryInfoServiceSoapType set hp on server hs.
run FullCountryInfoAllCountries in hp ( output lcresponse ).
// message string( substring( lcresponse, 1, 30000 ).
dataset ds:read-xml( "longchar", lcresponse, ?, ?, ? ).
for each tt:
display tt.
end.
delete procedure hp.
hs:disconnect().
delete object hs.

Related

Multiple To clauses in Data step

I have a data step where I have a few columns that need tied to one other column.
I have tried using multiple "from" statements and " to" statements and a couple other permutations of that, but nothing seems to do the trick. The code looks something like this:
data analyze;
set css_email_analysis;
from = bill_account_number;
to = customer_number;
output;
from = bill_account_number;
to = email_addr;
output;
from = bill_account_number;
to = e_customer_nm;
output;
run;
I would like to see two columns showing bill accounts in the "from" column, and the other values in the "to", but instead I get a bill account and its customer number, with some "..."'s for the other values.
Issue
This is most likely because SAS has two datatypes and the first time the to variable is set up, it has the value of customer_number. At your second to statement you attempt to set to to have the value of email_addr. Assuming email_addr is a character variable, two things can happen here:
Customer_number is a number - to has already been set up as a number, so SAS cannot force to to become a character, an error like this may appear:
NOTE: Invalid numeric data, 'me#mywebsite.com' , at line 15 column 8. to=.
ERROR=1 N=1
Customer_number is a character - to has been set up as a character, but without explicitly defining its length, if it happens to be shorter than the value of email_addr then the email address will be truncated. SAS will not show an error if this happens:
Code:
data _NULL_;
to = 'hiya';
to = 'me#mydomain.com';
put to=;
run;
short=me#m
to is set with a length of 4, and SAS does not expand it to fit the new data.
Detail
The thing to bear in mind here is how SAS works behind the scenes.
The data statement sets up an output location
The set statement adds the variables from first observation of the dataset specified to a space in memory called the PDV, inheriting lengths and data types.
PDV:
bill_account_number|customer_number|email_addr|e_customer_nm
===================================================================
010101 | 758|me#my.com |John Smith
The to statement adds another variable inheriting the characteristics of customer_number
PDV:
bill_account_number|customer_number|email_addr|e_customer_nm|to
===================================================================
010101 | 758|me#my.com |John Smith |758
(to is either char length 3 or a numeric)
Subsequent to statements will not alter the characteristics of the variable and SAS will continue processing
PDV (if customer_number is character = TRUNCATION):
bill_account_number|customer_number|email_addr|e_customer_nm|to
===================================================================
010101 | 758|me#my.com |John Smith |me#
PDV (if customer_number is numeric = DATA ERROR, to set to missing):
bill_account_number|customer_number|email_addr|e_customer_nm|to
===================================================================
010101 | 758|me#my.com |John Smith |.
Resolution
To resolve this issue it's probably easiest to set the length and type of to before your first to statement:
data analyze;
set css_email_analysis;
from = bill_account_number;
length to $200;
to = customer_number;
output;
...
You may get messages like this, where SAS has converted data on your behalf:
NOTE: Numeric values have been converted to character
values at the places given by: (Line):(Column).
27:8
N.B. it's not necessary to explicitly define the length and type of from, because as far as I can see, you only ever get the values for this variable from one variable in the source dataset. You could also achieve this with a rename if you don't need to keep the bill_account_number variable:
rename bill_account_number = from;

How to get ID of stored process?

i have some stored processes with identical names. To identify which process is running at the moment, i need to know the id of the stored process in the metadata. Can i retrieve the STP-id somewhere? I could not find a variable which holds the id.
I only found symget('sysjobid'); which returns the unix-processid, not the id of the stored process.
Typical a stored process id looks like this:
A5DF0R0G.B80001L7
I need to know the id from within the process which is running, to retrieve some properties of the process from the metadata.
Any other solution to identify the process exactly in the metadata would also be welcome, but i can not use his name, because it can occur several times for differents processes.
for example something like:
put 'name:' &_program; /*this already works and returns the name of the stored process*/
put 'id:' ?; /*need to know the id of the process, because name is not distinct*/
It is actually pretty easy now that I look at it.
I created this sample STP (named "Doms Hello World") in the "My Folder" folder.
data _temp;
X = "HELLO WORLD";
path = "&_PROGRAM";
format type ID $200.;
rc= metadata_pathobj("",path,"StoredProcess",type,ID);
run;
proc print data=_temp noobs;
run;
You can use the metadata_pathobj() function to get the ID and TYPE of an element by the path.
This returns
X path type ID rc
HELLO WORLD /User Folders/dpazzula/My Folder/Doms Hello World ClassifierMap A5XQ9K3Z.BA0002BQ 1
In both EG and via the Web App.
I don't think a stored process has an ID, but it is unique in terms of its location and name.
User _PROGRAM macro variable to determine what stored procedure is running. It will have a format of "/SAS Folder/Stored Procedure Folder/Stored Procedure Name".
Something like A5DF0R0G.B80001L7 ID of the stored procedure useful when running IOM applications, but I don't think it would be that useful when it comes determining what stored procedure created something and where it was saved at the time, so I would go with "_PROGRAM".
In case you after the ID anyways, use this code (credit: https://support.selerity.com.au/entries/23169736-Example-Data-Step-View-of-Stored-Procedures-in-Metadata):
******************************************************************************
* Purpose: Create a dynamic view of Stored Procedures registered in Metadata
* Notes : You must establish a Metadata connection prior to running
******************************************************************************;
data work.stplist(drop=_: label="SAS Stored Process List") / view=work.stplist;
length id $17 _uri name description _modified _created location _location $256;
length created modified 8;
format created modified datetime.;
label id="Metadata ID"
name="Stored Process Name"
description="Description"
location="Folder Location"
created="Created"
modified="Last Modified";
_nobj=1;
_n=1;
call missing(id, _uri, name, description, _modified, _created, _location);
do while(_n le _nobj);
_nobj=metadata_getnobj("omsobj:ClassifierMap?#PublicType='StoredProcess'",_n,_uri);
_rc=metadata_getattr(_uri,"Id",id);
_rc=metadata_getattr(_uri,"Name",name);
_rc=metadata_getattr(_uri,"Desc",description);
_rc=metadata_getattr(_uri,"MetadataCreated",_created);
_rc=metadata_getattr(_uri,"MetadataUpdated",_modified);
created=input(_created,anydtdtm.);
modified=input(_modified,anydtdtm.);
* Get folder object the current STP is in *;
_rc=metadata_getnasn(_uri,"Trees",1,_uri);
* Get folder name the current STP is in *;
_rc=metadata_getattr(_uri,"Name",location);
_tree=1;
* Loop up the folder hierarchy *;
do while (_tree>0);
* Get the parent folder object *;
_tree=metadata_getnasn(_uri,"ParentTree",1,_uri);
if _tree > 0 then do;
* If there was a parent folder, get the name *;
_rc=metadata_getattr(_uri,"Name",_location);
* Construct the path *;
location=catx('/',_location,location);
end;
end; * Folder Hierachy *;
location = '/'||location;
output;
_n=_n+1;
end;
run;
Regards,
Vasilij

ODBC error "String data, right truncation State code: 22001" with SQL Server database

I have a test table using a Microsoft SQL Server that is defined like this:
CREATE TABLE [dbo].[Table] (
[FirstName] NVARCHAR (255) NULL,
[LastName] NVARCHAR (255) NULL
);
There's just one row in the table with the values "person" and "man", respectively.
I'm trying to add a function that will update the values of that row but I keep running into this "[Microsoft][ODBC SQL Server Driver]String data, right truncation State code: 22001" error and I cannot figure out what the problem is. I've looked around and people say that it is caused by the data being too long to fit in the column but that's impossible because the string I'm trying to update with is only two characters, and as you can see in the table definition there is plenty of space for it.
I'm using a prepared statement for optimization purposes and the code creating it looks something like this:
const tString query("UPDATE \"" + tableName + "\" SET " + setClause + " WHERE " + whereClause + ";");
SQLHSTMT statement;
SQLAllocHandle(SQL_HANDLE_STMT, fSQLConnection, &statement);
SQLPrepareW(statement, (SQLWCHAR *) query.mWideStr(), SQL_NTS);`
The query string looks like this:
UPDATE "Table" SET "FirstName" = ?, "LastName" = ? WHERE "FirstName" = ? AND "LastName" = ?;
And then I am binding the parameters like this:
// We have our own string class that we use, which is where the mWideStr() and mGetStrSize()
// come from. mWideStr() returns a pointer to a UCS-2 buffer and mGetStrSize() returns the
// size in bytes.
SQLLEN pcbValue(SQL_NTS);
SQLUSMALLINT paramIndex(1);
// Call this for each parameter in the correct order they need to be bound, incrementing the
// index each time.
SQLBindParameter(statement, paramIndex++, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_VARCHAR, 255, 0, (SQLPOINTER) paramValue.mWideStr(), paramValue.mGetStrSize(), &pcbValue);
The first and second bound parameters are the new values which are both just "55", then third would be "person" and fourth would be "man".
Then to execute the statements it's just a call to SQLExecute():
SQLExecute(statement);
The call to SQLExecute() fails and then the error is generated and there is some more code that outputs the error message. As far as I can tell this should all be working perfectly fine. I have another database using Oracle that uses the exact same setup and code and it works without any issues, it's just SQL Server that's barfing for some reason. Is there something obviously wrong here that I'm missing? Does SQL Server have some weird rules that I need to add somewhere?
The SQLLEN pcbValue(SQL_NTS); variable being passed to SQLBindParameter() was going out of scope between binding the parameters and executing the statement, which means that some garbage data was being pointed to in the parameter binding. I also realized that you don't need to specify the last parameter. You can just pass NULL and it will act as if it is a nul-terminated string.
So the fix was to remove the SQLLEN pcbValue(SQL_NTS); variable and to just pass NULL to SQLBindParameter() for the last parameter instead.
Stupid mistake, but worth noting I suppose.

Progress 4GL web service procedure

I'm trying to update our stocks thought the Magento API using Progress 4GL (OpenEdge 10.2B)
So far so good, ie: I can update the stock if the SKU is a match. But if it isn't, it doesn't return an error.
So I looked into how ABL manages SAOP fault errors, and found some examples which I tried to implement. But to no avail.
My new code is as follows:
DEFINE VARIABLE hWebService AS HANDLE NO-UNDO.
DEFINE VARIABLE hMage_Api_Model_Server_V2_HandlerPortType AS HANDLE NO-UNDO.
DEFINE VARIABLE username AS CHARACTER NO-UNDO.
DEFINE VARIABLE apiKey AS CHARACTER NO-UNDO.
DEFINE VARIABLE stock AS CHARACTER NO-UNDO.
DEFINE VARIABLE codigo AS CHARACTER NO-UNDO.
DEFINE VARIABLE loginReturn AS CHARACTER NO-UNDO.
DEFINE VARIABLE product AS CHARACTER NO-UNDO.
DEFINE VARIABLE data AS LONGCHAR NO-UNDO.
DEFINE VARIABLE resultado AS INTEGER NO-UNDO.
DEFINE VARIABLE SOAP-FAULT-CODE AS CHARACTER NO-UNDO.
DEFINE VARIABLE SOAP-FAULT-DETAIL AS CHARACTER NO-UNDO.
DEFINE VARIABLE iError AS INTEGER NO-UNDO.
DEFINE VARIABLE cError AS CHARACTER NO-UNDO.
DO ON ERROR UNDO, THROW:
CREATE SERVER hWebService.
/* TODO: Definir variaveis globais */
username = 'username'.
apiKey = 'password'.
hWebService:CONNECT(" -WSDL 'http://www.medicalemcasa.com/api/v2_soap?wsdl'").
RUN Mage_Api_Model_Server_V2_HandlerPortType SET hMage_Api_Model_Server_V2_HandlerPortType ON hWebService.
RUN login IN hMage_Api_Model_Server_V2_HandlerPortType(INPUT username, INPUT apiKey, OUTPUT loginReturn).
product = "100asda001a".
data = "
<data>
<qty>'250'</qty>
</data>
".
PROCEDURE catalogInventoryStockItemUpdate:
DEFINE INPUT PARAMETER data AS CHARACTER NO-UNDO.
END PROCEDURE.
RUN catalogInventoryStockItemUpdate IN hMage_Api_Model_Server_V2_HandlerPortType (INPUT loginReturn, INPUT product, INPUT data, OUTPUT resultado).
DISPLAY resultado.
CATCH mySoapErrorObject AS Progress.Lang.SoapFaultError:
DO iError = 1 TO mySoapErrorObject:NumMessages:
cError = cError + mySoapErrorObject:getMessage(iError) + "~n".
END.
DELETE OBJECT mySoapErrorObject.
END CATCH.
CATCH mySystemErrorObject AS Progress.Lang.SysError:
DO iError = 1 TO mySystemErrorObject:NumMessages:
cError = cError + mySystemErrorObject:getMessage(iError) + "~n".
END.
DELETE OBJECT mySystemErrorObject.
END CATCH.
FINALLY:
IF cError <> "" THEN DO:
MESSAGE "Errors occured:" SKIP
cError
VIEW-AS ALERT-BOX ERROR.
END.
END FINALLY.
END.
hWebService:DISCONNECT().
DELETE OBJECT hWebService.
In those API's I've worked with SOAP errors only occur when there's a "bigger" error. For instance if the webservice is down, login criteria isn't met, datatypes are wrong etc. Usually a return value is rather in the response and not in the SOAP-envelope.
Could it be that it's simply OK to set a non existent product to inventory 0? What happens if you try to set it to 1? Perhaps you should double check from PHP (or whatever language you usually work with) that the web service actually provides the code you expect in this case?
Otherwise you should look at the wsdl-documentation created - are you 100% sure that the result-parameter (resultado in you code) is an INTEGER and not any form of more complicated xml-document (an object basically)? If it's really a HANDLE it might be that there's no run time error but no value is inserted into the INTEGER.
Also you should remove all your current error handling and replace it with a more general way to handle errors (and make that code more specific if needed rather than working from an example out of the documentation):
DEFINE VARIABLE iError AS INTEGER NO-UNDO.
DEFINE VARIABLE cError AS CHARACTER NO-UNDO.
CATCH mySoapErrorObject AS Progress.Lang.SoapFaultError:
DO iError = 1 TO mySoapErrorObject:NumMessages:
cError = cError + mySoapErrorObject:getMessage(iError) + "~n".
END.
DELETE OBJECT mySoapErrorObject.
END CATCH.
CATCH mySystemErrorObject AS Progress.Lang.SysError:
DO iError = 1 TO mySystemErrorObject:NumMessages:
cError = cError + mySystemErrorObject:getMessage(iError) + "~n".
END.
DELETE OBJECT mySystemErrorObject.
END CATCH.
And insert in the FINALLY-block:
IF cError <> "" THEN DO:
MESSAGE "Errors occured:" SKIP
cError
VIEW-AS ALERT-BOX ERROR.
END.

Oracle (pl-sql) - how to extract particular data from text into object type variable?

I have text provided as varchar2 variable, for exameple:
EXER/ATP-45/-//
MSGID/BIOCHEM3/-/-/-/-/-//
GEODATUM/UTM//
PAPAA/1KM/-//15KM/-//
So, every line separator is // (but there can be also spaces, new lines etc and they should be ignored). '-' is indicating blank field and should be ignored. I have also defined new object type, defined as follows:
TYPE t_promien IS RECORD(
EXER VARCHAR2,
MSGID VARCHAR2(1000),
PAPAA t_papaa
......
)
I need to extract data from corresponding rows into new variable that has t_promien type and set its field, for example - EXER should has 'ATP-45' value, MSGID should has 'BIOCHEM3', PAPAA should has ('1KM','15KM') value (t_papaa is my custom type too and it contains 2 varchar fields).
What is the best way to do this inside oracle PL-SQL procedure? I need to extract needed data into out parameter. Can I use regex for this (how?) Ufortunatelly, I'm totally newbie with oracle, so...
Can you give me some tips? Thanks.
You can do this with REGEXP_SUBSTR using something like this:
SELECT REGEXP_SUBSTR('EXER/ATP-45/-//
MSGID/BIOCHEM3/-/-/-/-/-//
GEODATUM/UTM//
PAPAA/1KM/-//15KM/-//', 'EXER/[^/]+/', 1, 1) AS EXER
FROM DUAL;
The important bit above is 'EXER/[^/]+/' which is looking for a string that starts with the literal EXER/ followed be a sequence of characters which are not / and ended by a final /.
The above query will return EXER/ATP-45/, but you can use standard string functions like SUBSTR, LTRIM or RTRIM to remove the bits you don't need.
A simple demonstration of the use of REGEXP_SUBSTR in PL/SQL.
CREATE OR REPLACE PROCEDURE TEST_REGEXP_PROC(VAR_PI_MSG IN VARCHAR2,
T_PO_PAPAA OUT T_PROMIEN) AS
VAR_L_EXER VARCHAR2(1000);
VAR_L_MSGID VARCHAR2(1000);
BEGIN
SELECT SUBSTR(REPLACE(REGEXP_SUBSTR(VAR_PI_MSG, 'EXER/[^/]+/', 1, 1),'/'),5)
INTO VAR_L_EXER
FROM DUAL;
T_PO_PAPAA.EXER := VAR_L_EXER;
SELECT SUBSTR(REPLACE(REGEXP_SUBSTR(VAR_PI_MSG, 'MSGID/[^/]+/', 1, 1),'/'),6)
INTO VAR_L_MSGID
FROM DUAL;
T_PO_PAPAA.MSGID := VAR_L_MSGID;
END;
Hope this will get you started.