diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bc3496..596401f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,4 +9,6 @@ on: jobs: build: - uses: axonivy-market/github-workflows/.github/workflows/ci.yml@v2 + uses: axonivy-market/github-workflows/.github/workflows/ci.yml@v4 + secrets: + mvnArgs: -Dazureblob.account=${{ secrets.AZURE_BLOB_ACCOUNT }} -Dazureblob.key=${{ secrets.AZURE_BLOB_ACCOUNT_KEY }} diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index a27a639..b672da0 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -8,4 +8,6 @@ on: jobs: build: - uses: axonivy-market/github-workflows/.github/workflows/dev.yml@v2 + uses: axonivy-market/github-workflows/.github/workflows/dev.yml@v4 + secrets: + mvnArgs: -Dazureblob.account=${{ secrets.AZURE_BLOB_ACCOUNT }} -Dazureblob.key=${{ secrets.AZURE_BLOB_ACCOUNT_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 128a183..ab5b0d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,4 +4,4 @@ on: workflow_dispatch jobs: build: - uses: axonivy-market/github-workflows/.github/workflows/release.yml@v2 + uses: axonivy-market/github-workflows/.github/workflows/release.yml@v4 diff --git a/.project b/.project new file mode 100644 index 0000000..2eac02d --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + azure-blob-connector-modules + + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + + diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..14b697b --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/MY-PRODUCT-NAME-product/README.md b/MY-PRODUCT-NAME-product/README.md deleted file mode 100644 index d593446..0000000 --- a/MY-PRODUCT-NAME-product/README.md +++ /dev/null @@ -1,32 +0,0 @@ - - -# MY-PRODUCT-NAME - -YOUR DESCRIPTION GOES HERE: Please just give a short description here without further headings. - - - -## Demo - -YOUR DEMO DESCRIPTION GOES HERE - - - -## Setup - -YOUR SETUP DESCRIPTION GOES HERE - - -``` -@variables.yaml@ -``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..64f1252 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ + +app_local_compose_up: + docker-compose -f local/docker-compose.yml up -d + +app_local_compose_down: + docker-compose -f local/docker-compose.yml down + +app_local_compose_stop: + docker-compose -f local/docker-compose.yml stop + +app_local_compose_start: + docker-compose -f local/docker-compose.yml start \ No newline at end of file diff --git a/README.md b/README.md index 0f94020..ff60a5a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# MY-PRODUCT-NAME +# Azure Blob Connector -[![CI Build](https://github.com/axonivy-market/REPO-NAME/actions/workflows/ci.yml/badge.svg)](https://github.com/axonivy-market/REPO-NAME/actions/workflows/ci.yml) +[![CI Build](https://github.com/axonivy-market/azure-blob-connector/actions/workflows/ci.yml/badge.svg)](https://github.com/axonivy-market/azure-blob-connector/actions/workflows/ci.yml) -"YOUR SHORT DESCRIPTION GOES HERE" +- Upload file to Azure blob +- Get temporary download link -Read our [documentation](MY-PRODUCT-NAME-product/README.md). +Read our [documentation](azure-blob-connector-product/README.md). diff --git a/azure-blob-connector-demo/.classpath b/azure-blob-connector-demo/.classpath new file mode 100644 index 0000000..a24d7cc --- /dev/null +++ b/azure-blob-connector-demo/.classpath @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector-demo/.gitignore b/azure-blob-connector-demo/.gitignore new file mode 100644 index 0000000..1b2547b --- /dev/null +++ b/azure-blob-connector-demo/.gitignore @@ -0,0 +1,19 @@ +# general +Thumbs.db +.DS_Store +*~ +*.log + +# java +*.class +hs_err_pid* + +# maven +target/ +lib/mvn-deps/ + +# ivy +classes/ +src_dataClasses/ +src_wsproc/ +logs/ diff --git a/azure-blob-connector-demo/.project b/azure-blob-connector-demo/.project new file mode 100644 index 0000000..5a776d0 --- /dev/null +++ b/azure-blob-connector-demo/.project @@ -0,0 +1,49 @@ + + + azure-blob-connector-demo + + + + + + ch.ivyteam.ivy.designer.dataClasses.ui.ivyDataClassBuilder + + + + + ch.ivyteam.ivy.designer.process.ui.ivyWebServiceProcessClassBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + ch.ivyteam.ivy.designer.ide.ivyModelValidationBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + ch.ivyteam.ivy.project.IvyProjectNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.jem.beaninfo.BeanInfoNature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/azure-blob-connector-demo/.settings/.jsdtscope b/azure-blob-connector-demo/.settings/.jsdtscope new file mode 100644 index 0000000..cf5ec79 --- /dev/null +++ b/azure-blob-connector-demo/.settings/.jsdtscope @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs new file mode 100644 index 0000000..e93fcf9 --- /dev/null +++ b/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs @@ -0,0 +1,5 @@ +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.connector.azure.blob.demo.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.connector.azure.blob.demo +ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=100000 +eclipse.preferences.version=1 diff --git a/azure-blob-connector-demo/.settings/org.eclipse.jdt.core.prefs b/azure-blob-connector-demo/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..f78f7f7 --- /dev/null +++ b/azure-blob-connector-demo/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,10 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/azure-blob-connector-demo/.settings/org.eclipse.wst.common.component b/azure-blob-connector-demo/.settings/org.eclipse.wst.common.component new file mode 100644 index 0000000..ea03e7b --- /dev/null +++ b/azure-blob-connector-demo/.settings/org.eclipse.wst.common.component @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/azure-blob-connector-demo/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml b/azure-blob-connector-demo/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml new file mode 100644 index 0000000..0d46547 --- /dev/null +++ b/azure-blob-connector-demo/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/azure-blob-connector-demo/.settings/org.eclipse.wst.common.project.facet.core.xml b/azure-blob-connector-demo/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000..c2098f9 --- /dev/null +++ b/azure-blob-connector-demo/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/azure-blob-connector-demo/.settings/org.eclipse.wst.css.core.prefs b/azure-blob-connector-demo/.settings/org.eclipse.wst.css.core.prefs new file mode 100644 index 0000000..96b96cd --- /dev/null +++ b/azure-blob-connector-demo/.settings/org.eclipse.wst.css.core.prefs @@ -0,0 +1,2 @@ +css-profile/=org.eclipse.wst.css.core.cssprofile.css3 +eclipse.preferences.version=1 diff --git a/azure-blob-connector-demo/.settings/org.eclipse.wst.jsdt.ui.superType.container b/azure-blob-connector-demo/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 0000000..3bd5d0a --- /dev/null +++ b/azure-blob-connector-demo/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.baseBrowserLibrary \ No newline at end of file diff --git a/azure-blob-connector-demo/.settings/org.eclipse.wst.jsdt.ui.superType.name b/azure-blob-connector-demo/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 0000000..05bd71b --- /dev/null +++ b/azure-blob-connector-demo/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Window \ No newline at end of file diff --git a/azure-blob-connector-demo/config/custom-fields.yaml b/azure-blob-connector-demo/config/custom-fields.yaml new file mode 100644 index 0000000..bb20b70 --- /dev/null +++ b/azure-blob-connector-demo/config/custom-fields.yaml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/custom-fields.json +# +# == Custom Fields Information == +# +# You can define here your project custom fields. +# Have a look at our documentation for more information. +# +CustomFields: +# Tasks: +# MyTaskCustomField: +# Label: My task custom field +# Description: This new task custom field can be used to ... +# Type: STRING +# Cases: +# MyCaseCustomField: +# Label: My case custom field +# Description: This new case custom field can be used to ... +# Type: STRING +# Starts: +# MyStartCustomField: +# Label: My start custom field +# Description: This new start custom field can be used to ... diff --git a/azure-blob-connector-demo/config/databases.yaml b/azure-blob-connector-demo/config/databases.yaml new file mode 100644 index 0000000..10319e2 --- /dev/null +++ b/azure-blob-connector-demo/config/databases.yaml @@ -0,0 +1,2 @@ +# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/databases.json +Databases: diff --git a/azure-blob-connector-demo/config/overrides.any b/azure-blob-connector-demo/config/overrides.any new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/azure-blob-connector-demo/config/overrides.any @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/azure-blob-connector-demo/config/persistence.xml b/azure-blob-connector-demo/config/persistence.xml new file mode 100644 index 0000000..d6b96d7 --- /dev/null +++ b/azure-blob-connector-demo/config/persistence.xml @@ -0,0 +1,2 @@ + + diff --git a/azure-blob-connector-demo/config/rest-clients.yaml b/azure-blob-connector-demo/config/rest-clients.yaml new file mode 100644 index 0000000..4bffaca --- /dev/null +++ b/azure-blob-connector-demo/config/rest-clients.yaml @@ -0,0 +1,2 @@ +# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/rest-clients.json +RestClients: diff --git a/azure-blob-connector-demo/config/roles.xml b/azure-blob-connector-demo/config/roles.xml new file mode 100644 index 0000000..59892fe --- /dev/null +++ b/azure-blob-connector-demo/config/roles.xml @@ -0,0 +1,4 @@ + + + Everybody + diff --git a/azure-blob-connector-demo/config/users.xml b/azure-blob-connector-demo/config/users.xml new file mode 100644 index 0000000..51a6906 --- /dev/null +++ b/azure-blob-connector-demo/config/users.xml @@ -0,0 +1,2 @@ + + diff --git a/azure-blob-connector-demo/config/variables.yaml b/azure-blob-connector-demo/config/variables.yaml new file mode 100644 index 0000000..245f493 --- /dev/null +++ b/azure-blob-connector-demo/config/variables.yaml @@ -0,0 +1,16 @@ +# == Variables == +# +# You can define here your project Variables. +# +Variables: + AzureBlob: + # The application ID that's assigned to your app. + ClientId: '' + # The client secret that you generated for your app in the app registration portal. + ClientSecret: '' + # The directory tenant the application plans to operate against, in GUID or domain-name format. + TenantId: '' + # https://.blob.core.windows.net/ + EndPoint: '' + # Your container name. + ContainterName: '' \ No newline at end of file diff --git a/azure-blob-connector-demo/config/webservice-clients.yaml b/azure-blob-connector-demo/config/webservice-clients.yaml new file mode 100644 index 0000000..688047a --- /dev/null +++ b/azure-blob-connector-demo/config/webservice-clients.yaml @@ -0,0 +1,2 @@ +# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/webservice-clients.json +WebServiceClients: diff --git a/azure-blob-connector-demo/dataclasses/com/axonivy/connector/azure/blob/demo/Data.ivyClass b/azure-blob-connector-demo/dataclasses/com/axonivy/connector/azure/blob/demo/Data.ivyClass new file mode 100644 index 0000000..82cb1e8 --- /dev/null +++ b/azure-blob-connector-demo/dataclasses/com/axonivy/connector/azure/blob/demo/Data.ivyClass @@ -0,0 +1,2 @@ +Data #class +com.axonivy.connector.azure.blob.demo #namespace diff --git a/azure-blob-connector-demo/dataclasses/com/axonivy/connector/azure/blob/demo/UploadData.ivyClass b/azure-blob-connector-demo/dataclasses/com/axonivy/connector/azure/blob/demo/UploadData.ivyClass new file mode 100644 index 0000000..39a8304 --- /dev/null +++ b/azure-blob-connector-demo/dataclasses/com/axonivy/connector/azure/blob/demo/UploadData.ivyClass @@ -0,0 +1,2 @@ +UploadData #class +com.axonivy.connector.azure.blob.demo #namespace diff --git a/azure-blob-connector-demo/pom.xml b/azure-blob-connector-demo/pom.xml new file mode 100644 index 0000000..a632b99 --- /dev/null +++ b/azure-blob-connector-demo/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + com.axonivy.connector.azure.blob + azure-blob-connector-demo + 10.0.22-SNAPSHOT + iar + + 10.0.16 + + + + com.axonivy.connector.azure.blob + azure-blob-connector + ${project.version} + iar + + + + + + com.axonivy.ivy.ci + project-build-plugin + ${project.build.plugin.version} + true + + + + diff --git a/azure-blob-connector-demo/processes/Start Processes/Upload.p.json b/azure-blob-connector-demo/processes/Start Processes/Upload.p.json new file mode 100644 index 0000000..1c02aa5 --- /dev/null +++ b/azure-blob-connector-demo/processes/Start Processes/Upload.p.json @@ -0,0 +1,71 @@ +{ + "format" : "10.0.0", + "id" : "19010D5E49BD2F7F", + "config" : { + "data" : "com.axonivy.connector.azure.blob.demo.UploadData" + }, + "elements" : [ { + "id" : "f0", + "type" : "RequestStart", + "name" : "upload.ivp", + "config" : { + "callSignature" : "upload", + "outLink" : "upload.ivp", + "case" : { } + }, + "visual" : { + "at" : { "x" : 96, "y" : 64 } + }, + "connect" : { "id" : "f4", "to" : "f3" } + }, { + "id" : "f1", + "type" : "TaskEnd", + "visual" : { + "at" : { "x" : 352, "y" : 64 } + } + }, { + "id" : "f3", + "type" : "DialogCall", + "name" : "Upload", + "config" : { + "dialogId" : "com.axonivy.connector.azure.blob.demo.Upload", + "startMethod" : "start()" + }, + "visual" : { + "at" : { "x" : 224, "y" : 64 } + }, + "connect" : { "id" : "f2", "to" : "f1" } + }, { + "id" : "f5", + "type" : "RequestStart", + "name" : "uploadByCallSubporcess.ivp", + "config" : { + "callSignature" : "uploadByCallSubporcess", + "outLink" : "uploadByCallSubporcess.ivp", + "startDescription" : "Upload by calling Sub-process.", + "case" : { } + }, + "visual" : { + "at" : { "x" : 104, "y" : 192 } + }, + "connect" : { "id" : "f8", "to" : "f6" } + }, { + "id" : "f6", + "type" : "DialogCall", + "name" : "Upload", + "config" : { + "dialogId" : "com.axonivy.connector.azure.blob.demo.UploadByCallSubprocess", + "startMethod" : "start()" + }, + "visual" : { + "at" : { "x" : 240, "y" : 192 } + }, + "connect" : { "id" : "f9", "to" : "f7" } + }, { + "id" : "f7", + "type" : "TaskEnd", + "visual" : { + "at" : { "x" : 352, "y" : 192 } + } + } ] +} \ No newline at end of file diff --git a/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/Blob.java b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/Blob.java new file mode 100644 index 0000000..3c4f2e0 --- /dev/null +++ b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/Blob.java @@ -0,0 +1,21 @@ +package com.axonivy.connector.azure.blob.demo.bean; + +import com.azure.storage.blob.models.BlobItem; + +public class Blob { + private BlobItem blobItem; + private String linkDownLoad; + + public BlobItem getBlobItem() { + return blobItem; + } + public void setBlobItem(BlobItem BlobItem) { + this.blobItem = BlobItem; + } + public String getLinkDownLoad() { + return linkDownLoad; + } + public void setLinkDownLoad(String linkDownLoad) { + this.linkDownLoad = linkDownLoad; + } +} diff --git a/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadBean.java b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadBean.java new file mode 100644 index 0000000..a3cb4f8 --- /dev/null +++ b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadBean.java @@ -0,0 +1,318 @@ +package com.axonivy.connector.azure.blob.demo.bean; + +import static org.apache.commons.lang3.StringUtils.isNotEmpty; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.primefaces.model.file.UploadedFile; + +import com.axonivy.connector.azure.blob.BlobServiceClientHelper; +import com.axonivy.connector.azure.blob.StorageService; +import com.axonivy.connector.azure.blob.demo.utils.UploadUtils; +import com.axonivy.connector.azure.blob.internal.AzureBlobStorageService; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.models.BlobItem; + +import ch.ivyteam.ivy.environment.Ivy; + +public class UploadBean { + private String url; + private String localPath; + private UploadedFile uploadedFile; + private String fileName; + private String fileNamePath; + private String fileNameURL; + private byte[] content; + private InputStream inputStreamContent; + private List blobs; + private String uploadToFolderByPrimefaces; + private String uploadToFolderByLocalPath; + private String uploadToFolderByURL; + private Date startDate; + private String blobName; + private Boolean isFileAlreadyExist; + private Boolean isOverwriteFile; + + private Boolean isFileAlreadyExistURL; + private Boolean isOverwriteFileURL; + + private Boolean isFileAlreadyExistPath; + private Boolean isOverwriteFilePath; + + private StorageService storageService = null; + private static BlobServiceClient blobServiceClient = null; + + + public void init() { + String clientId = Ivy.var().get("AzureBlob.ClientId"); + String clientSecret = Ivy.var().get("AzureBlob.ClientSecret"); + String tenantId = Ivy.var().get("AzureBlob.TenantId"); + String endPoint = Ivy.var().get("AzureBlob.EndPoint"); + String containerName = Ivy.var().get("AzureBlob.ContainterName"); + + blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(clientId, clientSecret, tenantId, endPoint); + storageService = new AzureBlobStorageService(blobServiceClient, containerName); + this.blobs = getAllBlobs(); + isFileAlreadyExist = false; + isFileAlreadyExistURL = false; + isFileAlreadyExistPath = false; + } + + public String getUrl() { + return url; + } + public void setUrl(String url) { + this.url = url; + } + public String getLocalPath() { + return localPath; + } + public void setLocalPath(String localPath) { + this.localPath = localPath; + } + public UploadedFile getUploadedFile() { + return uploadedFile; + } + public void setUploadedFile(UploadedFile uploadedFile) { + this.uploadedFile = uploadedFile; + } + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public List getBlobs() { + return blobs; + } + + public void setBlobs(List blobs) { + this.blobs = blobs; + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } + + public InputStream getInputStreamContent() { + return inputStreamContent; + } + + public void setInputStreamContent(InputStream inputStreamContent) { + this.inputStreamContent = inputStreamContent; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getFileNamePath() { + return fileNamePath; + } + + public void setFileNamePath(String fileNamePath) { + this.fileNamePath = fileNamePath; + } + + public String getFileNameURL() { + return fileNameURL; + } + + public void setFileNameURL(String fileNameURL) { + this.fileNameURL = fileNameURL; + } + + public String getBlobName() { + return blobName; + } + + public void setBlobName(String blobName) { + this.blobName = blobName; + } + + public Boolean getIsFileAlreadyExist() { + return isFileAlreadyExist; + } + + public void setIsFileAlreadyExist(Boolean isFileAlreadyExist) { + this.isFileAlreadyExist = isFileAlreadyExist; + } + + public Boolean getIsOverwriteFile() { + return isOverwriteFile; + } + + public void setIsOverwriteFile(Boolean isOverwriteFile) { + this.isOverwriteFile = isOverwriteFile; + } + + public Boolean getIsFileAlreadyExistURL() { + return isFileAlreadyExistURL; + } + + public void setIsFileAlreadyExistURL(Boolean isFileAlreadyExistURL) { + this.isFileAlreadyExistURL = isFileAlreadyExistURL; + } + + public Boolean getIsOverwriteFileURL() { + return isOverwriteFileURL; + } + + public void setIsOverwriteFileURL(Boolean isOverwriteFileURL) { + this.isOverwriteFileURL = isOverwriteFileURL; + } + + public Boolean getIsFileAlreadyExistPath() { + return isFileAlreadyExistPath; + } + + public void setIsFileAlreadyExistPath(Boolean isFileAlreadyExistPath) { + this.isFileAlreadyExistPath = isFileAlreadyExistPath; + } + + public Boolean getIsOverwriteFilePath() { + return isOverwriteFilePath; + } + + public void setIsOverwriteFilePath(Boolean isOverwriteFilePath) { + this.isOverwriteFilePath = isOverwriteFilePath; + } + + public StorageService getStorageService() { + return storageService; + } + + public void setStorageService(StorageService storageService) { + this.storageService = storageService; + } + + public String getUploadToFolderByPrimefaces() { + return uploadToFolderByPrimefaces; + } + + public void setUploadToFolderByPrimefaces(String uploadToFolderByPrimefaces) { + this.uploadToFolderByPrimefaces = uploadToFolderByPrimefaces; + } + + public String getUploadToFolderByLocalPath() { + return uploadToFolderByLocalPath; + } + + public void setUploadToFolderByLocalPath(String uploadToFolderByLocalPath) { + this.uploadToFolderByLocalPath = uploadToFolderByLocalPath; + } + + public String getUploadToFolderByURL() { + return uploadToFolderByURL; + } + + public void setUploadToFolderByURL(String uploadToFolderByURL) { + this.uploadToFolderByURL = uploadToFolderByURL; + } + + public void upload() throws Exception { + boolean isExistFile = checkFileAlreadyExist(fileName); + if (!isExistFile || isOverwriteFile) { + try { + String name = storageService.upload(content, fileName, uploadToFolderByPrimefaces, isOverwriteFile); + if (StringUtils.isNotEmpty(name)) { + fetchAllBlobsAndAddMessage("Uploaded blobs successfully"); + } + } catch (Exception e) { + throw new Exception("upload file error " + e.getMessage(), e); + } + } else { + isFileAlreadyExist = true; + } + } + + public void uploadFromURL() { + fileNameURL = UploadUtils.getFileNameFromUrl(url); + boolean isExistFile = checkFileAlreadyExist(fileNameURL); + if (!isExistFile || isOverwriteFileURL) { + String name = storageService.uploadFromUrl(url, uploadToFolderByURL, isOverwriteFileURL); + if (StringUtils.isNotEmpty(name)) { + fetchAllBlobsAndAddMessage("Uploaded blobs successfully"); + } + } else { + isFileAlreadyExistURL = true; + } + } + + public void uploadFromPath() { + fileNamePath = FilenameUtils.getName(localPath); + boolean isExistFile = checkFileAlreadyExist(fileNamePath); + if (!isExistFile || isOverwriteFilePath) { + String name = storageService.uploadFromFile(localPath, uploadToFolderByLocalPath, isOverwriteFilePath); + if (isNotEmpty(name)) { + fetchAllBlobsAndAddMessage("Uploaded blobs successfully"); + } + } else { + isFileAlreadyExistPath = true; + } + } + + public void deleteBlobs(Date date) { + storageService.delete(date); + fetchAllBlobsAndAddMessage("Delete blobs successfully"); + } + + public void deleteBlob(String blobName) { + if (storageService.delete(blobName)) { + fetchAllBlobsAndAddMessage("Delete blobs successfully"); + } + } + + public void undeleteBlob(String blobName) { + storageService.restore(blobName); + fetchAllBlobsAndAddMessage("Undelete blobs successfully"); + } + + public void showCopiedMessage() { + doAddInfoMessageForInstance("Download link is copied"); + } + + private void fetchAllBlobsAndAddMessage(String message) { + this.blobs = getAllBlobs(); + doAddInfoMessageForInstance(message); + } + + private List getAllBlobs() { + List blobItems = storageService.getBlobs(); + List blobs = new ArrayList<>(); + for (BlobItem item : blobItems) { + Blob b = new Blob(); + b.setBlobItem(item); + b.setLinkDownLoad(storageService.getDownloadLink(item.getName())); + blobs.add(b); + } + return blobs; + } + + private Boolean checkFileAlreadyExist(String name) { + return storageService.getBlobs().stream().anyMatch(b -> b.getName().equals(name)); + } + + private void doAddInfoMessageForInstance(String message) { + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, message, null)); + } +} + diff --git a/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadByCallSubprocess.java b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadByCallSubprocess.java new file mode 100644 index 0000000..54910fb --- /dev/null +++ b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadByCallSubprocess.java @@ -0,0 +1,233 @@ +package com.axonivy.connector.azure.blob.demo.bean; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; + +import org.apache.commons.lang3.StringUtils; +import org.primefaces.model.file.UploadedFile; + +import com.azure.storage.blob.models.BlobItem; + +public class UploadByCallSubprocess { + private String url; + private String localPath; + private UploadedFile uploadedFile; + private String fileName; + private String fileNamePath; + private String fileNameURL; + private byte[] content; + private InputStream inputStreamContent; + private List blobs; + private List blobItems; + private String uploadToFolderByPrimefaces; + private String uploadToFolderByLocalPath; + private String uploadToFolderByURL; + private Date startDate; + private String blobName; + private Boolean isFileAlreadyExist; + private Boolean isOverwriteFile; + + private Boolean isFileAlreadyExistURL; + private Boolean isOverwriteFileURL; + + private Boolean isFileAlreadyExistPath; + private Boolean isOverwriteFilePath; + + public void init() { + isFileAlreadyExist = false; + isFileAlreadyExistURL = false; + isFileAlreadyExistPath = false; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getLocalPath() { + return localPath; + } + + public void setLocalPath(String localPath) { + this.localPath = localPath; + } + + public UploadedFile getUploadedFile() { + return uploadedFile; + } + + public void setUploadedFile(UploadedFile uploadedFile) { + this.uploadedFile = uploadedFile; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public List getBlobs() { + return blobs; + } + + public void setBlobs(List blobs) { + this.blobs = blobs; + } + + public List getBlobItems() { + return blobItems; + } + + public void setBlobItems(List blobItems) { + this.blobItems = blobItems; + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } + + public InputStream getInputStreamContent() { + return inputStreamContent; + } + + public void setInputStreamContent(InputStream inputStreamContent) { + this.inputStreamContent = inputStreamContent; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getFileNamePath() { + return fileNamePath; + } + + public void setFileNamePath(String fileNamePath) { + this.fileNamePath = fileNamePath; + } + + public String getFileNameURL() { + return fileNameURL; + } + + public void setFileNameURL(String fileNameURL) { + this.fileNameURL = fileNameURL; + } + + public String getBlobName() { + return blobName; + } + + public void setBlobName(String blobName) { + this.blobName = blobName; + } + + public Boolean getIsFileAlreadyExist() { + return isFileAlreadyExist; + } + + public void setIsFileAlreadyExist(Boolean isFileAlreadyExist) { + this.isFileAlreadyExist = isFileAlreadyExist; + } + + public Boolean getIsOverwriteFile() { + return isOverwriteFile; + } + + public void setIsOverwriteFile(Boolean isOverwriteFile) { + this.isOverwriteFile = isOverwriteFile; + } + + public Boolean getIsFileAlreadyExistURL() { + return isFileAlreadyExistURL; + } + + public void setIsFileAlreadyExistURL(Boolean isFileAlreadyExistURL) { + this.isFileAlreadyExistURL = isFileAlreadyExistURL; + } + + public Boolean getIsOverwriteFileURL() { + return isOverwriteFileURL; + } + + public void setIsOverwriteFileURL(Boolean isOverwriteFileURL) { + this.isOverwriteFileURL = isOverwriteFileURL; + } + + public Boolean getIsFileAlreadyExistPath() { + return isFileAlreadyExistPath; + } + + public void setIsFileAlreadyExistPath(Boolean isFileAlreadyExistPath) { + this.isFileAlreadyExistPath = isFileAlreadyExistPath; + } + + public Boolean getIsOverwriteFilePath() { + return isOverwriteFilePath; + } + + public void setIsOverwriteFilePath(Boolean isOverwriteFilePath) { + this.isOverwriteFilePath = isOverwriteFilePath; + } + + public String getUploadToFolderByPrimefaces() { + return uploadToFolderByPrimefaces; + } + + public void setUploadToFolderByPrimefaces(String uploadToFolderByPrimefaces) { + this.uploadToFolderByPrimefaces = uploadToFolderByPrimefaces; + } + + public String getUploadToFolderByLocalPath() { + return uploadToFolderByLocalPath; + } + + public void setUploadToFolderByLocalPath(String uploadToFolderByLocalPath) { + this.uploadToFolderByLocalPath = uploadToFolderByLocalPath; + } + + public String getUploadToFolderByURL() { + return uploadToFolderByURL; + } + + public void setUploadToFolderByURL(String uploadToFolderByURL) { + this.uploadToFolderByURL = uploadToFolderByURL; + } + + public void getBlobs(List blobItems) { + blobs = new ArrayList<>(); + for (BlobItem item : blobItems) { + Blob b = new Blob(); + b.setBlobItem(item); + blobs.add(b); + } + } + + public String createBlobPath(String folderName, String fileName) { + String path = StringUtils.isNotBlank(folderName) ? folderName + "/" : StringUtils.EMPTY; + return path + fileName; + } + + public void showCopiedMessage() { + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Download link is copied", null)); + } + +} diff --git a/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/utils/UploadUtils.java b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/utils/UploadUtils.java new file mode 100644 index 0000000..30f9404 --- /dev/null +++ b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/utils/UploadUtils.java @@ -0,0 +1,21 @@ +package com.axonivy.connector.azure.blob.demo.utils; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Paths; + +import org.apache.commons.lang3.StringUtils; + +import ch.ivyteam.ivy.environment.Ivy; + +public class UploadUtils { + + public static String getFileNameFromUrl(String url) { + try { + return Paths.get(new URI(url).getPath()).getFileName().toString(); + } catch (URISyntaxException e) { + Ivy.log().warn("Can not get file name from " + url, e); + } + return StringUtils.EMPTY; + } +} diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.rddescriptor b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.rddescriptor new file mode 100644 index 0000000..ae605f0 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.rddescriptor @@ -0,0 +1,7 @@ + + + + viewTechnology + JSF + + diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.xhtml b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.xhtml new file mode 100644 index 0000000..17c6e5a --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.xhtml @@ -0,0 +1,158 @@ + + + + + UploadDialog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + +
+ +
+ + +
+
+
+
+ + + + diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadData.ivyClass b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadData.ivyClass new file mode 100644 index 0000000..b1ea3d2 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadData.ivyClass @@ -0,0 +1,6 @@ +UploadData #class +com.axonivy.connector.azure.blob.demo.Upload #namespace +bean com.axonivy.connector.azure.blob.demo.bean.UploadBean #field +bean PERSISTENT #fieldModifier +link String #field +link PERSISTENT #fieldModifier diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadProcess.p.json b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadProcess.p.json new file mode 100644 index 0000000..30aadea --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadProcess.p.json @@ -0,0 +1,447 @@ +{ + "format" : "10.0.0", + "id" : "19010B658CC93485", + "kind" : "HTML_DIALOG", + "config" : { + "data" : "com.axonivy.connector.azure.blob.demo.Upload.UploadData" + }, + "elements" : [ { + "id" : "f0", + "type" : "HtmlDialogStart", + "name" : "start()", + "config" : { + "callSignature" : "start", + "guid" : "19010B658D1EDEBA" + }, + "visual" : { + "at" : { "x" : 96, "y" : 64 } + }, + "connect" : { "id" : "f7", "to" : "f6" } + }, { + "id" : "f1", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 376, "y" : 64 } + } + }, { + "id" : "f3", + "type" : "HtmlDialogEventStart", + "name" : "close", + "config" : { + "guid" : "19010B658F2419A3" + }, + "visual" : { + "at" : { "x" : 96, "y" : 160 } + }, + "connect" : { "id" : "f5", "to" : "f4" } + }, { + "id" : "f4", + "type" : "HtmlDialogExit", + "visual" : { + "at" : { "x" : 224, "y" : 160 } + } + }, { + "id" : "f6", + "type" : "Script", + "name" : "init", + "config" : { + "output" : { + "code" : "in.bean.init();" + } + }, + "visual" : { + "at" : { "x" : 256, "y" : 64 } + }, + "connect" : { "id" : "f2", "to" : "f1" } + }, { + "id" : "f8", + "type" : "HtmlDialogMethodStart", + "name" : "uploadFileHandle(FileUploadEvent)", + "config" : { + "callSignature" : "uploadFileHandle", + "input" : { + "params" : [ + { "name" : "event", "type" : "org.primefaces.event.FileUploadEvent" } + ], + "map" : { + "out.bean.uploadedFile" : "param.event.getFile()" + } + }, + "guid" : "19010C11B0E3872B" + }, + "visual" : { + "at" : { "x" : 96, "y" : 264 } + }, + "connect" : { "id" : "f11", "to" : "f10" } + }, { + "id" : "f9", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 384, "y" : 264 } + } + }, { + "id" : "f10", + "type" : "Script", + "name" : "update content", + "config" : { + "output" : { + "code" : [ + "import org.apache.commons.io.IOUtils;", + "", + "if (!in.bean.uploadedFile.getFileName().isEmpty()) {", + " in.bean.fileName = in.bean.uploadedFile.getFileName();", + " in.bean.content = IOUtils.toByteArray(in.bean.uploadedFile.getInputStream());", + "} " + ] + } + }, + "visual" : { + "at" : { "x" : 264, "y" : 264 } + }, + "connect" : { "id" : "f12", "to" : "f9" } + }, { + "id" : "f13", + "type" : "HtmlDialogEventStart", + "name" : "addBlob", + "config" : { + "guid" : "19010C824702EA03" + }, + "visual" : { + "at" : { "x" : 96, "y" : 384 } + }, + "connect" : { "id" : "f63", "to" : "f14" } + }, { + "id" : "f14", + "type" : "Script", + "name" : "save blob", + "config" : { + "output" : { + "code" : "in.bean.upload();" + } + }, + "visual" : { + "at" : { "x" : 272, "y" : 384 } + }, + "connect" : { "id" : "f17", "to" : "f15" } + }, { + "id" : "f15", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 392, "y" : 384 } + } + }, { + "id" : "f18", + "type" : "HtmlDialogMethodStart", + "name" : "getLink(String)", + "config" : { + "callSignature" : "getLink", + "input" : { + "params" : [ + { "name" : "link", "type" : "String" } + ], + "map" : { + "out.link" : "param.link" + } + }, + "guid" : "19010CAC30297BE7" + }, + "visual" : { + "at" : { "x" : 96, "y" : 480 } + }, + "connect" : { "id" : "f20", "to" : "f19" } + }, { + "id" : "f19", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 344, "y" : 480 } + } + }, { + "id" : "f21", + "type" : "HtmlDialogEventStart", + "name" : "addBlobByLocalPath", + "config" : { + "guid" : "19010CCFF013F9CE" + }, + "visual" : { + "at" : { "x" : 96, "y" : 568 } + }, + "connect" : { "id" : "f24", "to" : "f22" } + }, { + "id" : "f22", + "type" : "Script", + "name" : "save blob", + "config" : { + "output" : { + "code" : "in.bean.uploadFromPath();" + } + }, + "visual" : { + "at" : { "x" : 216, "y" : 568 } + }, + "connect" : { "id" : "f25", "to" : "f23" } + }, { + "id" : "f23", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 344, "y" : 568 } + } + }, { + "id" : "f26", + "type" : "HtmlDialogEventStart", + "name" : "addBlobByURL", + "config" : { + "guid" : "19010D1A61BCD315" + }, + "visual" : { + "at" : { "x" : 96, "y" : 672 } + }, + "connect" : { "id" : "f29", "to" : "f27" } + }, { + "id" : "f27", + "type" : "Script", + "name" : "save blob", + "config" : { + "output" : { + "code" : "in.bean.uploadFromURL();" + } + }, + "visual" : { + "at" : { "x" : 216, "y" : 672 } + }, + "connect" : { "id" : "f30", "to" : "f28" } + }, { + "id" : "f28", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 336, "y" : 672 } + } + }, { + "id" : "f31", + "type" : "HtmlDialogEventStart", + "name" : "deleteBlobs", + "config" : { + "guid" : "1903507E6AA2A1EC" + }, + "visual" : { + "at" : { "x" : 88, "y" : 776 } + }, + "connect" : { "id" : "f34", "to" : "f32" } + }, { + "id" : "f32", + "type" : "Script", + "name" : "delete blob list", + "config" : { + "output" : { + "code" : "in.bean.deleteBlobs(in.bean.startDate);" + } + }, + "visual" : { + "at" : { "x" : 216, "y" : 776 } + }, + "connect" : { "id" : "f35", "to" : "f33" } + }, { + "id" : "f33", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 336, "y" : 776 } + } + }, { + "id" : "f36", + "type" : "HtmlDialogMethodStart", + "name" : "deleteBlob(String)", + "config" : { + "callSignature" : "deleteBlob", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "out.bean.blobName" : "param.blobName" + } + }, + "guid" : "19039F5750C9EF46" + }, + "visual" : { + "at" : { "x" : 88, "y" : 872 } + }, + "connect" : { "id" : "f39", "to" : "f37" } + }, { + "id" : "f37", + "type" : "Script", + "name" : "delete blob", + "config" : { + "output" : { + "code" : "in.bean.deleteBlob(in.bean.blobName);" + } + }, + "visual" : { + "at" : { "x" : 216, "y" : 872 } + }, + "connect" : { "id" : "f40", "to" : "f38" } + }, { + "id" : "f38", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 336, "y" : 872 } + } + }, { + "id" : "f41", + "type" : "HtmlDialogMethodStart", + "name" : "unDeleteBlob(String)", + "config" : { + "callSignature" : "unDeleteBlob", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "out.bean.blobName" : "param.blobName" + } + }, + "guid" : "1903A08B533830F4" + }, + "visual" : { + "at" : { "x" : 88, "y" : 976 } + }, + "connect" : { "id" : "f44", "to" : "f42" } + }, { + "id" : "f42", + "type" : "Script", + "name" : "undelete", + "config" : { + "output" : { + "code" : "in.bean.undeleteBlob(in.bean.blobName);" + } + }, + "visual" : { + "at" : { "x" : 216, "y" : 976 } + }, + "connect" : { "id" : "f45", "to" : "f43" } + }, { + "id" : "f43", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 336, "y" : 976 } + } + }, { + "id" : "f46", + "type" : "HtmlDialogEventStart", + "name" : "overwriteFile", + "config" : { + "guid" : "19058AF03B67EC96" + }, + "visual" : { + "at" : { "x" : 88, "y" : 1072 } + }, + "connect" : { "id" : "f49", "to" : "f47" } + }, { + "id" : "f47", + "type" : "Script", + "name" : "update status", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExist = false;" + } + }, + "visual" : { + "at" : { "x" : 216, "y" : 1072 } + }, + "connect" : { "id" : "f50", "to" : "f48" } + }, { + "id" : "f48", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 328, "y" : 1072 } + } + }, { + "id" : "f51", + "type" : "HtmlDialogEventStart", + "name" : "overwriteFilePath", + "config" : { + "guid" : "1905D2E31F62E71C" + }, + "visual" : { + "at" : { "x" : 88, "y" : 1168 } + }, + "connect" : { "id" : "f54", "to" : "f52" } + }, { + "id" : "f52", + "type" : "Script", + "name" : "update status", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExistPath = false;" + } + }, + "visual" : { + "at" : { "x" : 216, "y" : 1168 } + }, + "connect" : { "id" : "f55", "to" : "f53" } + }, { + "id" : "f53", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 336, "y" : 1168 } + } + }, { + "id" : "f56", + "type" : "HtmlDialogEventStart", + "name" : "overwriteFileURL", + "config" : { + "guid" : "1905D2F260AC5762" + }, + "visual" : { + "at" : { "x" : 88, "y" : 1288 } + }, + "connect" : { "id" : "f59", "to" : "f57" } + }, { + "id" : "f57", + "type" : "Script", + "name" : "update status", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExistURL = false;" + } + }, + "visual" : { + "at" : { "x" : 216, "y" : 1288 } + }, + "connect" : { "id" : "f60", "to" : "f58" } + }, { + "id" : "f58", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 336, "y" : 1288 } + } + }, { + "id" : "f16", + "type" : "HtmlDialogEventStart", + "name" : "showCopiedMessage", + "config" : { + "guid" : "1914EBCF1F569E49" + }, + "visual" : { + "at" : { "x" : 80, "y" : 1400 } + }, + "connect" : { "id" : "f62", "to" : "f61" } + }, { + "id" : "f61", + "type" : "Script", + "name" : "show messsage copied", + "config" : { + "output" : { + "code" : "in.bean.showCopiedMessage();" + } + }, + "visual" : { + "at" : { "x" : 272, "y" : 1400 } + }, + "connect" : { "id" : "f65", "to" : "f64" } + }, { + "id" : "f64", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 464, "y" : 1400 } + } + } ] +} \ No newline at end of file diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor new file mode 100644 index 0000000..ae605f0 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor @@ -0,0 +1,7 @@ + + + + viewTechnology + JSF + + diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml new file mode 100644 index 0000000..568b87f --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml @@ -0,0 +1,156 @@ + + + + + UploadDialog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + +
+
+ + +
+
+
+
+ + + + diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass new file mode 100644 index 0000000..5ea1c1a --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass @@ -0,0 +1,12 @@ +UploadByCallSubprocessData #class +com.axonivy.connector.azure.blob.demo.UploadByCallSubprocess #namespace +bean com.axonivy.connector.azure.blob.demo.bean.UploadByCallSubprocess #field +bean PERSISTENT #fieldModifier +link String #field +link PERSISTENT #fieldModifier +isSuccess Boolean #field +isSuccess PERSISTENT #fieldModifier +isExist Boolean #field +isExist PERSISTENT #fieldModifier +combineFileNameAndFolder String #field +combineFileNameAndFolder PERSISTENT #fieldModifier diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json new file mode 100644 index 0000000..c74c221 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json @@ -0,0 +1,1065 @@ +{ + "format" : "10.0.0", + "id" : "190719BB1F7DBAD8", + "kind" : "HTML_DIALOG", + "config" : { + "data" : "com.axonivy.connector.azure.blob.demo.UploadByCallSubprocess.UploadByCallSubprocessData" + }, + "elements" : [ { + "id" : "f0", + "type" : "HtmlDialogStart", + "name" : "start()", + "config" : { + "callSignature" : "start", + "guid" : "19010B658D1EDEBA" + }, + "visual" : { + "at" : { "x" : 96, "y" : 64 } + }, + "connect" : { "id" : "f7", "to" : "f6" } + }, { + "id" : "f1", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 696, "y" : 64 } + } + }, { + "id" : "f3", + "type" : "HtmlDialogEventStart", + "name" : "close", + "config" : { + "guid" : "19010B658F2419A3" + }, + "visual" : { + "at" : { "x" : 96, "y" : 160 } + }, + "connect" : { "id" : "f5", "to" : "f4" } + }, { + "id" : "f4", + "type" : "HtmlDialogExit", + "visual" : { + "at" : { "x" : 224, "y" : 160 } + } + }, { + "id" : "f6", + "type" : "Script", + "name" : "init", + "config" : { + "output" : { + "code" : "in.bean.init();" + } + }, + "visual" : { + "at" : { "x" : 256, "y" : 64 } + }, + "connect" : { "id" : "f76", "to" : "f73" } + }, { + "id" : "f8", + "type" : "HtmlDialogMethodStart", + "name" : "uploadFileHandle(FileUploadEvent)", + "config" : { + "callSignature" : "uploadFileHandle", + "input" : { + "params" : [ + { "name" : "event", "type" : "org.primefaces.event.FileUploadEvent" } + ], + "map" : { + "out.bean.uploadedFile" : "param.event.getFile()" + } + }, + "guid" : "19010C11B0E3872B" + }, + "visual" : { + "at" : { "x" : 96, "y" : 240 } + }, + "connect" : { "id" : "f11", "to" : "f10" } + }, { + "id" : "f9", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 384, "y" : 240 } + } + }, { + "id" : "f10", + "type" : "Script", + "name" : "update content", + "config" : { + "output" : { + "code" : [ + "import org.apache.commons.io.IOUtils;", + "", + "if (!in.bean.uploadedFile.getFileName().isEmpty()) {", + " in.bean.fileName = in.bean.uploadedFile.getFileName();", + " in.bean.inputStreamContent = in.bean.uploadedFile.getInputStream();", + "} " + ] + } + }, + "visual" : { + "at" : { "x" : 264, "y" : 240 } + }, + "connect" : { "id" : "f12", "to" : "f9" } + }, { + "id" : "f13", + "type" : "HtmlDialogEventStart", + "name" : "addBlob", + "config" : { + "guid" : "19010C824702EA03" + }, + "visual" : { + "at" : { "x" : 96, "y" : 352 } + }, + "connect" : { "id" : "f127", "to" : "f126" } + }, { + "id" : "f15", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 1240, "y" : 352 } + } + }, { + "id" : "f18", + "type" : "HtmlDialogMethodStart", + "name" : "getLink(String)", + "config" : { + "callSignature" : "getLink", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "out.bean.blobName" : "param.blobName" + } + }, + "guid" : "19010CAC30297BE7" + }, + "visual" : { + "at" : { "x" : 96, "y" : 504 } + }, + "connect" : { "id" : "f125", "to" : "f124" } + }, { + "id" : "f19", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 352, "y" : 504 } + } + }, { + "id" : "f21", + "type" : "HtmlDialogEventStart", + "name" : "addBlobByLocalPath", + "config" : { + "guid" : "19010CCFF013F9CE" + }, + "visual" : { + "at" : { "x" : 104, "y" : 624 } + }, + "connect" : { "id" : "f103", "to" : "f25" } + }, { + "id" : "f23", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 1200, "y" : 624 } + } + }, { + "id" : "f26", + "type" : "HtmlDialogEventStart", + "name" : "addBlobByURL", + "config" : { + "guid" : "19010D1A61BCD315" + }, + "visual" : { + "at" : { "x" : 104, "y" : 840 } + }, + "connect" : { "id" : "f116", "to" : "f29" } + }, { + "id" : "f28", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 1208, "y" : 840 } + } + }, { + "id" : "f31", + "type" : "HtmlDialogEventStart", + "name" : "deleteBlobs", + "config" : { + "guid" : "1903507E6AA2A1EC" + }, + "visual" : { + "at" : { "x" : 96, "y" : 1072 } + }, + "connect" : { "id" : "f78", "to" : "f77" } + }, { + "id" : "f33", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 808, "y" : 1072 } + } + }, { + "id" : "f36", + "type" : "HtmlDialogMethodStart", + "name" : "deleteBlob(String)", + "config" : { + "callSignature" : "deleteBlob", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "out.bean.blobName" : "param.blobName" + } + }, + "guid" : "19039F5750C9EF46" + }, + "visual" : { + "at" : { "x" : 96, "y" : 1184 } + }, + "connect" : { "id" : "f35", "to" : "f32" } + }, { + "id" : "f41", + "type" : "HtmlDialogMethodStart", + "name" : "unDeleteBlob(String)", + "config" : { + "callSignature" : "unDeleteBlob", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "out.bean.blobName" : "param.blobName" + } + }, + "guid" : "1903A08B533830F4" + }, + "visual" : { + "at" : { "x" : 96, "y" : 1280 } + }, + "connect" : { "id" : "f84", "to" : "f39" } + }, { + "id" : "f43", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 808, "y" : 1280 } + } + }, { + "id" : "f46", + "type" : "HtmlDialogEventStart", + "name" : "overwriteFile", + "config" : { + "guid" : "19058AF03B67EC96" + }, + "visual" : { + "at" : { "x" : 96, "y" : 1384 } + }, + "connect" : { "id" : "f49", "to" : "f47" } + }, { + "id" : "f47", + "type" : "Script", + "name" : "update status", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExist = false;" + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 1384 } + }, + "connect" : { "id" : "f50", "to" : "f48" } + }, { + "id" : "f48", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 336, "y" : 1384 } + } + }, { + "id" : "f51", + "type" : "HtmlDialogEventStart", + "name" : "overwriteFilePath", + "config" : { + "guid" : "1905D2E31F62E71C" + }, + "visual" : { + "at" : { "x" : 96, "y" : 1464 } + }, + "connect" : { "id" : "f54", "to" : "f52" } + }, { + "id" : "f52", + "type" : "Script", + "name" : "update status", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExistPath = false;" + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 1464 } + }, + "connect" : { "id" : "f55", "to" : "f53" } + }, { + "id" : "f53", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 344, "y" : 1464 } + } + }, { + "id" : "f56", + "type" : "HtmlDialogEventStart", + "name" : "overwriteFileURL", + "config" : { + "guid" : "1905D2F260AC5762" + }, + "visual" : { + "at" : { "x" : 96, "y" : 1552 } + }, + "connect" : { "id" : "f59", "to" : "f57" } + }, { + "id" : "f57", + "type" : "Script", + "name" : "update status", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExistURL = false;" + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 1552 } + }, + "connect" : { "id" : "f60", "to" : "f58" } + }, { + "id" : "f58", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 336, "y" : 1552 } + } + }, { + "id" : "f61", + "type" : "SubProcessCall", + "name" : "save blob", + "config" : { + "processCall" : "BlobStorage:uploadFromFile(java.io.InputStream,String,String,Boolean)", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobName" : "result.blobName" + } + }, + "call" : { + "params" : [ + { "name" : "content", "type" : "java.io.InputStream" }, + { "name" : "blobName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" }, + { "name" : "isOverwriteFile", "type" : "Boolean" } + ], + "map" : { + "param.content" : "in.bean.inputStreamContent", + "param.blobName" : "in.bean.fileName", + "param.uploadToFolder" : "in.bean.uploadToFolderByPrimefaces", + "param.isOverwriteFile" : "in.bean.isOverwriteFile" + } + } + }, + "visual" : { + "at" : { "x" : 688, "y" : 352 } + }, + "connect" : { "id" : "f67", "to" : "f66" } + }, { + "id" : "f14", + "type" : "Alternative", + "name" : "is exist file ?", + "visual" : { + "at" : { "x" : 504, "y" : 352 }, + "labelOffset" : { "x" : 16, "y" : -24 } + }, + "connect" : [ + { "id" : "f62", "to" : "f61", "label" : { + "name" : "no" + }, "condition" : "in.bean.isOverwriteFile || !in.isExist" }, + { "id" : "f64", "to" : "f63", "label" : { + "name" : "yes" + } } + ] + }, { + "id" : "f63", + "type" : "Script", + "name" : "update value isFileAlreadyExist", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExist = true;" + } + }, + "visual" : { + "at" : { "x" : 504, "y" : 432 } + }, + "connect" : { "id" : "f65", "to" : "f15", "via" : [ { "x" : 1240, "y" : 432 } ] } + }, { + "id" : "f66", + "type" : "Alternative", + "name" : [ + "is blob name ", + "not null ?" + ], + "visual" : { + "at" : { "x" : 816, "y" : 352 }, + "labelOffset" : { "x" : 16, "y" : 40 } + }, + "connect" : [ + { "id" : "f69", "to" : "f68", "label" : { + "name" : "yes" + }, "condition" : "org.apache.commons.lang3.StringUtils.isNotBlank(in.bean.blobName)" }, + { "id" : "f70", "to" : "f15", "via" : [ { "x" : 816, "y" : 280 }, { "x" : 1240, "y" : 280 } ], "label" : { + "name" : "no", + "segment" : 1.47 + } } + ] + }, { + "id" : "f68", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs()", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + } + }, + "visual" : { + "at" : { "x" : 944, "y" : 352 } + }, + "connect" : { "id" : "f72", "to" : "f71" } + }, { + "id" : "f71", + "type" : "Script", + "name" : "convert list an show message", + "config" : { + "output" : { + "code" : [ + "import javax.faces.application.FacesMessage;", + "import javax.faces.context.FacesContext;", + "", + "in.bean.getBlobs(in.bean.blobItems);", + "FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, \"Uploaded blobs successfully\", null));" + ] + } + }, + "visual" : { + "at" : { "x" : 1104, "y" : 352 } + }, + "connect" : { "id" : "f16", "to" : "f15" } + }, { + "id" : "f73", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs()", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + } + }, + "visual" : { + "at" : { "x" : 408, "y" : 64 } + }, + "connect" : { "id" : "f75", "to" : "f74" } + }, { + "id" : "f74", + "type" : "Script", + "name" : "convert list an show message", + "config" : { + "output" : { + "code" : "in.bean.getBlobs(in.bean.blobItems);" + } + }, + "visual" : { + "at" : { "x" : 568, "y" : 64 } + }, + "connect" : { "id" : "f2", "to" : "f1" } + }, { + "id" : "f77", + "type" : "SubProcessCall", + "name" : "delete by date", + "config" : { + "processCall" : "BlobStorage:delete(java.util.Date)", + "output" : { + "map" : { + "out" : "in", + "out.isSuccess" : "result.isSucess" + } + }, + "call" : { + "params" : [ + { "name" : "date", "type" : "java.util.Date" } + ], + "map" : { + "param.date" : "in.bean.startDate" + } + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 1072 } + }, + "connect" : { "id" : "f88", "to" : "f87" } + }, { + "id" : "f79", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs()", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + } + }, + "visual" : { + "at" : { "x" : 480, "y" : 1072 } + }, + "connect" : { "id" : "f81", "to" : "f80" } + }, { + "id" : "f80", + "type" : "Script", + "name" : "convert list an show message", + "config" : { + "output" : { + "code" : [ + "import javax.faces.application.FacesMessage;", + "import javax.faces.context.FacesContext;", + "", + "in.bean.getBlobs(in.bean.blobItems);", + "FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, \"Delete blobs successfully\", null));" + ] + } + }, + "visual" : { + "at" : { "x" : 656, "y" : 1072 } + }, + "connect" : { "id" : "f34", "to" : "f33" } + }, { + "id" : "f32", + "type" : "SubProcessCall", + "name" : "delete by name", + "config" : { + "processCall" : "BlobStorage:detete(String)", + "output" : { + "map" : { + "out" : "in", + "out.isSuccess" : "result.isSuccess" + } + }, + "call" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "param.blobName" : "in.bean.blobName" + } + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 1184 } + }, + "connect" : { "id" : "f89", "to" : "f87", "via" : [ { "x" : 352, "y" : 1184 } ] } + }, { + "id" : "f39", + "type" : "SubProcessCall", + "name" : "undelete", + "config" : { + "processCall" : "BlobStorage:restore(String)", + "output" : { + "map" : { + "out" : "in", + "out.isSuccess" : "result.isSuccess" + } + }, + "call" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "param.blobName" : "in.bean.blobName" + } + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 1280 } + }, + "connect" : { "id" : "f45", "to" : "f42" } + }, { + "id" : "f44", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs()", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + } + }, + "visual" : { + "at" : { "x" : 480, "y" : 1280 } + }, + "connect" : { "id" : "f38", "to" : "f37" } + }, { + "id" : "f37", + "type" : "Script", + "name" : "convert list an show message", + "config" : { + "output" : { + "code" : [ + "import javax.faces.application.FacesMessage;", + "import javax.faces.context.FacesContext;", + "", + "in.bean.getBlobs(in.bean.blobItems);", + "FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, \"Undelete blob successfully\", null));" + ] + } + }, + "visual" : { + "at" : { "x" : 656, "y" : 1280 } + }, + "connect" : { "id" : "f40", "to" : "f43" } + }, { + "id" : "f42", + "type" : "Alternative", + "name" : "is success", + "visual" : { + "at" : { "x" : 352, "y" : 1280 }, + "labelOffset" : { "x" : 8, "y" : -16 } + }, + "connect" : [ + { "id" : "f85", "to" : "f44", "label" : { + "name" : "yes" + }, "condition" : "in.isSuccess" }, + { "id" : "f86", "to" : "f43", "via" : [ { "x" : 352, "y" : 1328 }, { "x" : 808, "y" : 1328 } ], "label" : { + "name" : "no", + "segment" : 1.49 + } } + ] + }, { + "id" : "f87", + "type" : "Alternative", + "name" : "is success", + "visual" : { + "at" : { "x" : 352, "y" : 1072 }, + "labelOffset" : { "x" : -16, "y" : -8 } + }, + "connect" : [ + { "id" : "f82", "to" : "f79", "label" : { + "name" : "yes" + }, "condition" : "in.isSuccess" }, + { "id" : "f83", "to" : "f33", "via" : [ { "x" : 352, "y" : 1016 }, { "x" : 808, "y" : 1016 } ], "label" : { + "name" : "no", + "segment" : 1.49 + } } + ] + }, { + "id" : "f90", + "type" : "Alternative", + "name" : "is exist file ?", + "visual" : { + "at" : { "x" : 520, "y" : 624 }, + "labelOffset" : { "x" : 16, "y" : -24 } + }, + "connect" : [ + { "id" : "f99", "to" : "f92", "condition" : "in.bean.isOverwriteFilePath || !in.isExist" }, + { "id" : "f96", "to" : "f91" } + ] + }, { + "id" : "f91", + "type" : "Script", + "name" : "update value isFileAlreadyExistPath", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExistPath = true;" + } + }, + "visual" : { + "at" : { "x" : 520, "y" : 712 } + }, + "connect" : { "id" : "f22", "to" : "f23", "via" : [ { "x" : 1200, "y" : 712 } ] } + }, { + "id" : "f92", + "type" : "SubProcessCall", + "name" : "save blob", + "config" : { + "processCall" : "BlobStorage:uploadFromFile(String,String,String,Boolean)", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobName" : "result.blobName" + } + }, + "call" : { + "params" : [ + { "name" : "localPath", "type" : "String" }, + { "name" : "blobName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" }, + { "name" : "isOverwriteFile", "type" : "Boolean" } + ], + "map" : { + "param.localPath" : "in.bean.localPath", + "param.blobName" : "in.bean.fileNamePath", + "param.uploadToFolder" : "in.bean.uploadToFolderByLocalPath", + "param.isOverwriteFile" : "in.bean.isOverwriteFilePath" + } + } + }, + "visual" : { + "at" : { "x" : 640, "y" : 624 } + }, + "connect" : { "id" : "f100", "to" : "f93" } + }, { + "id" : "f93", + "type" : "Alternative", + "name" : [ + "is blob name ", + "not null ?" + ], + "visual" : { + "at" : { "x" : 776, "y" : 624 }, + "labelOffset" : { "x" : 16, "y" : 40 } + }, + "connect" : [ + { "id" : "f97", "to" : "f94", "label" : { + "name" : "yes" + }, "condition" : "org.apache.commons.lang3.StringUtils.isNotBlank(in.bean.blobName)" }, + { "id" : "f102", "to" : "f23", "via" : [ { "x" : 776, "y" : 552 }, { "x" : 1200, "y" : 552 } ], "label" : { + "name" : "no", + "segment" : 1.48 + } } + ] + }, { + "id" : "f94", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs()", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + } + }, + "visual" : { + "at" : { "x" : 912, "y" : 624 } + }, + "connect" : { "id" : "f98", "to" : "f95" } + }, { + "id" : "f95", + "type" : "Script", + "name" : "convert list an show message", + "config" : { + "output" : { + "code" : [ + "import javax.faces.application.FacesMessage;", + "import javax.faces.context.FacesContext;", + "", + "in.bean.getBlobs(in.bean.blobItems);", + "FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, \"Uploaded blobs successfully\", null));" + ] + } + }, + "visual" : { + "at" : { "x" : 1072, "y" : 624 } + }, + "connect" : { "id" : "f24", "to" : "f23" } + }, { + "id" : "f25", + "type" : "Script", + "name" : "get file name", + "config" : { + "output" : { + "code" : [ + "import org.apache.commons.io.FilenameUtils;", + "", + "in.bean.fileNamePath = FilenameUtils.getName(in.bean.localPath);", + "in.combineFileNameAndFolder = in.bean.createBlobPath(in.bean.uploadToFolderByLocalPath, in.bean.fileNamePath);" + ] + } + }, + "visual" : { + "at" : { "x" : 232, "y" : 624 } + }, + "connect" : { "id" : "f121", "to" : "f120" } + }, { + "id" : "f29", + "type" : "Script", + "name" : "get file name", + "config" : { + "output" : { + "code" : [ + "import com.axonivy.connector.azure.blob.demo.utils.UploadUtils;", + "in.bean.fileNameURL = UploadUtils.getFileNameFromUrl(in.bean.url);", + "in.combineFileNameAndFolder = in.bean.createBlobPath(in.bean.uploadToFolderByURL, in.bean.fileNameURL);" + ] + } + }, + "visual" : { + "at" : { "x" : 232, "y" : 840 } + }, + "connect" : { "id" : "f123", "to" : "f122" } + }, { + "id" : "f104", + "type" : "Alternative", + "name" : "is exist file ?", + "visual" : { + "at" : { "x" : 520, "y" : 840 }, + "labelOffset" : { "x" : 16, "y" : -24 } + }, + "connect" : [ + { "id" : "f111", "to" : "f106", "condition" : "in.bean.isOverwriteFileURL || !in.isExist" }, + { "id" : "f112", "to" : "f105" } + ] + }, { + "id" : "f105", + "type" : "Script", + "name" : [ + "update value ", + "isFileAlreadyExistURL" + ], + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExistURL = true;" + } + }, + "visual" : { + "at" : { "x" : 520, "y" : 928 } + }, + "connect" : { "id" : "f118", "to" : "f28", "via" : [ { "x" : 1208, "y" : 928 } ] } + }, { + "id" : "f106", + "type" : "SubProcessCall", + "name" : "save blob", + "config" : { + "processCall" : "BlobStorage:uploadFromUrl(String,String,String,Boolean)", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobName" : "result.blobName" + } + }, + "call" : { + "params" : [ + { "name" : "url", "type" : "String" }, + { "name" : "blobName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" }, + { "name" : "isOverwriteFile", "type" : "Boolean" } + ], + "map" : { + "param.url" : "in.bean.url", + "param.blobName" : "in.bean.fileNameURL", + "param.uploadToFolder" : "in.bean.uploadToFolderByURL", + "param.isOverwriteFile" : "in.bean.isOverwriteFileURL" + } + } + }, + "visual" : { + "at" : { "x" : 640, "y" : 840 } + }, + "connect" : { "id" : "f115", "to" : "f107" } + }, { + "id" : "f107", + "type" : "Alternative", + "name" : [ + "is blob name ", + "not null ?" + ], + "visual" : { + "at" : { "x" : 776, "y" : 840 }, + "labelOffset" : { "x" : 16, "y" : 40 } + }, + "connect" : [ + { "id" : "f114", "to" : "f108", "label" : { + "name" : "yes" + }, "condition" : "org.apache.commons.lang3.StringUtils.isNotBlank(in.bean.blobName)" }, + { "id" : "f27", "to" : "f28", "via" : [ { "x" : 776, "y" : 776 }, { "x" : 1208, "y" : 776 } ], "label" : { + "name" : "no", + "segment" : 1.49 + } } + ] + }, { + "id" : "f108", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs()", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + } + }, + "visual" : { + "at" : { "x" : 912, "y" : 840 } + }, + "connect" : { "id" : "f113", "to" : "f109" } + }, { + "id" : "f109", + "type" : "Script", + "name" : "convert list an show message", + "config" : { + "output" : { + "code" : [ + "import javax.faces.application.FacesMessage;", + "import javax.faces.context.FacesContext;", + "", + "in.bean.getBlobs(in.bean.blobItems);", + "FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, \"Uploaded blobs successfully\", null));" + ] + } + }, + "visual" : { + "at" : { "x" : 1072, "y" : 840 } + }, + "connect" : { "id" : "f117", "to" : "f28" } + }, { + "id" : "f30", + "type" : "SubProcessCall", + "name" : "check blob is exist", + "config" : { + "processCall" : "BlobStorage:checkBlobIsExist(String)", + "output" : { + "map" : { + "out" : "in", + "out.isExist" : "result.isExist" + } + }, + "call" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "param.blobName" : "in.combineFileNameAndFolder" + } + } + }, + "visual" : { + "at" : { "x" : 384, "y" : 352 } + }, + "connect" : { "id" : "f17", "to" : "f14" } + }, { + "id" : "f120", + "type" : "SubProcessCall", + "name" : "check blob is exist", + "config" : { + "processCall" : "BlobStorage:checkBlobIsExist(String)", + "output" : { + "map" : { + "out" : "in", + "out.isExist" : "result.isExist" + } + }, + "call" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "param.blobName" : "in.combineFileNameAndFolder" + } + } + }, + "visual" : { + "at" : { "x" : 392, "y" : 624 } + }, + "connect" : { "id" : "f101", "to" : "f90" } + }, { + "id" : "f122", + "type" : "SubProcessCall", + "name" : "check blob is exist", + "config" : { + "processCall" : "BlobStorage:checkBlobIsExist(String)", + "output" : { + "map" : { + "out" : "in", + "out.isExist" : "result.isExist" + } + }, + "call" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "param.blobName" : "in.combineFileNameAndFolder" + } + } + }, + "visual" : { + "at" : { "x" : 392, "y" : 840 } + }, + "connect" : { "id" : "f110", "to" : "f104" } + }, { + "id" : "f124", + "type" : "SubProcessCall", + "name" : "get link download", + "config" : { + "processCall" : "BlobStorage:getLinkDownload(String)", + "output" : { + "map" : { + "out" : "in", + "out.link" : "result.linkDownload" + } + }, + "call" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "param.blobName" : "in.bean.blobName" + } + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 504 } + }, + "connect" : { "id" : "f20", "to" : "f19" } + }, { + "id" : "f126", + "type" : "Script", + "name" : "combine name and folder", + "config" : { + "output" : { + "code" : "in.combineFileNameAndFolder = in.bean.createBlobPath(in.bean.uploadToFolderByPrimefaces, in.bean.fileName);" + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 352 } + }, + "connect" : { "id" : "f119", "to" : "f30" } + }, { + "id" : "f128", + "type" : "HtmlDialogEventStart", + "name" : "showCopiedMessage", + "config" : { + "guid" : "1914ECBCBC61A296" + }, + "visual" : { + "at" : { "x" : 80, "y" : 1672 } + }, + "connect" : { "id" : "f130", "to" : "f129" } + }, { + "id" : "f129", + "type" : "Script", + "name" : "show messsage copied", + "config" : { + "output" : { + "code" : "in.bean.showCopiedMessage();" + } + }, + "visual" : { + "at" : { "x" : 272, "y" : 1672 } + }, + "connect" : { "id" : "f132", "to" : "f131" } + }, { + "id" : "f131", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 464, "y" : 1672 } + } + } ] +} \ No newline at end of file diff --git a/azure-blob-connector-demo/webContent/layouts/basic-10.xhtml b/azure-blob-connector-demo/webContent/layouts/basic-10.xhtml new file mode 100644 index 0000000..2981ece --- /dev/null +++ b/azure-blob-connector-demo/webContent/layouts/basic-10.xhtml @@ -0,0 +1,67 @@ + + + + + + + + + + <ui:insert name="title">Ivy Html Dialog</ui:insert> + + + + + + + + + + + + + +
+ + default content + + + +
+
+ +
+
+
+ + + + +
+ \ No newline at end of file diff --git a/azure-blob-connector-demo/webContent/layouts/frame-10-full-width.xhtml b/azure-blob-connector-demo/webContent/layouts/frame-10-full-width.xhtml new file mode 100644 index 0000000..ee652b9 --- /dev/null +++ b/azure-blob-connector-demo/webContent/layouts/frame-10-full-width.xhtml @@ -0,0 +1,59 @@ + + + + + + + + + + <ui:insert name="title">Ivy Html Dialog</ui:insert> + + + + + + + + + + +
+ + default content + +
+ + + + + + + +
+ \ No newline at end of file diff --git a/azure-blob-connector-demo/webContent/layouts/includes/exception-details.xhtml b/azure-blob-connector-demo/webContent/layouts/includes/exception-details.xhtml new file mode 100644 index 0000000..a4979dc --- /dev/null +++ b/azure-blob-connector-demo/webContent/layouts/includes/exception-details.xhtml @@ -0,0 +1,109 @@ + + + + + + +

+ +

+ + +

Error id

+

#{errorPage.exceptionId}

+

Error Timestamp

+

#{errorPage.createdAt}

+
+ + + + +

Attributes

+
+ + + + + + + + + + + + + + + +
NameValue
+
+
+

Thrown by

+

Process: + +
Element: + +

+
+ + +

Process call stack

+ +
#{caller.callerElement}
+
+
+ +

Technical cause

+
#{causedBy.class.simpleName}: #{causedBy.message.trim()}
+
+
+ +

Request Uri

+

#{errorPage.getRequestUri()}

+
+

Servlet

+

#{errorPage.getServletName()}

+
+ +

Application

+

#{errorPage.applicationName}

+
+ + +

Thread local values

+
+ + + + + + + + + + + + + + + +
KeyValue
+
+
+
+ +

Stack-Trace

+
#{errorPage.getStackTrace()}
+
+ diff --git a/azure-blob-connector-demo/webContent/layouts/includes/exception.xhtml b/azure-blob-connector-demo/webContent/layouts/includes/exception.xhtml new file mode 100644 index 0000000..2303e7c --- /dev/null +++ b/azure-blob-connector-demo/webContent/layouts/includes/exception.xhtml @@ -0,0 +1,47 @@ + + + + + + + + + +
+
+ + +
+ + + + + + + + + +
+ + \ No newline at end of file diff --git a/azure-blob-connector-demo/webContent/layouts/includes/footer.xhtml b/azure-blob-connector-demo/webContent/layouts/includes/footer.xhtml new file mode 100644 index 0000000..3eb052b --- /dev/null +++ b/azure-blob-connector-demo/webContent/layouts/includes/footer.xhtml @@ -0,0 +1,18 @@ + + + +
+ + #{ivyAdvisor.applicationName} + + +
+
+ + \ No newline at end of file diff --git a/azure-blob-connector-demo/webContent/layouts/includes/progress-loader.xhtml b/azure-blob-connector-demo/webContent/layouts/includes/progress-loader.xhtml new file mode 100644 index 0000000..0d68a75 --- /dev/null +++ b/azure-blob-connector-demo/webContent/layouts/includes/progress-loader.xhtml @@ -0,0 +1,15 @@ + + + + +
+
+
Loading...
+
+
+ + + +
+
\ No newline at end of file diff --git a/azure-blob-connector-demo/webContent/layouts/styles/upload-view.css b/azure-blob-connector-demo/webContent/layouts/styles/upload-view.css new file mode 100644 index 0000000..a36de27 --- /dev/null +++ b/azure-blob-connector-demo/webContent/layouts/styles/upload-view.css @@ -0,0 +1,36 @@ +.column-name { + width : 40%; +} + +.column-status { + width : 20%; +} + +.column-created-date { + width : 20%; +} + +.column-action { + width : 20%; +} + +.link-input { + width : 75%; +} + +.copy-action { + width : 20%; + float : right; +} +.float-right { + float : right; +} + +.upload-fieldset { + margin-top: 10px; +} + +body .ui-fileupload .ui-fileupload-buttonbar , body .ui-fileupload .ui-fileupload-content{ + border : none !important; + padding-top: 0px; +} \ No newline at end of file diff --git a/azure-blob-connector-product/.classpath b/azure-blob-connector-product/.classpath new file mode 100644 index 0000000..e28d7fa --- /dev/null +++ b/azure-blob-connector-product/.classpath @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector-product/.gitignore b/azure-blob-connector-product/.gitignore new file mode 100644 index 0000000..ae3c172 --- /dev/null +++ b/azure-blob-connector-product/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/azure-blob-connector-product/.project b/azure-blob-connector-product/.project new file mode 100644 index 0000000..d327cb0 --- /dev/null +++ b/azure-blob-connector-product/.project @@ -0,0 +1,49 @@ + + + azure-blob-connector-product + + + + + + ch.ivyteam.ivy.designer.dataClasses.ui.ivyDataClassBuilder + + + + + ch.ivyteam.ivy.designer.process.ui.ivyWebServiceProcessClassBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + ch.ivyteam.ivy.designer.ide.ivyModelValidationBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + ch.ivyteam.ivy.project.IvyProjectNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.jem.beaninfo.BeanInfoNature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/azure-blob-connector-product/.settings/.jsdtscope b/azure-blob-connector-product/.settings/.jsdtscope new file mode 100644 index 0000000..cf5ec79 --- /dev/null +++ b/azure-blob-connector-product/.settings/.jsdtscope @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs new file mode 100644 index 0000000..3767d0e --- /dev/null +++ b/azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs @@ -0,0 +1,5 @@ +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.connector.azure.blob.product.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.connector.azure.blob.product +ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=100000 +eclipse.preferences.version=1 diff --git a/azure-blob-connector-product/.settings/org.eclipse.jdt.core.prefs b/azure-blob-connector-product/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..f78f7f7 --- /dev/null +++ b/azure-blob-connector-product/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,10 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/azure-blob-connector-product/.settings/org.eclipse.wst.common.component b/azure-blob-connector-product/.settings/org.eclipse.wst.common.component new file mode 100644 index 0000000..f435d2a --- /dev/null +++ b/azure-blob-connector-product/.settings/org.eclipse.wst.common.component @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector-product/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml b/azure-blob-connector-product/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml new file mode 100644 index 0000000..0d46547 --- /dev/null +++ b/azure-blob-connector-product/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/azure-blob-connector-product/.settings/org.eclipse.wst.common.project.facet.core.xml b/azure-blob-connector-product/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000..c2098f9 --- /dev/null +++ b/azure-blob-connector-product/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/azure-blob-connector-product/.settings/org.eclipse.wst.css.core.prefs b/azure-blob-connector-product/.settings/org.eclipse.wst.css.core.prefs new file mode 100644 index 0000000..96b96cd --- /dev/null +++ b/azure-blob-connector-product/.settings/org.eclipse.wst.css.core.prefs @@ -0,0 +1,2 @@ +css-profile/=org.eclipse.wst.css.core.cssprofile.css3 +eclipse.preferences.version=1 diff --git a/azure-blob-connector-product/.settings/org.eclipse.wst.jsdt.ui.superType.container b/azure-blob-connector-product/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 0000000..3bd5d0a --- /dev/null +++ b/azure-blob-connector-product/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.baseBrowserLibrary \ No newline at end of file diff --git a/azure-blob-connector-product/.settings/org.eclipse.wst.jsdt.ui.superType.name b/azure-blob-connector-product/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 0000000..05bd71b --- /dev/null +++ b/azure-blob-connector-product/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Window \ No newline at end of file diff --git a/azure-blob-connector-product/README.md b/azure-blob-connector-product/README.md new file mode 100644 index 0000000..87086b3 --- /dev/null +++ b/azure-blob-connector-product/README.md @@ -0,0 +1,168 @@ +# Azure Blob Storage + +Azure Blob Storage is a cloud-based object storage service provided by Microsoft Azure. It allows you to store large amounts of unstructured data, such as images, videos, and documents, in a scalable and cost-effective manner. Data is stored in containers within a storage account, and it can be accessed via HTTP/HTTPS, making it ideal for the integration in your Axon Ivy Business processes. + +This connector: +- Supports you in implementing access to Azure Blob Storage. +- Supports you in uploading content to your Azure Blob Storage - directly from an Axon Ivy business process. +- Creates a download link with an expiry date. + +## Setup + +In the project, you only add the dependency in your pom.xml and call public APIs + +**1. Add dependency** +```XML + + com.axonivy.connector + azure-blob-connector + ${process.analyzer.version} + +``` +**2. Azure Blob connection in variables** + +You need to provide Azure Blob connection in variables.yaml. Below is an example for connect by client secret +```yaml +Variables: + AzureBlob: + # The application ID that's assigned to your app. + ClientId: '' + # The client secret that you generated for your app in the app registration portal. + ClientSecret: '' + # The directory tenant the application plans to operate against, in GUID or domain-name format. + TenantId: '' + # https://.blob.core.windows.net/ + EndPoint: '' + # Your container name. + ContainterName: '' +``` + +### For Process GUI +**1. What is support in BlobStorage Callable Sub Process?** + ![azure-blob-connector](images/BlobStorageFunctions.png) + +**2. How to call an event from BlobStorage Callable Sub Process?** +- From Extensions on Tool Bar, we can see a BlobStorage element +![azure-blob-connector](images/ElementInExtensions.png) + +- We can draw a process with uploadFromUrl selection and field some information like: external url, blob name, the directory on Azure Blob Container, .. + +![azure-blob-connector](images/AddBlobStorageAndCallFunction.png) + +### For Java Developer +**1. Call the constructor to set some basic information. Each instance of the advanced process analyzer should care about one specific process model. This way we can store some private information (e.g. simplified model) in the instance and reuse it for different calculations on this object.** +```java + /** + * @param process - The process that should be analyzed. + */ + public AzureBlobStorageService(BlobServiceClient blobServiceClient, String container) +``` + +**2. Application requests to Azure Blob Storage must be authorized. You must to create a BlobServiceClient.** + + - This credential authenticates the created service principal through its client secret +```java + /** + * Create client to a storage account. + * @param clientId - the client ID of the application + * @param clientSecret - the secret value of the Microsoft Entra application. + * @param tenantId - the tenant ID of the application. + * @param endpoint - URL of the service + * @return a {@link BlobServiceClient} created from the configurations in this builder + */ + public static BlobServiceClient getBlobServiceClient(String clientId, String clientSecret, String tenantId, String endpoint) {} +``` + + - This credential authenticates the created service principal through its connection string +```java + /** + * Create client to a storage account. + * @param connectionString - connection string of the storage account + * @param endpoint - URL of the service + * @return a {@link BlobServiceClient} created from the configurations in this builder + */ + public static BlobServiceClient getBlobServiceClient(String connectionString, String endpoint) { +``` + + - This credential authenticates the created service principal through its account and key. +```java + /** + * * Create client to a storage account. + * @param accountName The account name associated with the request. + * @param accountKey The account access key used to authenticate the request. + * @param endpoint - URL of the service + * @return a {@link BlobServiceClient} created from the configurations in this builder + */ + public static BlobServiceClient getBlobServiceClient(String accountName, String accountKey, String endpoint) {} +``` + +**3. You can call `uploadFromUrl` to upload a file from url, `getDownloadLink` to get download link of a blob.** +```java + /** + * The API to copy operation from a source object URL + * @param url - The source URL to upload from + * @return - The blob name + */ + public String uploadFromUrl(String url); + + /** + * The API to create a temporary download link with expired time + * @param url - The blob name + * @return - The url for download + */ + public String getDownloadLink(String blobName); +``` + +### Example + +Below is a simple example for upload a file from url and get temporary download link. +``` + BlobServiceClient blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, SECRET_VALUE,TENANT_ID, END_POINT); + StorageService storageService = new AzureBlobStorageService(blobServiceClient, TEST_CONTAINTER); + + // Upload file from url + String blobName = storageService.uploadFromUrl("https://sample.com/video.mp4"); + // Get temporary download link + String downloadLink = storageService.getDownloadLink(blobName); +``` + +## Demo + +### Run with Azurite at local + +Start docker local: +You can run with docker or docker-compose + +#### Run Azurite V3 docker image + +> Note. Find more docker images tags in + +```bash +docker pull mcr.microsoft.com/azure-storage/azurite +``` + +```bash +docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite +``` + +`-p 10000:10000` will expose blob service's default listening port. +`-p 10001:10001` will expose queue service's default listening port. +`-p 10002:10002` will expose table service's default listening port. + +#### Run docker compose at root folder of project + +``` +make app_local_compose_up +``` + +For other ways, read out [DockerHub](https://github.com/Azure/Azurite/blob/main/README.md#dockerhub) + +### How to explorer data? + +- Install https://azure.microsoft.com/en-us/products/storage/storage-explorer +- Setup to access the local +Read our [Storage Explorer](https://learn.microsoft.com/en-us/azure/storage/storage-explorer/vs-azure-tools-storage-manage-with-storage-explorer) + +Provide the account name and account key in varibles.yaml with [Well Known Storage Account And Key](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage#well-known-storage-account-and-key) + +[StorageAccountAndKey](images/DevAccountKey.png) \ No newline at end of file diff --git a/azure-blob-connector-product/README_DE.md b/azure-blob-connector-product/README_DE.md new file mode 100644 index 0000000..2ec6943 --- /dev/null +++ b/azure-blob-connector-product/README_DE.md @@ -0,0 +1,168 @@ +# Azure Blob Storage + +Azure Blob Storage ist ein Cloud-basierter Objektspeicherdienst, der von Microsoft Azure bereitgestellt wird. Er ermöglicht es dir, große Mengen unstrukturierter Daten wie Bilder, Videos und Dokumente auf skalierbare und kostengünstige Weise zu speichern. Die Daten werden in Containern innerhalb eines Speicherkontos gespeichert und können über HTTP/HTTPS abgerufen werden, wodurch sie sich ideal für die Integration in Ihre Axon Ivy Business Prozesse eignen. + +Dieser Konnektor: +- Unterstützt dich bei der Implementierung des Zugriffs auf Azure Blob Storage. +- Unterstützt dich beim Hochladen von Inhalten auf Ihren Azure Blob Storage - direkt aus einem Axon Ivy Geschäftsprozess. +- Erstellt einen Download-Link mit expiry date (“Verfallsdatum”). + +## Setup + +In the project, you only add the dependency in your pom.xml and call public APIs + +**1. Add dependency** +```XML + + com.axonivy.connector + azure-blob-connector + ${process.analyzer.version} + +``` +**2. Azure Blob connection in variables** + +You need to provide Azure Blob connection in variables.yaml. Below is an example for connect by client secret +```yaml +Variables: + AzureBlob: + # The application ID that's assigned to your app. + ClientId: '' + # The client secret that you generated for your app in the app registration portal. + ClientSecret: '' + # The directory tenant the application plans to operate against, in GUID or domain-name format. + TenantId: '' + # https://.blob.core.windows.net/ + EndPoint: '' + # Your container name. + ContainterName: '' +``` + +### For Process GUI +**1. What is support in BlobStorage Callable Sub Process?** + ![azure-blob-connector](images/BlobStorageFunctions.png) + +**2. How to call an event from BlobStorage Callable Sub Process?** +- From Extensions on Tool Bar, we can see a BlobStorage element +![azure-blob-connector](images/ElementInExtensions.png) + +- We can draw a process with uploadFromUrl selection and field some information like: external url, blob name, the directory on Azure Blob Container, .. + +![azure-blob-connector](images/AddBlobStorageAndCallFunction.png) + +### For Java Developer +**1. Call the constructor to set some basic information. Each instance of the advanced process analyzer should care about one specific process model. This way we can store some private information (e.g. simplified model) in the instance and reuse it for different calculations on this object.** +```java + /** + * @param process - The process that should be analyzed. + */ + public AzureBlobStorageService(BlobServiceClient blobServiceClient, String container) +``` + +**2. Application requests to Azure Blob Storage must be authorized. You must to create a BlobServiceClient.** + + - This credential authenticates the created service principal through its client secret +```java + /** + * Create client to a storage account. + * @param clientId - the client ID of the application + * @param clientSecret - the secret value of the Microsoft Entra application. + * @param tenantId - the tenant ID of the application. + * @param endpoint - URL of the service + * @return a {@link BlobServiceClient} created from the configurations in this builder + */ + public static BlobServiceClient getBlobServiceClient(String clientId, String clientSecret, String tenantId, String endpoint) {} +``` + + - This credential authenticates the created service principal through its connection string +```java + /** + * Create client to a storage account. + * @param connectionString - connection string of the storage account + * @param endpoint - URL of the service + * @return a {@link BlobServiceClient} created from the configurations in this builder + */ + public static BlobServiceClient getBlobServiceClient(String connectionString, String endpoint) { +``` + + - This credential authenticates the created service principal through its account and key. +```java + /** + * * Create client to a storage account. + * @param accountName The account name associated with the request. + * @param accountKey The account access key used to authenticate the request. + * @param endpoint - URL of the service + * @return a {@link BlobServiceClient} created from the configurations in this builder + */ + public static BlobServiceClient getBlobServiceClient(String accountName, String accountKey, String endpoint) {} +``` + +**3. You can call `uploadFromUrl` to upload a file from url, `getDownloadLink` to get download link of a blob.** +```java + /** + * The API to copy operation from a source object URL + * @param url - The source URL to upload from + * @return - The blob name + */ + public String uploadFromUrl(String url); + + /** + * The API to create a temporary download link with expired time + * @param url - The blob name + * @return - The url for download + */ + public String getDownloadLink(String blobName); +``` + +### Example + +Below is a simple example for upload a file from url and get temporary download link. +``` + BlobServiceClient blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, SECRET_VALUE,TENANT_ID, END_POINT); + StorageService storageService = new AzureBlobStorageService(blobServiceClient, TEST_CONTAINTER); + + // Upload file from url + String blobName = storageService.uploadFromUrl("https://sample.com/video.mp4"); + // Get temporary download link + String downloadLink = storageService.getDownloadLink(blobName); +``` + +## Demo + +### Run with Azurite at local + +Start docker local: +You can run with docker or docker-compose + +#### Run Azurite V3 docker image + +> Note. Find more docker images tags in + +```bash +docker pull mcr.microsoft.com/azure-storage/azurite +``` + +```bash +docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite +``` + +`-p 10000:10000` will expose blob service's default listening port. +`-p 10001:10001` will expose queue service's default listening port. +`-p 10002:10002` will expose table service's default listening port. + +#### Run docker compose at root folder of project + +``` +make app_local_compose_up +``` + +For other ways, read out [DockerHub](https://github.com/Azure/Azurite/blob/main/README.md#dockerhub) + +### How to explorer data? + +- Install https://azure.microsoft.com/en-us/products/storage/storage-explorer +- Setup to access the local +Read our [Storage Explorer](https://learn.microsoft.com/en-us/azure/storage/storage-explorer/vs-azure-tools-storage-manage-with-storage-explorer) + +Provide the account name and account key in varibles.yaml with [Well Known Storage Account And Key](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage#well-known-storage-account-and-key) + +[StorageAccountAndKey](images/DevAccountKey.png) \ No newline at end of file diff --git a/azure-blob-connector-product/images/AddBlobStorageAndCallFunction.png b/azure-blob-connector-product/images/AddBlobStorageAndCallFunction.png new file mode 100644 index 0000000..c9ea09e Binary files /dev/null and b/azure-blob-connector-product/images/AddBlobStorageAndCallFunction.png differ diff --git a/azure-blob-connector-product/images/BlobStorageFunctions.png b/azure-blob-connector-product/images/BlobStorageFunctions.png new file mode 100644 index 0000000..b47f3fb Binary files /dev/null and b/azure-blob-connector-product/images/BlobStorageFunctions.png differ diff --git a/azure-blob-connector-product/images/DevAccountKey.png b/azure-blob-connector-product/images/DevAccountKey.png new file mode 100644 index 0000000..c0b4721 Binary files /dev/null and b/azure-blob-connector-product/images/DevAccountKey.png differ diff --git a/azure-blob-connector-product/images/ElementInExtensions.png b/azure-blob-connector-product/images/ElementInExtensions.png new file mode 100644 index 0000000..9696ef9 Binary files /dev/null and b/azure-blob-connector-product/images/ElementInExtensions.png differ diff --git a/MY-PRODUCT-NAME-product/pom.xml b/azure-blob-connector-product/pom.xml similarity index 70% rename from MY-PRODUCT-NAME-product/pom.xml rename to azure-blob-connector-product/pom.xml index dd59011..cc94407 100644 --- a/MY-PRODUCT-NAME-product/pom.xml +++ b/azure-blob-connector-product/pom.xml @@ -1,18 +1,25 @@ - + + 4.0.0 - com.axonivy.market - MY-PRODUCT-NAME-product - 10.0.0-SNAPSHOT + com.axonivy.connector.azure.blob + azure-blob-connector-product + 10.0.22-SNAPSHOT pom - - ../MY-PRODUCT-NAME/config/variables.yaml + ../azure-blob-connector/config/variables.yaml - + + + + maven-deploy-plugin + 3.0.0-M1 + + + - org.apache.maven.plugins maven-assembly-plugin 3.3.0 @@ -36,32 +43,23 @@ generate-sources + + run + ${skip-readme} - + - - + + - - run - - - - - org.apache.maven.plugins - maven-deploy-plugin - 3.0.0-M1 - - - diff --git a/MY-PRODUCT-NAME-product/product.json b/azure-blob-connector-product/product.json similarity index 59% rename from MY-PRODUCT-NAME-product/product.json rename to azure-blob-connector-product/product.json index a5a4b33..3c0edd7 100644 --- a/MY-PRODUCT-NAME-product/product.json +++ b/azure-blob-connector-product/product.json @@ -6,8 +6,14 @@ "data": { "projects": [ { - "groupId": "MY-GROUP-ID", - "artifactId": "MY-PRODUCT-NAME-demo", + "groupId": "com.axonivy.connector.azure.blob", + "artifactId": "azure-blob-connector-demo", + "version": "${version}", + "type": "iar" + }, + { + "groupId": "com.axonivy.connector.azure.blob", + "artifactId": "azure-blob-connector", "version": "${version}", "type": "iar" } @@ -28,8 +34,8 @@ "data": { "dependencies": [ { - "groupId": "MY-GROUP-ID", - "artifactId": "MY-PRODUCT-NAME", + "groupId": "com.axonivy.connector.azure.blob", + "artifactId": "azure-blob-connector", "version": "${version}", "type": "iar" } @@ -44,27 +50,6 @@ } ] } - }, - { - "id": "maven-dropins", - "data": { - "dependencies": [ - { - "groupId": "MY-GROUP-ID", - "artifactId": "MY-PRODUCT-NAME", - "version": "${version}" - } - ], - "repositories": [ - { - "id": "maven.axonivy.com", - "url": "https://maven.axonivy.com", - "snapshots": { - "enabled": "true" - } - } - ] - } } ] } diff --git a/MY-PRODUCT-NAME-product/zip.xml b/azure-blob-connector-product/zip.xml similarity index 100% rename from MY-PRODUCT-NAME-product/zip.xml rename to azure-blob-connector-product/zip.xml diff --git a/azure-blob-connector-test/.classpath b/azure-blob-connector-test/.classpath new file mode 100644 index 0000000..8912ca0 --- /dev/null +++ b/azure-blob-connector-test/.classpath @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector-test/.gitignore b/azure-blob-connector-test/.gitignore new file mode 100644 index 0000000..064fd4a --- /dev/null +++ b/azure-blob-connector-test/.gitignore @@ -0,0 +1,20 @@ +# general +Thumbs.db +.DS_Store +*~ +*.log + +# java +*.class +hs_err_pid* + +# maven +target/ +lib/mvn-deps/ + +# ivy +classes/ +src_dataClasses/ +src_wsproc/ +logs/ +credentials.properties diff --git a/azure-blob-connector-test/.project b/azure-blob-connector-test/.project new file mode 100644 index 0000000..5bb5287 --- /dev/null +++ b/azure-blob-connector-test/.project @@ -0,0 +1,49 @@ + + + azure-blob-connector-test + + + + + + ch.ivyteam.ivy.designer.dataClasses.ui.ivyDataClassBuilder + + + + + ch.ivyteam.ivy.designer.process.ui.ivyWebServiceProcessClassBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + ch.ivyteam.ivy.designer.ide.ivyModelValidationBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + ch.ivyteam.ivy.project.IvyProjectNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.jem.beaninfo.BeanInfoNature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/azure-blob-connector-test/.settings/.jsdtscope b/azure-blob-connector-test/.settings/.jsdtscope new file mode 100644 index 0000000..cf5ec79 --- /dev/null +++ b/azure-blob-connector-test/.settings/.jsdtscope @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs new file mode 100644 index 0000000..bcba9f6 --- /dev/null +++ b/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs @@ -0,0 +1,5 @@ +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.connector.azure.blob.test.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.connector.azure.blob.test +ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=100000 +eclipse.preferences.version=1 diff --git a/azure-blob-connector-test/.settings/org.eclipse.jdt.core.prefs b/azure-blob-connector-test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..f78f7f7 --- /dev/null +++ b/azure-blob-connector-test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,10 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/azure-blob-connector-test/.settings/org.eclipse.wst.common.component b/azure-blob-connector-test/.settings/org.eclipse.wst.common.component new file mode 100644 index 0000000..8edcac9 --- /dev/null +++ b/azure-blob-connector-test/.settings/org.eclipse.wst.common.component @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector-test/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml b/azure-blob-connector-test/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml new file mode 100644 index 0000000..0d46547 --- /dev/null +++ b/azure-blob-connector-test/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/azure-blob-connector-test/.settings/org.eclipse.wst.common.project.facet.core.xml b/azure-blob-connector-test/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000..c2098f9 --- /dev/null +++ b/azure-blob-connector-test/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/azure-blob-connector-test/.settings/org.eclipse.wst.css.core.prefs b/azure-blob-connector-test/.settings/org.eclipse.wst.css.core.prefs new file mode 100644 index 0000000..96b96cd --- /dev/null +++ b/azure-blob-connector-test/.settings/org.eclipse.wst.css.core.prefs @@ -0,0 +1,2 @@ +css-profile/=org.eclipse.wst.css.core.cssprofile.css3 +eclipse.preferences.version=1 diff --git a/azure-blob-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.container b/azure-blob-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 0000000..3bd5d0a --- /dev/null +++ b/azure-blob-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.baseBrowserLibrary \ No newline at end of file diff --git a/azure-blob-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.name b/azure-blob-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 0000000..05bd71b --- /dev/null +++ b/azure-blob-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Window \ No newline at end of file diff --git a/azure-blob-connector-test/pom.xml b/azure-blob-connector-test/pom.xml new file mode 100644 index 0000000..de909fe --- /dev/null +++ b/azure-blob-connector-test/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + com.axonivy.connector.azure.blob + azure-blob-connector-test + 10.0.22-SNAPSHOT + iar + + 10.0.16 + 1.19.8 + + + + com.axonivy.connector.azure.blob + azure-blob-connector + ${project.version} + iar + + + com.axonivy.ivy.test + unit-tester + 10.0.16 + test + + + org.testcontainers + testcontainers + ${lib.testcontainers.version} + test + + + org.testcontainers + junit-jupiter + ${lib.testcontainers.version} + test + + + + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + always + + + + + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + always + + + + + src_test + + + com.axonivy.ivy.ci + project-build-plugin + ${project.build.plugin.version} + true + + + maven-surefire-plugin + 3.0.0-M5 + + + ${azureblob.account} + ${azureblob.key} + + + + + + + + maven-deploy-plugin + 3.0.0-M1 + + true + + + + + + + diff --git a/azure-blob-connector-test/resource_test/picture/singapore.png b/azure-blob-connector-test/resource_test/picture/singapore.png new file mode 100644 index 0000000..ab8ffa9 Binary files /dev/null and b/azure-blob-connector-test/resource_test/picture/singapore.png differ diff --git a/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AbstractIntegrationTest.java b/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AbstractIntegrationTest.java new file mode 100644 index 0000000..a62a177 --- /dev/null +++ b/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AbstractIntegrationTest.java @@ -0,0 +1,67 @@ +package com.axonivy.connector.azure.blob.test.integration; + +import java.util.UUID; +import java.io.IOException; +import java.util.Properties; + +import org.apache.commons.lang3.StringUtils; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +import com.axonivy.connector.azure.blob.BlobServiceClientHelper; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.specialized.BlockBlobClient; + +abstract class AbstractIntegrationTest { + + protected static final String TEST_CONTAINTER = "test-container"; + private static final String END_POINT_FORMAT = "http://127.0.0.1:%s/%s"; + + protected static final String EXTENSION_TEST = ".test"; + protected static final String CONTENT_TEST = "testContent.txt"; + private static final int BLOB_PORT = 10000; + + public static final GenericContainer azure = new GenericContainer<>( + DockerImageName.parse("mcr.microsoft.com/azure-storage/azurite")).withExposedPorts(BLOB_PORT); + + static { + azure.start(); + } + + protected static BlobServiceClient getBlobServiceClient() throws IOException { + String account = System.getProperty("azureBlobAccount"); + String key = System.getProperty("azureBlobKey"); + + if (StringUtils.isEmpty(account)) { + try (var in = AbstractIntegrationTest.class.getResourceAsStream("credentials.properties")) { + if (in != null) { + Properties props = new Properties(); + props.load(in); + account = (String) props.get("account"); + key = (String) props.get("key"); + } + } + } + + final String endpoint = String.format(END_POINT_FORMAT, azure.getMappedPort(BLOB_PORT),account); + return BlobServiceClientHelper.getBlobServiceClient(account, key, endpoint); + } + + protected static BlockBlobClient getBlockBlobClient(BlobServiceClient blobServiceClient, String blobName) { + BlobContainerClient blobContainerClient = getBlobContainerClient(blobServiceClient); + BlockBlobClient blobClient = blobContainerClient.getBlobClient(blobName).getBlockBlobClient(); + return blobClient; + } + + protected static BlobContainerClient getBlobContainerClient(BlobServiceClient blobServiceClient) { + BlobContainerClient blobContainerClient = blobServiceClient.createBlobContainerIfNotExists(TEST_CONTAINTER); + return blobContainerClient; + } + + protected boolean isUUIDFomart(String value, String expectedExtension) { + String uuid = value.split("\\.")[0]; + + return UUID.fromString(uuid) != null && value.endsWith("." + expectedExtension); + } +} diff --git a/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AzureBlobStorageServiceTest.java b/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AzureBlobStorageServiceTest.java new file mode 100644 index 0000000..d613652 --- /dev/null +++ b/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AzureBlobStorageServiceTest.java @@ -0,0 +1,131 @@ +package com.axonivy.connector.azure.blob.test.integration; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.axonivy.connector.azure.blob.StorageService; +import com.axonivy.connector.azure.blob.internal.AzureBlobStorageService; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.specialized.BlockBlobClient; + +@Testcontainers +@TestMethodOrder(OrderAnnotation.class) +public class AzureBlobStorageServiceTest extends AbstractIntegrationTest { + + private static BlobServiceClient blobServiceClient = null; + private static StorageService storageService = null; + private static String blobNameOfFileUpload = null; + + @BeforeAll + public static void setup() throws IOException { + blobServiceClient = getBlobServiceClient(); + storageService = new AzureBlobStorageService(blobServiceClient, TEST_CONTAINTER); + } + + @Test + void shouldUploadContent() throws Exception { + final String content = "Test Content"; + storageService.upload(content, CONTENT_TEST); + BlockBlobClient blobClient = getBlockBlobClient(blobServiceClient, CONTENT_TEST); + String downloadContent = blobClient.downloadContent().toString(); + + assertEquals(content, downloadContent); + } + + @Test + @Order(1) + void shouldUploadFromFile() { + String path = new File("resource_test/picture/singapore.png").getAbsolutePath(); + String blobName = storageService.uploadFromFile(path, StringUtils.EMPTY, true); + blobNameOfFileUpload = blobName; + assertNotNull(blobName); + } + + @Test + void shouldUploadByteArray() throws Exception { + String sampleData = "Sample data for blob"; + String blobName = storageService.upload(sampleData.getBytes(), CONTENT_TEST, StringUtils.EMPTY, true); + assertNotNull(blobName); + } + + @Test + void shouldUploadByteArrayWithException() throws Exception { + var exception = assertThrows( + Exception.class, + () -> storageService.upload(null, "nullContent.txt", StringUtils.EMPTY, false)); + + assertEquals("Upload file error. Exception message: Cannot read the array length because \"buf\" is null", exception.getMessage()); + } + + @Test + @Order(2) + void shouldDownloadContent() { + byte[] result = storageService.downloadContent(blobNameOfFileUpload); + assertNotNull(result); + } + + @Test + @Order(3) + void shouldDownloadToFile() throws IOException { + File templeLocalFile = new File("resource_test/picture/local-file.png"); + storageService.downloadToFile(blobNameOfFileUpload, templeLocalFile.getAbsolutePath()); + boolean fileDeleted = Files.deleteIfExists(templeLocalFile.toPath()); + assertTrue(fileDeleted); + } + + @Test + @Order(4) + void shouldDownloadStream() throws IOException { + ByteArrayOutputStream baos = storageService.downloadStream(blobNameOfFileUpload); + assertNotNull(baos); + } + + @Test + @Order(5) + void shouldDeletedWithBlobIsExists() { + boolean result = storageService.delete(blobNameOfFileUpload); + assertTrue(result); + } + + @Test + @Order(6) + void shouldDeletedWithBlobIsNotExists() { + boolean result = storageService.delete(blobNameOfFileUpload); + assertFalse(result); + } + + @Test + @Order(7) + void shouldRestoreBlobIsDeleted() { + // Azurite: Current API is not implemented yet. + } + + + + @Disabled + void shouldGetDownloadLink() throws Exception { + // Azurite: Only authentication scheme Bearer is supported + } + + @Disabled + void shouldUploadFromUrl() throws Exception { + // Azurite: Current API is not implemented yet. + } +} diff --git a/azure-blob-connector/.classpath b/azure-blob-connector/.classpath new file mode 100644 index 0000000..75b41fa --- /dev/null +++ b/azure-blob-connector/.classpath @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector/.gitignore b/azure-blob-connector/.gitignore new file mode 100644 index 0000000..86ba893 --- /dev/null +++ b/azure-blob-connector/.gitignore @@ -0,0 +1,20 @@ +# general +Thumbs.db +.DS_Store +*~ +*.log + +# java +*.class +hs_err_pid* + +# maven +target/ +lib/mvn-deps/ + +# ivy +classes/ +src_dataClasses/ +src_wsproc/ +logs/ +.azure diff --git a/azure-blob-connector/.project b/azure-blob-connector/.project new file mode 100644 index 0000000..5f506ad --- /dev/null +++ b/azure-blob-connector/.project @@ -0,0 +1,49 @@ + + + azure-blob-connector + + + + + + ch.ivyteam.ivy.designer.dataClasses.ui.ivyDataClassBuilder + + + + + ch.ivyteam.ivy.designer.process.ui.ivyWebServiceProcessClassBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + ch.ivyteam.ivy.designer.ide.ivyModelValidationBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + ch.ivyteam.ivy.project.IvyProjectNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.jem.beaninfo.BeanInfoNature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/azure-blob-connector/.settings/.jsdtscope b/azure-blob-connector/.settings/.jsdtscope new file mode 100644 index 0000000..cf5ec79 --- /dev/null +++ b/azure-blob-connector/.settings/.jsdtscope @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs new file mode 100644 index 0000000..3bfd371 --- /dev/null +++ b/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs @@ -0,0 +1,5 @@ +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.connector.azure.blob.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.connector.azure.blob +ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=100000 +eclipse.preferences.version=1 diff --git a/azure-blob-connector/.settings/org.eclipse.jdt.core.prefs b/azure-blob-connector/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..f78f7f7 --- /dev/null +++ b/azure-blob-connector/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,10 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/azure-blob-connector/.settings/org.eclipse.wst.common.component b/azure-blob-connector/.settings/org.eclipse.wst.common.component new file mode 100644 index 0000000..ea172dc --- /dev/null +++ b/azure-blob-connector/.settings/org.eclipse.wst.common.component @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml b/azure-blob-connector/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml new file mode 100644 index 0000000..0d46547 --- /dev/null +++ b/azure-blob-connector/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/azure-blob-connector/.settings/org.eclipse.wst.common.project.facet.core.xml b/azure-blob-connector/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000..c2098f9 --- /dev/null +++ b/azure-blob-connector/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/azure-blob-connector/.settings/org.eclipse.wst.css.core.prefs b/azure-blob-connector/.settings/org.eclipse.wst.css.core.prefs new file mode 100644 index 0000000..96b96cd --- /dev/null +++ b/azure-blob-connector/.settings/org.eclipse.wst.css.core.prefs @@ -0,0 +1,2 @@ +css-profile/=org.eclipse.wst.css.core.cssprofile.css3 +eclipse.preferences.version=1 diff --git a/azure-blob-connector/.settings/org.eclipse.wst.jsdt.ui.superType.container b/azure-blob-connector/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 0000000..3bd5d0a --- /dev/null +++ b/azure-blob-connector/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.baseBrowserLibrary \ No newline at end of file diff --git a/azure-blob-connector/.settings/org.eclipse.wst.jsdt.ui.superType.name b/azure-blob-connector/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 0000000..05bd71b --- /dev/null +++ b/azure-blob-connector/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Window \ No newline at end of file diff --git a/azure-blob-connector/config/custom-fields.yaml b/azure-blob-connector/config/custom-fields.yaml new file mode 100644 index 0000000..bb20b70 --- /dev/null +++ b/azure-blob-connector/config/custom-fields.yaml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/custom-fields.json +# +# == Custom Fields Information == +# +# You can define here your project custom fields. +# Have a look at our documentation for more information. +# +CustomFields: +# Tasks: +# MyTaskCustomField: +# Label: My task custom field +# Description: This new task custom field can be used to ... +# Type: STRING +# Cases: +# MyCaseCustomField: +# Label: My case custom field +# Description: This new case custom field can be used to ... +# Type: STRING +# Starts: +# MyStartCustomField: +# Label: My start custom field +# Description: This new start custom field can be used to ... diff --git a/azure-blob-connector/config/databases.yaml b/azure-blob-connector/config/databases.yaml new file mode 100644 index 0000000..10319e2 --- /dev/null +++ b/azure-blob-connector/config/databases.yaml @@ -0,0 +1,2 @@ +# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/databases.json +Databases: diff --git a/azure-blob-connector/config/overrides.any b/azure-blob-connector/config/overrides.any new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/azure-blob-connector/config/overrides.any @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/azure-blob-connector/config/persistence.xml b/azure-blob-connector/config/persistence.xml new file mode 100644 index 0000000..d6b96d7 --- /dev/null +++ b/azure-blob-connector/config/persistence.xml @@ -0,0 +1,2 @@ + + diff --git a/azure-blob-connector/config/rest-clients.yaml b/azure-blob-connector/config/rest-clients.yaml new file mode 100644 index 0000000..4bffaca --- /dev/null +++ b/azure-blob-connector/config/rest-clients.yaml @@ -0,0 +1,2 @@ +# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/rest-clients.json +RestClients: diff --git a/azure-blob-connector/config/roles.xml b/azure-blob-connector/config/roles.xml new file mode 100644 index 0000000..59892fe --- /dev/null +++ b/azure-blob-connector/config/roles.xml @@ -0,0 +1,4 @@ + + + Everybody + diff --git a/azure-blob-connector/config/users.xml b/azure-blob-connector/config/users.xml new file mode 100644 index 0000000..51a6906 --- /dev/null +++ b/azure-blob-connector/config/users.xml @@ -0,0 +1,2 @@ + + diff --git a/azure-blob-connector/config/variables.yaml b/azure-blob-connector/config/variables.yaml new file mode 100644 index 0000000..245f493 --- /dev/null +++ b/azure-blob-connector/config/variables.yaml @@ -0,0 +1,16 @@ +# == Variables == +# +# You can define here your project Variables. +# +Variables: + AzureBlob: + # The application ID that's assigned to your app. + ClientId: '' + # The client secret that you generated for your app in the app registration portal. + ClientSecret: '' + # The directory tenant the application plans to operate against, in GUID or domain-name format. + TenantId: '' + # https://.blob.core.windows.net/ + EndPoint: '' + # Your container name. + ContainterName: '' \ No newline at end of file diff --git a/azure-blob-connector/config/webservice-clients.yaml b/azure-blob-connector/config/webservice-clients.yaml new file mode 100644 index 0000000..688047a --- /dev/null +++ b/azure-blob-connector/config/webservice-clients.yaml @@ -0,0 +1,2 @@ +# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/webservice-clients.json +WebServiceClients: diff --git a/azure-blob-connector/dataclasses/com/axonivy/connector/azure/blob/BlobStorageData.ivyClass b/azure-blob-connector/dataclasses/com/axonivy/connector/azure/blob/BlobStorageData.ivyClass new file mode 100644 index 0000000..62ff5a2 --- /dev/null +++ b/azure-blob-connector/dataclasses/com/axonivy/connector/azure/blob/BlobStorageData.ivyClass @@ -0,0 +1,15 @@ +BlobStorageData #class +com.axonivy.connector.azure.blob #namespace +date java.util.Date #field +azureBlobStorageBean com.axonivy.connector.azure.blob.internal.bean.AzureBlobStorageBean #field +isSuccess Boolean #field +blobItems java.util.List #field +blobName String #field +linkDownload String #field +localPath String #field +uploadToFolder String #field +isOverwriteFile Boolean #field +url String #field +content java.io.InputStream #field +functionName String #field +isExist Boolean #field diff --git a/azure-blob-connector/pom.xml b/azure-blob-connector/pom.xml new file mode 100644 index 0000000..22cd148 --- /dev/null +++ b/azure-blob-connector/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + com.axonivy.connector.azure.blob + azure-blob-connector + 10.0.22-SNAPSHOT + iar + + 10.0.16 + 1.2.23 + + + + + + com.azure + azure-sdk-bom + ${azure.sdk.pom.version} + pom + import + + + + + + + com.azure + azure-storage-blob + + + com.azure + azure-storage-common + + + com.azure + azure-identity + + + com.azure + azure-core + + + + + + + always + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + + + + src + + + com.axonivy.ivy.ci + project-build-plugin + ${project.build.plugin.version} + true + + + maven-release-plugin + 3.0.0-M4 + + azure-blob-connector-v@{project.version} + + + + + diff --git a/azure-blob-connector/processes/BlobStorage.p.json b/azure-blob-connector/processes/BlobStorage.p.json new file mode 100644 index 0000000..4d8f9b5 --- /dev/null +++ b/azure-blob-connector/processes/BlobStorage.p.json @@ -0,0 +1,588 @@ +{ + "format" : "10.0.0", + "id" : "1905E51E1156B82C", + "kind" : "CALLABLE_SUB", + "config" : { + "data" : "com.axonivy.connector.azure.blob.BlobStorageData" + }, + "elements" : [ { + "id" : "f0", + "type" : "CallSubStart", + "name" : "getBlobs", + "config" : { + "callSignature" : "getBlobs", + "input" : { + "map" : { + "out.functionName" : "\"getBlobs\"" + } + }, + "result" : { + "params" : [ + { "name" : "blobItems", "type" : "java.util.List" } + ], + "map" : { + "result.blobItems" : "in.blobItems" + } + }, + "tags" : "connector" + }, + "visual" : { + "at" : { "x" : 96, "y" : 64 }, + "description" : [ + "Description: Get all blobs", + "Result:", + "- blobItems: java.util.List" + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" + }, + "connect" : { "id" : "f41", "to" : "f40" } + }, { + "id" : "f3", + "type" : "CallSubStart", + "name" : "delete(Date)", + "config" : { + "callSignature" : "delete", + "input" : { + "params" : [ + { "name" : "date", "type" : "java.util.Date" } + ], + "map" : { + "out.date" : "param.date", + "out.functionName" : "\"deleteByDate\"" + } + }, + "result" : { + "params" : [ + { "name" : "isSucess", "type" : "Boolean" } + ], + "map" : { + "result.isSucess" : "in.isSuccess" + } + }, + "tags" : "connector" + }, + "visual" : { + "at" : { "x" : 96, "y" : 136 }, + "labelOffset" : { "x" : 9, "y" : 41 }, + "description" : [ + "Description: delete blob by date", + "Parammeter:", + "- date: java.util.Date", + "Result:", + "- isSucess: java.lang.Boolean" + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" + }, + "connect" : { "id" : "f42", "to" : "f40", "via" : [ { "x" : 240, "y" : 136 } ] } + }, { + "id" : "f5", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 616, "y" : 144 } + } + }, { + "id" : "f4", + "type" : "Script", + "name" : "delete blob by date", + "config" : { + "output" : { + "code" : [ + "", + "in.azureBlobStorageBean.getAzureBlobStorageService().delete(in.date);", + "in.isSuccess = true;" + ] + } + }, + "visual" : { + "at" : { "x" : 488, "y" : 144 } + }, + "connect" : { "id" : "f7", "to" : "f5" } + }, { + "id" : "f10", + "type" : "CallSubStart", + "name" : "detete(String)", + "config" : { + "callSignature" : "detete", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "out.blobName" : "param.blobName", + "out.functionName" : "\"deleteByName\"" + } + }, + "result" : { + "params" : [ + { "name" : "isSuccess", "type" : "Boolean" } + ], + "map" : { + "result.isSuccess" : "in.isSuccess" + } + }, + "tags" : "connector" + }, + "visual" : { + "at" : { "x" : 96, "y" : 200 }, + "description" : [ + "Description: delete blob by blob name", + "Parammeter:", + "- blobName: java.lang.String", + "Result:", + "- isSucess: java.lang.Boolean" + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" + }, + "connect" : { "id" : "f45", "to" : "f40", "via" : [ { "x" : 240, "y" : 200 } ] } + }, { + "id" : "f11", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 616, "y" : 224 } + } + }, { + "id" : "f12", + "type" : "Script", + "name" : "delete blob by name", + "config" : { + "output" : { + "code" : "in.isSuccess = in.azureBlobStorageBean.getAzureBlobStorageService().delete(in.blobName);" + } + }, + "visual" : { + "at" : { "x" : 488, "y" : 224 } + }, + "connect" : { "id" : "f14", "to" : "f11" } + }, { + "id" : "f15", + "type" : "CallSubStart", + "name" : "restore(String)", + "config" : { + "callSignature" : "restore", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "out.blobName" : "param.blobName", + "out.functionName" : "\"restore\"" + } + }, + "result" : { + "params" : [ + { "name" : "isSuccess", "type" : "Boolean" } + ], + "map" : { + "result.isSuccess" : "in.isSuccess" + } + }, + "tags" : "connector" + }, + "visual" : { + "at" : { "x" : 96, "y" : 264 }, + "description" : [ + "Description: restore blob by blob name", + "Parammeter:", + "- blobName: java.lang.String", + "Result:", + "- isSucess: java.lang.Boolean" + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" + }, + "connect" : { "id" : "f46", "to" : "f40", "via" : [ { "x" : 240, "y" : 264 } ] } + }, { + "id" : "f16", + "type" : "Script", + "name" : "restore", + "config" : { + "output" : { + "code" : [ + "in.azureBlobStorageBean.getAzureBlobStorageService().restore(in.blobName);", + "in.isSuccess = true;" + ] + } + }, + "visual" : { + "at" : { "x" : 488, "y" : 304 } + }, + "connect" : { "id" : "f19", "to" : "f17" } + }, { + "id" : "f17", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 616, "y" : 304 } + } + }, { + "id" : "f20", + "type" : "CallSubStart", + "name" : "getLinkDownload(String)", + "config" : { + "callSignature" : "getLinkDownload", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "out.blobName" : "param.blobName", + "out.functionName" : "\"getLink\"" + } + }, + "result" : { + "params" : [ + { "name" : "linkDownload", "type" : "String" } + ], + "map" : { + "result.linkDownload" : "in.linkDownload" + } + }, + "tags" : "connector" + }, + "visual" : { + "at" : { "x" : 96, "y" : 328 }, + "labelOffset" : { "x" : 33, "y" : 33 }, + "description" : [ + "Description: get link download by blob name", + "Parammeter:", + "- blobName: java.lang.String", + "Result:", + "- linkDownload: java.lang.String" + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" + }, + "connect" : { "id" : "f47", "to" : "f40", "via" : [ { "x" : 240, "y" : 328 } ] } + }, { + "id" : "f21", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 616, "y" : 384 } + } + }, { + "id" : "f22", + "type" : "Script", + "name" : "get link", + "config" : { + "output" : { + "code" : "in.linkDownload = in.azureBlobStorageBean.getAzureBlobStorageService().getDownloadLink(in.blobName);" + } + }, + "visual" : { + "at" : { "x" : 488, "y" : 384 } + }, + "connect" : { "id" : "f23", "to" : "f21" } + }, { + "id" : "f25", + "type" : "CallSubStart", + "name" : "uploadFromFile(String,String,String,Boolean)", + "config" : { + "callSignature" : "uploadFromFile", + "input" : { + "params" : [ + { "name" : "localPath", "type" : "String" }, + { "name" : "blobName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" }, + { "name" : "isOverwriteFile", "type" : "Boolean" } + ], + "map" : { + "out.blobName" : "param.blobName", + "out.functionName" : "\"uploadByPath\"", + "out.isOverwriteFile" : "param.isOverwriteFile", + "out.localPath" : "param.localPath", + "out.uploadToFolder" : "param.uploadToFolder" + } + }, + "result" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "result.blobName" : "in.blobName" + } + }, + "tags" : "connector" + }, + "visual" : { + "at" : { "x" : 96, "y" : 392 }, + "labelOffset" : { "x" : 9, "y" : 33 }, + "description" : [ + "Description: upload from file", + "Parammeter:", + "- localPath: java.lang.String", + "- blobName: java.lang.String", + "- uploadToFolder: java.lang.String", + "- isOverwriteFile: java.lang.Boolean", + "Result:", + "- blobName: java.lang.String" + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" + }, + "connect" : { "id" : "f48", "to" : "f40", "via" : [ { "x" : 240, "y" : 392 } ] } + }, { + "id" : "f26", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 616, "y" : 464 } + } + }, { + "id" : "f27", + "type" : "Script", + "name" : "upload", + "config" : { + "output" : { + "code" : "in.blobName = in.azureBlobStorageBean.getAzureBlobStorageService().uploadFromFile(in.localPath, in.uploadToFolder, in.isOverwriteFile);" + } + }, + "visual" : { + "at" : { "x" : 488, "y" : 464 } + }, + "connect" : { "id" : "f29", "to" : "f26" } + }, { + "id" : "f30", + "type" : "CallSubStart", + "name" : "uploadFromUrl(String,String,String,Boolean)", + "config" : { + "callSignature" : "uploadFromUrl", + "input" : { + "params" : [ + { "name" : "url", "type" : "String" }, + { "name" : "blobName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" }, + { "name" : "isOverwriteFile", "type" : "Boolean" } + ], + "map" : { + "out.blobName" : "param.blobName", + "out.functionName" : "\"uploadByURL\"", + "out.isOverwriteFile" : "param.isOverwriteFile", + "out.uploadToFolder" : "param.uploadToFolder", + "out.url" : "param.url" + } + }, + "result" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "result.blobName" : "in.blobName" + } + }, + "tags" : "connector" + }, + "visual" : { + "at" : { "x" : 96, "y" : 456 }, + "labelOffset" : { "x" : 25, "y" : 33 }, + "description" : [ + "Description: upload from file", + "Parammeter:", + "- url: java.lang.String", + "- blobName: java.lang.String", + "- uploadToFolder: java.lang.String", + "- isOverwriteFile: java.lang.Boolean", + "Result:", + "- blobName: java.lang.String" + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" + }, + "connect" : { "id" : "f49", "to" : "f40", "via" : [ { "x" : 240, "y" : 456 } ] } + }, { + "id" : "f31", + "type" : "Script", + "name" : "upload", + "config" : { + "output" : { + "code" : "in.blobName = in.azureBlobStorageBean.getAzureBlobStorageService().uploadFromUrl(in.url, in.uploadToFolder, in.isOverwriteFile);" + } + }, + "visual" : { + "at" : { "x" : 488, "y" : 544 } + }, + "connect" : { "id" : "f34", "to" : "f32" } + }, { + "id" : "f32", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 616, "y" : 544 } + } + }, { + "id" : "f35", + "type" : "CallSubStart", + "name" : "uploadFromFile(InputStream,String,String,Boolean)", + "config" : { + "callSignature" : "uploadFromFile", + "input" : { + "params" : [ + { "name" : "content", "type" : "java.io.InputStream" }, + { "name" : "blobName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" }, + { "name" : "isOverwriteFile", "type" : "Boolean" } + ], + "map" : { + "out.blobName" : "param.blobName", + "out.content" : "param.content", + "out.functionName" : "\"uploadByFile\"", + "out.isOverwriteFile" : "param.isOverwriteFile", + "out.uploadToFolder" : "param.uploadToFolder" + } + }, + "result" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "result.blobName" : "in.blobName" + } + }, + "tags" : "connector" + }, + "visual" : { + "at" : { "x" : 96, "y" : 512 }, + "labelOffset" : { "x" : 17, "y" : 33 }, + "description" : [ + "Description: upload from inputStream", + "Parammeter:", + "- content: java.io.InputStream", + "- blobName: java.lang.String", + "- uploadToFolder: java.lang.String", + "- isOverwriteFile: java.lang.Boolean", + "Result:", + "- blobName: java.lang.String" + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" + }, + "connect" : { "id" : "f50", "to" : "f40", "via" : [ { "x" : 240, "y" : 512 } ] } + }, { + "id" : "f36", + "type" : "Script", + "name" : "upload", + "config" : { + "output" : { + "code" : [ + "import org.apache.commons.io.IOUtils;", + "", + "in.blobName = in.azureBlobStorageBean.getAzureBlobStorageService().upload(IOUtils.toByteArray(in.content), in.blobName, in.uploadToFolder, in.isOverwriteFile);" + ] + } + }, + "visual" : { + "at" : { "x" : 488, "y" : 624 } + }, + "connect" : { "id" : "f38", "to" : "f37" } + }, { + "id" : "f37", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 616, "y" : 624 } + } + }, { + "id" : "f8", + "type" : "Script", + "name" : "get list blob", + "config" : { + "output" : { + "code" : [ + "", + "in.blobItems = in.azureBlobStorageBean.getAzureBlobStorageService().getBlobs();" + ] + } + }, + "visual" : { + "at" : { "x" : 488, "y" : 64 } + }, + "connect" : { "id" : "f2", "to" : "f1" } + }, { + "id" : "f1", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 640, "y" : 64 } + } + }, { + "id" : "f40", + "type" : "Script", + "name" : "init service", + "config" : { + "output" : { + "code" : [ + "import com.axonivy.connector.azure.blob.internal.bean.AzureBlobStorageBean;", + "", + "in.azureBlobStorageBean = new AzureBlobStorageBean();" + ] + } + }, + "visual" : { + "at" : { "x" : 240, "y" : 64 } + }, + "connect" : { "id" : "f13", "to" : "f9" } + }, { + "id" : "f9", + "type" : "Alternative", + "name" : "check function name", + "visual" : { + "at" : { "x" : 376, "y" : 64 }, + "labelOffset" : { "x" : 0, "y" : -16 } + }, + "connect" : [ + { "id" : "f6", "to" : "f8", "condition" : "\"getBlobs\".equalsIgnoreCase(in.functionName)" }, + { "id" : "f18", "to" : "f4", "via" : [ { "x" : 376, "y" : 144 } ], "condition" : "\"deleteByDate\".equalsIgnoreCase(in.functionName)" }, + { "id" : "f24", "to" : "f12", "via" : [ { "x" : 376, "y" : 224 } ], "condition" : "\"deleteByName\".equalsIgnoreCase(in.functionName)" }, + { "id" : "f28", "to" : "f16", "via" : [ { "x" : 376, "y" : 304 } ], "condition" : "\"restore\".equalsIgnoreCase(in.functionName)" }, + { "id" : "f33", "to" : "f22", "via" : [ { "x" : 376, "y" : 384 } ], "condition" : "\"getLink\".equalsIgnoreCase(in.functionName)" }, + { "id" : "f39", "to" : "f27", "via" : [ { "x" : 376, "y" : 464 } ], "condition" : "\"uploadByPath\".equalsIgnoreCase(in.functionName)" }, + { "id" : "f43", "to" : "f31", "via" : [ { "x" : 376, "y" : 544 } ], "condition" : "\"uploadByURL\".equalsIgnoreCase(in.functionName)" }, + { "id" : "f44", "to" : "f36", "via" : [ { "x" : 376, "y" : 624 } ], "condition" : "\"uploadByFile\".equalsIgnoreCase(in.functionName)" }, + { "id" : "f54", "to" : "f53", "via" : [ { "x" : 376, "y" : 704 } ], "condition" : "\"checkBlobExist\".equalsIgnoreCase(in.functionName)" } + ] + }, { + "id" : "f51", + "type" : "CallSubStart", + "name" : "checkBlobIsExist(String)", + "config" : { + "callSignature" : "checkBlobIsExist", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "out.blobName" : "param.blobName", + "out.functionName" : "\"checkBlobExist\"" + } + }, + "result" : { + "params" : [ + { "name" : "isExist", "type" : "Boolean" } + ], + "map" : { + "result.isExist" : "in.isExist" + } + } + }, + "visual" : { + "at" : { "x" : 96, "y" : 576 }, + "description" : [ + "Description: check blob is exist by blob name", + "Parammeter:", + "- blobName: java.lang.String", + "Result:", + "- isExist: java.lang.Boolean" + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" + }, + "connect" : { "id" : "f52", "to" : "f40", "via" : [ { "x" : 240, "y" : 576 } ] } + }, { + "id" : "f53", + "type" : "Script", + "name" : "check blob exist", + "config" : { + "output" : { + "code" : "in.isExist = in.azureBlobStorageBean.isBlobExist(in.blobName);" + } + }, + "visual" : { + "at" : { "x" : 488, "y" : 704 } + }, + "connect" : { "id" : "f57", "to" : "f56" } + }, { + "id" : "f56", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 616, "y" : 704 } + } + } ] +} \ No newline at end of file diff --git a/azure-blob-connector/src/com/axonivy/connector/azure/blob/BlobServiceClientHelper.java b/azure-blob-connector/src/com/axonivy/connector/azure/blob/BlobServiceClientHelper.java new file mode 100644 index 0000000..47c4b12 --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/connector/azure/blob/BlobServiceClientHelper.java @@ -0,0 +1,66 @@ +package com.axonivy.connector.azure.blob; + +import com.azure.identity.ClientSecretCredential; +import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.common.StorageSharedKeyCredential; + +public class BlobServiceClientHelper { + + /** + * Create client to a storage account. + * @param clientId - the client ID of the application + * @param clientSecret - the secret value of the Microsoft Entra application. + * @param tenantId - the tenant ID of the application. + * @param endpoint - URL of the service + * @return a {@link BlobServiceClient} created from the configurations in this builder + */ + public static BlobServiceClient getBlobServiceClient(String clientId, String clientSecret, String tenantId, String endpoint) { + + ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder() + .clientId(clientId) + .clientSecret(clientSecret) + .tenantId(tenantId) + .build(); + + BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() + .endpoint(endpoint) + .credential(clientSecretCredential) + .buildClient(); + + return blobServiceClient; + } + + /** + * Create client to a storage account. + * @param connectionString - onnection string of the storage account + * @param endpoint - URL of the service + * @return a {@link BlobServiceClient} created from the configurations in this builder + */ + public static BlobServiceClient getBlobServiceClient(String connectionString, String endpoint) { + BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() + .endpoint(endpoint) + .connectionString(connectionString) + .buildClient(); + + return blobServiceClient; + } + + /** + * * Create client to a storage account. + * @param accountName The account name associated with the request. + * @param accountKey The account access key used to authenticate the request. + * @param endpoint - URL of the service + * @return a {@link BlobServiceClient} created from the configurations in this builder + */ + public static BlobServiceClient getBlobServiceClient(String accountName, String accountKey, String endpoint) { + StorageSharedKeyCredential credential = new StorageSharedKeyCredential(accountName, accountKey); + BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() + .endpoint(endpoint) + .credential(credential) + .buildClient(); + + return blobServiceClient; + } +} diff --git a/azure-blob-connector/src/com/axonivy/connector/azure/blob/StorageService.java b/azure-blob-connector/src/com/axonivy/connector/azure/blob/StorageService.java new file mode 100644 index 0000000..926528f --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/connector/azure/blob/StorageService.java @@ -0,0 +1,128 @@ +package com.axonivy.connector.azure.blob; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Date; +import java.util.List; + +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.models.BlobItem; + +public interface StorageService { + + /** + * The API to create a blob string with content + * @param content - The string content + * @param fileName - The file name + * @return - the blob name + */ + public String upload(String content, String fileName); + + /** + * The API to create a blob from local machine with specific path file + * @param path - path file + * @return - The blob name + * */ + public String uploadFromFile(String path); + + /** + * The API to create a blob from local machine with specific path file + * @param path - path file + * @param uploadToFolder - The name of folder + * @param isOverwrite - boolean + * @return - The blob name + * */ + public String uploadFromFile(String path, String uploadToFolder, boolean isOverwite); + + /** + * The API to create a blob from GUI with upload function + * @param content - byte[] + * @param fileName - file name + * @return - The blob name + * */ + public String upload(byte[] content, String fileName) throws Exception; + + /** + * The API to create a blob from GUI with upload function + * @param content - byte[] + * @param fileName - file name + * @param uploadToFolder - The name of folder + * @param isOverwrite - boolean + * @return - The blob name + * */ + public String upload(byte[] content, String fileName, String uploadToFolder, boolean isOverwite) throws Exception; + + /** + * The API to copy operation from a source object URL + * @param url - The source URL to upload from + * @return - The blob name + */ + public String uploadFromUrl(String url); + + /** + * The API to copy operation from a source object URL + * @param url - The source URL to upload from + * @param uploadToFolder - The name of folder + * @param isOverwrite - boolean + * @return - The blob name + */ + public String uploadFromUrl(String url, String uploadToFolder, boolean isOverwite); + + /** + * The API to delete blob + * @param blob name + * @return - boolean + * */ + public boolean delete(String blobName); + + /** + * The API to delete blob + * @param specific date to delete all blobs + * */ + public void delete(Date date); + + /** + * The API to restore an deleted blob if soft deleted for blobs is enable. + * @param blob name + * */ + public void restore(String blobName); + + /** + * The API to download content + * @param blob name + * @return bye[] + * */ + public byte[] downloadContent(String blobName); + + /** + * The API to download from file + * @param blob name + * @param filePath + * */ + public void downloadToFile(String blobName, String filePath); + + /** + * The API to download to a stream + * @param blob name + * */ + public ByteArrayOutputStream downloadStream(String blobName) throws IOException; + + /** + * The API to get the list blob + * */ + public List getBlobs(); + + /** + * The API to create a temporary download link with expired time + * @param blobName - The blob name + * @return - The url for download + */ + public String getDownloadLink(String blobName); + + /** + * The API to get blob client + * @param blobName - The blob name + * @return BlobClient + * */ + public BlobClient getBlobClient(String blobName); +} diff --git a/azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/AzureBlobStorageService.java b/azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/AzureBlobStorageService.java new file mode 100644 index 0000000..7134b1e --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/AzureBlobStorageService.java @@ -0,0 +1,252 @@ +package com.axonivy.connector.azure.blob.internal; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpStatus; + +import com.axonivy.connector.azure.blob.StorageService; +import com.axonivy.connector.azure.blob.internal.helper.BlobSASHelper; +import com.azure.core.http.rest.PagedResponse; +import com.azure.core.http.rest.Response; +import com.azure.core.util.BinaryData; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.BlobListDetails; +import com.azure.storage.blob.models.DeleteSnapshotsOptionType; +import com.azure.storage.blob.models.ListBlobsOptions; +import com.azure.storage.blob.options.BlobDownloadToFileOptions; +import com.azure.storage.blob.specialized.BlockBlobClient; +import com.azure.storage.common.ParallelTransferOptions; + +import ch.ivyteam.ivy.environment.Ivy; + +/** + * For first version, only upload file to demo-container. In next, maybe create + * virtual directory inside container. + */ +public class AzureBlobStorageService implements StorageService { + private static final String DATE_PATTERN = "yyyy-MM-dd"; + private BlobContainerClient destinationContainer = null; + private BlobServiceClient blobServiceClient = null; + private Duration downloadLinkLiveTime = Duration.ofHours(8); + private static final long FIVE_MB = (5 * 1024 * 1024); + private static final long FOUR_MB = (4 * 1024 * 1024); + private static final int MAX_CONCURRENCY = 4; + + /** + * @param blobServiceClient - A client to interact with the Blob Service at the account level + * @param container - The container name + */ + public AzureBlobStorageService(BlobServiceClient blobServiceClient, String container) { + this.blobServiceClient = blobServiceClient; + this.destinationContainer = getBlobContainerClient(this.blobServiceClient, container); + } + + public AzureBlobStorageService(BlobServiceClient blobServiceClient, String container, + Duration downloadLinkLiveTime) { + this.blobServiceClient = blobServiceClient; + this.destinationContainer = getBlobContainerClient(this.blobServiceClient, container); + this.downloadLinkLiveTime = downloadLinkLiveTime; + } + + @Override + public String upload(String content, String fileName) { + BlockBlobClient blockBlobClient = getBlobClient(fileName).getBlockBlobClient(); + blockBlobClient.upload(BinaryData.fromString(content)); + return blockBlobClient.getBlobName(); + } + + @Override + public String uploadFromUrl(String sourceURL, String uploadToFolder, boolean isOverwite) { + String fileName = getFileNameFromUrl(sourceURL); + String blobName = createBlobPath(uploadToFolder, fileName); + BlobClient destination = getBlobClient(blobName); + + destination.getBlockBlobClient().uploadFromUrl(sourceURL, isOverwite); + return destination.getBlockBlobClient().getBlobName(); + } + + @Override + public String uploadFromFile(String path, String uploadToFolder, boolean isOverwite) { + String fileName = FilenameUtils.getName(path); + String blobName = createBlobPath(uploadToFolder, fileName); + BlobClient blobClient = getBlobClient(blobName); + blobClient.uploadFromFile(path, isOverwite); + return blobClient.getBlockBlobClient().getBlobName(); + } + + @Override + public String getDownloadLink(String blobName) { + BlobClient blobClient = getBlobClient(blobName); + String sasToken = BlobSASHelper.createServiceSASBlob(blobClient, this.downloadLinkLiveTime); + String downloadLink = blobClient.getBlobUrl() + "?" + sasToken; + return downloadLink; + } + + @Override + public String upload(byte[] content, String fileName, String uploadToFolder, boolean isOverwite) throws Exception { + String blobName = createBlobPath(uploadToFolder, fileName); + BlockBlobClient blockBlobClient = getBlobClient(blobName).getBlockBlobClient(); + + try (ByteArrayInputStream dataStream = new ByteArrayInputStream(content)) { + blockBlobClient.upload(dataStream, content.length, isOverwite); + return blockBlobClient.getBlobName(); + } catch (Exception ex) { + throw new Exception("Upload file error. Exception message: " + ex.getMessage(), ex); + } + } + + @Override + public boolean delete(String blobName) { + BlobClient blobClient = getBlobClient(blobName); + + Response response = blobClient.deleteIfExistsWithResponse(DeleteSnapshotsOptionType.INCLUDE, null, null, null); + return response.getStatusCode() != HttpStatus.SC_NOT_FOUND; + } + + @Override + public void restore(String blobName) { + BlobClient blobClient = getBlobClient(blobName); + blobClient.undelete(); + } + + @Override + public byte[] downloadContent(String blobName) { + BlockBlobClient blockBlobClient = getBlobClient(blobName).getBlockBlobClient(); + BinaryData data = blockBlobClient.downloadContent(); + return data.toBytes(); + } + + @Override + public void downloadToFile(String blobName, String filePath) { + BlockBlobClient blockBlobClient = getBlobClient(blobName).getBlockBlobClient(); + long blogSize = blockBlobClient.getProperties().getBlobSize(); + if (blogSize >= FIVE_MB) { + downloadFileWithLargeSize(blobName, filePath); + } else { + blockBlobClient.downloadToFile(filePath); + } + } + + @Override + public ByteArrayOutputStream downloadStream(String blobName) throws IOException { + BlockBlobClient blockBlobClient = getBlobClient(blobName).getBlockBlobClient(); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + blockBlobClient.downloadStream(outputStream); + return outputStream; + } catch (IOException e) { + throw new IOException ("download file error. Exception message: " + e.getMessage(), e); + } + } + + @Override + public BlobClient getBlobClient(String blobName) { + return this.destinationContainer.getBlobClient(blobName); + } + + private void downloadFileWithLargeSize(String blobName, String filePath) { + BlockBlobClient blockBlobClient = getBlobClient(blobName).getBlockBlobClient(); + ParallelTransferOptions parallelTransferOptions = new ParallelTransferOptions() + .setBlockSizeLong(FOUR_MB) + .setMaxConcurrency(MAX_CONCURRENCY); + + BlobDownloadToFileOptions options = new BlobDownloadToFileOptions(filePath); + options.setParallelTransferOptions(parallelTransferOptions); + + blockBlobClient.downloadToFileWithResponse(options, null, null); + } + + private BlobContainerClient getBlobContainerClient(BlobServiceClient blobServiceClient, String container) { + if (ObjectUtils.anyNull(container)) { + throw new NullPointerException("The container are not allow null."); + } + + BlobContainerClient blobContainerClient = blobServiceClient.createBlobContainerIfNotExists(container); + return blobContainerClient; + } + + private static String getFileNameFromUrl(String url) { + try { + return Paths.get(new URI(url).getPath()).getFileName().toString(); + } catch (URISyntaxException e) { + Ivy.log().warn("Can not get file name from " + url); + } + return StringUtils.EMPTY; + } + + private String createBlobPath(String folderName, String fileName) { + String path = StringUtils.isNotBlank(folderName) ? folderName + "/" : StringUtils.EMPTY; + return path + fileName; + } + + @Override + public String uploadFromUrl(String url) { + String blobName = getFileNameFromUrl(url); + BlobClient destination = getBlobClient(blobName); + destination.getBlockBlobClient().uploadFromUrl(url, false); + return destination.getBlockBlobClient().getBlobName(); + } + + @Override + public String uploadFromFile(String path) { + String blobName = FilenameUtils.getName(path); + BlobClient blobClient = getBlobClient(blobName); + blobClient.uploadFromFile(path); + return blobClient.getBlockBlobClient().getBlobName(); + } + + @Override + public String upload(byte[] content, String fileName) throws Exception { + BlockBlobClient blockBlobClient = getBlobClient(fileName).getBlockBlobClient(); + + try (ByteArrayInputStream dataStream = new ByteArrayInputStream(content)) { + blockBlobClient.upload(dataStream, content.length); + return blockBlobClient.getBlobName(); + } catch (Exception ex) { + throw new Exception("Upload file error. Exception message: " + ex.getMessage(), ex); + } + } + + @Override + public List getBlobs() { + List blobItems = new ArrayList<>(); + ListBlobsOptions options = new ListBlobsOptions() + .setDetails(new BlobListDetails().setRetrieveDeletedBlobs(true)); + Iterable> blobPages = destinationContainer.listBlobs(options, null).iterableByPage(); + for (PagedResponse page : blobPages) { + page.getElements().forEach(blob -> blobItems.add(blob)); + } + return blobItems; + } + + @Override + public void delete(Date date) { + List bi = destinationContainer.listBlobs().stream() + .filter(blobItem -> isSameDate(blobItem, date)) + .collect(Collectors.toList()); + bi.forEach(blob -> delete(blob.getName())); + } + + private boolean isSameDate(BlobItem blobItem, Date date) { + String creationTime2String = blobItem.getProperties().getCreationTime().format(DateTimeFormatter.ofPattern(DATE_PATTERN)); + String date2String = new SimpleDateFormat(DATE_PATTERN).format(date); + return creationTime2String.equals(date2String); + } +} diff --git a/azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/bean/AzureBlobStorageBean.java b/azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/bean/AzureBlobStorageBean.java new file mode 100644 index 0000000..a97a5bc --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/bean/AzureBlobStorageBean.java @@ -0,0 +1,36 @@ +package com.axonivy.connector.azure.blob.internal.bean; + +import com.axonivy.connector.azure.blob.BlobServiceClientHelper; +import com.axonivy.connector.azure.blob.StorageService; +import com.axonivy.connector.azure.blob.internal.AzureBlobStorageService; +import com.azure.storage.blob.BlobServiceClient; + +import ch.ivyteam.ivy.environment.Ivy; + +public class AzureBlobStorageBean { + + private StorageService azureBlobStorageService; + + public AzureBlobStorageBean() { + String clientId = Ivy.var().get("AzureBlob.ClientId"); + String clientSecret = Ivy.var().get("AzureBlob.ClientSecret"); + String tenantId = Ivy.var().get("AzureBlob.TenantId"); + String endPoint = Ivy.var().get("AzureBlob.EndPoint"); + String containerName = Ivy.var().get("AzureBlob.ContainterName"); + + BlobServiceClient blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(clientId, clientSecret, tenantId, endPoint); + azureBlobStorageService = new AzureBlobStorageService(blobServiceClient, containerName); + } + + public StorageService getAzureBlobStorageService() { + return azureBlobStorageService; + } + + public void setAzureBlobStorageService(StorageService azureBlobStorageService) { + this.azureBlobStorageService = azureBlobStorageService; + } + + public boolean isBlobExist(String blobName) { + return azureBlobStorageService.getBlobClient(blobName).exists(); + } +} diff --git a/azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/helper/BlobSASHelper.java b/azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/helper/BlobSASHelper.java new file mode 100644 index 0000000..cbf1adf --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/helper/BlobSASHelper.java @@ -0,0 +1,32 @@ +package com.axonivy.connector.azure.blob.internal.helper; + +import java.time.Duration; +import java.time.OffsetDateTime; + +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.blob.sas.BlobSasPermission; +import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; + +public class BlobSASHelper { + + public static String createServiceSASBlob(BlobClient blobClient, Duration liveTime) { + + OffsetDateTime now = OffsetDateTime.now(); + UserDelegationKey key = blobClient.getBlockBlobClient() + .getContainerClient() + .getServiceClient() + .getUserDelegationKey(now.minusMinutes(1), now.plusSeconds(liveTime.getSeconds())); + + OffsetDateTime expiryTime = OffsetDateTime.now().plusSeconds(liveTime.toSeconds()); + + // Assign read permissions to the SAS token + BlobSasPermission sasPermission = new BlobSasPermission().setReadPermission(true); + + var sasSignatureValues = new BlobServiceSasSignatureValues(expiryTime, sasPermission) + .setStartTime(now.minusMinutes(1)); + + String sasToken = blobClient.generateUserDelegationSas(sasSignatureValues, key); + return sasToken; + } +} diff --git a/azure-blob-connector/webContent/icon/azure-blob-icon.png b/azure-blob-connector/webContent/icon/azure-blob-icon.png new file mode 100644 index 0000000..0130ca2 Binary files /dev/null and b/azure-blob-connector/webContent/icon/azure-blob-icon.png differ diff --git a/local/.env b/local/.env new file mode 100644 index 0000000..b330665 --- /dev/null +++ b/local/.env @@ -0,0 +1 @@ +COMPOSE_PROJECT_NAME=market-azure-blob-connector diff --git a/local/docker-compose.yml b/local/docker-compose.yml new file mode 100644 index 0000000..4129bf0 --- /dev/null +++ b/local/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.9" +services: + azurite: + image: mcr.microsoft.com/azure-storage/azurite:3.9.0 + container_name: "azurite" + hostname: azurite + restart: always + ports: + - "10000:10000" + - "10001:10001" + - "10002:10002" diff --git a/pom.xml b/pom.xml index 517336b..cc3cfbd 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,9 @@ 4.0.0 - com.axonivy.market - my-product - my-product-modules - 10.0.0-SNAPSHOT + com.axonivy.connector.azure.blob + azure-blob-connector + azure-blob-connector-modules + 10.0.22-SNAPSHOT pom