├── sc-list.txt
├── .dockerignore
├── .gitignore
├── bscript.sh
├── ascript.sh
├── docker-compose.yml
├── .gitattributes
├── .github
├── workflows
│ └── objectscript-quality.yml
├── workflows_runtests.yml
├── workflows_build-push-gcr.yaml
├── workflows_bump-module-version.yml
└── workflows_github-registry.yml
├── module.xml
├── Dockerfile
├── iris.script
├── .vscode
├── launch.json
├── extensions.json
└── settings.json
├── LICENSE
├── src
└── Converter
│ ├── Utils
│ └── XML.cls
│ ├── LibreOffice.cls
│ ├── Common.cls
│ └── Footer.cls
├── README.md
└── dev.md
/sc-list.txt:
--------------------------------------------------------------------------------
1 | Converter.pkg
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.DS_Store
2 | iris-main.log
3 | .git
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | iris-main.log
3 | .env
4 | .git
5 |
6 |
--------------------------------------------------------------------------------
/bscript.sh:
--------------------------------------------------------------------------------
1 | apt-get update
2 | apt-get install -y libreoffice-core libreoffice-writer
--------------------------------------------------------------------------------
/ascript.sh:
--------------------------------------------------------------------------------
1 | cd /home/irisowner/dev
2 | iris view
3 | iris session iris < iris.script
4 | exit 0
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.6'
2 | services:
3 | iris:
4 | build:
5 | context: .
6 | dockerfile: Dockerfile
7 | restart: always
8 |
9 | ports:
10 | - 41773:1972
11 | - 42773:52773
12 | volumes:
13 | - ./:/home/irisowner/dev
14 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.cls linguist-language=ObjectScript
2 | *.mac linguist-language=ObjectScript
3 | *.int linguist-language=ObjectScript
4 | *.inc linguist-language=ObjectScript
5 | *.csp linguist-language=Html
6 |
7 | *.sh text eol=lf
8 | *.cls text eol=lf
9 | *.mac text eol=lf
10 | *.int text eol=lf
11 | Dockerfil* text eol=lf
12 |
--------------------------------------------------------------------------------
/.github/workflows/objectscript-quality.yml:
--------------------------------------------------------------------------------
1 | name: objectscriptquality
2 | on: push
3 |
4 | jobs:
5 | linux:
6 | name: Linux build
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - name: Execute ObjectScript Quality Analysis
11 | run: wget https://raw.githubusercontent.com/litesolutions/objectscriptquality-jenkins-integration/master/iris-community-hook.sh && sh ./iris-community-hook.sh
12 |
13 |
--------------------------------------------------------------------------------
/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | converter
6 | 2.0.0
7 | Convert documents easily
8 | module
9 | src
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG IMAGE=intersystemsdc/iris-community
2 | FROM $IMAGE
3 |
4 | USER root
5 | RUN apt-get update && apt-get install -y libreoffice-core libreoffice-writer
6 |
7 | WORKDIR /opt/irisapp
8 | RUN chown ${ISC_PACKAGE_MGRUSER}:${ISC_PACKAGE_IRISGROUP} /opt/irisapp
9 |
10 | USER ${ISC_PACKAGE_MGRUSER}
11 |
12 | COPY src src
13 | COPY module.xml module.xml
14 | COPY iris.script iris.script
15 |
16 | RUN iris start IRIS \
17 | && iris session IRIS < iris.script \
18 | && iris stop IRIS quietly
19 |
--------------------------------------------------------------------------------
/iris.script:
--------------------------------------------------------------------------------
1 | zn "%SYS"
2 |
3 | // Unexpire passwords and set up passwordless mode to simplify dev use.
4 | do ##class(Security.Users).UnExpireUserPasswords("*")
5 | zpm "install passwordless"
6 |
7 | zn "USER"
8 | // Create /_vscode web app to support intersystems-community.testingmanager VS Code extension
9 | zpm "install vscode-per-namespace-settings"
10 | zpm "install webterminal"
11 |
12 | zpm "load /opt/irisapp/ -v":1
13 | zpm "list"
14 |
15 | halt
16 |
17 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "objectscript",
6 | "request": "launch",
7 | "name": "ObjectScript Debug Class",
8 | "program": "##class(dc.sample.ObjectScript).Test()",
9 | },
10 | {
11 | "type": "objectscript",
12 | "request": "attach",
13 | "name": "ObjectScript Attach",
14 | "processId": "${command:PickProcess}",
15 | "system": true
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "eamodio.gitlens",
4 | "georgejames.gjlocate",
5 | "github.copilot",
6 | "intersystems-community.servermanager",
7 | "intersystems-community.sqltools-intersystems-driver",
8 | "intersystems-community.testingmanager",
9 | "intersystems-community.vscode-objectscript",
10 | "intersystems.language-server",
11 | "mohsen1.prettify-json",
12 | "ms-azuretools.vscode-docker",
13 | "ms-python.python",
14 | "ms-python.vscode-pylance",
15 | "ms-vscode-remote.remote-containers"
16 | ]
17 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 |
4 | "Dockerfile*": "dockerfile",
5 | "iris.script": "objectscript"
6 | },
7 | "objectscript.conn" :{
8 | "active": true,
9 | "ns": "USER",
10 | "username": "_SYSTEM",
11 | "password": "SYS",
12 | "docker-compose": {
13 | "service": "iris",
14 | "internalPort": 52773
15 | },
16 | "links": {
17 | "UnitTest Portal": "${serverUrl}/csp/sys/%25UnitTest.Portal.Home.cls?$NAMESPACE=IRISAPP"
18 | }
19 | },
20 | "intersystems.testingManager.client.relativeTestRoot": "tests"
21 |
22 | }
--------------------------------------------------------------------------------
/.github/workflows_runtests.yml:
--------------------------------------------------------------------------------
1 | name: unittest
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - main
8 | pull_request:
9 | branches:
10 | - master
11 | - main
12 | release:
13 | types:
14 | - released
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v2
21 | - name: Build and Test
22 | uses: docker/build-push-action@v2
23 | with:
24 | context: .
25 | push: false
26 | load: true
27 | tags: ${{ github.repository }}:${{ github.sha }}
28 | build-args: TESTS=1
29 |
--------------------------------------------------------------------------------
/.github/workflows_build-push-gcr.yaml:
--------------------------------------------------------------------------------
1 | name: Cloud Run Deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - main
8 | workflow_dispatch:
9 |
10 | jobs:
11 | deploy:
12 | uses: intersystems-community/demo-deployment/.github/workflows/deployment.yml@master
13 | with:
14 | # Replace the name: parameter below to have your application deployed at
15 | # https://project-name.demo.community.intersystems.com/
16 | name: project-name
17 | secrets:
18 | # Do not forget to add Secret in GitHub Repoository Settings with name SERVICE_ACCOUNT_KEY
19 | SERVICE_ACCOUNT_KEY: ${{ secrets.SERVICE_ACCOUNT_KEY }}
20 |
--------------------------------------------------------------------------------
/.github/workflows_bump-module-version.yml:
--------------------------------------------------------------------------------
1 | name: versionbump
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - main
8 | release:
9 | types:
10 | - released
11 | permissions:
12 | contents: write
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Bump version
20 | run: |
21 | git config --global user.name 'ProjectBot'
22 | git config --global user.email 'bot@users.noreply.github.com'
23 | VERSION=$(sed -n '0,/.*\(.*\)<\/Version>.*/s//\1/p' module.xml)
24 | VERSION=`echo $VERSION | awk -F. '/[0-9]+\./{$NF++;print}' OFS=.`
25 | sed -i "0,/\(.*\)<\/Version>/s//$VERSION<\/Version>/" module.xml
26 | git add module.xml
27 | git commit -m 'auto bump version'
28 | git push
29 |
--------------------------------------------------------------------------------
/.github/workflows_github-registry.yml:
--------------------------------------------------------------------------------
1 | name: Build and publish a Docker image to ghcr.io
2 | on:
3 |
4 | # publish on pushes to the main branch (image tagged as "latest")
5 | # image name: will be: ghcr.io/${{ github.repository }}:latest
6 | # e.g.: ghcr.io/intersystems-community/intersystems-iris-dev-template:latest
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | docker_publish:
13 | runs-on: "ubuntu-20.04"
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 |
18 | # https://github.com/marketplace/actions/push-to-ghcr
19 | - name: Build and publish a Docker image for ${{ github.repository }}
20 | uses: macbre/push-to-ghcr@master
21 | with:
22 | image_name: ${{ github.repository }}
23 | github_token: ${{ secrets.GITHUB_TOKEN }}
24 | # optionally push to the Docker Hub (docker.io)
25 | # docker_io_token: ${{ secrets.DOCKER_IO_ACCESS_TOKEN }} # see https://hub.docker.com/settings/security
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 eduard93
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/src/Converter/Utils/XML.cls:
--------------------------------------------------------------------------------
1 | /// Utilities for XSL transformation
2 | Class Converter.Utils.XML [ Abstract ]
3 | {
4 |
5 | /// Prepare XSLT transformation
6 | ClassMethod prepareTransform(ByRef stream As %Stream.Object = "", Output transformedStream As %XML.FileCharacterStream, ByRef params, outputEncoding As %String) [ Private ]
7 | {
8 | set transformedStream = ##class(%XML.FileCharacterStream).%New()
9 | set transformedStream.TranslateTable = outputEncoding
10 |
11 | #dim key As %String = $order(params(""))
12 | while(key '= "")
13 | {
14 | set params(key) = "'" _ $replace(params(key), "'", "`") _ "'"
15 | set key = $order(params(key))
16 | }
17 |
18 | if (stream = "") set stream = ..getDummyXml()
19 | }
20 |
21 | /// XSL-transformation
22 | ClassMethod transform(stream As %Stream.Object = "", xslStream As %Stream.Object, Output transformedStream As %XML.FileCharacterStream, ByRef params, callbackHandler As %XML.XSLT.CallbackHandler = {$$$NULLOREF}, outputEncoding As %String = "UTF8") As %Status
23 | {
24 | do ..prepareTransform(.stream, .transformedStream, .params, outputEncoding)
25 | quit ##class(%XML.XSLT.Transformer).TransformStream(stream, xslStream, transformedStream,,.params, callbackHandler)
26 | }
27 |
28 | /// XSLT from XData block
29 | ClassMethod transformByXDataXsl(stream As %Stream.Object = "", classNameOrObject, xdataName, Output transformedStream As %XML.FileCharacterStream, ByRef params, callbackHandler As %XML.XSLT.CallbackHandler = {$$$NULLOREF}, outputEncoding As %String = "UTF8") As %Status
30 | {
31 | #dim className As %String
32 |
33 | if $isObject(classNameOrObject)
34 | {
35 | set className = classNameOrObject.%ClassName(1)
36 | }
37 | else
38 | {
39 | set className = classNameOrObject
40 | }
41 |
42 | #dim xslStream As %Stream.Object = ..getClassXData(className, xdataName)
43 |
44 | quit ..transform(stream, xslStream, .transformedStream, .params, callbackHandler, outputEncoding)
45 | }
46 |
47 | /// Get class XData as a stream
48 | ClassMethod getClassXData(className, xdataName) As %Stream.Object
49 | {
50 | quit ##class(%Dictionary.CompiledXData).%OpenId(className _ "||" _ xdataName).Data
51 | }
52 |
53 | /// Get minimal xml
54 | ClassMethod getDummyXml() As %Stream.Object
55 | {
56 | quit ..getClassXData(..%ClassName(1), "dummyXml")
57 | }
58 |
59 | /// Minimal xml
60 | XData dummyXml
61 | {
62 |
63 | }
64 |
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Converter
2 | Convert documents from InterSystems Cache easily using:
3 |
4 | - LibreOffice
5 |
6 | [InterSystems Developer Community article](https://community.intersystems.com/post/converting-documents-using-cach%C3%A9-and-libreoffice).
7 |
8 | # Install
9 |
10 | 1. Download and import code
11 | 2. In OS:
12 | - Linux: apt-get install libreoffice-core libreoffice-writer
13 | - Windows: install [libreoffice](https://www.libreoffice.org/download/libreoffice-fresh/)
14 | 3. Add `soffice` to PATH
15 |
16 | # Use
17 |
18 | Call from the terminal:
19 |
20 | ```
21 | set sc = ##class(Converter.LibreOffice).convert(source, target, format)
22 | write $System.Status.GetErrorText(sc)
23 | ```
24 |
25 | Where:
26 | - source - file to convert
27 | - target - result file
28 | - format - specification for target file. Possible values: `docx,html,mediawiki,csv,pptx,ppt,wmf,emf,svg,xlsx,xls`. More possible values [here](wiki.openoffice.org/wiki/Framework/Article/Filter/FilterList_OOo_3_0).
29 |
30 | # Errors
31 |
32 | 1. Libreoffice errors
33 | - Instal latest stable Libreoffice (5.2.5 atm). Minimally supported version is 4.2
34 | - Don't run more than one process of LibreOffice
35 |
36 | # Footer
37 | Add footer to MS office documents from InterSystems Caché.
38 |
39 | # Install
40 |
41 | 1. Download and import code
42 | 2. In OS:
43 | - Windows: [zip](http://gnuwin32.sourceforge.net/packages/zip.htm), [unzip](http://gnuwin32.sourceforge.net/packages/unzip.htm), [libxml2](http://xmlsoft.org/downloads.html), [git](https://git-scm.com/download/win), [TortoiseGit](https://tortoisegit.org/download/)
44 | - Linux: ```apt-get install zip unzip libxml2 libxml2-utils git```
45 | 3. Add binaries to path
46 |
47 | # Use
48 |
49 | Call from the terminal:
50 |
51 | ```cos
52 | do ##class(Converter.Footer).modifyFooter(source, target, text)
53 | ```
54 |
55 | Where:
56 | - source - file to convert
57 | - target - result file
58 | - text - text to add to footer
59 | ## Docker
60 | ### Prerequisites
61 | Make sure you have [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) and [Docker desktop](https://www.docker.com/products/docker-desktop) installed.
62 | ### Installation
63 | Clone/git pull the repo into any local directory
64 | ```
65 | $ git clone https://github.com/rcemper/PR_Converter.git
66 | ```
67 | ```
68 | $ docker compose up -d && docker compose logs -f
69 | ```
70 | Container start
71 | creates appropriate directory "/home/irisowner/dev/Unit Tests"
72 | sets ^UnitTestRoot = "/home/irisowner/dev/"
73 |
74 | To open IRIS Terminal do:
75 | ```
76 | $ docker-compose exec iris iris session iris
77 | USER>
78 | ```
79 | or using **WebTerminal**
80 | http://localhost:42773/terminal/
81 |
82 | To access IRIS System Management Portal
83 | http://localhost:42773/csp/sys/UtilHome.csp
84 |
--------------------------------------------------------------------------------
/src/Converter/LibreOffice.cls:
--------------------------------------------------------------------------------
1 | Class Converter.LibreOffice Extends Common [ Abstract ]
2 | {
3 |
4 | /// Convert source file into target file. format - target format. Supported formats: https://wiki.openoffice.org/wiki/Framework/Article/Filter/FilterList_OOo_3_0
5 | /// w $System.Status.GetErrorText(##class(Converter.LibreOffice).convert("C:\temp\1.doc", "C:\temp\1.docx", "docx"))
6 | ClassMethod convert(source As %String, target As %String, format As %String(VALUELIST=",docx,html,mediawiki,csv,pptx,ppt,wmf,emf,svg,xlsx,xls") = "docx") As %Status
7 | {
8 | #dim sc As %Status = $$$OK
9 |
10 | // Basic checks
11 | return:'##class(%File).Exists(source) $$$ERROR($$$FileDoesNotExist, source)
12 | set target = ##class(%File).NormalizeFilenameWithSpaces(target)
13 | return:##class(%File).Exists(target) $$$ERROR($$$GeneralError, "Target file already exists")
14 |
15 | // Temp dir to store target file
16 | set tempDir = ..tempDir()
17 | set success = ##class(%File).CreateDirectory(tempDir, .out)
18 | return:'success $$$ERROR($$$GeneralError, "Unable to create directory " _ tempDir _ ", code: " _ out)
19 |
20 | // Conversion
21 | set sc = ..executeConvert(source, tempDir, format)
22 | quit:$$$ISERR(sc) sc
23 |
24 | // Move conversion result into target
25 | set sourceName = ##class(%File).GetFilename(source)
26 | set tempTargetName = tempDir _ $p(sourceName, ".", 1, *-1) _ "." _ format
27 | set result = ##class(%File).Rename(tempTargetName, target,.code)
28 | if result=0 {
29 | set sc = $$$ERROR($$$GeneralError, "Error moving '" _ tempTargetName _ '" to '" _ target _ "' with code: " _ code)
30 | }
31 | quit:$$$ISERR(sc) sc
32 |
33 | // Delete temp folder
34 | set result = ##class(%File).RemoveDirectoryTree(tempDir)
35 | if result=0 {
36 | set sc = $$$ERROR($$$GeneralError, "Error removing: " _ tempDir)
37 | }
38 | quit sc
39 | }
40 |
41 | /// Convert source into format and place it into targetDir
42 | ClassMethod executeConvert(source, targetDir, format) As %Status
43 | {
44 | // Libreoffice needs targetDir without last slash
45 | set:$e(targetDir,*)=..#SLASH targetDir = $e(targetDir, 1, *-1)
46 |
47 | set timeout = 100
48 | set cmd = ..getSO() // Get executable
49 | if '$$$isWINDOWS {
50 | // export HOME=/tmp && unset LD_LIBRARY_PATH && soffice
51 | set args($i(args)) = "HOME=/tmp"
52 | set args($i(args)) = "&&"
53 | set args($i(args)) = "unset"
54 | set args($i(args)) = "LD_LIBRARY_PATH"
55 | set args($i(args)) = "&&"
56 | set args($i(args)) = "soffice"
57 | }
58 |
59 | set args($i(args)) = "--headless" // Do not run GUI
60 | set args($i(args)) = "--writer" // Use writer converter
61 | set args($i(args)) = "--convert-to" // Target format
62 | set args($i(args)) = format
63 | set args($i(args)) = "--outdir" // Directory to output converted files to
64 | set args($i(args)) = targetDir
65 | set args($i(args)) = source
66 |
67 | return ..execute(cmd, .args, timeout)
68 | }
69 |
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/dev.md:
--------------------------------------------------------------------------------
1 | # useful commands
2 | ## clean up docker
3 | use it when docker says "There is no space left on device". It will remove built but not used images and other temporary files.
4 | ```
5 | docker system prune -f
6 | ```
7 |
8 | ```
9 | docker rm -f $(docker ps -qa)
10 | ```
11 |
12 | ## build container with no cache
13 | ```
14 | docker-compose build --no-cache --progress=plain
15 | ```
16 | ## start iris container
17 | ```
18 | docker-compose up -d
19 | ```
20 |
21 | ## open iris terminal in docker
22 | ```
23 | docker exec iris iris session iris -U IRISAPP
24 | ```
25 |
26 |
27 | ## import objectscirpt code
28 |
29 | do $System.OBJ.LoadDir("/home/irisowner/dev/src","ck",,1)
30 | ## map iris key from Mac home directory to IRIS in container
31 | - ~/iris.key:/usr/irissys/mgr/iris.key
32 |
33 | ## install git in the docker image
34 | ## add git in dockerfile
35 | USER root
36 | RUN apt update && apt-get -y install git
37 |
38 | USER ${ISC_PACKAGE_MGRUSER}
39 |
40 |
41 | ## install docker-compose
42 | ```
43 | sudo curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
44 |
45 | sudo chmod +x /usr/local/bin/docker-compose
46 |
47 | ```
48 |
49 | ## load and test module
50 | ```
51 |
52 | zpm "load /home/irisowner/dev"
53 |
54 | zpm "test dc-sample"
55 | ```
56 |
57 | ## select zpm test registry
58 | ```
59 | repo -n registry -r -url https://test.pm.community.intersystems.com/registry/ -user test -pass PassWord42
60 | ```
61 |
62 | ## get back to public zpm registry
63 | ```
64 | repo -r -n registry -url https://pm.community.intersystems.com/ -user "" -pass ""
65 | ```
66 |
67 | ## export a global in runtime into the repo
68 | ```
69 | d $System.OBJ.Export("GlobalD.GBL","/irisrun/repo/src/gbl/GlobalD.xml")
70 | ```
71 |
72 | ## create a web app in dockerfile
73 | ```
74 | zn "%SYS" \
75 | write "Create web application ...",! \
76 | set webName = "/csp/irisweb" \
77 | set webProperties("NameSpace") = "IRISAPP" \
78 | set webProperties("Enabled") = 1 \
79 | set webProperties("CSPZENEnabled") = 1 \
80 | set webProperties("AutheEnabled") = 32 \
81 | set webProperties("iKnowEnabled") = 1 \
82 | set webProperties("DeepSeeEnabled") = 1 \
83 | set sc = ##class(Security.Applications).Create(webName, .webProperties) \
84 | write "Web application "_webName_" has been created!",!
85 | ```
86 |
87 |
88 |
89 | ```
90 | do $SYSTEM.OBJ.ImportDir("/opt/irisbuild/src",, "ck")
91 | ```
92 |
93 |
94 | ### run tests described in the module
95 |
96 | IRISAPP>zpm
97 | IRISAPP:zpm>load /irisrun/repo
98 | IRISAPP:zpm>test package-name
99 |
100 | ### install ZPM with one line
101 | // Install ZPM
102 | set $namespace="%SYS", name="DefaultSSL" do:'##class(Security.SSLConfigs).Exists(name) ##class(Security.SSLConfigs).Create(name) set url="https://pm.community.intersystems.com/packages/zpm/latest/installer" Do ##class(%Net.URLParser).Parse(url,.comp) set ht = ##class(%Net.HttpRequest).%New(), ht.Server = comp("host"), ht.Port = 443, ht.Https=1, ht.SSLConfiguration=name, st=ht.Get(comp("path")) quit:'st $System.Status.GetErrorText(st) set xml=##class(%File).TempFilename("xml"), tFile = ##class(%Stream.FileBinary).%New(), tFile.Filename = xml do tFile.CopyFromAndSave(ht.HttpResponse.Data) do ht.%Close(), $system.OBJ.Load(xml,"ck") do ##class(%File).Delete(xml)
103 |
104 |
105 |
106 |
107 | docker run --rm --name iris-sql -d -p 9091:1972 -p 9092:52773 -e IRIS_PASSWORD=demo -e IRIS_USERNAME=demo intersystemsdc/iris-community
108 |
109 |
110 | docker run --rm --name iris-ce -d -p 9091:1972 -p 9092:52773 -e IRIS_PASSWORD=demo -e IRIS_USERNAME=demo intersystemsdc/iris-community -a "echo 'zpm \"install webterminal\"' | iriscli"
111 |
112 |
113 |
114 | docker run --rm --name iris-sql -d -p 9092:52773 containers.intersystems.com/intersystems/iris-community:2023.1.0.229.0
115 |
116 |
117 | docker run --rm --name iris-ce -d -p 9092:52773 containers.intersystems.com/intersystems/iris-community:2023.1.0.229.0
--------------------------------------------------------------------------------
/src/Converter/Common.cls:
--------------------------------------------------------------------------------
1 | Class Converter.Common [ Abstract ]
2 | {
3 |
4 | Parameter SLASH = {$case($system.Version.GetOS(),"Windows":"\",:"/")};
5 |
6 | /// Execute OS command cmd.
7 | /// timeout - how long to wait for command completion.
8 | /// If debug is true then output debug information.
9 | ClassMethod execute(cmd As %String, ByRef args As %String, timeout As %Integer = 60, debug As %Boolean = {$$$NO}) As %Status
10 | {
11 | #dim sc As %Status = $$$OK
12 | set code = ""
13 | set out = ""
14 | write:debug !, "cmd: ", ..buildFullCommand(cmd, .args), !
15 | set sc = ..runCommandViaZF(cmd, .args , .out, timeout, $$$YES, .code)
16 | if debug {
17 | write "status: "
18 | if $$$ISERR(sc) {
19 | write $System.Status.GetErrorText(sc)
20 | } else {
21 | write sc
22 | }
23 | write !,"code: ", code, !, "out: ", out, !
24 | }
25 |
26 | if code'=0 {
27 | set sc1 = $$$ERROR($$$GeneralError, "Command: " _ ..buildFullCommand(cmd, .args) _ $$$NL _ " Error code: " _ code _ $$$NL _ "Output: " _ out)
28 | set sc = $$$ADDSC(sc, sc1)
29 | }
30 | return sc
31 | }
32 |
33 | /// do ##class(Converter.Common).runCommandViaZF()
34 | ClassMethod runCommandViaZF(cmd As %String, ByRef args As %String, Output out As %String, timeout As %Integer = 60, deleteTempFile As %Boolean = 1, Output code As %String) As %Status [ CodeMode = objectgenerator ]
35 | {
36 | set argsCount = $l($$$defMemberKeyGet("%Net.Remote.Utility",$$$cCLASSmethod,"RunCommandViaZF",$$$cMETHformalspec),",")
37 | if argsCount = 6 {
38 | do %code.WriteLine($$$TAB _ "set cmd = ..buildFullCommand(cmd, .args)")
39 | do %code.WriteLine($$$TAB _ "quit ##class(%Net.Remote.Utility).RunCommandViaZF(cmd, , .out, timeout, $$$YES, .code)")
40 | } else {
41 | do %code.WriteLine($$$TAB _ "quit ..runCommandViaZFInternal(cmd, , .out, timeout, $$$YES, .code, .args, $$$NO, 1)")
42 | }
43 | quit $$$OK
44 | }
45 |
46 | /// Run a command using $ZF(-100) and an external temporary file to store the command output.
47 | /// If pDeleteTempFile is 0 (false), the temporary file is not deleted; in this case, it is up to the caller to delete it when done with it.
48 | ClassMethod runCommandViaZFInternal(pCmd As %String, Output pTempFileName As %String, Output pOutput As %String, pOpenTimeout As %Integer = 5, pDeleteTempFile As %Boolean = 1, Output pRetCode As %String, ByRef pCmdArgs, pAsynchronous As %Boolean = 0, pUseShell As %Boolean = 0) As %Status
49 | {
50 | Set tSC = $$$OK
51 | Set pOutput = ""
52 | Set pRetCode = ""
53 | Set IO = $IO
54 | Set ZEOFMode = $ZU(68,40,1)
55 | Set pTempFileName = ""
56 |
57 | Try {
58 | Set (tFile,pTempFileName) = ##class(%File).TempFilename("txt")
59 | If tFile="" Set tSC = $$$ERROR($$$CacheError, "Failed to obtain a temporary file name") Quit
60 | Set cmdFlags = $Select(pUseShell:"/SHELL",1:"") _ $Select(pAsynchronous:"/ASYNC",1:"") _"/STDOUT="""_tFile_"""/STDERR="""_tFile_""""
61 | #if $l($$$defMemberKeyGet("%Net.Remote.Utility",$$$cCLASSmethod,"RunCommandViaZF",$$$cMETHformalspec),",")>6
62 | Set pRetCode = $ZF(-100,cmdFlags,pCmd,.pCmdArgs)
63 | #endif
64 |
65 | Close tFile Open tFile:("RS"):pOpenTimeout
66 | If '$T Set tSC = $$$ERROR($$$CacheError, "Failed to open temporary file '"_tFile_"'") Quit
67 | Set TooMuch = 0
68 | Use tFile
69 | For {
70 | // Keep reading through end of file; save only first 32,000 characters
71 | Set tLine = "" Read tLine:1
72 | If '$T && (tLine=$C(-1)) Quit // Exit by timeout
73 | If 'TooMuch {
74 | Set:pOutput'="" pOutput = pOutput_$C(13,10)
75 | If $L(pOutput)+$l(tLine)<32000 {
76 | Set pOutput = pOutput_tLine
77 | }
78 | Else {
79 | Set pOutput = pOutput_$E(tLine,1,32000-$L(pOutput))_" (more...)"
80 | Set TooMuch = 1
81 | }
82 | }
83 | If ($ZEOF=-1) Quit // Exit by EOF
84 | }
85 | }
86 | Catch (ex) {
87 | Set tSC = ex.AsStatus()
88 | }
89 |
90 | Try {
91 | If pDeleteTempFile {
92 | Close tFile:"D"
93 | }
94 | Else {
95 | Close tFile
96 | }
97 | } Catch (ex) {
98 | // don't overwrite the error status if it's already populated
99 | Set:$$$ISOK(tSC) tSC = ex.AsStatus()
100 | }
101 |
102 | If 'ZEOFMode Do $ZU(68,40,0) // Restore ZEOF mode
103 | Use IO
104 |
105 | Quit tSC
106 | }
107 |
108 | /// w ##class(Converter.Common).buildFullCommand()
109 | ClassMethod buildFullCommand(cmd As %String, ByRef args As %String) As %String
110 | {
111 | quit:$d(args)<10 cmd
112 | set result = $lb(cmd)
113 | set key = ""
114 | for {
115 | set key=$order(args(key),1,arg)
116 | quit:key=""
117 | set result = result _ $lb(arg)
118 | }
119 | quit $lts(result, " ")
120 | }
121 |
122 | /// Get name of temporary not-existstig sub-directory inside dir
123 | /// w ##class(Converter.Common).tempDir()
124 | ClassMethod tempDir(dir = {##class(%SYS.System).TempDirectory()}) As %String
125 | {
126 | set dir = ##class(%File).NormalizeDirectory(dir)
127 | set exists = ##class(%File).DirectoryExists(dir)
128 | throw:exists=$$$NO ##class(%Exception.General).%New("", "Converter.LibreOffice", , "Directory " _ dir _ " does not exist")
129 | do {
130 | set subDir = $random(1000000)
131 | set subDirFull = ##class(%File).SubDirectoryName(dir, subDir, $$$YES)
132 | set exists = ##class(%File).DirectoryExists(subDirFull)
133 | } while exists
134 | return subDirFull
135 | }
136 |
137 | /// Get path to libreoffice/soffice
138 | ClassMethod getSO()
139 | {
140 | if $$$isWINDOWS {
141 | set path = "soffice"
142 | } else {
143 | set path = "export" // "export HOME=/tmp && unset LD_LIBRARY_PATH && soffice"
144 | }
145 | return path
146 | }
147 |
148 | /// Get path to zip
149 | ClassMethod getZip()
150 | {
151 | if $$$isWINDOWS {
152 | set path = "zip"
153 | } else {
154 | set path = "zip"
155 | }
156 | return path
157 | }
158 |
159 | /// Get path to unzip
160 | ClassMethod getUnzip()
161 | {
162 | if $$$isWINDOWS {
163 | set path = "unzip"
164 | } else {
165 | set path = "unzip"
166 | }
167 | return path
168 | }
169 |
170 | }
171 |
172 |
--------------------------------------------------------------------------------
/src/Converter/Footer.cls:
--------------------------------------------------------------------------------
1 | Include %occIO
2 |
3 | Class Converter.Footer Extends Converter.Common
4 | {
5 |
6 | Parameter FOOTERMASK = "footer*.xml";
7 |
8 | /// Add text to docx source footer. Output result into target
9 | /// w $System.Status.GetErrorText(##class(Converter.Footer).modifyFooter("C:\Temp\docx\2.docx", "C:\Temp\docx\21.docx", "TESTEST"))
10 | ClassMethod modifyFooter(source As %String, target As %String = {source}, text As %String) As %Status
11 | {
12 | // Basic checks
13 | return:'##class(%File).Exists(source) $$$ERROR($$$FileDoesNotExist, source)
14 | set target = ##class(%File).NormalizeFilenameWithSpaces(target)
15 | return:##class(%File).Exists(target) $$$ERROR($$$GeneralError, "Target file already exists")
16 |
17 | // Temp dir to store target file
18 | set tempDir = ..tempDir()
19 | set success = ##class(%File).CreateDirectory(tempDir, .out)
20 | return:'success $$$ERROR($$$GeneralError, "Unable to create directory " _ tempDir _ ", code: " _ out)
21 |
22 | // Unpack document into a folder
23 | set sc = ..executeUnzip(source, tempDir)
24 | quit:$$$ISERR(sc) sc
25 |
26 | // Add footer into each doc section
27 | set sc = ..addFooterToSections(tempDir)
28 | quit:$$$ISERR(sc) sc
29 |
30 | // Add empty (invalid)footer and register it
31 | set sc = ..addDefaultFooter(tempDir)
32 | quit:$$$ISERR(sc) sc
33 |
34 | // Replace all footer*.xml files with ours
35 | set sc = ..modifyFooterFiles(tempDir, text)
36 | quit:$$$ISERR(sc) sc
37 |
38 | // Pack document
39 | set sc = ..executeZip(tempDir, target)
40 | quit:$$$ISERR(sc) sc
41 |
42 | // Delete temp dir
43 | set result = ##class(%File).RemoveDirectoryTree(tempDir)
44 | if result=0 {
45 | set sc = $$$ERROR($$$GeneralError, "Error removing: " _ tempDir)
46 | }
47 |
48 | quit sc
49 | }
50 |
51 | /// Unpack source file into targetDir
52 | ClassMethod executeUnzip(source, targetDir) As %Status
53 | {
54 | set timeout = 100
55 |
56 | set cmd = ..getUnzip()
57 | set arg(1) = source
58 | set arg(2) = targetDir
59 |
60 |
61 | return ..execute(cmd, .args, timeout)
62 | }
63 |
64 | /// Replace all footer*.xml files with ours
65 | ClassMethod modifyFooterFiles(targetDir, text) As %Status
66 | {
67 | #dim sc As %Status = $$$OK
68 |
69 | set footerName = ##class(%File).TempFilename("xml")
70 | set sc = ..generateNewFooter(footerName, text)
71 | quit:$$$ISERR(sc) sc
72 |
73 | set targetDir = ##class(%File).SubDirectoryName(targetDir, "word", $$$YES)
74 | #dim rs As %SQL.ClassQueryResultSet = ##class(%File).FileSetFunc(targetDir, ..#FOOTERMASK)
75 | while rs.%Next() {
76 | set result = ##class(%File).CopyFile(footerName, rs.Name, $$$YES, .code)
77 | if result=0 {
78 | set sc = $$$ERROR($$$GeneralError, "Error replacing '" _ rs.Name _ '" with '" _ newFooter _ "'. Code: " _ code)
79 | }
80 | quit:$$$ISERR(sc)
81 | }
82 | return sc
83 | }
84 |
85 | /// Create new valid footer with tiext into fileName
86 | ClassMethod generateNewFooter(fileName, text) As %Status
87 | {
88 | #dim sc As %Status = $$$OK
89 | set file = ##class(%Stream.FileCharacter).%New()
90 | set sc = file.LinkToFile(fileName)
91 | quit:$$$ISERR(sc)
92 |
93 | #dim stream As %Stream.TmpCharacter = ##class(%Stream.TmpCharacter).%New()
94 | #dim transformedStream As %Stream.Object
95 |
96 | do stream.Write("" _ text _ "")
97 | set sc = ##class(Converter.Utils.XML).transformByXDataXsl(stream, $classname(), "footerXSL", .transformedStream)
98 | quit:$$$ISERR(sc)
99 |
100 | set sc = file.CopyFromAndSave(transformedStream)
101 | kill file
102 | quit sc
103 | }
104 |
105 | /// Pack folder into a document
106 | /// do ##class(Converter.Footer).generateDocx("C:\Temp\docx\out", "C:\Temp\docx\21.docx")
107 | ClassMethod executeZip(targetDir, docx) As %Status
108 | {
109 | set oldDir = $system.Process.CurrentDirectory(targetDir)
110 |
111 | set timeout = 100
112 |
113 | set cmd = ..getZip()
114 | set args(1) = "-r"
115 | set args(2) = docx
116 | set args(3) = "./"
117 |
118 | set sc = ..execute(cmd, timeout)
119 | do $system.Process.CurrentDirectory(oldDir)
120 | quit sc
121 | }
122 |
123 | /// Add default footer to a document
124 | ClassMethod addDefaultFooter(targetDir) As %Status
125 | {
126 | set sc = ..addDefaultFooterType(targetDir)
127 | quit:$$$ISERR(sc) sc
128 |
129 | set sc = ..addDefaultFooterFile(targetDir)
130 | quit:$$$ISERR(sc) sc
131 |
132 | set sc = ..addDefaultFooterRelationship(targetDir)
133 | quit sc
134 | }
135 |
136 | /// Add overrride into [Content_Types].xml
137 | ClassMethod addDefaultFooterType(targetDir) As %Status
138 | {
139 | set filename = targetDir _ "[Content_Types].xml"
140 |
141 | set file = ##class(%Stream.FileCharacter).%New()
142 | set sc = file.LinkToFile(filename)
143 | quit:$$$ISERR(sc) sc
144 |
145 | set stream = ##class(%Stream.TmpCharacter).%New()
146 | do stream.Write($ZCVT(file.Read($$$MaxLocalLength),"O","UTF8"))
147 |
148 | set sc = ##class(Converter.Utils.XML).transformByXDataXsl(stream, $classname(), $$$CurrentMethod, .transformedStream)
149 | quit:$$$ISERR(sc) sc
150 |
151 | set sc = file.CopyFromAndSave(transformedStream)
152 | quit sc
153 | }
154 |
155 | /// Add empty invalid footer file
156 | ClassMethod addDefaultFooterFile(targetDir) As %Status
157 | {
158 | set targetDir = ##class(%File).SubDirectoryName(targetDir, "word", $$$YES)
159 | set footerFile = ##class(%Stream.FileCharacter).%New()
160 | do footerFile.LinkToFile(targetDir _ "footer0.xml")
161 | do footerFile.Write($$$OK)
162 | set sc = footerFile.%Save()
163 | quit sc
164 | }
165 |
166 | /// Add rId0 relationship
167 | ClassMethod addDefaultFooterRelationship(targetDir) As %Status
168 | {
169 | set targetDir = ##class(%File).SubDirectoryName(targetDir, "word", $$$YES)
170 | set relsDir = ##class(%File).SubDirectoryName(targetDir, "_rels", $$$YES)
171 |
172 | set filename = relsDir _ "document.xml.rels"
173 |
174 | set file = ##class(%Stream.FileCharacter).%New()
175 | set sc = file.LinkToFile(filename)
176 | quit:$$$ISERR(sc) sc
177 |
178 | set stream = ##class(%Stream.TmpCharacter).%New()
179 | do stream.Write($ZCVT(file.Read($$$MaxLocalLength),"O","UTF8"))
180 |
181 | set sc = ##class(Converter.Utils.XML).transformByXDataXsl(stream, $classname(), $$$CurrentMethod, .transformedStream)
182 | quit:$$$ISERR(sc) sc
183 |
184 | set sc = file.CopyFromAndSave(transformedStream)
185 | quit:$$$ISERR(sc) sc
186 |
187 |
188 | set footerFile = ##class(%Stream.FileCharacter).%New()
189 | do footerFile.LinkToFile(targetDir _ "footer0.xml")
190 | set sc = footerFile.Write($$$OK)
191 | quit:$$$ISERR(sc) sc
192 | set sc = footerFile.%Save()
193 |
194 | quit sc
195 | }
196 |
197 | XData addDefaultFooterType
198 | {
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 | }
213 |
214 | XData addDefaultFooterRelationship
215 | {
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 | }
231 |
232 | /// Add footer to each document section
233 | ClassMethod addFooterToSections(targetDir) As %Status
234 | {
235 | #dim sc As %Status = $$$OK
236 | set targetDir = ##class(%File).SubDirectoryName(targetDir, "word", $$$YES)
237 | set filename = targetDir _ "document.xml"
238 |
239 | set file = ##class(%Stream.FileCharacter).%New()
240 | //set file.TranslateTable = "UTF8"
241 | set sc = file.LinkToFile(filename)
242 | quit:$$$ISERR(sc) sc
243 |
244 | set stream = ##class(%Stream.TmpCharacter).%New()
245 | do stream.Write($ZCVT(file.Read($$$MaxLocalLength),"O","UTF8"))
246 |
247 | set sc = ##class(Converter.Utils.XML).transformByXDataXsl(stream, $classname(), $$$CurrentMethod, .transformedStream)
248 | quit:$$$ISERR(sc) sc
249 |
250 | set sc = file.CopyFromAndSave(transformedStream)
251 | quit sc
252 | }
253 |
254 | XData addFooterToSections
255 | {
256 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 | }
279 |
280 | /// Footer
281 | XData footerXSL
282 | {
283 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 | }
302 |
303 | }
304 |
305 |
--------------------------------------------------------------------------------