Limpiar filtro
Artículo
Ricardo Paiva · 12 sep, 2022

En primer lugar, ¿qué es la anonimización de datos?
Según la [Wikipedia](https://en.wikipedia.org/wiki/Data_anonymization):
> **La anonimización** es un tipo de [sanitización de información](https://en.wikipedia.org/wiki/Sanitization_(classified_information)) cuya intención es la protección de la privacidad. Es el proceso de eliminar [información personal](https://es.wikipedia.org/wiki/Informaci%C3%B3n_personal) de los conjuntos de datos, de modo que las personas que son descritas por los datos permanecen en el anonimato.
>
En otras palabras, la anonimización de datos es un proceso que conserva los datos pero mantiene la fuente anónima.
Según la técnica de anonimización que se adopte, los datos son editados, ocultados o sustituidos.
Y ese es el propósito de iris-Disguise, ofrecer un conjunto de herramientas de anonimización.
Lo puedes usar de dos formas diferentes, por método de ejecución o especificar tu estrategia de anonimización dentro de la definición de la clase persistente en sí misma.
La actual versión de iris-Disguise ofrece 6 estrategias para anonimizar datos:
- ***Destruction* (Destrucción)**
- ***Scramble* (Codificación)**
- ***Shuffling* (Reorganización)**
- ***Partial masking* (Ocultación parcial)**
- ***Randomization* (Distribución aleatoria)**
- ***Faking* (Falsificación)**
Vamos a explicar cada estrategia. Mostraré un método de ejecución con un ejemplo como he mecionado, y también mostraré cómo aplicarlo dentro de la definición de la clase persistente.
Para usar **iris-Disguise** de esta forma, necesitaréis "*llevar unas gafas de disfraz"*.
En la clase persistente, se puede extender la clase **dc.Disguise.Glasses** y cambiar cualquier propiedad con los tipos de datos con la estrategia que prefieras.
Tras ello, en cualquier momento, simplemente llama al método **DisguiseProcess** en la clase. Todos los valores serán sustituidos usando la estrategia del tipo de datos.
Así que... abrochaos el cinturón que empezamos!
### *Destruction* (Destrucción)
Esta estrategia reemplazará una columna entera con una palabra ('CONFIDENTIAL' (CONFIDENCIAL) por defecto).
```
Do ##class(dc.Disguise.Strategy).Destruction("classname", "propertyname", "Word to replace")
```
El tercer parámetro es opcional. Si no se aporta, se usará la palabra 'CONFIDENTIAL' (CONFIDENCIAL).
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As dc.Disguise.DataTypes.String(FieldStrategy = "DESTRUCTION");
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### *Scramble* (Codificación)
Esta estrategia codificará todos los caracteres de una propiedad.
```
Do ##class(dc.Disguise.Strategy).Scramble("classname", "propertyname")
```
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As dc.Disguise.DataTypes.String(FieldStrategy = "SCRAMBLE");
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### *Shuffling* (Reorganización)
*Shuffling* reorganizará todos los valores de una propiedad dada. No es una estrategia de ocultación porque trabaja "verticalmente".
Esta estrategia es útil para relaciones, porque se mantendrá la integridad referencial.
Hasta esta versión, este método solo funciona en **relaciones de uno a muchos**.
```
Do ##class(dc.Disguise.Strategy).Shuffling("classname", "propertyname")
```
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As %String;
Property Weapon As dc.Disguise.DataTypes.String(FieldStrategy = "SHUFFLING");
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### Partial Masking (Ocultación parcial)
Esta estrategia ocultará parte de los datos. Por ejemplo, el número de una tarjeta de crédito puede ser sustituido por 456X XXXX XXXX X783
```
Do ##class(dc.Disguise.Strategy).PartialMasking("classname", "propertyname", prefixLength, suffixLength, "mask")
```
PrefixLength, suffixLength y mask son opcionales. Si no se aporta, se usarán los valores por defecto.
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As %String;
Property SSN As dc.Disguise.DataTypes.PartialMaskString(prefixLength = 2, suffixLength = 2);
Property Weapon As %String;
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### *Randomization* (Distribución aleatoria)
Esta estrategia generará sencillamente datos aleatorios. Hay tres tipos de distribución aleatoria: integer, numeric y date.
```
Do ##class(dc.Disguise.Strategy).Randomization("classname", "propertyname", "type", from, to)
```
**type**: "integer", "numeric" o "date". "integer" es el predeterminado.
"from" y "to" son opcionales. Es para definir el rango de distribución aleatoria.
Para el tipo "integer" el rango por defecto es de 1 a 100. Para el tipo "numeric" el rango por defecto es de 1.00 a 100.00.
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As %String;
Property Age As dc.Disguise.DataTypes.RandomInteger(MINVAL = 10, MAXVAL = 25);
Property SSN As %String;
Property Weapon As %String;
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### Faking (Falsificación)
La idea de la falsificación es reemplazar datos con valores aleatorios pero plausibles.
**iris-Disguise** proporciona una pequeño conjunto de métodos para generar datos falsos.
```
Do ##class(dc.Disguise.Strategy).Fake("classname", "propertyname", "type")
```
**type**: "firstname", "lastname", "fullname", "company", "country", "city" y "email"
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As dc.Disguise.DataTypes.FakeString(FieldStrategy = "FIRSTNAME");
Property Age As %Integer;
Property SSN As %String;
Property Weapon As %String;
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### ¿Qué os parece?
Me gustaría conocer vuestra opinión y leer vuestros comentarios - qué os parece, si se ajusta a vuestras necesidades y qué funcionalidades echais de menos.
Y quería agradecer de forma especial a @Henrique.GonçalvesDias, @Oliver.Wilms, @Robert.Cemper1003, @Yuri.Gomes y @Evgeny.Shvarov sus comentarios, revisiones, sugerencias y valiosos aportes, que me han inspirado tanto y ayudado a crear y mejorar iris-Disguise.
Este artículo ha sido etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Yaron Munz · 23 sep, 2022
Resumen
Empezamos a usar Azure Service Bus (ASB) como solución de mensajería empresarial hace tres años. La hemos usado para publicar y consumir datos entre muchas aplicaciones de la organización. Como el flujo de datos es complejo, y normalmente se necesitan los datos de una aplicación en muchas otras aplicaciones, el modelo publicador -> múltiples subscriptores resultó muy adecuado. El uso de ASB en la organización es de docenas de millones de mensajes por día, mientras que la plataforma IRIS tiene unos 2-3 millones de mensajes/día.
El problema con ASB
Cuando empezamos con la integración de ASB, encontramos que el protocolo AMQP no tiene la configuración predeterminada para la implementación de IRIS, por lo que estuvimos buscando una solución alternativa para poder comunicar con ASB.
La solución
Desarrollamos un servicio local en Windows (.NET y Swagger) que se encargaba de la comunicación con ASB. Se instaló en la misma máquina que IRIS. Enviamos los mensajes a ASB dirigidos a este servicio local en Windows (usando: localhost y haciendo una API REST) y el propio servicio hacía el resto. Esta solución funcionó bien durante años, pero era muy difícil monitorizar el tráfico, depurar los mensajes "perdidos" y medir el rendimiento general. El hecho de tener este servicio local en Windows como un agente intermedio no resultaba la mejor arquitectura.
Programa de Acceso Preferente a Python Embebido (EAP)
Me pidieron (o me presenté voluntario, no lo recuerdo bien) participar en el Programa de Acceso Preferente a Python Embebido. Fue muy interesante para mí, porque pude poder probar nuevas funcionalidades y dar feedback al equipo de desarrollo. También me gustó mucho participar de forma activa e influir en el desarrollo de este producto. En esa fase, empezamos a probar Python Embebido para el ERP y decidimos comprobar si podíamos utilizar Python Embebido para resolver problemas “reales”. Nos plantearnos entonces intentar resolver la conectividad directa de IRIS a ASB.
El POC (prueba de concepto)
Empezamos a programar la solución y vimos que usar la librería ASB de Microsoft para Python nos haría nuestra vida (y nuestro trabajo) mucho más fácil. El primer paso fue desarrollar una función que pueda conectar con un topic específico en ASB y recibir mensajes. Esto se hizo bastante rápido (1 día de desarrollo) y avanzamos a la fase publicar (enviar mensajes a ASB).
Nos llevó varios días desarrollar toda la cubierta para esas funciones enviar y recibir. Preparamos lo siguiente:
Un “área de preparación” entrante y saliente adecuada, para controlar el flujo de mensajes entrantes/salientes.
Un mecanismo central para almacenar el tráfico ASB entrante y saliente para poder tener estadísticas adecuadas y medidas del rendimiento.
Una página de monitorización CSP para activar/desactivar servicios, mostrar estadísticas y dar alertas sobre cualquier incidencia.
La implementación
Configuración Preliminar
Antes de usar las librerías ASB de Microsoft para Python, hay que instalarlas en un directorio ..\python dedicado.Nosotros elegimos usar ../mge/python/ pero se puede usar cualquier carpeta que se quiera (inicialmente era una carpeta vacía)Los siguientes comandos hay que ejecutarlos bien con una sesión CMD con privilegios (Windows) o con un usuario con los privilegios suficientes (Linux):
..\bin\irispip install --target ..\mgr\python asyncio
..\bin\irispip install --target ..\mgr\python azure.servicebus
Recibir mensajes desde ASB (consumo)
Tenemos una ClassMethod con varios parámetros:
The topiId - El ASB se divide en topics de los cuales se consumen los mensajes
subscriptionName - Nombre de la subscripción Azure
connectionString - Para poder conectar con el topic al que estás suscrito
Ten en cuenta que se está usando [ Language = python ] para indicar que este ClassMethod está escrito en Python (!)
ClassMethod retrieveASB(topicId As %Integer = 1, topicName As %String = "", subscriptionName As %String = "", connectionString As %String = "", debug As %Integer = 1, deadLetter As %Integer = 0) As %Integer [ Language = python ]
Para usar la librería ASB, primero necesitamos importar las librerías ServiceBusClient y ServiceBusSubQueue:
from azure.servicebus import ServiceBusClient,ServiceBusSubQueue
para poder interactuar con IRIS (por ejemplo, ejecutar código) también necesitamos:
import iris
En este punto, podemos usar las librerías ASB:
with ServiceBusClient.from_connection_string(connectionString) as client:
with client.get_subscription_receiver(topicName, subscriptionName, max_wait_time=1, sub_queue=subQueue) as receiver:
for msg in receiver:
En este punto, tenemos un objeto Python "msg" (stream) donde podemos pasarlo (como un stream, por supuesto) a IRIS y almacenarlo directamente en la base de datos:
result = iris.cls("anyIRIS.Class").StoreFrom Python(stream)
Enviar mensajes a ASB (publicación)
Tenemos un ClassMethod con varios parámetros:
topicName - El Topic en el que queremos publicar (aquí tenemos que pasar el nombre, no el id)
connectionString - Para poder conectar con el topic al que se está suscrito
JSONmessage - el mensaje que queremos enviar (publicar)
Ten en cuenta que se está usando [ Language = python ] para indicar que este ClassMethod está escrito en Python (!)
ClassMethod publishASB(topicName As %String = "", connectionString As %String = "", JSONmessage As %String = "") As %Status [ Language = python ]
Para usar la librería ASB, primero necesitamos importar las librerías ServiceBusClient y ServiceBusMessage:
from azure.servicebus import ServiceBusClient, ServiceBusMessage
Usar las librerías ASB es muy sencillo:
try:
result=1
with ServiceBusClient.from_connection_string(connectionString) as client:
with client.get_queue_sender(topicName) as sender:
single_message = ServiceBusMessage(JSONmessage)
sender.send_messages(single_message)
except Exception as e:
print(e)
result=0
return result
Beneficios de usar la conectividad ASB directa
Mucho más rápida que usar la antigua alternativa del servicio local en Windows
Fácil de monitorizar, recoger estadísticas, resolver incidencias
Retirar el agente intermedio (servicio local en Windows) reduce un potencial punto de fallo
Posibilidad de gestionar mensajes perdidos para cualquier topic automáticamente y hacer que ASB re-envíe esos mensajes a los subscriptores del topic
Agradecimientos
Me gustaría dar las gracias a @David.Satorres5186 (Senior Developer para IRIS) por su enorme contribución en el diseño, programación y pruebas. Sin su ayuda este proyecto no hubiera sido posible.
Este artículo ha sido etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Bernardo Linarez · 30 mar, 2020
¡Hola Comunidad!
En este artículo hablaré sobre las pruebas y la depuración de las aplicaciones web de Caché (principalmente REST) con herramientas externas. La segunda parte trata sobre las herramientas de Caché.
Usted escribió el código del lado del servidor y quiere probarlo con un cliente, o ya tiene una aplicación web pero no funciona. Aquí es donde entra la depuración. En este artículo abarcaré desde las herramientas más fáciles de utilizar (el navegador), hasta las más completas (el analizador de paquetes), pero primero conversemos un poco sobre los errores más comunes y cómo pueden resolverse.
Los errores
Error 401 No autorizado
Creo que este error es el que encontramos con más frecuencia durante la implementación de la producción. El servidor de desarrollo local normalmente tiene una configuración de seguridad mínima o normal pero muy convencional. Sin embargo, el servidor de producción puede tener un esquema con más restricciones. Entonces:
Compruebe que ha iniciado sesión
Compruebe que el usuario tiene acceso a la base de datos/tabla/procedimiento/fila/columna a la que desea acceder
Compruebe que la solicitud para las OPCIONES puede realizarse por un usuario no autorizado
Error 404 No encontrado
Compruebe:
Que la URL es correcta
Si es una nueva aplicación y está utilizando un servidor web externo, cargar nuevamente el servidor web puede ser útil
Errores en la aplicación
En cierto modo estos son los más fáciles de encontrar - realizar un seguimiento del stack será muy útil. La resolución es totalmente específica para la aplicación.
Herramientas de depuración
Navegador web
La primera herramienta de depuración que siempre está disponible es un navegador web, es preferible que utilice Chrome, pero Firefox también sería suficiente. Las solicitudes GET pueden probarse al ingresar en la URL que se encuentra en la barra de direcciones, todas las demás solicitudes necesitan de una aplicación web o que estén escritas en un código js. El método general es el siguiente:
Presione F12 para abrir las herramientas del desarrollador
Vaya a la pestaña Network
Marque la casilla Preserve Log, si no está marcada
Despliegue únicamente las solicitudes XHR
Realice una acción para depurar errores en la aplicación web
Desde aquí, puede examinar las solicitudes y enviarlas nuevamente. En Firefox también pueden editarse las solicitudes antes de repetirlas.
Ventajas:
Siempre está disponible
Es fácil de utilizar (los usuarios finales pueden enviar capturas de pantalla de la red y las pestañas de la consola)
Este es el entorno del usuario final
Desventajas:
No muestra respuestas parciales como send/broken/ etc.
Es lento con las respuestas que son grandes
Es lento cuando hay una gran cantidad de respuestas
Todo se hace manualmente
Cliente REST
El cliente REST es una aplicación web independiente o un complemento del navegador, que se diseñó específicamente para probar aplicaciones web. Aquí utilicé Postman, pero existen muchos otros. Así es como se ven las depuraciones en Postman:
Postman funciona mediante solicitudes agrupadas en colecciones. La solicitud puede enviarse a un entorno. El entorno es una colección de variables. Por ejemplo, en mi entorno CACHE@localhost la variable del host está configurada como localhost y el usuario como _SYSTEM. Cuando se envía una solicitud, las variables se reemplazan por sus valores en el entorno seleccionado y entonces la solicitud se envía.
Aquí puede consultar una recopilación de ejemplos y del entorno para el proyecto MDX2JSON.
Ventajas:
Solo es necesario escribirlo una vez y podrá utilizarlo en todas partes
Mejor control de las solicitudes
Optimización de respuestas
Desventajas:
La depuración de solicitudes encadenadas sigue siendo manual (la respuesta a la solicitud 1 puede forzar una respuesta, ya sea, en la solicitud 2 o en la solicitud 2B)
Algunas veces tiene errores en las respuestas parciales como send/broken/etc.
Proxy de depuraciones HTTP
Es una aplicación independiente que registra el tráfico en los HTTP. Las solicitudes registradas pueden modificarse y reenviarse. Yo utilizo Charles y Fiddler.
Ventajas:
Procesa respuestas parciales como send/broken/ etc.
Optimización de respuestas
Mejor soporte técnico para el tráfico HTTPS (que con el analizador de paquetes)
Puede guardar sesiones de captura
Desventajas:
Algunas cosas son necesarias para enviar la solicitud (por ejemplo, aplicaciones web/cliente REST/código JS)
Analizador de paquetes
Es un programa computacional que puede interceptar y registrar el tráfico que pasa por una red. Conforme el flujo de datos fluye a través de la red, el rastreador captura cada paquete y, si es necesario, decodifica los datos sin procesar del paquete. Esta es la opción más completa, pero también requiere de ciertas habilidades para funcionar correctamente. Yo utilizo WireShark. Aquí tiene una pequeña guía sobre cómo instalarlo y utilizarlo:
Si va a capturar paquetes locales, lea acerca del loopback e instale los requisitos previos del software (npcap para Windows)
Instale WireShark
Configure los filtros de captura (por ejemplo, un filtro para capturar únicamente el tráfico de la dirección 57772: con el puerto 57772
Inicie la captura
Configure los filtros de visualización (por ejemplo, un filtro para mostrar únicamente el tráfico http para una IP específica: ip.addr == 1.2.3.4 && http
A continuación, se muestra un ejemplo de captura para el tráfico http (filtro de visualización) en el puerto 57772 (filtro de captura):
Ventajas:
Procesa respuestas parciales como send/broken/ etc.
Puede capturar grandes cantidades de tráfico
Puede capturar cualquier cosa
Puede guardar sesiones de captura
Desventajas:
Algunas cosas son necesarias para enviar la solicitud (por ejemplo, aplicaciones web/cliente REST/código JS)
Qué utilizar
Bueno, eso depende de cuál sea el objetivo. En primer lugar, podemos registrar (depurar el proxy, analizar paquetes) o generar solicitudes (navegadores, clientes REST).
Si está desarrollando una API Web para REST, el cliente REST es la forma más rápida de probar que funciona.
Sin embargo, si las solicitudes del cliente REST funcionan, pero la aplicación web del cliente no, posiblemente necesite un navegador, un proxy de depuraciones http y un analizador de paquetes.
Si tiene clientes y necesita desarrollar una API del lado del servidor para trabajar con ellos, necesitará un proxy de depuraciones http o un analizador de paquetes.
Lo mejor es estar familiarizado con los 4 tipos de herramientas y cambiar rápidamente entre una y otra si la que utiliza actualmente no es suficiente para realizar el trabajo.
Algunas veces resulta evidente cuál es la herramienta correcta.
Por ejemplo, recientemente desarrollé una API en el lado del servidor para un protocolo de extensión http muy popular, los requisitos fueron:
No podemos cambiar el código que ya habían escrito los clientes
Clientes diferentes se comportan de distintas maneras
El comportamiento entre http y https es diferente
El comportamiento con distintos tipos de autenticación es diferente
Es posible procesar hasta cien solicitudes por segundo, por cliente
Todo el mundo ignora el RFC
Aquí solamente existe una solución, el analizador de paquetes.
O si estoy desarrollando una API REST para el consumo de JS, el cliente REST es la herramienta perfecta para realizar pruebas.
Cuando depure la aplicación web, siempre comience con el navegador.
En la parte 2 discutiremos todas las cosas (muchas) que puede hacer para depurar la web en el lado de Caché.
¿Qué métodos utiliza para depurar las comunicaciones entre el cliente y el servidor?
Este artículo ha sido etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Kurro Lopez · 11 feb, 2022
El tiempo dirá, siempre lo hace.
El Doctor.
No es una tarea facil dominar fechas y horas, siempre es un problema y a veces confuso en cualquier lenguaje de programación, vamos a aclarar y a poner unos cuantos tips para que esta tarea sea lo mas sencilla posible.
Súbete a la TARDIS que te voy a convertir en un Señor del tiempo
Empecemos por lo básico
Si vienes de otros lenguajes, comentar que las fechas en Intersystems Object Script (en adelante IOS, no confundir con los móviles) son un poco peculiares.Cuando ejecutamos el comando $HOROLOG en el terminal, para tener la fecha y hora actual, verás que se divide en dos partes:
WRITE $HOROLOG
> 66149,67164
El primer valor es el día, para ser exactos el número de días desde el 31 de diciembre de 1840, osea, que el valor 1 es el 1 de enero de 1841 y el segundo los segundos desde la 00:00 de ese día.
En este ejemplo, 66149 corresponde con 09/02/2022 y 67164 con las 18:39:24 horas. A este formato, lo vamos a llamar formato interno de fecha y hora.
¿Confuso? pues vamos a empezar a desvelar los grandes secretos del universo (de fechas y horas)
¿Cómo puedo convertir el formato interno en formato mas claro?
Para ello vamos a usar el comando $ZDATETIME
El comando básico sería
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATETIME(AhoraMismo)
> 02/09/2022 18:39:24
Por defecto, utiliza el formato americano (mm/dd/yyyy). Si quieres utilizar la fecha en otro formato, vamos a utilizar el segundo parámetro, como por ejemplo el europeo (dd/mm/yyyy), en ese caso le vamos a dar el valor 4 (para mas formatos, ver la documentación $ZDATETIME.dformat)
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATETIME(AhoraMismo,4)
> 09/02/2022 18:39:24
Esta opción utiliza como separador y formato de año lo que tengamos definido en las variables locales
Si queremos además poner otro formato la hora, por ejempo en formato 12 horas (AM/PM) en lugar de formato 24 horas, utilizamos el tercer parámetro con el valor 3, si no queremos que muestre los segundos, usaremos el valor 4 (ver la documentación $ZDATETIME.tformat)
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATETIME(AhoraMismo,4,3)
> 09/02/2022 06:39:24PM
WRITE $ZDATETIME(AhoraMismo,4,4)
> 09/02/2022 06:39PM
¿Ahora está mas claro? pues vamos a profundizar mas
Formato ODBC
Este formato es independiente de tu configuración local, siempre se mostrará como yyyy-mm-dd, su valor es 3. Se recomiendo utilizarlo si queremos crear datos que se van a exportar en ficheros, como ficheros CSV, HL7, etc..
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATETIME(AhoraMismo,3)
> 2022-02-09 18:39:24
Día de la semana, nombre del día, día del año
Valor
Descripción
10
El día de la semana será un valor entre 0 y 6, siendo el 0 el domingo y el 6 el sábado.
11
El nombre del día de la semana abreviado, lo devolverá según la configuración local definda, la instalación por defecto de IRIS es enuw (English, United States, Unicode)
12
El nombre del día de la semana en formato largo. Igual que 11
14
El día del año, pues eso, el número de días desde el 1 de enero
Si solo queremos tratar fechas y horas por separado, debes de usar los comandos $ZDATE y $ZTIME respectivamente. El parámetro para los formatos son los mismos que define en $ZDATETIME.dformat y $ZDATETIME.tformat
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATE(AhoraMismo,10)
> 3
WRITE $ZDATE(AhoraMismo,11)
> Wed
WRITE $ZDATE(AhoraMismo,12)
> Wednesday
¿Y cómo convierto una fecha en formato interno?
Pues ahora vamos a ver el paso contrario, es decir, tener un texto con una fecha y lo convertimos en formato IOS. Para ello vamos a usar el comando $ZDATETIMEH
Esta vez, tenemos que indicar en que formato está la fecha y la hora (si usamos $ZDATETIMEH) o la fecha ($ZDATEH) o la hora ($ZTIMEH) por separado.
Los formatos siguen siendo los mismos, es decir si tenemos una cadena con la fecha en formato ODBC (yyyy-mm-dd), pues utilizaremos el valor 3
SET MiFecha = "2022-02-09 18:39:24"
SET FechaInterna1 = $ZDATETIMEH(MiFecha, 3, 1) // Formato ODBC
SET MiFecha = "09/02/2022 18:39:24"
SET FechaInterna2 = $ZDATETIMEH(MiFecha, 4, 1) // Formato europeo
SET MiFecha = "02/09/2022 06:39:24PM"
SET FechaInterna3 = $ZDATETIMEH(MiFecha, 1, 3) // Formato Americano con hora en 12h AM/PM
WRITE FechaInterna1,!,FechaInterna2,!,FechaInterna3
> 66149,67164
66149,67164
66149,67164
Lógicamente, si le decimos que es un formato y le damos el valor erroneo, puede pasar cualquier cosa, como que en lugar de 9 de febrero lo entiende como 2 de septiembre.
No mezclar formatos que luego vienen los problemas.
SET MiFecha = "09/02/2022"
/// Formato Americano
SET FechaInterna = $ZDATEH(MiFecha, 1)
/// Formato Europeo
SET OtraFecha = $ZDATETIME(FechaInterna, 4)
WRITE FechaInterna,!,OtraFecha
> 66354
02/09/2022
Ni que decir, si intentamos poner fecha europea e intentar transformarlo en americano... ¿Que pasaría en San Valentín?
SET MiFecha = "14/02/2022"
SET FechaInterna = $ZDATEH(MiFecha, 1) // Formato Americano. OJO, el mes 14 no existe
^
<ILLEGAL VALUE>
Pues como todos los San Valentín.. corazones rotos, bueno... mas bién código roto.
Pues vamos a hacer algo con esto que ya has aprendido
READ !,"Por favor, indica tu fecha de nacimiento (dd/mm/yyyy): ",fechaNacimiento
SET formatoInterno = $ZDATEH(fechaNacimiento, 4)
SET diaSemana = $ZDATE(formatoInterno, 10)
SET nombreDia = $ZDATE(formatoInterno, 12)
WRITE !,"El día de la semana de tu nacimiento es: ",nombreDia
IF diaSemana = 5 WRITE "Siempre te ha gustado la fiesta!!!" // Nació un viernes
Pero mas adelante veremos otras formas de hacer las cosas, y como controlar los errores.
Próximo capítulo: Cómo viajar en el tiempo
Curiosidad
Si quieres saber porque se toma el valor de 01/01/1841 como el valor 1, viene porque fué elegido esta fecha por ser el año no bisiesto anterior al nacimiento del ciudadano estadounidense de mayor edad vivo, que era un veterano de la guerra civil con 121 años, cuando se diseñó el lenguaje de programación MUMPS, del cual extiende Object Script
Artículo
Ricardo Paiva · 31 mayo, 2022
Hola desarrolladores,
En el artículo anterior, describimos cómo utilizar config-api para configurar IRIS.
Ahora, vamos a intentar combinar la biblioteca con el cliente ZPM.
El objetivo es cargar un documento de configuración durante `zpm install` en la `configure phase`.
Para realizar este ejercicio, hay un repositorio de plantillas disponible [aquí](https://github.com/lscalese/objectscript-docker-template-with-config-api) (está basado en [objectscript-docker-template](https://github.com/intersystems-community/objectscript-docker-template)).
Tratamos de:
* Crear una base de datos `MYAPPDATA`.
* Configurar el mapeo de Globals para `dc.PackageSample.*`.
* Añadir un usuario llamado `SQLUserRO` con acceso a la función SQL de solo lectura.
* Añadir una configuración SSL denominada `SSLAppDefault`.
* Crear una aplicación REST `/rest/myapp`.
El archivo de configuración [`iris-config.json`](https://github.com/lscalese/objectscript-docker-template-with-config-api/blob/master/config-api/iris-config.json) se encuentra en el subdirectorio `config-api`. Es una plantilla vacía que se puede completar con el siguiente contenido:
```json
{
"Security.Roles":{
"MyRoleRO" : {
"Descripion" : "SQL Read Only Role for dc_PackageSample schema.",
"Resources" : "%Service_SQL:U",
"GrantedRoles" : "%SQL"
}
},
"Security.SQLPrivileges": [{
"Grantee": "MyRoleRO",
"PrivList" : "s",
"SQLObject" : "1,dc_PackageSample.*"
}],
"Security.SQLAdminPrivilegeSet" : {
"${namespace}": [
]
},
"Security.Users": {
"${sqlusr}": {
"Name":"${sqlusr}",
"Password":"$usrpwd$",
"AccountNeverExpires":true,
"AutheEnabled":0,
"ChangePassword":false,
"Comment":"Demo SQLUserRO",
"EmailAddress":"",
"Enabled":true,
"ExpirationDate":"",
"FullName":"Demo SQLUserRO",
"NameSpace":"${namespace}",
"PasswordNeverExpires":false,
"PhoneNumber":"",
"PhoneProvider":"",
"Roles":"MyRoleRO",
"Routine":""
}
},
"Security.SSLConfigs": {
"SSLAppDefault":{}
},
"Security.Applications" : {
"/rest/myapp": {
"NameSpace" : "${namespace}",
"Enabled" : 1,
"DispatchClass" : "Your.Dispatch.class",
"CSPZENEnabled" : 1,
"AutheEnabled": 32
}
},
"SYS.Databases":{
"${mgrdir}myappdata" : {
"ExpansionSize":64
}
},
"Databases":{
"MYAPPDATA" : {
"Directory" : "${mgrdir}myappdata"
}
},
"MapGlobals":{
"${namespace}": [{
"Name" : "dc.PackageSample.*",
"Database" : "MYAPPDATA"
}]
},
"Library.SQLConnection": {
}
}
```
Puedes ver el uso de `${namespace}`, `${mgrdir}`. Se trata de variables predefinidas y se evaluarán en el tiempo de ejecución con el *namespace* actual y el directorio de IRIS mgr. Otras variables predefinidas son:
* `${cspdir}`: subdirectorio CSP en el directorio de instalación de IRIS.
* `${bindir}`: directorio bin `$SYSTEM.Util.BinaryDirectory()`
* `${libdir}`: subdirectorio LIB en el directorio de instalación de IRIS.
* `${username}`: nombre de usuario actual `$USERNAME`
* `${roles}`: funciones actuales otorgadas `$ROLES`
Además, utilizamos `${sqlusr}`, no es una variable predefinida y debemos establecer el valor desde `module.xml`. Realmente no lo necesitamos, pero es solo para demostrar cómo los desarrolladores pueden definir y establecer variables desde modules.xml y transmitirlas al archivo de configuración.
Para ayudarte a crear tu propio archivo de configuración, una guía rápida ([cheat sheet `config-api.md`])(https://github.com/lscalese/objectscript-docker-template-with-config-api/blob/master/config-api.md) también está disponible en el repositorio de plantillas.
El archivo de configuración ya está listo, el siguiente paso es el archivo `module.xml`.
Tenemos que:
* Añadir el módulo `config-api` como una dependencia.
* Invocar un método para cargar nuestro archivo de configuración `./config-api/iris-config.json` con el argumento `${sqlusr}`.
```xml
objectscript-template-with-config-api
0.0.1
module
${root}config-api/iris-config.json
sqlusr
SQLUserRO
src
config-api
1.0.0
```
Echa un vistazo a la etiqueta Invoke:
```xml
${root}config-api/iris-config.json
sqlusr
SQLUserRO
```
El primer argumento es la ruta al archivo de configuración, `${root}` contiene el directorio del módulo usado por zpm durante la implementación.
Después de eso, todos los demás argumentos deben emparejarse. En primer lugar, una cadena con el nombre de la variable seguido del valor relacionado.
En nuestro ejemplo `sqlusr` para la variable `${sqlusr}` seguido del valor `SQLUserRO`.
Puedes pasar tantos argumentos emparejados como necesites.
Ten en cuenta que hay una etiqueta FileCopy: `` Esta etiqueta existe solo para forzar al zpm a empaquetar el archivo de configuración. Deberíamos agregar una etiqueta Invoke para eliminar el directorio que se va a limpiar, así:
```xml
${libdir}config-api/
```
Module.xml está listo, es hora de probarlo. Compila e inicia el contenedor `docker-compose up -d --build`.
Deberías ver estos registros durante la fase de configuración:
```
[objectscript-template-with-config-api] Configure START
2021-04-08 19:24:43 Load from file /opt/irisbuild/config-api/iris-config.json ...
2021-04-08 19:24:43 Start load configuration
2021-04-08 19:24:43 {
"Security.Roles":{
"MyRoleRO":{
"Descripion":"SQL Read Only Role for dc_PackageSample schema.",
"Resources":"%Service_SQL:U",
"GrantedRoles":"%SQL"
}
},
"Security.SQLPrivileges":[
{
"Grantee":"MyRoleRO",
"PrivList":"s",
"SQLObject":"1,dc_PackageSample.*"
}
],
"Security.SQLAdminPrivilegeSet":{
"USER":[
]
},
"Security.Users":{
"SQLUserRO":{
"Name":"SQLUserRO",
"Password":"$usrpwd$",
"AccountNeverExpires":true,
"AutheEnabled":0,
"ChangePassword":false,
"Comment":"Demo SQLUserRO",
"EmailAddress":"",
"Enabled":true,
"ExpirationDate":"",
"FullName":"Demo SQLUserRO",
"NameSpace":"USER",
"PasswordNeverExpires":false,
"PhoneNumber":"",
"PhoneProvider":"",
"Roles":"MyRoleRO",
"Routine":""
}
},
"Security.SSLConfigs":{
"SSLAppDefault":{
}
},
"Security.Applications":{
"/rest/myapp":{
"NameSpace":"USER",
"Enabled":1,
"DispatchClass":"Your.Dispatch.class",
"CSPZENEnabled":1,
"AutheEnabled":32
}
},
"SYS.Databases":{
"/usr/irissys/mgr/myappdata":{
"ExpansionSize":64
}
},
"Databases":{
"MYAPPDATA":{
"Directory":"/usr/irissys/mgr/myappdata"
}
},
"MapGlobals":{
"USER":[
{
"Name":"dc.PackageSample.*",
"Database":"MYAPPDATA"
}
]
},
"Library.SQLConnection":{
}
}
2021-04-08 19:24:43 * Security.Roles
2021-04-08 19:24:43 + Create MyRoleRO ... OK
2021-04-08 19:24:43 * Security.SQLPrivileges
2021-04-08 19:24:43 + Create {"Grantee":"MyRoleRO","PrivList":"s","SQLObject":"1,dc_PackageSample.*"} ... OK
2021-04-08 19:24:43 * Security.SQLAdminPrivilegeSet
2021-04-08 19:24:43 * Security.Users
2021-04-08 19:24:43 + Create SQLUserRO ... OK
2021-04-08 19:24:43 * Security.SSLConfigs
2021-04-08 19:24:43 + Create SSLAppDefault ... OK
2021-04-08 19:24:43 * Security.Applications
2021-04-08 19:24:43 + Create /api/config ... OK
2021-04-08 19:24:43 * SYS.Databases
2021-04-08 19:24:43 + Create /usr/irissys/mgr/myappdata ... OK
2021-04-08 19:24:43 * Databases
2021-04-08 19:24:43 + Create MYAPPDATA ... OK
2021-04-08 19:24:44 * MapGlobals
2021-04-08 19:24:44 + Create USER dc.PackageSample.* ... OK
2021-04-08 19:24:44 * Library.SQLConnection
[objectscript-template-with-config-api] Configure SUCCESS
```
Tu configuración se aplicó y el archivo de configuración se empaquetará cuando se publique. Puedes comprobarlo en el Portal de administración.
¡Gracias!
Artículo
Carlos Castro · 28 feb, 2023
Buenas a todos,
una de las herramientas potentes que tiene Intersystems es la posibilidad de implementar en el propio sistema la autenticación OAuth2. Esta herramienta nos da la posibilidad de poder controlar quien accede a nuestros recursos y como accede.
A continuación planteo una solución ante el problema de querer controlar quien accede a mis recursos y la posibilidad de monitorizarlo. Para ello deberemos seguir los siguientes pasos:1.- Definir un Servidor de Autenticacion
1.1.- Crear Servidor
1.2.- Crear Cliente del Servidor
1.3.- Crear Servidor de Recursos
1.4.- Crear cliente de Servidor de Recursos
2.- Definir un servicio SOPA/REST para acceder a los recursos.
2.1.- Crear Servicio
2.2.- Validar Acceso desde el servicio
** Os intentaré ir dejando capturas del portal de gestión para situaros mejor en el proceso. Si lo seguís secuencialmente no deberíais tener problema para completar el proceso.
Teniendo claros los pasos a seguir, ¡COMENCEMOS!
Presentación del Problema:
Supongamos que tenemos un Servicio que accede a unos recursos determinados. La publicación de este servicio es accesible por todo aquel cliente que ataque el servicio, y deseamos controlar quien accede a estos recursos, pues no queremos que todo el mundo acceda a los recursos, ademas que tampoco queremos que los clientes autorizados para acceder a los mismos puedan acceder a los recursos de otros que también tengan permisos, por lo que vamos a ponerle un "portero" al servicio para que nos asegure que este cliente tiene autorizado acceder a nuestros recursos.
Para solucionar este problema de forma ágil, haremos uso del servidor de autenticación OAuth2 que podemos utilizar en IRIS siguiendo los siguientes pasos:
1.1.- Crear Servidor
En la Producción Web seleccionamos Administración>seguridad>oauth2.0>servidor y deberíamos observar:
A continuación, necesitamos escribir en el formulario:
URL: DNS o IP del Servidor donde se va a alojar el Servidor de Autorización
Puerto: Puerto interno del Servidor Web [p.e: 57773]
El puerto del Apache interno es el que se debe utilizar, seguramente sea necesario habilitar el acceso a estos puertos con edición del fichero httpd.conf
Prefijo: Indicar la aplicación donde se va a alojar el Servidor de Autorización. De forma automática se crea la aplicación al guardar el servidor. [p.e: oauth2]
Tipos de concesiones: deja solo credenciales de cliente
Configuración SSL/TLS: Certificado SSL que tengamos
En la segunda pestaña, nombrada como “Ámbitos” define my/scope así:
Ámbito | my/scope | Introducir ámbito compatible: my/scopeDescripción | First Scope | Introducir descripción del ámbito: First Scope
Además, en la 5ª pestaña, “Personalización”; hay que configurar el cuarto valor:
Generar clase de token: %OAuth2.Server.JWT
Guardar y ya deberíamos ver que se ha creado.
En este punto, tenemos el servidor creado.
1.2.- Crear Cliente del Servidor
Estando en la pantalla de: Administración>seguridad>oauth2.0>servidor; pulsar en el botón “Descripciones de cliente”.
Esto nos permitirá indicar qué clientes pudieran autorizarse en nuestro servidor. En concreto nos creamos uno de ejemplo:
Nombre
cliente desde postman
Identificador del Cliente a nivel local.
ID del cliente
HbOAGs6byL-gTlabW8gCDguQnYyHCWT82EToGTjP5fQ
Valor del ID del Cliente.
Tipo de cliente
confidential
URL de redireccionamiento
https://DNS/IP:57773
DNS y Puerto del Servidor de Autorización configurados en el apartado 2.1
Descripción
cliente servidor de autenticación
Descripción del Cliente.
Al guardarlo deberíamos observar:
Llegados aquí, ya disponemos de un servidor de autenticación con un cliente. El proceso ahora consiste en obtener el token de acceso del servidor en base a las credenciales de acceso que acabamos de crear. Una vez obtenemos este token, deberemos validarlo al intentar acceder a los recursos.
Procedimiento para obtener el token:A) Atacar por REST a: https://DNS/IP:57773/oauth2/token?grant_type=client_credentials&scope=my/scope
con datos:
Authorizacion= Basic OAuth
Username= clientID (obtenido de la definición del cliente creada en el paso 1.2)
Password=SecretID (obtenido de la definición del cliente creada en el paso 1.2)
Esta petición nos devolverá un TOKEN de acceso que tendremos que validar a continuación.
1.3.- Crear Servidor de Recursos
En Sistema > Gestión de seguridad > Cliente de OAuth 2.0 > Descripción del servidor - (Configuración de seguridad)
1.4.- Crear cliente de Servidor de Recursos
Con el objetivo de definir a qué información se accede una vez el Sistema Externo se ha validado, se construye el “Servidor de Recursos”.
Para generar el Servidor de Recursos, debemos estar en: Sistema > Gestión de seguridad > Configuración del servidor de autorización OAuth 2.0 > Servidor de OAuth 2.0 y pulsar en el botón “Crear la descripción de cliente” tal y como se muestra en la siguiente captura:
NOMBRE: resserver
Descripción: [Texto descriptivo]
Tipo de cliente: Servidor de recursos
Tipo de concesiones: Código de autorización
Tipo de concesiones compatibles: código
Tipo de autenticación: básico
Llegados a este punto, Ya hemos definido un servidor de autenticación y un servidor de recursos, ¡Vamos a unirlo todo!
A continuación debemos incluir en nuestro servicio SOAP o REST el control para validar el token generado por el servidor de autenticación, para ello incluiremos en nuestro servicio lo siguiente:
Parameter OAUTH2APPNAME = "resserver";
Parameter OAUTH2SCOPES = "openid profile email my/scope";
Dentro del método del servicio al principio:
set authorization = $tr(pInput.GetAttribute("authorization"),"")set tokenSinBearer = $PIECE(authorization,"Bearer ",2)
set tSC= ..ResServer(tokenSinBearer)
Método:
ClassMethod ResServer(accessToken As %String(MAXLEN="")) As %Status{ // validate token set isJWTValid = ##class(%SYS.OAuth2.Validation).ValidateJWT("resserver",accessToken,"","",.jwtPayload ,.securityParameters,.sc) if (('isJWTValid) || ($$$ISERR(sc))) { quit '$$$OK } // introspection set sc = ##class(%SYS.OAuth2.AccessToken).GetIntrospection("resserver", accessToken, .jsonObject) if $$$ISERR(sc) { quit '$$$OK } quit $$$OK}
Con este método lo que vamos a realizar es la validación del token recibido en el servicio contra el servidor de autenticación. La respuesta tSC es la que deberemos validar para dar acceso o no al resto de recursos a los que accede el servicio, de una forma esquematizada sería: Method misRecursos(pInput As %Stream.Object, Output pOutput As %Stream.Object) As %Status
{
set authorization = $tr(pInput.GetAttribute("authorization"),"")set tokenSinBearer = $PIECE(authorization,"Bearer ",2)
set tSC= ..ResServer(tokenSinBearer)
if (tSC= 1){
ACCESO A LOS RECURSOS
quit pOutput
}else{
quit '$$$OK
}
}
En resumidas cuentas, el servidor de autenticación OAUTH2 nos sirve para generar un token en base al clientID y secretID que hemos generado al crear el cliente para el mismo Servidor OUATH2. Este Token seguidamente lo enviaremos al servidor de recursos que previo al acceso de los mismos, validara este token contra el servidor de autenticación, y si es correcto, nos dará acceso a los mismos.
El proceso completo os lo dejo aquí indicado:
Finalmente si quisiéramos aplicar algún control extra, lo podemos realizar dentro del Business Process incluyéndole en el control la información de clientID que queramos monitorizar y/o validar.
Pregunta
Yone Moreno · 17 abr, 2023
Buenas tardes,
Antes que nada, muchísimas gracias por leer esta duda, y sobre todo por dedicar tiempo en entenderla y en responderla. Gracias.
Por favor, necesitaríamos su ayuda. Actualmente estamos desarrollando una Integración REST, y se nos da un caso que nos gustaría comentar con ustedes, a fin de hallar pistas, documentación, ejemplos, o mecanismos para gestionarlo y depurarlo.
En la Operación REST recibimos:
[{"codigo":"5128","descripcion":"LAS ENFERMERAS FRENTE A LOS PROBLEMAS DE SALUD MENTAL","programa":"Probabilidad de contagio ante un accidente hemático.","admitido":1,"desdefecha":"26/10/2022","hastafecha":"26/10/2029","cursohorario":[{"aula":"AULA 1","desdefecha":"2022/12/26","hastafecha":"2022/12/26","desdehora":"16:00:00","hastahora":"20:30:00"},{"aula":"AULA 2","desdefecha":"27/10/2022","hastafecha":"27/12/2022","desdehora":"10:00:00","hastahora":"12:45:00"}],"error":null},{"codigo":"5129","descripcion":"HISTORIA DE SALUD ELECTRONICA DRAGO-AP","programa":"XXXX","admitido":0,"desdefecha":"15/01/2022","hastafecha":"15/01/2029","cursohorario":[{"aula":"AULA MAGNA","desdefecha":"15/01/2022","hastafecha":"15/01/2022","desdehora":"16:00:00","hastahora":"20:30:00"}],"error":null}]
E ingenuamente transformamos el String a objeto Mensaje Response mediante:
// se transforma el objeto JSON a un objeto local
set claseAux = ##class(%ZEN.Auxiliary.jsonProvider).%New()
set tSC= claseAux.%ConvertJSONToObject(.linea,"Mensajes.Response.miFormacion.GetCursosAdmitidosResponse",.pResponse,1)
Lo cual implica Excepción:
Id.:
273670753
Tipo:
Error
Texto:
ERROR #5002: Error de cache: <LIST>%GetSerial+1^%Library.ListOfObjects.1
Registrado:
2023-04-17 14:04:55.913
Origen:
Operaciones.REST.miFormacionv01r00
Sesión:
19206868
Job:
423529
Clase:
Ens.MessageHeader
Método:
NewResponseMessage
Seguimiento paso a paso:
(ninguno)
Pila:
$$^%GetSerial+1^%Library.ListOfObjects.1 +4
$$^%SerializeObject+2^%Library.ListOfObjects.1 +1
$$^%GetSwizzleObject+13^%Library.ListOfObjects.1 +9
$$^zNewResponseMessage+5^Ens.MessageHeader.1 +1
$$^zMessageHeaderHandler+208^Operaciones.REST.miFormacionv01r00.1 +1
$$^zOnTask+42^Ens.Host.1 +1
DO^zStart+62^Ens.Job.1 +2
Para recapitular, el Sistema Destino nos responde con esta String; la cual sí es un JSON válido tal y como se puede comrpobar en https://jsonlint.com/
[
{
"codigo": "5128",
"descripcion": "LAS ENFERMERAS FRENTE A LOS PROBLEMAS DE SALUD MENTAL",
"programa": "Probabilidad de contagio ante un accidente hemático.",
"admitido": 1,
"desdefecha": "26/10/2022",
"hastafecha": "26/10/2029",
"cursohorario": [
{
"aula": "AULA 1",
"desdefecha": "2022/12/26",
"hastafecha": "2022/12/26",
"desdehora": "16:00:00",
"hastahora": "20:30:00"
},
{
"aula": "AULA 2",
"desdefecha": "27/10/2022",
"hastafecha": "27/12/2022",
"desdehora": "10:00:00",
"hastahora": "12:45:00"
}
],
"error": null
},
{
"codigo": "5129",
"descripcion": "HISTORIA DE SALUD ELECTRONICA DRAGO-AP",
"programa": "XXXX",
"admitido": 0,
"desdefecha": "15/01/2022",
"hastafecha": "15/01/2029",
"cursohorario": [
{
"aula": "AULA MAGNA",
"desdefecha": "15/01/2022",
"hastafecha": "15/01/2022",
"desdehora": "16:00:00",
"hastahora": "20:30:00"
}
],
"error": null
}
]
Siendo nuestros Mensaje Response:
Class Mensajes.Response.miFormacion.GetCursosAdmitidosResponse Extends Ens.Response
{
Property cursos As list Of EsquemasDatos.miFormacion.CursoAdmitido;
Y el EsquemasDatos interno:
Class EsquemasDatos.miFormacion.CursoAdmitido Extends Ens.Response
{
Property codigo As %String(MAXLEN = "");
Property descripcion As %String(MAXLEN = "");
Property programa As %String(MAXLEN = "");
Property admitido As %Boolean;
Property desdefecha As %String(MAXLEN = "");
Property hastafecha As %String(MAXLEN = "");
Property cursohorario As list Of EsquemasDatos.miFormacion.CursoHorario;
Property error As EsquemasDatos.Seguridad.Error;
¿De qué manera nos aconsejan depurar, documentarnos, buscar la causa de la Excepción?
Para tratar de resolverlo por nosotros mismos hemos considerado utilizar alguna herramienta para convertir el objeto JSON en un objeto que podamos manipular en nuestro código. Por ejemplo, en lugar de utilizar la clase %ZEN.Auxiliary.jsonProvider, podríamos utilizar la clase %Object, que es una clase básica de InterSystems IRIS que permite trabajar con objetos de forma dinámica. El código es:
Set pResponse = {} // Creamos un objeto vacío
Set tSC = pResponse.%FromJSON(linea) // Convertimos la respuesta JSON en un objeto
If $$$ISERR(tSC) {
Write "Error al convertir respuesta JSON: ", tSC.GetErrorText(), !
Quit
}
$$$LOGINFO("pResponse: "_pResponse)
Sin embargo de esta forma, sí es verdad que el LOGINFO pinta que disponemos de:
Info
2023-04-17 15:10:46.749
pResponse: 8@%Library.DynamicObject
Ahora bien, en el visor de mensaje se nos ve en azul:
¿De qué manera nos aconsejan depurar, documentarnos, buscar la causa de la Excepción?
¿Pudiera ser que la respuesta JSON de Sistema Destino, no coincida con la definición interna hecha a mano del Mensaje Response y el EsquemasDatos?
Lo pregunto porque: la excepción que se muestra parece indicar que hay un problema al serializar la lista de objetos en Cache ObjectScript. Para depurar este problema, es posible que necesite verificar si la definición de clase de "Mensajes.Response.miFormacion.GetCursosAdmitidosResponse" coincide con el formato del objeto JSON devuelto por el servicio REST. Lo cual en teoría puede hacerse comparando la definición de clase con el objeto JSON.
➕🔎🔍Además hemos indagado:
https://community.intersystems.com/post/convert-string-json
https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GJSON_INTRO
https://community.intersystems.com/post/how-convert-xml-string-json-object-script
¿De qué manera nos aconsejan depurar, documentarnos, buscar la causa de la Excepción?
Muchísimas gracias por su ayuda. Y muchas gracias por respondernos con pasos a seguir, documentación y/o ejemplos. Gracias porque es un alivio contar con el apoyo de ustedes que tienen gran cantidad de práctica, experiencia y conocimientos valiosísimos y muy únicos.
Un saludo. Buenas Yone. Sospecho que quizás el problema pueda estar en la definición del objeto GetCursosAdmitidosResponse, según su definición tiene una propiedad cursos:
Property cursos As list Of EsquemasDatos.miFormacion.CursoAdmitido;
Pero lo que enviais para hacer el mapeo de json al objeto comienza así:
[
{
"codigo": "5128",
"descripcion": "LAS ENFERMERAS FRENTE A LOS PROBLEMAS DE SALUD MENTAL",
"programa": "Probabilidad de contagio ante un accidente hemático.",
"admitido": 1,
"desdefecha": "26/10/2022",
"hastafecha": "26/10/2029",
"cursohorario": [
{
"aula": "AULA 1",
Cuando debiera ser:
"cursos": [
{
"codigo": "5128",
"descripcion": "LAS ENFERMERAS FRENTE A LOS PROBLEMAS DE SALUD MENTAL",
"programa": "Probabilidad de contagio ante un accidente hemático.",
"admitido": 1,
"desdefecha": "26/10/2022",
"hastafecha": "26/10/2029",
"cursohorario": [
{
"aula": "AULA 1",
Yo probaría modificando el JSON que estáis enviado para ver si es ese el problema. ¡Hola Yone! ¿Funcionó la solución propuesta?
Artículo
Miguelio · 7 jun, 2025
¿Conoces a Google? Seguro que si 😄 a menudo hacemos login en webs con nuestra cuenta de Gmail por la comodidad de simplemente hacer click! sin tener que escribir email ni contraseña, esto es posible porque nuestro navegador guarda un token de acceso que nos identifica y, en este caso Google, comparte un acceso para poder consultar información de nosotros como el correo electrónico.
🔐 Existen unas pautas o proceso para hacer esta identificación de forma segura, lo que se conoce como Oauth.
En este artículo no voy a explicar cómo funciona Oauth, te mostraré cómo hacer para persistir la sesión en el Oauth de IRIS sin tener que introducir usuario y contraseña cada vez que entras y de paso cómo saltar la pantalla de aceptación de permisos.
Aquí tienes una imagen marcando el flujo que vamos a crear.
¡¡Vamos al lío!!
Como primer paso puedes montar todo un sistema de Oauth en IRIS con el proyecto https://github.com/intersystems-ib/workshop-iris-oauth2, en el archivo Readme tienes los pasos para hacerlo funcionar con Docker.
Abre desde VS code el área de trabajo del proyecto.
Crea una clase que extienda de %OAuth2.Server.Authenticate, en nuestro caso la hemos llamado cysnet.oauth.server.Authenticate.
Configura Oauth para usar la clase personalizada.
Aquí la joya de la corona, crea dos métodos en la clase personalizada.
ClassMethod LoginFromCookie(authorizationCode As %String) As %Status [ Internal, ServerOnly = 1 ]
{
#dim sc As %Status = $$$OK
Set currentNS = $NAMESPACE
Try {
// Get cookie with jwt access token
Set cookieToken = %request.GetCookie("access_token")
If cookieToken '= "" {
ZNspace "%SYS"
// Get valid access token from cookie
Set accessToken = ##class(OAuth2.Server.AccessToken).OpenByToken(cookieToken,.sc)
If $$$ISOK(sc) && $ISOBJECT(accessToken) {
// Get current access token
Set currentToken = ##class(OAuth2.Server.AccessToken).OpenByCode(authorizationCode,.sc)
If $$$ISOK(sc) && $ISOBJECT(currentToken) {
// Get oauth client
Set client = ##class(OAuth2.Server.Client).Open(currentToken.ClientId,.sc)
If $$$ISOK(sc) && $ISOBJECT(client) {
// Skip login page
Set currentToken.Username = accessToken.Username
#dim propertiesNew As %OAuth2.Server.Properties = currentToken.Properties
Set key=""
For {
Set value=accessToken.Properties.ResponseProperties.GetNext(.key)
If key="" Quit
Do ..SetTokenProperty(.propertiesNew,key,value)
}
Do ##class(OAuth2.Server.Auth).AddClaimValues(currentToken, currentToken.ClientId, accessToken.Username)
Set currentToken.Properties = propertiesNew
Set currentToken.Stage = "permission"
Do currentToken.Save()
// Skip permissions page
Set url = %request.URL_"?AuthorizationCode="_authorizationCode_"&Accept=Aceptar"
Set %response.Redirect = url
} Else {
Set sc = $$$ERROR($$$GeneralError, "Error getting oauth client")
}
} Else {
Set sc = $$$ERROR($$$GeneralError, "Error getting current token")
}
} Else {
Set sc = $$$ERROR($$$GeneralError, "Error getting cookie token")
// Clear cookie access_token
Set %response.Headers("Set-Cookie") = "access_token=; Path=/; Max-Age=0"
}
ZNspace currentNS
} Else {
Set sc = $$$ERROR($$$GeneralError, "Error cookie access_token missing")
}
} Catch ex {
ZNspace currentNS
If $$$ISOK(sc) {
Set sc = ex.AsStatus()
}
}
Quit sc
}
ClassMethod SetTokenProperty(Output properties As %OAuth2.Server.Properties, Name, Value, Type = "string") [ Internal, ServerOnly = 1 ]
{
// Add claims and more
Set tClaim = ##class(%OAuth2.Server.Claim).%New()
Do properties.ResponseProperties.SetAt(Value,Name)
Do properties.IntrospectionClaims.SetAt(tClaim,Name)
Do properties.UserinfoClaims.SetAt(tClaim,Name)
Do properties.JWTClaims.SetAt(tClaim,Name)
Do properties.IDTokenClaims.SetAt(tClaim,Name)
Do properties.SetClaimValue(Name,Value,Type)
Quit $$$OK
}
Sobrescribe el método DisplayLogin
ClassMethod DisplayLogin(authorizationCode As %String, scope As %ArrayOfDataTypes, properties As %OAuth2.Server.Properties, loginCount As %Integer = 1) As %Status
{
Set sc = ..LoginFromCookie(authorizationCode)
If $$$ISOK(sc) {
Quit sc
} Else {
$$$LOGERROR($system.Status.GetErrorText(sc))
}
Quit ..DisplayLogin(authorizationCode, scope, properties, loginCount)
}
Veamos paso por paso que hace el método LoginFromCookie, supongamos que ya hemos hecho login con anterioridad y tenemos el token JWT de OpenID en una cookie llamada access_token.
Obtiene la cookie access_token.
Set cookieToken = %request.GetCookie("access_token")
Cambia al namespace %SYS para usar los métodos de las librerías de Oauth.
ZNspace "%SYS"
Obtiene el token con la cookie, este método elimina los tokens expirados, nos vale para saber que el token es válido y no ha sido modificado.
Set accessToken = ##class(OAuth2.Server.AccessToken).OpenByToken(cookieToken,.sc)
Obtiene el token recién creado para el cliente de Oauth que solicita el login de usuario.
Set currentToken = ##class(OAuth2.Server.AccessToken).OpenByCode(authorizationCode,.sc)
Obtiene el cliente de Oauth.
Set client = ##class(OAuth2.Server.Client).Open(currentToken.ClientId,.sc)
Replica el usuario y las propiedades en el token nuevo.
Set currentToken.Username = accessToken.Username
#dim propertiesNew As %OAuth2.Server.Properties = currentToken.Properties
Set key=""
For {
Set value=accessToken.Properties.ResponseProperties.GetNext(.key)
If key="" Quit
Do ..SetTokenProperty(.propertiesNew,key,value)
}
Do ##class(OAuth2.Server.Auth).AddClaimValues(currentToken, currentToken.ClientId, accessToken.Username)
Set currentToken.Properties = propertiesNew
Salta el login y guarda los cambios del token.
Set currentToken.Stage = "permission"
Do currentToken.Save()
En este punto podríamos llamar al método DisplayPermissions si queremos mostrar la pantalla de aceptación de permisos.
Saltar pantalla de permisos y enviar el código de autorización al callback del cliente de oauth
Set url = %request.URL_"?AuthorizationCode="_authorizationCode_"&Accept=Aceptar"
Set %response.Redirect = url
Otros apuntes
Nos ha pasado en otros entornos con versiones anteriores a la actual de IRIS que la redirección no funciona, una posible solución a esto es devolver un javascript que lo haga:
&html<<script type="text/javascript">
window.location.href="#(url)#";
</script>>
Respecto a la pantalla de permisos lo ideal sería almacenar en una persistencia: Cliente de OAuth, Usuario y Scopes aceptados, y no mostrar la pantalla si los permisos fueron aceptados con anterioridad.Este es mi primer post publicado, espero haberlo hecho de manera clara y que sea de utilidad para llevar el Oauth de IRIS al siguiente nivel.Si has llegado hasta aquí agradecerte de corazón el tiempo de leer este post y si tienes alguna duda puedes escribirla en un comentario.No olvides darle like 👍Un saludo, Miguelio, Cysnet.
Artículo
Alberto Fuentes · 24 mayo, 2019
Al igual que con Pattern Matching, se pueden utilizar Expresiones Regulares para identificar patrones en textos en ObjectScript, sólo que con una potencia mucho mayor.
En este artículo se proporciona una breve introducción sobre las Expresiones Regulares y lo que puede hacerse con ellas en ObjectScript. La información que se proporciona aquí se basa en varias fuentes, entre las que destaca el libro “Mastering Regular Expressions” (Dominando las expresiones regulares) escrito por Jeffrey Friedl y, por supuesto, la documentación online de la plataforma. El procesamiento de textos utiliza patrones que algunas veces pueden ser complejos. Cuando utilizamos expresiones regulares, normalmente tenemos varias estructuras: el texto en el que buscamos los patrones, el patrón en sí mismo (la expresión regular) y las coincidencias (las partes del texto que coinciden con el patrón). Para que sea más sencillo distinguir entre estas estructuras, se utilizan las siguientes convenciones a lo largo de este documento:
Los ejemplos en el texto se muestran en monospace y sin comillas adicionales:
Esta es una "cadena de texto" en la cual deseamos encontrar "algo".
A menos que sean evidentes, las expresiones regulares que estén dentro del texto principal se visualizan en un fondo gris, como en el siguiente ejemplo: \".*?\".
Cuando sea necesario, las coincidencias se resaltarán mediante colores diferentes:
Esta es una "cadena de texto" en la cual deseamos encontrar "algo".
Los ejemplos de códigos que sean más grandes se mostrarán en cuadros:
set t="Esta es una ""cadena de texto"" en la cual queremos encontrar ""algo""."set r="\"".*?\"""w $locate(t,r,,,tMatch)
1. Un poco de historia (y algunas curiosidades)
A principios de los años 40, los neurofisiólogos desarrollaron modelos para representar el sistema nervioso de los humanos. Unos años después, un matemático describió estos modelos mediante expresiones algebraicas que a las que llamó “conjuntos regulares”. La notación que se utilizó en estas expresiones algebraicas se denominó “expresiones regulares”.
En 1965, las expresiones regulares se mencionaron por primera vez en el contexto de la informática. Con qed, un editor que formó parte del sistema operativo UNIX, el uso de las expresiones regulares se extendió. Las siguientes versiones de ese editor proporcionan secuencias de comandos g/expresiones regulares/p (global, expresión regular, imprimir), las cuales realizan búsquedas de coincidencias de las expresiones regulares en todas las líneas del texto y muestran los resultados. Estas secuencias de comandos se convirtieron finalmente en la utilidad grep.
Hoy en día, existen varias implementaciones de expresiones regulares (RegEx) para muchos lenguajes de programación.
2. Regex 101
En esta sección se describen los componentes de las expresiones regulares, su evaluación y algunos de los motores disponibles. Los detalles de cómo utilizarlos se describen en la sección 4.
2.1. Componentes de las expresiones regulares
2.1.1. Metacaracteres
Los siguientes caracteres tienen un significado especial en las expresiones regulares.
. * + ? ( ) [ ] \ ^ $ |
Para utilizarlos como valores literales, deben utilizarse utilizando la contrabarra \. También pueden definirse explícitamente secuencias de valores literales utilizando \Q <literal sequence> \E.
2.1.2. Valores literales
El texto normal y caracteres de escape se tratan como valores literales, por ejemplo:
abc
abc
\f
salto de página
\n
salto de línea
\r
retorno de carro
\r
tabulación
\0+tres dígitos (por ejemplo,\0101)
Número octal.El motor de RegEx que utiliza Caché / IRIS (ICU), es compatible con los números octales hasta el \0377 (255 en el sistema decimal). Al migrar expresiones regulares desde otro motor hay que considerar cómo procesa los octales dicho motor.
\x+dos dígitos (por ejemplo, \x41)
Número hexadecimal.La biblioteca ICU cuenta con varias opciones para procesar los números hexadecimales, consulte los documentos de apoyo sobre ICU (los enlaces están disponibles en la sección 5.8)
2.1.3. Anclas
Las anclas (anchors) permiten encontrar ciertos puntos en un texto/cadena, por ejemplo:
\A Inicio de la cadena
\Z Final de la cadena
^ Inicio del texto o línea
$ Final de un texto o línea
\b Límite de palabras
\B No en un límite de palabras
\< Inicio de una palabra
\> Final de una palabra
Algunos motores de regex se comportan de forma diferente, por ejemplo, al definir lo que constituye exactamente una palabra y cuáles caracteres se consideran delimitadores de palabras.
2.1.4. Cuantificadores
Con los cuantificadores, puede definir qué tan frecuentemente el elemento anterior puede crear una coincidencia:
{x} exactamente x número de veces
{x,y} mínimo x, máximo y número de veces
* 0 ó más, es equivalente a {0,}
+ 1 ó más, es equivalente a {1,}
? 0 ó 1
Codicia
Se dice que los cuantificadores son “codiciosos” (greedy), ya que toman tantos caracteres como sea posible. Supongamos que tenemos la siguiente cadena de texto y queremos encontrar el texto que está entre las comillas:
Este es "un texto" con "cuatro comillas".
Debido a la naturaleza codiciosa de los selectores, la expresión regular \".*\" encontrará demasiado texto:
Este es "un texto" con "cuatro comillas".
En este ejemplo, la expresión regular .* pretende incluir tantos caracteres que se encuentren entre comillas como sea posible. Sin embargo, debido a la presencia del selector punto ( . ) también buscará las comillas, de modo que no obtendremos el resultado que deseamos.
Con algunos motores de regex (incluyendo el que utiliza Caché / IRIS) se puede controlar la codicia de los cuantificadores, al incluir un signo de interrogación en ellos. Entonces, la expresión regular \".*?\" ahora hace que coincidan las dos partes del texto que se encuentran entre las comillas, es decir, exactamente lo que buscábamos:
Este es "un texto" con "cuatro comillas".
2.1.5. Clases de caracteres (rangos)
Los corchetes se utilizan para definir los rangos o conjuntos de caracteres, por ejemplo, [a-zA-Z0-9] o [abcd] . En el lenguaje de las expresiones regulares esta notación se refiere a una clase de caracteres. Un rango coincide con los caracteres individuales, de modo que el orden de los caracteres dentro de la definición del rango no es importante: [dbac] devuelve tantas coincidencias como [abcd].
Para excluir un rango de caracteres, simplemente se coloca el símbolo ^ frente a la definición del rango (¡dentro de los corchetes!): [^abc] buscará todas las coincidencias excepto con a, b o c.
Algunos motores de regex proporcionan clases de caracteres previamente definidas (POSIX), por ejemplo:
[:alnum:] [a-zA-z0-9]
[:alpha:] [a-zA-Z]
[:blank:] [ \t]
…
2.1.6. Grupos
Los componentes de una expresión regular pueden agruparse utilizando un par de paréntesis. Esto resulta útil para aplicar cuantificadores a un grupo de selectores, así como para referirse a los grupos tanto desde las expresiones regulares como desde ObjectScript. Los grupos también pueden anidarse.
Las siguientes coincidencias en las cadenas regex consisten en un número de tres dígitos, seguidos por un guion, a continuación, tres pares de letras mayúsculas y un dígito, seguidas por un guion, y para finalizar el mismo número de tres dígitos como en la primera parte:
([0-9]{3})-([A-Z][[0-9]){3}-\1
El siguiente ejemplo muestra cómo utilizar las referencias previas para que coincidan no solamente la estructura, sino también el contenido: el punto de referencia (en morado) le indica al motor que busque el mismo número de tres dígitos al principio y al final de la cadena (en amarillo). El ejemplo también muestra cómo aplicar un cuantificador a las estructuras más complejas (en verde).
La expresión regular de arriba coincidiría con la siguiente cadena:
123-D1E2F3-123
Pero no coincidiría con las siguientes cadenas:
123-D1E2F3-456 (los últimos tres dígitos son diferentes de los primeros tres)
123-1DE2F3-123 (la parte central no consiste en tres letras/pares de dígitos)
123-D1E2-123 (la parte central incluye únicamente dos letras/pares de dígitos)
Los grupos también puede accederse utilizando los búferes de captura desde ObjectScript ( sección 4.5.1). Esta es una característica muy útil que permite buscar y extraer información al mismo tiempo.
2.1.7. Alternancia
Con el carácter de barra vertical se especifica alternancia entre opciones, por ejemplo, skyfall|done. Esto permite comparar expresiones más complejas, como las clases de caracteres que se describieron en la sección 3.1.5.
2.1.8. Referencias
Los referencias permiten consultar grupos previamente definidos (selectores dentro de los paréntesis). En el siguiente ejemplo se muestra una expresión regular que coincide con tres caracteres consecutivos, los cuales deben ser iguales:
([a-zA-Z])\1\1
Los puntos de referencia se especifican con \x, donde x representa la expresión x-ésima entre paréntesis.
2.1.9. Reglas de precedencia
[] antes de ()
, + y ? antes de una secuencia: ab es equivalente a a(b*), y no a (ab)*
Secuencia antes de una alternancia: ab|c es equivalente a (ab)|c, y no a a(b|c)
2.2. Un poco de teoría
La evaluación de las expresiones regulares, por lo general, se realiza mediante la implementación de alguno de los dos siguientes métodos (las descripciones se han simplificado, en las referencias que se mencionan en el capítulo 5 se pueden encontrar más detalles):
Orientado por el texto (DFA – Autómata finito determinista) El motor de búsqueda avanza a través del texto de entrada, carácter por carácter, e intenta encontrar coincidencias en el texto recorrido hasta el momento. Cuando llega al final del texto que de entrada, declara que el proceso se realizó con éxito.
Orientado por las expresiones regulares (NFA – Autómata finito no determinista) El motor de búsqueda avanza a través de la expresión regular, token por token, e intenta aplicarla al texto. Cuando llega al último token (y encuentra todas las coincidencias), declara que el proceso se realizó con éxito.
El método 1 es determinista, su tiempo de ejecución depende únicamente de la longitud del texto introducido. El orden de los selectores en las expresiones regulares no influye en el tiempo de ejecución.
El método 2 no es determinista, el motor reproduce todas las combinaciones posibles de los selectores en la expresión regular, hasta que encuentre una coincidencia o se produzca algún error. Por ende, este método es particularmente lento cuando no encuentra una coincidencia (debido a que tiene que reproducir todas la combinaciones posibles). El orden de los selectores influye en el tiempo de ejecución. Sin embargo, este método le permite retroceder y capturar los búferes.
2.3. Motores de búsqueda
Existen muchos motores de regex disponibles, algunos ya están incorporados en los lenguajes de programación o en los sistemas operativos, otros son librerías que pueden utilizarse en casi cualquier parte. Aquí se muestran algunos motores de regex, los cuales se agruparon por el tipo de método de evaluación:
DFA: grep, awk, lex
NFA: Perl, Tcl, Python, Emacs, sed, vi, ICU
En la siguiente tabla se realiza una comparación de las características que están disponibles en regex, entre varias bibliotecas y en lenguajes de programación:
Puede encontrar más información aquí: https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines
3. RegEx y Caché
InterSystems Caché/IRIS utiliza la biblioteca ICU para buscar expresiones regulares, en los documentación online se describen muchas de estas características. El objetivo de las siguientes secciones es hacer una introducción rápida sobre cómo utilizarlas.
3.4. $match() y $locate()
En ObjectScript (COS), las funciones $match() y $locate() brindan acceso directo a la mayoría de las características de regex a las que se tiene acceso desde la biblioteca de ICU. $match(String, Regex) busca en la cadena de entrada un patrón de regex. Cuando la función encuentra una coincidencia devuelve 1, en caso contrario devuelve 0.
Ejemplos:
w $match("baaacd",".*(a)\1\1.*") devuelve 1
w $match("bbaacd",".*(a)\1\1.*") devuelve 0
$locate(String,Regex,Start,End,Value) busca en la cadena de entrada un patrón de regex, del mismo modo que lo hace $match(). Sin embargo, $locate() proporciona mayor control del proceso y también devuelve más información. En Start, se puede indicar a $locate en qué punto de la cadena de entrada debe comenzar la búsqueda. Cuando $locate() encuentra una coincidencia, regresa al punto donde se encuentra el primer carácter de la coincidencia y establece End para la siguiente posición del carácter después de la coincidencia. El contenido de la coincidencia es devuelto en Value.
Si $locate() no encuentra alguna coincidencia devuelve 0 y no tocará los contenidos de End y Value (si se especificaron). End y Value se pasan por referencia, por lo que hay que ser cuidadoso si utilizan continuamente (por ejemplo en bucles).
Ejemplo:
w $locate("abcdexyz",".d.",1,e,x) devuelve 3, e se establece en 6, x se establece en "cde"
$locate() realiza la búsqueda de coincidencias en los patrones y puede devolver el contenido de la primera coincidencia, al mismo tiempo. Si necesita extraer el contenido de todas las coincidencias, puede llamar a $locate() continuamente mediante un bucle o puede utilizar los métodos que se proporcionan en %Regex.Matcher (siguiente sección).
3.5.% Regex.Matcher
%Regex.Matcher proporciona acceso a funciones de regex de la librería ICU del mismo modo que lo hacen las funciones $match() y $locate() pero además cuenta con características avanzadas. En las siguientes secciones se retomará el concepto de los búferes de captura, se analizará la posibilidad de sustituir cadenas con expresiones regulares y las formas para controlar el comportamiento del tiempo de ejecución.
3.5.1. Búferes de captura
Como ya hemos visto con los grupos, las referencias y $locate(), las expresiones regulares le permiten buscar simultáneamente los patrones en el texto y devolver el contenido con las coincidencias. Esto funciona al colocar las partes del patrón que desea extraer entre un par de paréntesis (agrupación). Si la búsqueda de coincidencias es exitosa, los búferes de captura tendrán el contenido de todos los grupos en los que se hayan encontrado coincidencias. Hay que tener en cuenta que este procedimiento es un poco diferente de lo que $locate() proporciona con sus parámetros de valores: $locate() devuelve el contenido de todas la coincidencias en sí, mientras que los búferes de captura le dan acceso a algunas partes de las coincidencias (los grupos).
Para utilizarlo, hay crear un objeto a partir de la clase %Regex.Matcher y pasar la expresión regular y la cadena de entrada. Después, puede llamar a uno de los métodos proporcionados por %Regex.Matcher para que realice el trabajo.
Ejemplo 1 (grupos simples):
set m=##class(%Regex.Matcher).%New("(a|b).*(de)", "abcdeabcde")
w m.Locate() returns 1
w m.Group(1) returns a
w m.Group(2) returns de
Ejemplo 2 (grupos anidados y referencias):
set m=##class(%Regex.Matcher).%New("((a|b).*?(de))(\1)", "abcdeabcde")
w m.Match() returns 1
w m.GroupCount returns 4
w m.Group(1) returns abcde
w m.Group(2) returns a
w m.Group(3) returns de
w m.Group(4) returns abcde
(hay que tener en cuenta el orden de los grupos anidados, porque el paréntesis de apertura marca el inicio de un grupo, los grupos internos tienen un índice numérico mayor que los externos)
Como se mencionó anteriormente, los búferes de captura son una característica muy poderosa, ya que permiten buscar coincidencias en los patrones y extraer el contenido de las coincidencias al mismo tiempo. Sin las expresiones regulares, tendría que buscar las coincidencias como se indica en el paso uno (por ejemplo, utilizando el operador para coincidencias de patrones) y extraer el contenido de las coincidencias (o partes de las mismas) basándose en algunos de los criterios que se indican en el paso dos.
Si necesita agrupar alguna parte de su patrón (por ejemplo, para aplicarle un cuantificador a esa parte), pero no desea rellenar un búfer de captura con el contenido de la parte coincidente, puede definir el grupo como "sin captura" o "pasivo" al colocarle un signo de interrogación seguido de dos puntos adelante del grupo, como en el ejemplo 3, el cual se muestra a continuación.
Ejemplo 3 (grupo pasivo):
set m=##class(%Regex.Matcher).%New("((a|b).*?(?:de))(\1)","abcdeabcde")
w m.Match() returns 1
w m.Group(1) returns abcde
w m.Group(2) returns a
w m.Group(3) returns abcde
w m.Group(4) returns <REGULAR EXPRESSION>zGroupGet+3^%Regex.Matcher.1
3.5.2. Reemplazar
%Regex.Matcher también proporciona métodos para reemplazar rápidamente el contenido de las coincidencias: ReplaceAll() y ReplaceFirst():
set m=##class(%Regex.Matcher).%New(".c.","abcdeabcde")
w m.ReplaceAll("xxxx") returns axxxxeaxxxxe
w m.ReplaceFirst("xxxx") returns axxxxeabcde
También puede referirse a los grupos en sus cadenas de reemplazo. Si añadimos un grupo al patrón del ejemplo anterior, podemos referirnos a su contenido al incluir $1 en la cadena de reemplazo:
set m=##class(%Regex.Matcher).%New(".(c).","abcdeabcde")
w m.ReplaceFirst("xx$1xx") returns axxcxxeabcde
Se puede utilizar $0 para incluir todo el contenido de la coincidencia en la cadena de reemplazo:
w m.ReplaceFirst("xx$0xx") returns axxbcdxxeabcde
3.5.3. La propiedad OperationLimit
En la sección 3.2 hablamos sobre los dos métodos para evaluar una expresión regular (DFA y NFA). El motor regex que se utiliza en Caché es un Autómata finito no determinista (NFA, por sus siglas en inglés). Por lo tanto, el tiempo requerido para evaluar varias expresiones regulares en una determinada cadena de entrada puede variar. [1]
Se puede utilizar la propiedad OperationLimit de un objeto %Regex.Matcher para limitar el número de unidades de ejecución (llamados clusters). El tiempo exacto para la ejecución de un cluster depende del entorno. Por lo general, el tiempo requerido para la ejecución de un cluster es de unos cuantos milisegundos. La propiedad OperationLimit se establece de manera predeterminada en 0 (sin límites).
3.6. Un ejemplo de la vida real: la migración de Perl hacia Caché
Esta sección describe la parte relacionada con regex en un caso de migración de Perl a Caché. En este caso, el script en Perl consiste en docenas de expresiones regulares con mayor o menor complejidad, las cuales se utilizaban tanto para buscar coincidencias como para extraer contenido.
Si no existieran funciones de regex disponibles en Caché, el proyecto de migración implicaría un enorme esfuerzo. Sin embargo, las funciones de regex están disponibles en ObjectScript, y las expresiones regulares de los scripts en Perl pueden utilizarse casi sin realizar modificaciones.
Aquí se muestra una parte del script en Perl:
El único cambio necesario en las expresiones regulares para la migración de Perl hacia Caché fue en el modificador /i- (este provoca que regex no distinga entre las mayúsculas y minúsculas), el cual tuvo que moverse desde el final hacia el principio en las expresiones regulares.
En Perl, el contenido de los búferes de captura se copia en variables especiales ($1 y $2 en el código de Perl que vimos anteriormente). Casi todas las expresiones regulares en el proyecto Perl utilizaban este mecanismo. Para emularlo, se escribió un simple método de encapsulamiento en ObjectScript. Este método utiliza a %Regex.Matcher para evaluar una expresión regular contra una cadena de texto y devuelve el contenido del buffer de captura en forma de una lista ($lb()).
El código correspondiente en ObjectScript es:
if ..RegexMatch(
tVCSFullName,
"(?i)[\\\/]([^\\^\/]+)[\\\/]ProjectDB[\\\/](.+)[\\\/]archives[\\\/]",
.tCaptureBufferList)
{
set tDomainPrefix=$zcvt($lg(tCaptureBufferList,1), "U")
set tDomain=$zcvt($lg(tCaptureBufferList,2), "U")
}
…
Classmethod RegexMatch(pString as %String, pRegex as %String, Output pCaptureBuffer="") {
#Dim tRetVal as %Boolean=0
set m=##class(%Regex.Matcher).%New(pRegex,pString)
while m.Locate() {
set tRetVal=1
fori=1:1:m.GroupCount {
set pCaptureBuffer=pCaptureBuffer_$lb(m.Group(i))
}
}
quit tRetVal
}
4. Información de referencia
4.7. Información general
Información general y tutoriales:
http://www.regular-expressions.info/engine.html
Tutoriales y ejemplos:
http://www.sitepoint.com/demystifying-regex-with-practical-examples/
Comparaciones entre varios motores regex:
https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines
Hoja de referencia:
https://www.cheatography.com/davechild/cheat-sheets/regular-expressions/pdf/
Libros:
Jeffrey E. F. Friedl: “Mastering Regular Expressions (Dominando las expresiones regulares)” (consulte http://regex.info/book.html)
4.8. Documentación online
Resumen sobre "El uso de las expresiones regulares en Caché": http://docs.intersystems.com/latest/csp/docbook/ DocBook.UI.Page.cls?KEY=GCOS_regexp
Documentación sobre $match(): http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fmatch
Documentación sobre $locate(): http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_flocate
Documentación de la clase %Regex.Matcher: http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=%25SYS&CLASSNAME=%25Regex.Matcher
4.9. ICU
Como se mencionó anteriormente, InterSystems Caché/IRIS utiliza los motores de ICU. La información completa está disponible en línea:
http://userguide.icu-project.org/strings/regexp
http://userguide.icu-project.org/strings/regexp#TOC-Regular-Expression-Metacharacters
http://userguide.icu-project.org/strings/regexp#TOC-Regular-Expression-Operators
http://userguide.icu-project.org/strings/regexp#TOC-Replacement-Text
http://userguide.icu-project.org/strings/regexp#TOC-Flag-Options
This is excellent as far as my poor Spanish (inherited from Italian) allows.It would be great to see it also in the English version. It's already available in English! https://community.intersystems.com/post/using-regular-expressions-cach%C3%A9This here is our Spanish version of the great article by Michael Brosdorf
Este artículo está etiquetado como "Mejores prácticas" ("Best practices")
(Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems).
Artículo
Javier Lorenzo Mesa · 11 mar, 2020
¡Hola a tod@s!
El desarrollo completo en JavaScript (Full-Stack) permite crear aplicaciones de última generación con Caché. En cualquiera de las aplicaciones (web) que se desarrollan hoy en día, hay que tomar muchas decisiones estructurales y debemos saber cuales son las decisiones correctas. Con el conector Node.js disponible para Caché, se puede crear un potente servidor de aplicaciones, que permitirá utilizar la última tecnología de JavaScript y marcos de aplicaciones (frameworks) tanto del lado del cliente como del servidor.
Con todas estas nuevas tecnologías, lo más importante es integrarlas de la manera más eficiente posible y que permitan generar una experiencia de desarrollo muy productiva. Este artículo pretende introducirte paso a paso en la tecnología Node.js.
Antes de empezar a desarrollar aplicaciones con Node.js para Caché, lo primero que hay que hacer es configurar un entorno.
Para utilizar Node.js con Caché, se necesita un módulo (conector) Node.js para Caché.
En cada kit de distribución de Caché, encontrarás diferentes versiones de este módulo en el directorio bin de Caché, los cuales se llaman, por ejemplo: cache0100.node, cache0120.node... Como las versiones de Node.js se actualizan con frecuencia, recomiendo que primero solicites al Centro de Soporte Internacional (WRC) que te envíen la última versión.
Una vez que recibas del WRC el paquete zip con la última versión, verás que hay diferentes números de versión dentro del directorio bin. No es difícil elegir la versión correcta, pero es muy importante saber que estos módulos son nativos y que la plataforma de Caché, el procesador (x86 o x64) y la versión del módulo Node.js deben coincidir con tu sistema. Este es un paso muy importante a tener en cuenta, pues de lo contrario el conector de Node.js no funcionará en tu sistema.
Las versiones de Node.js están disponibles en dos opciones: las "LTS" y las "actuales". Para nuestras aplicaciones, siempre recomiendo utilizar las LTS, ya que son las más adecuadas para usarse con Caché (proporcionan soporte a largo plazo y estabilidad).
* Si deseas conocer todos los detalles sobre las versiones, puedes encontrar un resumen de todas las versiones en la página web de Node.js. Verás que es una lista larga, pero solo necesitas la última versión de LTS (incluso con los principales números de la versión, que actualmente son v6.x.x y v4.x.x). No utilices las versiones principales que tengan números impares, son versiones que no están diseñadas para usarse en programación y se utilizan para introducir y probar las funciones más recientes de JavaScript la versión 8.
Dos ejemplos:
si utilizas Caché 2016.2 ejecutándose en un sistema Windows x64 y quieres utilizar Node.js v6.9.4, necesitarás que el archivo cache610.node esté en el directorio bin\winx64.
si utilizas Caché 2008.2 ejecutándose en un sistema Windows x86 y quieres utilizar Node.js v4.8.2, necesitarás que el archivo cache421.node esté dentro del directorio bin\winx86.
Ahora describiré paso a paso cómo descargar e instalar Node.js en un sistema Windows x64 y conectarlo con Caché.
Primero, se comienza con la descarga del último Node.js LTS release:
La versión actual es la v6.10.2, solo hay que hacer clic en el botón verde para descargarla. Instala esta versión en tu sistema con la configuración predeterminada, asegurándote de que la opción "add to PATH" esté instalada/verificada:
Ahora que has instalado el entorno de ejecución (runtime) en Node.js, se puede verificar si se instaló correctamente al abrir una línea de comandos y verificar la versión del nodo:
Como se puede ver, actualmente tengo instalada la versión Node.js v6.9.1 en mi sistema.
Por cierto, Node.js ahora está instalado dentro de tu sistema de Windows (en la carpeta "Archivos de programa").
En el portal de gestión de Caché, también deben asegurarse de que el %Service_CallIn se encuentra entre tus servicios de seguridad.
Cuando se inicia una nueva aplicación con Node.js, es una práctica habitual crear primero un nuevo directorio para la aplicación. Lo primero que necesitamos crear es una pequeña aplicación/script en Node.js para probar la conexión entre Node.js y Caché. Este es el paso más importante para comprobar, en primer lugar, si nuestra configuración de Node.js funciona bien antes de comenzar con el desarrollo de otros módulos en Node.js.
Crea un directorio vacío para la prueba, por ejemplo C:\Temp\nodetest
Crea un subdirectorio node_modules (C:\Temp\nodetest\node_modules)
Inserta el cache610.node correcto (que se encuentra dentro del directorio bin\winx64 en el archivo del conector Node.js para Caché) dentro del directorio node_modules y cambia su nombre a cache.node. En todas sus aplicaciones Node.js del lado del servidor, siempre es necesario que el conector para Caché cambie de nombre a cache.node, independientemente de la versión de Node.js, el procesador y sistema operativo en el que se esté ejecutando. ¡Esto hará que tus aplicaciones multiplataforma sean independientes de la versión y sistema operativos instalados!
* Cada vez que desarrolles una nueva aplicación del lado del servidor en Node.js, tendrás que añadir este módulo cache.node al directorio node_modules. Simplemente copia la instancia desde tu aplicación de prueba al directorio node_modules, que se encuentra en la nueva aplicación en sus sistemas. Recuerda que si instalas/implementas tus aplicaciones del lado del servidor en/hacia otro sistema, necesitará revisar cuál de las versiones debes instalar en dicho sistema, ya que la versión de Node.js, el procesador del sistema y el sistema operativo pueden ser diferentes!
Ahora, solo tenemos que crear nuestro script de prueba nodeTest.js dentro de C:\Temp\nodetest mediante este código:
// first, load the Cache connector module inside node_modules
let cacheModule = require('cache');
// instantiate a Cache connector object in JavaScript
let db = new cacheModule.Cache();
console.log('Caché database object instance: ', db);
// Open the connection to the Caché database (adjust parameters for your Cache system):
let ok = db.open({
path: 'C:\\InterSystems\\Cache\\Mgr',
username: '_SYSTEM',
password: 'SYS',
namespace: 'USER'
});
console.log('Open result: ', ok);
console.log('Version: ', db.version());
let d = new Date();
// construct a JavaScript global node object to set a test global in the USER namespace
let node = {
global: 'nodeTest',
subscripts: [1],
data: 'At ' + d.toUTCString() + ': global set from Node.js'
};
// set the global in the database
db.set(node);
// retrieve the global contents back from Cache
let result = db.get(node);
// show it on the console
console.log('Set global ^nodeTest(1) result: ', result);
// close the database connection
db.close();
Si guardas este script e instalas todo correctamente, este código debería funcionar inmediatamente cuando lo llames dentro de una línea de comandos:
¡Enhorabuena! Ya has probado tu servidor para aplicaciones Node.js y lo has conectado con éxito a tu base de datos en Caché.
Con el script de prueba te darás cuenta de que el módulo cache.node contiene toda la funcionalidad que necesitas para acceder a tus datos en Caché (¡no solo los globales, también las funciones, las clases y el SQL!). Encontrarás toda la documentación sobre el módulo Node.js para Caché en los documentos disponibles online en Using Node.js with Caché. Dentro del archivo zip que te proporcione el WRC, también encontrarás un archivo PDF en el directorio de los documentos con todos los detalles sobre las funciones.
También te darás cuenta de que cuando comiences a programar con el módulo Node.js para Caché , este te proporcionará funciones de nivel inferior para acceder a Caché.
En el siguiente artículo, mostraré cómo se pueden utilizar algunos módulos de Node.js para escribir el código fuente de JavaScript, para acceder a sus globales en Caché, las funciones, las clases y las preguntas sobre SQL que utilizan una abstracción funcional de alto nivel.
Artículo
Ricardo Paiva · 14 ene, 2022
Hola desarrolladores,
Escribir un *script* para el despliegue de una aplicación puede ser muy interesante para garantizar un despliegue rápido sin olvidarse de nada.
config-api es una biblioteca para ayudar a los desarrolladores a escribir *scripts* de configuración basados en un documento JSON.
Características implementadas:
* Establecer la configuración del sistema
* Establecer la configuración de seguridad
* Habilitar servicios
* Configurar *namespaces*, bases de datos y mapeos
* Exportar configuración existente
* Todas las funciones están expuestas con una API RESTful
Esta biblioteca se centra en la configuración de IRIS para ayudar a la implementación de aplicaciones. Por lo tanto, config-api no importa/compila la función de código, considerando queesa debería ser la función del módulo de instalación de tu aplicación o el registro del cliente. config-api podría usarse con el cliente ZPM para configurar los ajustes de IRIS en la implementación del módulo; aprenderemos cómo combinar esta biblioteca con ZPM en otro artículo.
## Instalación
```
zpm “install config-api”
```
Si no eres usuario de ZPM, descarga la última versión en formato XML con dependencias [página de publicación](https://github.com/lscalese/iris-config-api/releases/) importación y compilación.
## Primer paso
Vamos a escribir un documento JSON de configuración simple, para establecer algunas configuraciones del sistema.
En este primer documento:
* Habilitamos *journal freeze* en caso de error.
* Establecemos el tamaño límite de *journal* en 256 MB.
* Establecemos SystemMode para desarrollo.
* Aumentamos el `locksiz`.
* Aumentamos el `LockThreshold`.
```
Set config = {
"Journal": { /* Service class Api.Config.Journal */
"FreezeOnError":1,
"FileSizeLimit":256
},
"SQL": { /* Service class Api.Config.SQL */
"LockThreshold" : 2500
},
"config": { /* Service class Api.Config.config */
"locksiz" : 33554432
},
"Startup":{ /* Service class Api.Config.Startup */
"SystemMode" : "DEVELOPMENT"
}
}
Set sc = ##class(Api.Config.Services.Loader).Load(config)
```
### Estructura del documento JSON de configuración
Las claves de primer nivel (`Journal`,`SQL`,`config`,`Startup`) están relacionadas con las clases en el *namespace* %SYS (usando una clase intermedia en el paquete Api.Config.Services). Significa que `Journal` admite todas las propiedades disponibles en [Config.Journal](https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?&LIBRARY=%25SYS&CLASSNAME=Config.Journal), ` SQL`todas las propiedades en [Config.SQL](https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?&LIBRARY=%25SYS&CLASSNAME=Config.SQL), etc...
Salida:
```
2021-03-31 18:31:54 Start load configuration
2021-03-31 18:31:54 {
"Journal":{
"FreezeOnError":1,
"FileSizeLimit":256
},
"SQL":{
"LockThreshold":2500
},
"config":{
"locksiz":33554432
},
"Startup":{
"SystemMode":"DEVELOPMENT"
}
}
2021-03-31 18:31:54 * Journal
2021-03-31 18:31:54 + Update Journal ... OK
2021-03-31 18:31:54 * SQL
2021-03-31 18:31:54 + Update SQL ... OK
2021-03-31 18:31:54 * config
2021-03-31 18:31:54 + Update config ... OK
2021-03-31 18:31:54 * Startup
2021-03-31 18:31:54 + Update Startup ... OK
```
**Truco**: el método `Load` es compatible con un argumento de cadena, en este caso, la cadena debe ser un nombre de archivo para un documento de configuración JSON (también se permite objetos *stream*).
## Crear un entorno de aplicación
En esta sección, escribimos un documento de configuración para crear:
* Un *namespace* "MYAPP"
* 4 bases de datos (MYAPPDATA, MYAPPCODE, MYAPPARCHIVE,MYAPPLOG)
* 1 aplicación web CSP (/csp/zwebapp)
* 1 aplicación web REST (/csp/zrestapp)
* Configuración de mapeo de *globals*
```
Set config = {
"Defaults":{
"DBDIR" : "${MGRDIR}",
"WEBAPPDIR" : "${CSPDIR}",
"DBDATA" : "${DBDIR}myappdata/",
"DBARCHIVE" : "${DBDIR}myapparchive/",
"DBCODE" : "${DBDIR}myappcode/",
"DBLOG" : "${DBDIR}myapplog/"
},
"SYS.Databases":{
"${DBDATA}" : {"ExpansionSize":128},
"${DBARCHIVE}" : {},
"${DBCODE}" : {},
"${DBLOG}" : {}
},
"Databases":{
"MYAPPDATA" : {
"Directory" : "${DBDATA}"
},
"MYAPPCODE" : {
"Directory" : "${DBCODE}"
},
"MYAPPARCHIVE" : {
"Directory" : "${DBARCHIVE}"
},
"MYAPPLOG" : {
"Directory" : "${DBLOG}"
}
},
"Namespaces":{
"MYAPP": {
"Globals":"MYAPPDATA",
"Routines":"MYAPPCODE"
}
},
"Security.Applications": {
"/csp/zrestapp": {
"DispatchClas" : "my.dispatch.class",
"Namespace" : "MYAPP",
"Enabled" : "1",
"AuthEnabled": "64",
"CookiePath" : "/csp/zrestapp/"
},
"/csp/zwebapp": {
"Path": "${WEBAPPDIR}zwebapp/",
"Namespace" : "MYAPP",
"Enabled" : "1",
"AuthEnabled": "64",
"CookiePath" : "/csp/zwebapp/"
}
},
"MapGlobals":{
"MYAPP": [{
"Name" : "Archive.Data",
"Database" : "MYAPPARCHIVE"
},{
"Name" : "App.Log",
"Database" : "MYAPPLOG"
}]
}
}
Set sc = ##class(Api.Config.Services.Loader).Load(config)
```
Salida:
```
2021-03-31 20:20:07 Start load configuration
2021-03-31 20:20:07 {
"SYS.Databases":{
"/usr/irissys/mgr/myappdata/":{
"ExpansionSize":128
},
"/usr/irissys/mgr/myapparchive/":{
},
"/usr/irissys/mgr/myappcode/":{
},
"/usr/irissys/mgr/myapplog/":{
}
},
"Databases":{
"MYAPPDATA":{
"Directory":"/usr/irissys/mgr/myappdata/"
},
"MYAPPCODE":{
"Directory":"/usr/irissys/mgr/myappcode/"
},
"MYAPPARCHIVE":{
"Directory":"/usr/irissys/mgr/myapparchive/"
},
"MYAPPLOG":{
"Directory":"/usr/irissys/mgr/myapplog/"
}
},
"Namespaces":{
"MYAPP":{
"Globals":"MYAPPDATA",
"Routines":"MYAPPCODE"
}
},
"Security.Applications":{
"/csp/zrestapp":{
"DispatchClas":"my.dispatch.class",
"Namespace":"MYAPP",
"Enabled":"1",
"AuthEnabled":"64",
"CookiePath":"/csp/zrestapp/"
},
"/csp/zwebapp":{
"Path":"/usr/irissys/csp/zwebapp/",
"Namespace":"MYAPP",
"Enabled":"1",
"AuthEnabled":"64",
"CookiePath":"/csp/zwebapp/"
}
},
"MapGlobals":{
"MYAPP":[
{
"Name":"Archive.Data",
"Database":"MYAPPARCHIVE"
},
{
"Name":"App.Log",
"Database":"MYAPPLOG"
}
]
}
}
2021-03-31 20:20:07 * SYS.Databases
2021-03-31 20:20:07 + Create /usr/irissys/mgr/myappdata/ ... OK
2021-03-31 20:20:07 + Create /usr/irissys/mgr/myapparchive/ ... OK
2021-03-31 20:20:07 + Create /usr/irissys/mgr/myappcode/ ... OK
2021-03-31 20:20:07 + Create /usr/irissys/mgr/myapplog/ ... OK
2021-03-31 20:20:07 * Databases
2021-03-31 20:20:07 + Create MYAPPDATA ... OK
2021-03-31 20:20:07 + Create MYAPPCODE ... OK
2021-03-31 20:20:07 + Create MYAPPARCHIVE ... OK
2021-03-31 20:20:07 + Create MYAPPLOG ... OK
2021-03-31 20:20:07 * Namespaces
2021-03-31 20:20:07 + Create MYAPP ... OK
2021-03-31 20:20:07 * Security.Applications
2021-03-31 20:20:07 + Create /csp/zrestapp ... OK
2021-03-31 20:20:07 + Create /csp/zwebapp ... OK
2021-03-31 20:20:07 * MapGlobals
2021-03-31 20:20:07 + Create MYAPP Archive.Data ... OK
2021-03-31 20:20:07 + Create MYAPP App.Log ... OK
```
¡Funciona! La configuración se cargó con éxito.
En el próximo artículo, aprenderemos a usar config-api con ZPM para implementar tu aplicación.
Este artículo está etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Estevan Martinez · 23 jul, 2019
Los globales de InterSystems Caché proporcionan un conjunto de funciones muy útiles para los desarrolladores. Pero, ¿por qué los globales son tan rápidos y eficientes?TeoríaBásicamente, la base de datos de Caché es un catálogo con el mismo nombre que la base de datos y contiene el archivo CACHE.DAT. En los sistemas Unix, la base de datos también puede ser una partición normal del disco.Todos los datos en Caché se almacenan en bloques que, a su vez, se organizan como un árbol-B* balanceado. Si tenemos en cuenta que todos los globales básicamente se almacenan en un árbol, los subíndices de los globales se representarán como ramas, mientras que los valores de los subíndices globales se almacenarán como hojas. La diferencia entre un árbol-B* balanceado y un árbol-B ordinario es que sus ramas también tienen los enlaces apropiados para realizar iteraciones a través de los subíndices (por ejemplo, en nuestro caso los globales) usando rápidamente las funciones $Order y $Query sin necesidad de regresar al tronco del árbol. De manera predeterminada, cada bloque en el archivo de la base de datos tiene un tamaño fijo de 8,192 bytes. Por lo tanto, no podrá modificar el tamaño de los bloques en una base de datos que ya exista. Mientras crea una nueva base de datos puede elegir entre bloques de 16 KB, 32 KB o incluso bloques de 64 KB, dependiendo del tipo de datos que almacenará. Sin embargo, siempre tenga en cuenta que todos los datos se leen bloque por bloque, en otras palabras, incluso si solicita un solo valor de 1 byte de tamaño el sistema leerá muchos bloques, entre los cuales, el bloque de datos que solicitó será el último. También debe recordar que Caché utiliza búferes globales para almacenar los bloques de las bases de datos en la memoria para utilizarlos después, y que los búferes de bloques tienen el mismo tamaño. No podrá utilizar una base de datos existente o crear una nueva cuando el búfer global, con el tamaño de bloques correspondiente, falte en el sistema. Además, deberá definir la cantidad de memoria que desea asignar para el tamaño específico de los bloques. Es posible utilizar búferes con bloques más grandes que los bloques en las bases de datos, pero en este caso, cada bloque en el búfer solamente almacenará un bloque en dicha base, que será incluso más pequeño.En esta imagen, se asignó memoria para un búfer global con un tamaño de 8 KB, con la finalidad de utilizarla en bases de datos conformadas por bloques de 8 KB. En esta base de datos, los bloques que no están vacíos se definieron en los mapas, de tal modo que alguno de los mapas contenga 62,464 bloques (para bloques con capacidad de 8KB).Tipos de BloquesEl sistema es compatible con varios tipos de bloques. En cada nivel, los enlaces correctos de un bloque deben señalar un bloque del mismo tipo o un bloque nulo que defina el final de los datos.Tipo 9: Bloques de un catálogo de globales. En estos bloques generalmente se describen todos los globales existentes junto con sus parámetros, incluida la compilación de los subíndices globales, el cual es uno de los parámetros más importantes y no puede modificarse una vez que el global se creóTipo 66: Bloques punteros de alto nivel. Solo un bloque de un catálogo de globales puede estar en la parte superior de estos bloques.Tipo 6: Bloques punteros de nivel inferior. Únicamente los bloques punteros de alto nivel pueden estar en la parte superior de estos bloques y solamente los bloques de datos pueden colocarse en niveles inferiores.Tipo 70: Bloques punteros tanto de alto nivel como de nivel inferior. Estos bloques se utilizan cuando el global correspondiente almacena una pequeña cantidad de valores y, por lo tanto, no es necesario tener varios niveles de bloques. Estos bloques generalmente señalan hacia los bloques de datos, al igual que los bloques de un catálogo de globales.Tipo 2: Bloques punteros para almacenar una cantidad relativamente grande de globales. Para asignar valores de manera equitativa entre los bloques de datos, es posible que desee crear niveles adicionales de bloques punteros. Estos bloques generalmente se colocan entre los bloques punteros.Tipo 8: Bloques de datos. En estos bloques generalmente se almacenan varios nodos globales en lugar de un solo nodo.Tipo 24: Bloques para cadenas largas. Cuando el valor de un solo global es mayor que un bloque, dicho valor se registrará en un bloque especial para cadenas largas, mientras que el nodo del bloque de datos almacenará enlaces en la lista de bloques para cadenas largas junto con la longitud total de este valor.Tipo 16: Diagrama de bloques. Estos bloques están diseñados para almacenar información sobre bloques que no han sido asignados.Por lo tanto, el primer bloque en una base de datos típica de Caché contiene la información de servicio acerca del mismo archivo en la base de datos, mientras que los segundos bloques constituyen un diagrama de bloques. El primer bloque del catálogo va en el tercer lugar (bloque #3), y una sola base de datos puede tener varios bloques en el catálogo. Los siguientes son bloques punteros (ramas), bloques de datos (hojas) y bloques de cadenas largas. Como ya mencioné anteriormente, los bloques en los catálogos globales almacenan información sobre todos los globales que existen en la base de datos o en la configuración global (si no hay datos disponibles en este tipo de globales). En este caso, un nodo que describa a dicho global tendrá un puntero nulo de nivel inferior. Puede consultar la lista de los globales que están disponibles en el catálogo de globales que se encuentra en el portal de administración. En este portal también se le permitirá guardar un global en el catálogo después de haberlo eliminado (por ejemplo, guardar su secuencia de compilación) así como crear un nuevo global con un tipo de compilación ya sea predeterminada o personalizada.En general, el árbol de bloques puede representarse como la imagen que se muestra a continuación. Tenga en cuenta que los enlaces a estos bloques se muestran en color rojo.Integridad de la Base de DatosEn la versión actual de Caché, resolvimos los temas y los problemas más importantes con las bases de datos, entonces el riesgo de degradación en las bases de datos es extremadamente bajo. Sin embargo, le recomendamos que realice comprobaciones de la integridad automáticas en las bases de datos de manera regular utilizando nuestra herramienta ^Integrity, la cual puede ejecutarse en el terminal desde el namespace %SYS, mediante nuestro portal de administración, en la página de la Base de datos o desde el administrador de tareas. Por defecto, la comprobación de la integridad automática ya está configurada y predefinida, por lo tanto lo único que debe hacer es activarla: La opción "integrity check" incluye la verificación de los enlaces en los niveles inferiores, la validación de los tipos de bloques, el análisis de los enlaces correctos y la correspondencia de los nodos globales con la secuencia para la aplicación de la compilación. Si el programa encuentra algún error durante la comprobación de la integridad, puede ejecutar nuestra herramienta ^REPAIR desde el namespace %SYS. Al utilizar esta herramienta, podrá visualizar cualquier bloque y modificarlo según sea necesario, por ejemplo, para reparar su base de datos. PrácticaEsto fue solo la teoría. Todavía es difícil comprender qué hace un global y cómo se ven en realidad sus bloques. Actualmente, la única manera en que podemos visualizar los bloques es utilizando nuestra herramienta ^REPAIR, que mencionamos anteriormente. A continuación, se muestra la forma característica en que se visualizan los resultados de este programa: Hace tres años, Dmitry comenzó un nuevo proyecto para desarrollar una herramienta que nos permitiera iterar a través de un árbol de bloques sin ningún riesgo de que la base de datos se dañara, visualizar estos bloques en una IU web y proporcionara opciones para guardar dichas visualizaciones en formatos SVG o PNG. El proyecto se llama CacheBlocksExplorer, y su código fuente está disponible para que lo descargue en GitHub.A partir del momento de la redacción del artículo original en 2016.Las características que se implementaron incluyen:Visualizar cualquier base de datos que se haya configurado o simplemente instalado en el sistema;Visualizar la información por bloque, los tipos de bloques, el puntero correcto o una lista de nodos con sus enlaces;Visualizar a detalle la información sobre cualquier nodo que señale hacia un bloque de nivel inferior;Ocultar bloques al eliminar los enlaces hacia ellos (sin dañar los datos almacenados en estos bloques).Lista de cosas pendientes:Mostrar los enlaces correctos: en la versión actual los enlaces correctos se muestran en la información sobre los bloques, pero lo mejor sería que pudiéramos visualizarlos como flechas,Mostrar los bloques de las cadenas largas: en la versión actual simplemente no se muestran,Mostrar todos los bloques que se encuentran en el catálogo de globales en lugar de que solamente se visualice el terceroAsimismo, me gustaría mostrar el árbol completo, pero todavía no encuentro ninguna biblioteca capaz de renderizar rápidamente los cientos de miles de bloques junto con sus enlaces. Con la biblioteca actual es posible renderizarlos en los navegadores web, pero mucho más lentamente que el tiempo que le toma a Caché leer toda la estructura.En el próximo artículo, intentaré describir con más detalle cómo funciona, proporcionar algunos ejemplos sobre su uso y mostrar cómo se recuperan muchos datos que pueden procesarse sobre los globales y los bloques mediante mi Cache Block Explorer.
Artículo
Nancy Martínez · 13 mar, 2020
Introducción
Un requisito frecuente en muchas aplicaciones es registrar en una base de datos los cambios que se realizan en los datos- qué datos se modificaron, quién los modificó y cuándo (control de cambios). Hay muchos artículos relacionados con el tema y existen diferentes métodos sobre cómo hacer esto en Caché.
Por ello, comparto un procedimiento que puede ayudar con la implementación de una estructura para seguir y registrar los cambios en los datos. Este procedimiento crea un trigger mediante un método "objectgenarator" cuando su clase persistente la hereda de "Audit Abstract Class" (Sample.AuditBase). Debido a que su clase persistente hereda Sample.AuditBase, cuando sea compilada, el activador generará automáticamente el control de las modificaciones.
Audit Class
Esta es la clase en la que se registrarán las modificaciones.
Class Sample.Audit Extends %Persistent{ Property Date As %Date; Property UserName As %String(MAXLEN = ""); Property ClassName As %String(MAXLEN = ""); Property Id As %Integer; Property Field As %String(MAXLEN = ""); Property OldValue As %String(MAXLEN = ""); Property NewValue As %String(MAXLEN = "");}
Audit Abstract Class
Esta es la clase abstracta de la que heredará su clase persistente. Esta clase contiene el método trigger (objectgenerator), que sabe cómo identificar cuáles fueron los campos que se modificaron, quién los modificó, cuáles son los antiguos y los nuevos valores, etc.; además escribirá las modificaciones en la tabla de auditoría (Sample.Audit).
Class Sample.AuditBase [ Abstract ]{Trigger SaveAuditAfter [ CodeMode = objectgenerator, Event = INSERT/UPDATE, Foreach = row/object, Order = 99999, Time = AFTER ]{ #dim %compiledclass As %Dictionary.CompiledClass #dim tProperty As %Dictionary.CompiledProperty #dim tAudit As Sample.Audit Do %code.WriteLine($Char(9)_"; get username and ip adress") Do %code.WriteLine($Char(9)_"Set tSC = $$$OK") Do %code.WriteLine($Char(9)_"Set tUsername = $USERNAME") Set tKey = "" Set tProperty = %compiledclass.Properties.GetNext(.tKey) Set tClassName = %compiledclass.Name Do %code.WriteLine($Char(9)_"Try {") Do %code.WriteLine($Char(9,9)_"; Check if the operation is an update - %oper = UPDATE") Do %code.WriteLine($Char(9,9)_"if %oper = ""UPDATE"" { ") While tKey '= "" { set tColumnNbr = $Get($$$EXTPROPsqlcolumnnumber($$$pEXT,%classname,tProperty.Name)) Set tColumnName = $Get($$$EXTPROPsqlcolumnname($$$pEXT,%classname,tProperty.Name)) If tColumnNbr '= "" { Do %code.WriteLine($Char(9,9,9)_";") Do %code.WriteLine($Char(9,9,9)_";") Do %code.WriteLine($Char(9,9,9)_"; Audit Field: "_tProperty.SqlFieldName) Do %code.WriteLine($Char(9,9,9)_"if {" _ tProperty.SqlFieldName _ "*C} {") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit = ##class(Sample.Audit).%New()") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.ClassName = """_tClassName_"""") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.Id = {id}") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.UserName = tUsername") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.Field = """_tColumnName_"""") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.Date = +$Horolog") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.OldValue = {"_tProperty.SqlFieldName_"*O}") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.NewValue = {"_tProperty.SqlFieldName_"*N}") Do %code.WriteLine($Char(9,9,9,9)_"Set tSC = tAudit.%Save()") do %code.WriteLine($Char(9,9,9,9)_"If $$$ISERR(tSC) $$$ThrowStatus(tSC)") Do %code.WriteLine($Char(9,9,9)_"}") } Set tProperty = %compiledclass.Properties.GetNext(.tKey) } Do %code.WriteLine($Char(9,9)_"}") Do %code.WriteLine($Char(9)_"} Catch (tException) {") Do %code.WriteLine($Char(9,9)_"Set %msg = tException.AsStatus()") Do %code.WriteLine($Char(9,9)_"Set %ok = 0") Do %code.WriteLine($Char(9)_"}") Set %ok = 1}}
Data Class (Persistent Class)
Esta es la clase de datos del usuario, en la que el usuario (la aplicación) realiza modificaciones, genera y elimina historiales, o hace cualquier cosa que se le permita hacer :)
En resumen, esta generalmente es su clase %Persistent.
Para iniciar el seguimiento y registro de los cambios, necesitará que la clase persistente se herede de la clase abstracta (Sample.AuditBase).
Class Sample.Person Extends (%Persistent, %Populate, Sample.AuditBase){ Property Name As %String [ Required ]; Property Age As %String [ Required ]; Index NameIDX On Name [ Data = Name ];}
Prueba
Como se ha heredado la clase de datos (Sample.Person) de la clase de Audit Abstract Class (Sample.AuditBase) se podrá insertar datos, realizar modificaciones y analizar los cambios que se registraron en Audit Class (Sample. Audit).
Para comprobar esto, es necesario crear un método Test() para la clase, en la clase Sample.Person o en cualquier otra clase se elija.
ClassMethod Test(pKillExtent = 0){ If pKillExtent '= 0 { Do ##class(Sample.Person).%KillExtent() Do ##class(Sample.Audit).%KillExtent() } &SQL(INSERT INTO Sample.Person (Name, Age) VALUES ('TESTE', '01')) Write "INSERT INTO Sample.Person (Name, Age) VALUES ('TESTE', '01')",! Write "SQLCODE: ",SQLCODE,!!! Set tRS = $SYSTEM.SQL.Execute("SELECT * FROM Sample.Person") Do tRS.%Display() &SQL(UPDATE Sample.Person SET Name = 'TESTE 2' WHERE Name = 'TESTE') Write !!! Write "UPDATE Sample.Person SET Name = 'TESTE 2' WHERE Name = 'TESTE'",! Write "SQLCODE:",SQLCODE,!!! Set tRS = $SYSTEM.SQL.Execute("SELECT * FROM Sample.Person") Do tRS.%Display() Quit}
Ejecutar el método Test():
d ##class(Sample.Person).Test(1)
Parameter 1 will kill extent from Sample.Person and Sample.Audit classes.
El método Test para clase realiza lo siguiente:
Introduce una nueva persona con el nombre "TEST";
Muestra el resultado de la introducción;
Actualiza la persona "TEST" a "TEST ABC";
Muestra el resultado de la actualización;
Ahora se podrá verificar la tabla de control de cambios. Para ello, entra en el Portal de Administración del Sistema -> Explorador del Sistema -> SQL. (No olvides cambiarse a su namespace)
Ejecuta el siguiente comando en SQL y verifica los resultados:
SELECT * FROM Sample.Audit
Debes Tener en cuenta que la variable OldValue es "TEST" y la NewValue es "TEST ABC". A partir de ahora podrás realizar tus propias pruebas al cambiar el nombre "TEST ABC" por "Su propio nombre" y/o cambiar, por ejemplo, los valores de Age. Consulte:
UPDATE Sample.Person SET Name = 'Fabio Goncalves' WHERE Name = 'TEST ABC'
Generación del Código
Teniendo en cuenta que hemos implementado el procedimiento de control de cambios que se muestra a continuación, inicia Studio (o Atelier) en tu equipo, abre la clase persistente (Sample.Person) y analiza el código intermedio que se generó después de compilar la clase Sample.Person. Para hacerlo, presiona Ctrl + Shift + V (Consultar otro código fuente), y analiza .INT. Desplázate hacia abajo hasta la etiqueta zSaveAuditAfterExecute y échele un vistazo al código que se generó:
Ventajas
Es muy sencillo implementar el control de cambios basado en el despliegue de datos antiguos. No es necesario tablas adicionales. El mantenimiento también es sencillo. Si decides eliminar datos antiguos, entonces debes utilizar un SQL.
Si se necesita implementar el control de cambios en más tablas, solo es necesario heredarlo desde la clase abstracta (Sample.AuditBase)
Realiza los cambios conforme a tus necesidades, por ejemplo, registra los cambios en Streams.
Registra solamente los campos que se han modificado. No guarda todo el historial de cambios.
Desventajas
Un problema puede ser que cuando se modifican los datos, también se copia todo el historial, es decir, también se copian los datos que no se modificaron.
Si la tabla persona tiene una columna "foto" con los datos binarios (stream) que contiene la fotografía, entonces cada vez que el usuario cambie la imagen también se registrará la función stream (la cual consume espacio en el disco).
Otra desventaja es que también aumenta la complejidad con cada tabla complementaria que añade en el control de cambios. Deberas tener en cuenta, todo el tiempo, que no es tan sencillo recuperar los historiales. Siempre debes utilizar la cláusula SELECT con el condicional: "...WHERE Status = active" o considere algún "DATE INTERVAL"
Todas las modificaciones que se realicen a los datos se registran en una tabla común.
Piensa en las translocaciones y los rollbacks.
El control de cambios es un requisito importante para que algunas aplicaciones sean eficientes. Por lo general, para determinar si hubo alguna modificación en los datos, los desarrolladores deben implementar un método de seguimiento personalizado en sus aplicaciones, mediante una combinación de triggers, columnas para registrar la hora y tablas adicionales. Crear estos procedimientos normalmente requiere mucho trabajo para su implementación, conduce a actualizaciones de esquemas y con frecuencia, implica una disminución en el rendimiento. Este es un ejemplo sencillo que te podría ayudar a establecer tu propia estructura.
Este artículo ha sido etiquetado como "Best practices"
(Los artículos con la etiqueta "Best practices" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems).
Artículo
Ricardo Paiva · 26 mar, 2020
Este es el primero de dos artículos sobre los índices SQL.
Parte 1 - Conoce tus índices
¿Qué es un índice?
Recuerda la última vez que fuiste a una biblioteca. Normalmente, los libros están ordenados por temática (y luego autor y título) y cada repisa tiene un cartel en el extremo con un código que describe la temática de los libros. Si necesitaras libros de un cierto tema, en lugar de caminar por cada pasillo y leer la descripción en la parte interior de cada libro, podrías dirigirte directamente al estante cuyo cartel describa la temática que buscas y elegir tus libros de allí. Sin esos carteles, el proceso de encontrar los libros que quieres, habría sido muy lento.
Un índice SQL tiene la misma función general: mejorar el rendimiento, al ofrecer una referencia rápida del valor de los campos para cada fila de una tabla.
Configurar índices es uno de los pasos más importantes a la hora de preparar tus clases para un rendimiento óptimo de SQL.
En este artículo veremos:
¿Qué es un índice y por qué/cuando se deben usar?
¿Qué tipo de índices existen y en qué situaciones son perfectos?
¿A qué se parece un índice?
¿Cómo se puede crear uno?
Y cuando tengo índices, ¿qué hago con ellos?
Me referiré a las clases de nuestro esquema Sample, que se incluye en el namespace Samples en instalaciones de Caché y Ensemble. Puedes descargar estas clases de nuestro repositorio en Github:
https://github.com/intersystems/Samples-Data
Lo fundamental
Puedes indexar cualquier propiedad persistente y cualquier propiedad que puede ser calculada de forma fiable a partir de datos persistentes.
Supongamos que queremos indexar la propiedad TaxID (identificador impositivo) en Sample.Company. En Studio o Atelier, añadiríamos lo siguiente a la definición de la clase:
Index TaxIDIdx On TaxID [ Type = index, Unique ];
La sentencia DDL SQL equivalente se vería como algo así:
CREATE UNIQUE INDEX TaxIDIdx ON Sample.Company (TaxID);
La estructura del índice global predeterminado es la siguiente:
^Sample.CompanyI("TaxIDIdx ",<TaxIDValueAtRowID>,<RowID>) = ""
Ten en cuenta que hay menos subíndices para leer que campos en un global de datos típicos.
Mira la consulta “SELECT Name,TaxID FROM Sample.Company WHERE TaxID = 'J7349'”. Es lógica y simple, y el plan de consulta para ejecutar esta consulta lo refleja:
Este plan dice, básicamente, que buscamos columnas con el valor dado de TaxID en el global del índice, y luego volvemos a buscar en el global de datos ("master map") para recuperar la fila coincidente.
Ahora mira la misma consulta sin un índice en TaxIDX. El plan de consulta resultante es, como se esperaría, menos eficiente:
Sin índices, la ejecución de consultas subyacente de Caché se basa en leer en la memoria y aplicar la condición de la cláusula WHERE a cada fila de la tabla. Y como TaxID es único, ¡estamos haciendo todo este trabajo tan solo para una fila!
Por supuesto, tener índices significa tener datos de índices y filas en disco. Dependiendo de en qué tengamos una condición y cuántos datos contenga nuestra tabla, esto puede generar sus propios desafíos al crear y poblar un índice.
Entonces, ¿cuándo agregamos un índice a una propiedad?
El caso general es cuando condicionamos con frecuencia en base a una propiedad. Algunos ejemplos son identificar información como el número de seguridad social (SSN) de una persona o su número de cuenta bancaria. También puede pensar en fechas de nacimiento o en los fondos de una cuenta. Volviendo a Sample.Company, quizás la clase se vería beneficiada de indexar la propiedad Revenue (ingresos) si quisiéramos recopilar datos sobre organizaciones con altos ingresos. Por otra parte, las propiedades que es poco probable que condicionemos son menos adecuadas para indexar, como por ejemplo el eslogan o descripción de una empresa.
Simple, ¡excepto que también debemos considerar qué tipo de índice es el mejor!
Tipos de índices
Hay seis tipos principales de índices que describiré aquí: estándar, bitmap, compuesto, recopilación, bitslice y datos. También describiré brevemente los índices iFind, que se basan en flujos. Aquí hay posibles solapamientos, y ya hemos visto los índices estándar con el ejemplo anterior.
Compartiré ejemplos de cómo crear índices en tu definición de clase, pero añadir nuevos índices a una clase es más complejo que tan solo añadir una línea a tu definición de clase. En la próxima parte analizaremos todas las consideraciones adicionales.
Usemos Sample.Person como ejemplo. Ten en cuenta que Person tiene la subclase Employee (empleado), que será relevante para entender algunos ejemplos. Employee comparte su almacenamiento de global de datos con Person, y todos los índices de Person son heredados por Employee. Esto significa que Employee usa el global de índices de Person para estos índices heredados.
Si no estás familiarizado con ellas, esta es una descripción general de las clases: Person tiene propiedades SSN (número de seguridad social), DOB (fecha de nacimiento), Name (nombre), Home (un objeto embebido de dirección tipo Address que contiene State (estado) y City (ciudad)), Office (oficina, también de tipo Address) y la colección de listas FavoriteColors (colores favoritos). Employee tiene la propiedad adicional Salary (salario, que definí yo misma).
Estándar
Index DateIDX On DOB;
Aquí uso "estándar" de forma de forma poco precisa, para referirme a índices que almacenan el valor sencillo de una propiedad (a diferencia de una representación binaria). Si el valor es una cadena, se almacenará bajo alguna compilación (collation) – SQLUPPER por defecto.
En comparación con índices bitmap o bitslice, los índices estándar son mucho más fáciles de leer por una persona y su mantenimiento es bastante sencillo. Tenemos un nodo global para cada fila de la tabla.
A continuación se muestra cómo se almacena DateIDX a nivel global.
^Sample.PersonI("DateIDX",51274,100115)="~Sample.Employee~" ; Date is 05/20/81
Ten en cuenta que el primer subscript después del nombre del índice es el valor de la fecha, el último subscript es el ID de Person con esa DOB (fecha de nacimiento) y el valor almacenado en este nodo global indica que esta persona también es miembro de la subclase Sample.Employee. Si esa persona no fuera miembro de ninguna subclase, el valor en el nodo sería una cadena vacía.
Esta estructura base será consistente con la mayoría de los índices que no sean bits, en los cuales los índices en más de una propiedad crean más subscripts en el global y tener más de un valor almacenado en el nodo genera un objeto $listbuild, por ejemplo:
^Package.ClassI(IndexName,IndexValue1,IndexValue2,IndexValue3,RowID) = $lb(SubClass,DataValue1,DataValue2)
Bitmap – Una representación bit a bit (bitwise) del conjunto de IDs que corresponden al valor de una propiedad.
Index HomeStateIDX On Home.State [ Type = bitmap];
Los índices de bitmap se guardan por valor único, a diferencia de los índices estándar, que se almacenan por fila.
Continuando con el ejemplo anterior, digamos que la persona con el ID 1 vive en Massachusetts, el ID 2 en Nueva York, ID 3 en Massachusetts e ID 4 en Rhode Island. HomeStateIDX básicamente se almacena así:
ID
1
2
3
4
(…)
(…)
0
0
0
0
-
MA
1
0
1
0
-
NY
0
1
0
0
-
RI
0
0
0
1
-
(…)
0
0
0
0
-
Si quisiéramos que una consulta devuelva datos de las personas que viven en New England, el sistema realizaría un OR bit a bit (bitwise) sobre las filas relevantes del índice bitmap. Se puede ver rápidamente que debemos cargar en memoria los objetos Person con ID 1, 3 y 4 como mínimo.
Los bitmaps pueden ser eficientes para operadores AND, RANGE y OR en tus claúsulas WHERE.
Si bien no hay un límite oficial sobre la cantidad de valores únicos que puede tener para una propiedad antes de que un índice tipo bitmap sea menos eficiente que un índice estándar, la regla general es de hasta unos 10.000 valores distintos. Entonces, mientras un índice tipo bitmap podría ser efectivo en un estado de los EE. UU., un índice bitmap para una ciudad o condado no sería tan útil.
Otro concepto a tener en cuenta es la eficiencia de almacenamiento. Si piensas añadir o eliminar filas de tu tabla con frecuencia, el almacenamiento de tu índice tipo bitmap podría volverse menos eficiente. Veamos el ejemplo anterior: si elimináramos muchas filas por algún motivo y ya no tenemos a nadie en nuestra tabla que viva en estados menos poblados como Wyoming o Dakota del Norte, el bitmap entonces tendría varias filas solo con ceros. Por otra parte, crear nuevas filas en tablas grandes al final puede volverse más lento, ya que el almacenamiento de bitmaps grandes debe alojar más valores únicos.
En estos ejemplos tengo unas 150.000 filas en Sample.Person. Cada nodo global almacena hasta 64.000 ID's, por lo que el global del índice bitmap en el valor MA está dividido en tres partes:
^Sample.PersonI("HomeStateIDX"," MA",1)=$zwc(135,7992)_$c(0,(...))
^Sample.PersonI("HomeStateIDX"," MA",2)=$zwc(404,7990,(…))
^Sample.PersonI("HomeStateIDX"," MA",3)=$zwc(132,2744)_$c(0,(…))
Caso especial: Extent Bitmap
Un bitmap extent, a menudo llamado $<ClassName>, es un índice tipo bitmap sobre los ID de una clase. Esto brinda a Caché una forma rápida de saber si una fila existe y puede ser útil para consultas COUNT o consultas sobre subclases. Estos índices se generan cuando un índice tipo bitmap se añade a la clase. También puedes crear manualmente un índice extent bitmap en una definición de clase de la siguiente forma:
Index Company [ Extent, SqlName = "$Company", Type = bitmap ];
O mediante la DDL keyword BITMAPEXTENT:
CREATE BITMAPEXTENT INDEX "$Company" ON TABLE Sample.Company
Compuesto – Índices basados en dos o más propiedades
Index OfficeAddrIDX On (Office.City, Office.State);
El caso de uso general de los índices compuestos es tener consultas frecuentes con condiciones sobre dos o más propiedades.
El orden de las propiedades en un índice compuesto importa, debido a la forma en que se almacena el índice en un nivel global. Tener primero la propiedad más selectiva es más eficiente para el rendimiento, ya que ahorrará lecturas de disco iniciales del global de índices. En este ejemplo. Office.City está primero debido a que hay más ciudades únicas que estados en los EE. UU.
Tener primero una propiedad menos selectiva es más eficiente para el espacio. En términos de estructura global, el árbol de índices estaría mejor equilibrado si el estado (State) estuviera primero. Piénsalo: cada estado contiene varias ciudades, pero algunos nombres de ciudades pertenecen a un único estado.
También puedes considerar si esperas ejecutar consultas frecuentes que condicionan solo una de las propiedades. Esto puede ahorrarte definir otro índice más.
Este es un ejemplo de la estructura global del índice compuesto:
^Sample.PersonI("OfficeAddrIDX"," BOSTON"," MA",100115)="~Sample.Employee~"
¿Índice compuesto o índices de bitmap?
Para consultas con condiciones sobre múltiples propiedades, puede que también quieras evaluar si índices tipo bitmap separados serían más efectivos que un único índice compuesto.
Las operaciones bit a bit en dos índices distintos podrían ser más eficientes, considerando que los índices bitmap se adecuan bien a cada propiedad.
También puedes tener índices tipo bitmap compuestos: son índices tipo bitmap en los que el valor único es la intersección de las múltiples propiedades sobre las que estás indexando. Por ejemplo, la tabla de la sección anterior, pero si en lugar de estados tenemos cada par posible de estado y ciudad (p. ej. Boston, MA, Cambridge, MA, incluso Los Angeles, MA, etc.) y las celdas reciben valores 1 por las filas que cumplen ambos valores.
Recopilación – Índices basados en propiedades de recopilación
Aquí tenemos la propiedad FavoriteColors definida de la siguiente forma:
Property FavoriteColors As list Of %String(JAVATYPE = "java.util.List", POPSPEC = "ValueList("",Red,Orange,Yellow,Green,Blue,Purple,Black,White""):2");
Con cada uno de los siguientes índices definidos con fines de demostración:
Index fcIDX1 On FavoriteColors(ELEMENTS);Index fcIDX2 On FavoriteColors(KEYS);
Aquí uso "recopilación" para referirme de forma más amplia a propiedades de una celda que contienen más de un valor. Las propiedades List Of y Array Of son relevantes aquí e incluso las cadenas delimitadas.
Las propiedades de recopilación se analizan automáticamente para construir sus índices. Para propiedades delimitadas, como un número telefónico, deberá definir este método, <PropertyName>BuildValueArray(value, .valueArray), de forma explícita.
Dado el ejemplo anterior para FavoriteColors, fcIDX1 se vería como algo así para una persona cuyos colores favoritos fueran azul y blanco:
^Sample.PersonI("fcIDX1"," BLUE",100115)="~Sample.Employee~"
(…)
^Sample.PersonI("fcIDX1"," WHITE",100115)="~Sample.Employee~"
fcIDX2 se vería así:
^Sample.PersonI("fcIDX2",1,100115)="~Sample.Employee~"
^Sample.PersonI("fcIDX2",2,100115)="~Sample.Employee~"
En este caso, como FavoriteColors es una colección de List, un índice basado en sus claves es menos útil que un índice basado en sus elementos.
Consulta nuestra documentación para cuestiones más específicas sobre crear y gestionar índices basados en propiedades de recopilación:
https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_indices#GSQLOPT_indices_collections
Bitslice – Representación bitmap de la representación de la cadena de bits de datos numéricos
Index SalaryIDX On Salary [ Type = bitslice ]; //In Sample.Employee
A diferencia de los índices bitmap, que contienen marcas (flags) que representan qué filas contienen un valor específico, los índices bitslice primero convierten valores numéricos de decimal a binario y luego crean un bitmap en cada dígito del valor binario.
Tomemos el ejemplo anterior y, para ser realistas, simplifiquemos Salary (salario) como unidades de $1000. Así, si el salario de un empleado se guarda como 65, se entiende que representa $65.000.
Digamos por ejemplo que tenemos Employee con ID 1 y salario 15, ID2 con salario 40, ID 3 con salario 64 e ID 4 con salario 130. Los valores de bits correspondientes son:
15
0
0
0
0
1
1
1
1
40
0
0
1
0
1
0
0
0
64
0
1
0
0
0
0
0
0
130
1
0
0
0
0
0
1
0
Nuestra cadena de bits abarca 8 dígitos. La representación bitmap correspondiente (los valores de índices bitslice) se almacena básicamente así:
^Sample.PersonI("SalaryIDX",1,1) = "1000" ; Row 1 has value in 1’s place
^Sample.PersonI("SalaryIDX",2,1) = "1001" ; Rows 1 and 4 have values in 2’s place
^Sample.PersonI("SalaryIDX",3,1) = "1000" ; Row 1 has value in 4’s place
^Sample.PersonI("SalaryIDX",4,1) = "1100" ; Rows 1 and 2 have values in 8’s place
^Sample.PersonI("SalaryIDX",5,1) = "0000" ; etc…
^Sample.PersonI("SalaryIDX",6,1) = "0100"
^Sample.PersonI("SalaryIDX",7,1) = "0010"
^Sample.PersonI("SalaryIDX",8,1) = "0001"
Ten en cuenta que las operaciones que modifican Sample.Employee o los salarios de sus filas (es decir, INSERT, UPDATES y DELETE) ahora requieren actualizar cada uno de estos nodos globales, o bitslices. Añadir un índice bitslice a múltiples propiedades de una tabla o una propiedad que se modifica con frecuencia puede conllevar riesgos de rendimiento. En general, mantener un índice bitslice es más costoso que mantener índices de tipo estándar o bitmap.
Los índices bitslice son altamente especializados y por eso tienen casos de uso específicos: consultas que deben realizar cálculos agregados (p.ej. SUM, COUNT o AVG).
Además, solo pueden usarse de forma efectiva sobre valores numéricos (las cadenas de caracteres se convierten a un 0 binario).
Si es necesario leer la tabla de datos, en lugar de los índices, para verificar la condición de una consulta, los índices bitslice no se elegirán para ejecutar la consulta. Digamos que Sample.Person no tiene un índice en Name. Si quisiéramos calcular el salario medio de los empleados cuyo apellido es Smith (“SELECT AVG(Salary) FROM Sample.Employee WHERE Name %STARTSWITH 'Smith,' “) necesitaríamos leer filas de datos para aplicar la condición WHERE, por lo que en la práctica no se usaría el índice bitslice.
Hay varios problemas de almacenamiento similares para índices bitslice y bitmap en tablas en las que se crean o eliminan filas frecuentemente.
Datos - Índices con datos almacenados en sus nodos globales.
Index QuickSearchIDX On Name [ Data = (SSN, DOB, Name) ];
En varios de los ejemplos anteriores, puedes haber observado la cadena “~Sample.Employee~” almacenada como el valor del propio nodo. Recuerda que Sample.Employee hereda índices de Sample.Person. Cuando hacemos una consulta sobre los empleados (Employees) en particular, leemos el valor en los nodos de índices que coinciden con la condición de nuestra propiedad para verificar que dicha persona (Person) también sea un empleado.
También podemos definir explícitamente qué valores almacenar. Definir los datos en tus nodos globales de índices puede ahorrarte lecturas de todos los datos globales. Esto puede ser útil para consultas ordenadas o selectivas frecuentes.
Tomamos como ejemplo el índice anterior. Si quisiéramos extraer información identificatoria sobre una persona dado su nombre completo o parcial (p. ej. para buscar información del cliente en una aplicación de atención al público), podríamos tener una consulta como “SELECT SSN, Name, DOB FROM Sample.Person WHERE Name %STARTSWITH 'Smith,J' ORDER BY Name”. Como las condiciones de nuestra consulta sobre Name y los valores que estamos recuperando se encuentran todos dentro de los nodos globales QuickSearchIDX, solo necesitamos leer nuestro global I para ejecutar esta consulta.
Ten en cuenta que los valores de datos no pueden almacenarse con índices bitmap o bitslice.
^Sample.PersonI("QuickSearchIDX"," LARSON,KIRSTEN A.",100115)=$lb("~Sample.Employee~","555-55-5555",51274,"Larson,Kirsten A.")
Índices iFind
¿Alguna vez ha oído hablar de ellos? Yo tampoco. Los índices iFind se usan en propiedades de flujo, pero para usarlos se deben especificar sus nombres con palabras clave en la consulta.
Podría explicarlo más en detalle, pero Kyle Baxter tiene un artículo muy útil sobre esto:
https://community.intersystems.com/post/free-text-search-way-search-your-text-fields-sql-developers-are-hiding-you
Continúa leyendo la Parte 2, sobre la gestión de índices definidos.
Este artículo ha sido etiquetado como "Best practices"
(Los artículos con la etiqueta "Best practices" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems).
Artículo
Rizmaan Marikar · 21 abr, 2022
Hay varias maneras de generar ficheros Excel usando tecnología InterSystems: por ejemplo utilizando informes generados con InterSystems Reports, o los antiguos informes ZEN, o incluso haciendo uso de librerías Java de terceros. Las posibilidades son casi infinitas.
Pero, ¿qué pasa si quieres crear una sencilla hoja de cálculo sólo con ObjectScript? (sin aplicaciones de terceros)
En mi caso, necesito generar informes que contengan muchos datos sin procesar (a los financieros les encantan), pero mi antiguo informe ZEN fallaba y me da lo que me gusta llamar un "archivo con cero bytes". Básicamente, Java se queda sin memoria y provoca una sobrecarga en el servidor de informes.
Esto se puede hacer usando Office Open XML (OOXML). El formato Office Open XML está compuesto por un número de archivos XML dentro de un paquete ZIP. Así que, básicamente, necesitamos generar estos archivos XML y comprimirlos renombrandolos a .xslx. Así de fácil.
Los archivos siguen un sencillo conjunto de convenciones llamadas Open Packaging Conventions (OPC). Hay que declar los tipos de contenido de las partes, así como indicar a la aplicación que lo consumirá donde debería empezar.
Parar crear una sencilla hoja de cálculo, necesitamos un mínimo de 5 ficheros:
workbook.xml
worksheet.xml
[Content_Types].xml
styles.xml
_rels
.rels
workbook.xml.rels
workbook.xmlEl workbook es el contenedor de diferentes worksheets. El workbook es donde puedes referenciar estilos, tablas de cadenas de texto compartidas, y otras piezas de información cuyo ámbito es la totalidad de la hoja de cálculo.
ClassMethod GenerateWorkbookXML(){
set status =$$$OK
set xmlfile = tempDirectoryPath_"workbook.xml"
try{
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>")
do stream.WriteLine("<workbook xmlns='http://schemas.openxmlformats.org/spreadsheetml/2006/main' xmlns:r='http://schemas.openxmlformats.org/officeDocument/2006/relationships'>")
do stream.WriteLine("<sheets> <sheet name='"_workSheetName_"' sheetId='1' r:id='rId1'/>")
do stream.WriteLine("</sheets> </workbook>")
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
_rels/workbook.xml.relsSólo necesitamos crear una relación que tenga un id de rId1 de manera que coincida con la referencia desde la parte de workbook.xml
ClassMethod CreateRelsXML(){
set status =$$$OK
set isunix=$zcvt($p($zv," ",3,$l($p($zv," (")," ")),"U")["UNIX"
if isunix {
set ext="/"
}else{
set ext="\"
}
set xmlfile = fileDirectory_"_rels"_ext_"workbook.xml.rels"
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>")
do stream.WriteLine("<Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'>")
do stream.WriteLine("<Relationship Id='rId1' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet' Target='worksheet.xml'/>")
do stream.WriteLine("<Relationship Id='rId2' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles' Target='styles.xml' />")
do stream.WriteLine("</Relationships>")
try{
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
set xmlfile = fileDirectory_"_rels"_ext_".rels"
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>")
do stream.WriteLine("<Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'>")
do stream.WriteLine("<Relationship Id='rId1' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument' Target='workbook.xml'/>")
do stream.WriteLine("</Relationships>")
try{
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
[Content_Types].xmlArchivo estático (por el momento, aunque debería ser un archivo dinámico dependiendo del número de worksheets) que vincula workbook, worksheet y estilos. Cada archivo Office Open XML debe declarar los tipos de contenido usados en el paquete ZIP. Eso se hace con el archivo [Content_Types].xml.
ClassMethod GenerateConntentTypesXML(){
set status =$$$OK
set xmlfile = tempDirectoryPath_"[Content_Types].xml"
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
try{
do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>")
do stream.WriteLine("<Types xmlns='http://schemas.openxmlformats.org/package/2006/content-types'>")
do stream.WriteLine("<Default Extension='rels' ContentType='application/vnd.openxmlformats-package.relationships+xml'/>")
do stream.WriteLine("<Override PartName='/workbook.xml' ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'/>")
do stream.WriteLine("<Override PartName='/worksheet.xml' ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'/>")
do stream.WriteLine("<Override PartName='/styles.xml' ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml' />")
do stream.WriteLine("</Types>")
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
styles.xmlTodo lo necesario para el formateo se coloca aquí. Por el momento hay varios estilos estáticos (aunque debería ser dinámico dependiendo del workbook)
Excel Styles
ID
Style
Excel Format
1
default
Text
2
#;[Red]-#
Number
3
#.##;[Red]-#.##
Number
4
yyyy/mm/dd
Date
5
hh:mm
Date
6
Header and Center Aligned
Text
7
Header 2 Left Aligned
Text
8
Good(Green Highlight)
General
9
Bad(Red Highlight)
General
10
Neutral(Orange Highlight)
General
11
yyyy/mm/dd hh:mm
Date
ClassMethod CreateStylesXML(){
set status =$$$OK
set xmlfile = tempDirectoryPath_"styles.xml"
try{
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>")
do stream.WriteLine("<styleSheet xmlns=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"" xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006"" mc:Ignorable=""x14ac x16r2 xr"" xmlns:x14ac=""http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"" xmlns:x16r2=""http://schemas.microsoft.com/office/spreadsheetml/2015/02/main"" xmlns:xr=""http://schemas.microsoft.com/office/spreadsheetml/2014/revision"">")
do stream.WriteLine("<numFmts count=""4"">")
do stream.WriteLine("<numFmt numFmtId=""166"" formatCode=""#,##0;[Red]\-#,##0""/>")
do stream.WriteLine("<numFmt numFmtId=""168"" formatCode=""#,##0.00;[Red]\-#,##0.00""/>")
do stream.WriteLine("<numFmt numFmtId=""169"" formatCode=""dd\/mm\/yyyy;@""/>")
do stream.WriteLine("<numFmt numFmtId=""170"" formatCode=""dd/mm/yyyy\ hh:mm""/></numFmts>")
do stream.WriteLine("<fonts count=""5"" x14ac:knownFonts=""1"">")
do stream.WriteLine("<font><sz val=""10""/><color theme=""1""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>")
do stream.WriteLine("<font><sz val=""10""/><color rgb=""FF006100""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>")
do stream.WriteLine("<font><sz val=""10""/><color rgb=""FF9C0006""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>")
do stream.WriteLine("<font><sz val=""10""/><color rgb=""FF9C5700""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>")
do stream.WriteLine("<font><b/><sz val=""10""/><color theme=""1""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font></fonts>")
do stream.WriteLine("<fills count=""5"">")
do stream.WriteLine("<fill><patternFill patternType=""none""/></fill>")
do stream.WriteLine("<fill><patternFill patternType=""gray125""/></fill>")
do stream.WriteLine("<fill><patternFill patternType=""solid""><fgColor rgb=""FFC6EFCE""/></patternFill></fill>")
do stream.WriteLine("<fill><patternFill patternType=""solid""><fgColor rgb=""FFFFC7CE""/></patternFill></fill>")
do stream.WriteLine("<fill><patternFill patternType=""solid""><fgColor rgb=""FFFFEB9C""/></patternFill></fill></fills>")
do stream.WriteLine("<borders count=""1""><border><left/><right/><top/><bottom/><diagonal/></border></borders>")
do stream.WriteLine("<cellStyleXfs count=""4"">")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""1"" fillId=""2"" borderId=""0"" applyNumberFormat=""0"" applyBorder=""0"" applyAlignment=""0"" applyProtection=""0""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""2"" fillId=""3"" borderId=""0"" applyNumberFormat=""0"" applyBorder=""0"" applyAlignment=""0"" applyProtection=""0""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""3"" fillId=""4"" borderId=""0"" applyNumberFormat=""0"" applyBorder=""0"" applyAlignment=""0"" applyProtection=""0""/></cellStyleXfs>")
do stream.WriteLine("<cellXfs count=""12""><xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0""/>")
do stream.WriteLine("<xf numFmtId=""49"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" quotePrefix=""1"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""166"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""168"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""169"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""20"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""49"" fontId=""4"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1"" applyFont=""1""/>")
do stream.WriteLine("<xf numFmtId=""49"" fontId=""4"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1"" applyFont=""1"" applyAlignment=""1""><alignment horizontal=""center""/>")
do stream.WriteLine("</xf>")
do stream.WriteLine("<xf numFmtId=""49"" fontId=""1"" fillId=""2"" borderId=""0"" xfId=""1"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""2"" fillId=""3"" borderId=""0"" xfId=""2""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""3"" fillId=""4"" borderId=""0"" xfId=""3""/>")
do stream.WriteLine("<xf numFmtId=""170"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/></cellXfs>")
do stream.WriteLine("<cellStyles count=""4""><cellStyle name=""Bad"" xfId=""2"" builtinId=""27""/>")
do stream.WriteLine("<cellStyle name=""Good"" xfId=""1"" builtinId=""26""/><cellStyle name=""Neutral"" xfId=""3"" builtinId=""28""/>")
do stream.WriteLine("<cellStyle name=""Normal"" xfId=""0"" builtinId=""0""/></cellStyles><dxfs count=""0""/>")
do stream.WriteLine("<tableStyles count=""0"" defaultTableStyle=""TableStyleMedium2"" defaultPivotStyle=""PivotStyleLight16""/> ")
do stream.WriteLine("<extLst><ext uri=""{EB79DEF2-80B8-43e5-95BD-54CBDDF9020C}"" xmlns:x14=""http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"">")
do stream.WriteLine("<x14:slicerStyles defaultSlicerStyle=""SlicerStyleLight1""/></ext><ext uri=""{9260A510-F301-46a8-8635-F512D64BE5F5}"" xmlns:x15=""http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"">")
do stream.WriteLine("<x15:timelineStyles defaultTimelineStyle=""TimeSlicerStyleLight1""/></ext></extLst>")
do stream.WriteLine("</styleSheet>")
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
worksheet.xmlAquí es donde se colocan nuestros datos. La primera fila tendrá los títulos de las columnas. Las siguientes filas sólo tendrán datos.Aquí definiremos los anchos de columna para cada columna; si no, las columnas por defecto se configurarán con ajuste automático.
Worksheet de muestra en xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="https://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="https://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheetData>
<row>
<c t="inlineStr">
<is>
<t>Name</t>
</is>
</c>
<c t="inlineStr">
<is>
<t>Amount</t>
</is>
</c>
</row>
<row>
<c t="inlineStr">
<is>
<t>Jhon Smith</t>
</is>
</c>
<c>
<v>1000.74</v>
</c>
</row>
<row>
<c t="inlineStr">
<is>
<t>Tracy A</t>
</is>
</c>
<c>
<v>6001.74</v>
</c>
</row>
</sheetData>
</worksheet>
Excel de muestra
Las fórmulas dentro de la worksheet las podemos incluir utilizando una etiqueta <f>
<c >
<f>B2*0.08</f >
</c >
<c >
<f>B2+C2</f >
</c>
y finalmente lo empaquetamos y renombramos a .xlsx (usando unix zip)
set cmd ="cd "_fileDirectory_" && find . -type f | xargs zip .."_ext_xlsxFile
Cómo generar un documento Excel
Este código de muestra genera un documento Excel.
set file = "/temp/test.xlsx"
set excelObj = ##class(XLSX.writer).%New(file)
do excelObj.SetWorksheetName("test1")
set status = excelObj.BeginWorksheet()
set row = 0
set row = row+1
;----------- excelObj.Cells(rowNumber,columnNumber,style,content)
set status = excelObj.Cells(row,1,1,"Header1")
set row = row+1
set status = excelObj.Cells(row,1,2,"Content 1")
set status = excelObj.EndWorksheet()
W !,excelObj.fileName
Podéis encontrar el código en :
https://github.com/RizmaanMarikar/ObjectScriptExcelGenerator