Rechercher

Article
· Fév 20 8m de lecture

Servicio REST/JSON para carga y descarga de documentos largos

En este articulo voy a mostrar como se pueden manipular documentos extensos, es decir de tamaño superior a 3.6 MB en una interfaz REST y con JSON. La interfaz permite:

  • Enviar un documento PDF para grabarlo en un servidor IRIS
  • Pedir un documento PDF para descargarlo de IRIS.

El articulo cubre los siguientes temas:

  • Ejemplo de desarrollo de API REST con paradigma SPEC first. Se define primero la interfaz rest y se implementan despues el cliente y servidor
  • Gestión de JSON con cadenas largas y Streams en ambos sentidos:
    • Lectura de un Stream contenido en un objeto JSON %DynamicObject.
    • Generación de un Stream JSON que contiene una propiedad larga de tipo Stream
  • Conversión de Streams a Base64
    • Base64Encode para Streams
    • Base64Decode para Streams

Los Detalles de implementación de este articulo se pueden encontar en:

https://github.com/es-comunidad-intersystems/documentservice.git

API Rest

Para la la definición de la API Rest, uso el Swagger Editor para escribir una definición en formato OpenAPI 2.0. Se tiene que exportar a una definición en fichero formato JSON para importalo en IRIS con la herramienta do ^%REST.

La Definición tiene 2 métodos:

  • Un POST, que permite enviar un documento en base64. Se envían el nombre y contenido base64 del documento. El POST devuelve una referencia del documento para su posterior lectura.
  • Un GET, que permite descargar un documento especifico.

El POST /document simplificado

 /document:
    post:
      operationId: PostDocument
       consumes:
        - application/json
      produces:
        - application/json
      parameters:
        - name: body
          in: body
          required: true
          schema:
            type: object
            properties:
              fileName:
                type: string
              content:
                type: string
    

El GET /document simplificado

  /document/{reference}:
    get:
      operationId: GetDocument
      consumes:
        - application/json
      produces:
        - application/json
      parameters:
        - name: reference
          in: path
          required: true
          type: string
      responses:
        '200':
          schema:
            type: object
            properties:
              content:
                type: string

Una vez La Definición de OpenAPI 2.0 importada en IRIS, es necesario implementar la clase de implementación.

Implementación y manipulación de JSON

PostDocument: extraer una propiedad de más de 3.6MB de un JSON

Para la cargar de documento en la función PostDocument, el framework de REST nos proporciona esta definición del método:

ClassMethod PostDocument(body As %DynamicObject) As %DynamicObject {}

