├── 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 | --------------------------------------------------------------------------------