El body contiene 2 propiedades: fileName y content. Pero el %DyamicObject no permite acceder a la propiedad "content" con la sintaxis habitual body.content si contiene mas de 3.6MB, el limite de tamaño de los %String en IRIS 2024.
La documentación de IRIS ( https://docs.intersystems.com/iris20243/csp/docbook/DocBook.UI.Page.cls?... ) proporciona soluciones cuando el "body" contiene mas de 3.6MB de datos, pero las propiedades individuales caben en %String, lo cual no es el caso aquí.
La Solución para extraer la propiedad contenido consiste en usar una nueva clase de sistema %Stream.DynamicBinary, que ofrece acceso de solo lectura al Stream. El Stream se extrae con la siguiente sintaxis:

    //Extraer el campo contenido como stream  %Stream.DynamicBinary (necesario para contenidos largos >3.6MB)
	set tBase64=body.%Get("content","","stream")

Este Stream se puede despues copiar fácilmente, para convertirlo de base64 hacia binario y grabarlo en un fichero binario en el servidor. El metodo completo se puede ver en gitHub.

GetDocument: generar un JSON con una propiedad de más de 3.6MB

En base a la definición de OpenAPI 2.0, el framework de REST ha proporcionado esta definición para el método de implementación

ClassMethod GetDocument(reference As %String) As %DynamicObject

Para Generar un %DynamicObject que contenga una propiedad "content" con un Stream de más de 3.6GB, la interfaz de %DynamicObject no proporciona ninguna utilidad, ya que la clase %Stream.DynamicBinary no se puede instanciar desde otro Stream ni permite escrituras.
El Framework de la aplicación REST definida permite que la función GetDocument devuelva un %Stream en vez de un %DynamicObject. Para no definir el contenido del Stream manualmente, es posible usar una clase que defina el contenido a devolver y el %JSON.Adaptor:

Class documentservice.data.document Extends (%RegisteredObject, %JSON.Adaptor)
{
Property content As %Stream.TmpBinary;
}

En GetDocument, simplemente se copian los datos a la propiedad "content" y se exporta todo a un %Stream mediante la interfaz %JSON.Adaptor:%JSONExportToStream:
Una peculiaridad de las interfaces Web de IRIS (CSP, Servicios SOAP, Servicios REST) es que las propiedades de tipo %Binary y %Stream.Binary ya se codifican/decodifican de manera automática en base64 al enviarse/recibirse, por lo cual esta operación se ha comentado en el codigo de ejemplo.

    set tOut=##class(documentservice.data.document).%New()
    set tOut.content=##class(%Stream.TmpBinary).%New()
    //Binary Streams are automatically Base64Encoded through Rest, so we don't need to do it here
    //set tSC=..Base64Encode(tInFile,tOut.content)
    do tOut.content.CopyFrom(tInFile)
    do tOut.%JSONExportToStream(.binStream)
    return binStream

A notar que la superclase %JSON.Adapter nos permite también instanciar objetos complejos de IRIS a partir de Streams que contienen un formato JSON, incluso cuando estos contienen cadenas largas. Se podría haber usado una subclase  de %JSON.Adapter y el método .%JSONImport(<stream>) para leer el Stream de PostDocument e instancia el objeto, pero la Interfaz de REST nos proporciona ya un %DynamicObject en vez de Stream de REST original (esto es por la definición del objeto de entrada que se ha puesto en OpenAPI).

 

Cliente de Prueba

Las Herramientas tradicionales como PostMan o SOAPUI no son muy practicas para probar este servicio REST de envio y recepción de fichero largo. Un método conveniente es usar scripts de python para hacer el POST de un fichero PDF y el GET del mismo documento ( y así poder validar si se recibe un fichero PDF valido). Python ayuda también en hacer las conversiones hacia base64 al enviar y recibir el documento.

Inicializar el entorno python (venv)

cd client
setup_env.bat

Carga del fichero

cd client
runpost.bat

Resultado:

python post_doc.py http://localhost:32773/csp/documentservice _SYSTEM SYS ./sampledata/IRIS2024-UsingJSON.pdf
Response Status Code: 200
Response Body: {'reference': 'File_4.bin'}

Descarga del fichero

cd client
runget.bat

Resultado:

python get_doc.py http://localhost:32773/csp/documentservice _SYSTEM SYS File_1.bin
File downloaded successfully to File_1.bin


Anexo: Manipulación Base64 para Streams

InterSystems IRIS contiene funciones para codificar y decodificar cadenas de caracteres (%String) hacia y desde base64. Son las funciones

  set base64=$SYSTEM.Encryption.Base64Encode(cadena)
  set cadena2=$SYSTEM.Encryption.Base64Decode(base64)

Sin embargo, no hay funcionalidad para trabajar con Streams en IRIS. La Funcionalidad ha sido implementada en un paquete de IRIS for Health, de dónde se recupera la implementación ya probada y validada. Esta implementación separa el %Stream en %Strings más cortos para , y debe tener en cuenta, para el "Decode", de la longitud variable que puede tener la representación de un carácter en base64.

Base64Encode

/// Base64 encode a string or a stream into a stream
/// 
/// <example>
/// Do ..Base64Encode("Hello",.tEncodedStream)
/// </example>
/// 
/// By default a %Stream.GlobalCharacter is returned. Caller may override by
/// sending in any stream type that has a LineTerminator property. For example:
/// <example>
/// Set tInputFile=##class(%Stream.FileBinary).%New()
/// Set tInputFile.Filename="c:\data.dat"
/// Set tOutputFile=##class(%Stream.FileBinary).%New()
/// Set tOutputFile.Filename="c:\data.b64"
/// Do ##class(HS.Util.StreamUtils).Base64Encode(tInputFile,tOutputFile)
/// </example>
ClassMethod Base64Encode(pInput = "", ByRef pOutput As %Stream.GlobalCharacter = "") As %Status
{
	Set tSC=$$$OK
	Try {
		If '$IsObject(pOutput) Set pOutput=##class(%Stream.GlobalCharacter).%New()
		
		#; Get the biggest buffer possible
		#; Assumes output is 78 characters (76 + 2 newlines) per base-64 line
		#; ($$$MaxLocalLength\78*57) should work, but it doesn't so just use 50% floored to nearst power of 57
		Set tMax=$$$MaxLocalLength*.5\1
		Set tMax=tMax-(tMax#57)
		
		If $IsObject(pInput) {
			Set tCurrLineLen=0
			While 'pInput.AtEnd { 
				Set tData=pInput.Read(tMax)
				Set tValue=$SYSTEM.Encryption.Base64Encode(tData)
				Do pOutput.Write(tValue) 
				If 'pInput.AtEnd Do pOutput.Write(pOutput.LineTerminator)
			}
		} Else {
			Set tPos=1
			While tPos<$l(pInput) {
				Set tEnd=tPos+tMax-1
				Set tValue=$e(pInput,tPos,tEnd)
				Set tValue=$SYSTEM.Encryption.Base64Encode(pInput)
				Do pOutput.Write(tValue)
				Set tPos=tEnd+1
				If tPos<$l(pInput) Do pOutput.Write(pOutput.LineTerminator)
			}
		}	
			Do pOutput.Rewind()
	} Catch ex { Set tSC=ex.AsStatus() }
	Quit tSC
}

Base64Decode

/// Base64 decode a string or a stream into a stream
/// 
/// <example>
/// Do ..Base64Decode("SGVsbG8=",.tDecodedStream)
/// </example>
/// 
/// By default a %Stream.GlobalBinary is returned. Caller may override by
/// sending in a different stream. For example:
/// <example>
/// Set tInputFile=##class(%Stream.FileBinary).%New()
/// Set tInputFile.Filename="c:\data.b64"
/// Set tOutputFile=##class(%Stream.FileBinary).%New()
/// Set tOutputFile.Filename="c:\data.dat"
/// Do ##class(HS.Util.StreamUtils).Base64Decode(tInputFile,tOutputFile)
/// 
/// </example>
ClassMethod Base64Decode(pInput = "", ByRef pOutput As %Stream.GlobalBinary = "") As %Status
{
	Set tSC=$$$OK
	Try {
		If '$IsObject(pOutput) Set pOutput=##class(%Stream.GlobalBinary).%New()
		
		#; Remove newlines from input then decode the largest value
		#; possible that has a length which is a factor of 4
		If $IsObject(pInput) {
			Set tLeftOver=""
			While 'pInput.AtEnd { 
				Set tValue=pInput.Read($$$MaxLocalLength-4)  
				Set tValue=tLeftOver_$TR(tValue,$C(13,10))
				Set tPos=$L(tValue)-($L(tValue)#4)
				Set tLeftOver=$e(tValue,tPos+1,*)
				Set tValue=$e(tValue,1,tPos)
				Do pOutput.Write($SYSTEM.Encryption.Base64Decode(tValue))
			}
		} Else {
			Do pOutput.Write($SYSTEM.Encryption.Base64Decode(pInput))
		}	
		Do pOutput.Rewind()
	} Catch ex { Set tSC=ex.AsStatus() }
	Quit tSC
}
1 Comment
Discussion (1)2
Connectez-vous ou inscrivez-vous pour continuer
Question
· Fév 20

Need to query the rule data from the tables in the InterSystems database

I need read only access using a JDBC query to the tables that contain the rules data for a particular interface. I'm having difficulty locating the tables that house the rule data and I'm wondering if someone could help me with that information and any sample queries if possible.

Thanks in advance!

6 Comments
Discussion (6)2
Connectez-vous ou inscrivez-vous pour continuer
Article
· Fév 20 2m de lecture

Celebrating a True Educator of the Developer Community

Among the numerous authors of the InterSystems Developer Community, some members stand out for their dedication and lasting impact. One such member is @Mihoko Iijima, whose journey with InterSystems spans over two decades. From her early experiences with Caché to her deep involvement in the Developer Community, she has continuously contributed to knowledge-sharing and collaboration, shaping the experience for fellow developers.

🤩 Let's take a closer look at Mihoko's journey with InterSystems technology and our Developer Community...

Mihoko's journey with InterSystems began in the early 2000s when she first worked with Caché 3.2.1. She joined InterSystems Japan in 2003 and became an active member of the Developer Community in 2015. Over the years, Mihoko has witnessed significant technological changes, including the launch of InterSystems IRIS, which broadened her knowledge in areas like machine learning and containerization.

One of Mihoko’s most memorable projects involved developing a web application interface, an experience that provided a foundation for her future career. Taking on responsibilities from customer interactions to configuration and testing, she gained invaluable skills that she applies to her work today. As a trainer at InterSystems, she faced challenges such as technical failures during training. Thanks to support from colleagues, she developed strategies to troubleshoot issues efficiently and enhance training sessions.

Mihoko’s involvement with the Developer Community began with a ZenMojo training course. She and her colleague, @Megumi Kakechi, saw value in sharing their experiences, leading them to post questions and solutions in the Community. This initiative grew into active participation in the Japanese Developer Community, where Mihoko contributed to the FAQ system, helped manage contests, and promoted events.

Seeing her articles translated into different languages and receiving international feedback has been especially rewarding for Mihoko. She believes that even with language barriers, developers worldwide share common challenges and goals. Her advice to newcomers is simple: start by engaging with content, whether through likes, questions, or articles - every contribution is important.

Outside of work, Mihoko pursues her passion for karate, working toward a black belt while participating in competitions.

We are immensely grateful for Mihoko's unwavering support of the Developer Community. Her journey reflects dedication, adaptability, and a commitment to learning, making a lasting impact on everyone she (virtually) meets. Here's to Mihoko - a true champion of the Developer Community, whose journey inspires us all to strive for more and embrace the future with learning and collaboration.

👏 Let's all congratulate Mihoko and thank her for her contributions to InterSystems and our Developer Community! 

13 Comments
Discussion (13)6
Connectez-vous ou inscrivez-vous pour continuer
Question
· Fév 20

Automatizar tarefa de extração de dados

Prezados, é possivel automatizar tarefas de extração de dados no caché?

Ex: Gerar uma tarefa que executa diariamente uma consulta e armazena os dados em um arquivo csv. 

6 Comments
Discussion (6)2
Connectez-vous ou inscrivez-vous pour continuer
Question
· Fév 20

MAXSTRING error on conversion to JSON in FHIR R4 DTL

I'm working on FHIR project and using this code to convert an incoming request to FHIR 

Method OnRequest(request As HS.FHIRServer.Interop.Request, Output response As HS.FHIRServer.Interop.Response) As %Status

{

    #dim tSC As %Status = $$$OK

    Try {

        // Process incoming request

       

        set stream = ##class(HS.SDA3.QuickStream).%OpenId(request.QuickStreamId)

        set bundle = ##class(HS.FHIR.DTL.vR4.Model.Resource.Bundle).FromJSON(stream,"vR4")

 



It's working ok but when I include a realistic PDF in a FHIR Binary resource (contained in the Bundle) I get a MAXSTRING error. 

Looking at the documentation for HS.FHIR.DTL.vR4.Model.Resource.Binary, I wondering if MAXLEN setting for data is missing and data MAXLEN = 50. 
 

Property contentType As %String(MAXLEN = 1000000, XMLNAME = "contentType", XMLPROJECTION = "ATTRIBUTE") [ Required ];

Property data As %Binary(XMLNAME = "data", XMLPROJECTION = "ATTRIBUTE");


 

9 Comments
Discussion (9)2
Connectez-vous ou inscrivez-vous pour continuer