From 2a6334ca9bbeeb673c1b08283551a162d2092a3d Mon Sep 17 00:00:00 2001 From: Trung Mai Date: Wed, 15 May 2024 10:13:02 +0700 Subject: [PATCH 01/13] TE-581: Create base project --- .editorconfig | 12 ++ .gitignore | 18 ++ .project | 17 ++ .settings/org.eclipse.m2e.core.prefs | 4 + LICENSE | 201 ++++++++++++++++++ README.md | 15 ++ azure-blob-connector-demo/.classpath | 28 +++ azure-blob-connector-demo/.gitignore | 19 ++ azure-blob-connector-demo/.project | 49 +++++ .../.settings/.jsdtscope | 12 ++ .../.settings/ch.ivyteam.ivy.designer.prefs | 5 + .../.settings/org.eclipse.jdt.core.prefs | 10 + .../org.eclipse.wst.common.component | 10 + ...se.wst.common.project.facet.core.prefs.xml | 7 + ....eclipse.wst.common.project.facet.core.xml | 8 + .../.settings/org.eclipse.wst.css.core.prefs | 2 + ...rg.eclipse.wst.jsdt.ui.superType.container | 1 + .../org.eclipse.wst.jsdt.ui.superType.name | 1 + .../config/custom-fields.yaml | 22 ++ .../config/databases.yaml | 2 + .../config/overrides.any | 1 + .../config/persistence.xml | 2 + .../config/rest-clients.yaml | 2 + azure-blob-connector-demo/config/roles.xml | 4 + azure-blob-connector-demo/config/users.xml | 2 + .../config/variables.yaml | 6 + .../config/webservice-clients.yaml | 2 + .../azure/blob/connector/demo/Data.ivyClass | 2 + azure-blob-connector-demo/pom.xml | 19 ++ azure-blob-connector-product/.classpath | 28 +++ azure-blob-connector-product/.gitignore | 19 ++ azure-blob-connector-product/.project | 49 +++++ .../.settings/.jsdtscope | 12 ++ .../.settings/ch.ivyteam.ivy.designer.prefs | 5 + .../.settings/org.eclipse.jdt.core.prefs | 10 + .../org.eclipse.wst.common.component | 10 + ...se.wst.common.project.facet.core.prefs.xml | 7 + ....eclipse.wst.common.project.facet.core.xml | 8 + .../.settings/org.eclipse.wst.css.core.prefs | 2 + ...rg.eclipse.wst.jsdt.ui.superType.container | 1 + .../org.eclipse.wst.jsdt.ui.superType.name | 1 + azure-blob-connector-product/README.md | 3 + azure-blob-connector-product/pom.xml | 67 ++++++ azure-blob-connector-product/product.json | 70 ++++++ azure-blob-connector-product/zip.xml | 26 +++ azure-blob-connector-test/.classpath | 28 +++ azure-blob-connector-test/.gitignore | 19 ++ azure-blob-connector-test/.project | 49 +++++ .../.settings/.jsdtscope | 12 ++ .../.settings/ch.ivyteam.ivy.designer.prefs | 5 + .../.settings/org.eclipse.jdt.core.prefs | 10 + .../org.eclipse.wst.common.component | 10 + ...se.wst.common.project.facet.core.prefs.xml | 7 + ....eclipse.wst.common.project.facet.core.xml | 8 + .../.settings/org.eclipse.wst.css.core.prefs | 2 + ...rg.eclipse.wst.jsdt.ui.superType.container | 1 + .../org.eclipse.wst.jsdt.ui.superType.name | 1 + .../config/custom-fields.yaml | 22 ++ .../config/databases.yaml | 2 + .../config/overrides.any | 1 + .../config/persistence.xml | 2 + .../config/rest-clients.yaml | 2 + azure-blob-connector-test/config/roles.xml | 4 + azure-blob-connector-test/config/users.xml | 2 + .../config/variables.yaml | 6 + .../config/webservice-clients.yaml | 2 + .../azure/blob/connector/test/Data.ivyClass | 2 + azure-blob-connector-test/pom.xml | 19 ++ azure-blob-connector/.classpath | 28 +++ azure-blob-connector/.gitignore | 19 ++ azure-blob-connector/.project | 49 +++++ azure-blob-connector/.settings/.jsdtscope | 12 ++ .../.settings/ch.ivyteam.ivy.designer.prefs | 5 + .../.settings/org.eclipse.jdt.core.prefs | 10 + .../org.eclipse.wst.common.component | 10 + ...se.wst.common.project.facet.core.prefs.xml | 7 + ....eclipse.wst.common.project.facet.core.xml | 8 + .../.settings/org.eclipse.wst.css.core.prefs | 2 + ...rg.eclipse.wst.jsdt.ui.superType.container | 1 + .../org.eclipse.wst.jsdt.ui.superType.name | 1 + azure-blob-connector/pom.xml | 71 +++++++ pom.xml | 43 ++++ 82 files changed, 1283 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .project create mode 100644 .settings/org.eclipse.m2e.core.prefs create mode 100644 LICENSE create mode 100644 README.md create mode 100644 azure-blob-connector-demo/.classpath create mode 100644 azure-blob-connector-demo/.gitignore create mode 100644 azure-blob-connector-demo/.project create mode 100644 azure-blob-connector-demo/.settings/.jsdtscope create mode 100644 azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs create mode 100644 azure-blob-connector-demo/.settings/org.eclipse.jdt.core.prefs create mode 100644 azure-blob-connector-demo/.settings/org.eclipse.wst.common.component create mode 100644 azure-blob-connector-demo/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml create mode 100644 azure-blob-connector-demo/.settings/org.eclipse.wst.common.project.facet.core.xml create mode 100644 azure-blob-connector-demo/.settings/org.eclipse.wst.css.core.prefs create mode 100644 azure-blob-connector-demo/.settings/org.eclipse.wst.jsdt.ui.superType.container create mode 100644 azure-blob-connector-demo/.settings/org.eclipse.wst.jsdt.ui.superType.name create mode 100644 azure-blob-connector-demo/config/custom-fields.yaml create mode 100644 azure-blob-connector-demo/config/databases.yaml create mode 100644 azure-blob-connector-demo/config/overrides.any create mode 100644 azure-blob-connector-demo/config/persistence.xml create mode 100644 azure-blob-connector-demo/config/rest-clients.yaml create mode 100644 azure-blob-connector-demo/config/roles.xml create mode 100644 azure-blob-connector-demo/config/users.xml create mode 100644 azure-blob-connector-demo/config/variables.yaml create mode 100644 azure-blob-connector-demo/config/webservice-clients.yaml create mode 100644 azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/demo/Data.ivyClass create mode 100644 azure-blob-connector-demo/pom.xml create mode 100644 azure-blob-connector-product/.classpath create mode 100644 azure-blob-connector-product/.gitignore create mode 100644 azure-blob-connector-product/.project create mode 100644 azure-blob-connector-product/.settings/.jsdtscope create mode 100644 azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs create mode 100644 azure-blob-connector-product/.settings/org.eclipse.jdt.core.prefs create mode 100644 azure-blob-connector-product/.settings/org.eclipse.wst.common.component create mode 100644 azure-blob-connector-product/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml create mode 100644 azure-blob-connector-product/.settings/org.eclipse.wst.common.project.facet.core.xml create mode 100644 azure-blob-connector-product/.settings/org.eclipse.wst.css.core.prefs create mode 100644 azure-blob-connector-product/.settings/org.eclipse.wst.jsdt.ui.superType.container create mode 100644 azure-blob-connector-product/.settings/org.eclipse.wst.jsdt.ui.superType.name create mode 100644 azure-blob-connector-product/README.md create mode 100644 azure-blob-connector-product/pom.xml create mode 100644 azure-blob-connector-product/product.json create mode 100644 azure-blob-connector-product/zip.xml create mode 100644 azure-blob-connector-test/.classpath create mode 100644 azure-blob-connector-test/.gitignore create mode 100644 azure-blob-connector-test/.project create mode 100644 azure-blob-connector-test/.settings/.jsdtscope create mode 100644 azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs create mode 100644 azure-blob-connector-test/.settings/org.eclipse.jdt.core.prefs create mode 100644 azure-blob-connector-test/.settings/org.eclipse.wst.common.component create mode 100644 azure-blob-connector-test/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml create mode 100644 azure-blob-connector-test/.settings/org.eclipse.wst.common.project.facet.core.xml create mode 100644 azure-blob-connector-test/.settings/org.eclipse.wst.css.core.prefs create mode 100644 azure-blob-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.container create mode 100644 azure-blob-connector-test/.settings/org.eclipse.wst.jsdt.ui.superType.name create mode 100644 azure-blob-connector-test/config/custom-fields.yaml create mode 100644 azure-blob-connector-test/config/databases.yaml create mode 100644 azure-blob-connector-test/config/overrides.any create mode 100644 azure-blob-connector-test/config/persistence.xml create mode 100644 azure-blob-connector-test/config/rest-clients.yaml create mode 100644 azure-blob-connector-test/config/roles.xml create mode 100644 azure-blob-connector-test/config/users.xml create mode 100644 azure-blob-connector-test/config/variables.yaml create mode 100644 azure-blob-connector-test/config/webservice-clients.yaml create mode 100644 azure-blob-connector-test/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/test/Data.ivyClass create mode 100644 azure-blob-connector-test/pom.xml create mode 100644 azure-blob-connector/.classpath create mode 100644 azure-blob-connector/.gitignore create mode 100644 azure-blob-connector/.project create mode 100644 azure-blob-connector/.settings/.jsdtscope create mode 100644 azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs create mode 100644 azure-blob-connector/.settings/org.eclipse.jdt.core.prefs create mode 100644 azure-blob-connector/.settings/org.eclipse.wst.common.component create mode 100644 azure-blob-connector/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml create mode 100644 azure-blob-connector/.settings/org.eclipse.wst.common.project.facet.core.xml create mode 100644 azure-blob-connector/.settings/org.eclipse.wst.css.core.prefs create mode 100644 azure-blob-connector/.settings/org.eclipse.wst.jsdt.ui.superType.container create mode 100644 azure-blob-connector/.settings/org.eclipse.wst.jsdt.ui.superType.name create mode 100644 azure-blob-connector/pom.xml create mode 100644 pom.xml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..74d7800 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_spaces = true +insert_final_newline = true + +# trailing spaces in markdown indicate word wrap +[*.md] +trim_trailing_spaces = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6cc0de --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# general +Thumbs.db +.DS_Store +*~ +*.log + +# java +*.class +hs_err_pid* + +# maven +target/ + +# ivy +lib/mvn-deps/ +logs/ +src_dataClasses/ +src_wsproc/ 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/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b87f833 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Advanced Process Analyzer + +[![CI Build](https://github.com/axonivy-professional-services/market-process-analyzer/actions/workflows/ci.yml/badge.svg)](https://github.com/axonivy-professional-services/market-process-analyzer/actions/workflows/ci.yml) + +- Configure needed information directly in the process model + - Default duration of a task for multiple use cases. Each task can have multiple named default durations. + - Different “happy path” flows. It’s possible to set multiple named process paths. +- Possibilities to override settings of the process model + - Override duration + - Override default path for the gateways +- Create a list of all tasks in the process. +- Get configured duration for a task. +- Get all upcoming tasks on a configured process path with expected start timestamp for each task. + +Read our [documentation](process-analyzer-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..1e40de3 --- /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.cloud.storage.azure.blob.connector.demo.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector.demo +ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=112000 +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..fd14458 --- /dev/null +++ b/azure-blob-connector-demo/config/variables.yaml @@ -0,0 +1,6 @@ +# == Variables == +# +# You can define here your project Variables. +# +Variables: +# myVariable: value 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/cloud/storage/azure/blob/connector/demo/Data.ivyClass b/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/demo/Data.ivyClass new file mode 100644 index 0000000..78024a7 --- /dev/null +++ b/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/demo/Data.ivyClass @@ -0,0 +1,2 @@ +Data #class +com.axonivy.cloud.storage.azure.blob.connector.demo #namespace diff --git a/azure-blob-connector-demo/pom.xml b/azure-blob-connector-demo/pom.xml new file mode 100644 index 0000000..89ae415 --- /dev/null +++ b/azure-blob-connector-demo/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + com.axonivy.cloud.storage + azure-blob-connector-demo + 11.2.1-SNAPSHOT + iar + + + + com.axonivy.ivy.ci + project-build-plugin + 11.2.0 + true + + + + diff --git a/azure-blob-connector-product/.classpath b/azure-blob-connector-product/.classpath new file mode 100644 index 0000000..a24d7cc --- /dev/null +++ b/azure-blob-connector-product/.classpath @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector-product/.gitignore b/azure-blob-connector-product/.gitignore new file mode 100644 index 0000000..1b2547b --- /dev/null +++ b/azure-blob-connector-product/.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-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..fdac6ba --- /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.cloud.storage.azure.blob.connector.product.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector.product +ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=112000 +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..d8c5a11 --- /dev/null +++ b/azure-blob-connector-product/.settings/org.eclipse.wst.common.component @@ -0,0 +1,10 @@ + + + + + + + + + + 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..0b54ff5 --- /dev/null +++ b/azure-blob-connector-product/README.md @@ -0,0 +1,3 @@ +# Azure Blob Connector + + diff --git a/azure-blob-connector-product/pom.xml b/azure-blob-connector-product/pom.xml new file mode 100644 index 0000000..5981dcd --- /dev/null +++ b/azure-blob-connector-product/pom.xml @@ -0,0 +1,67 @@ + + 4.0.0 + com.axonivy.cloud.storage + azure-blob-connector-product + 11.2.1-SNAPSHOT + pom + + + ../workflow-estimator/config/variables.yaml + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + package + + single + + + false + + zip.xml + + + + + + + maven-antrun-plugin + 1.7 + + + generate-sources + + ${skip-readme} + + + + + + + + + + + run + + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.0.0-M1 + + + + + diff --git a/azure-blob-connector-product/product.json b/azure-blob-connector-product/product.json new file mode 100644 index 0000000..074ccfb --- /dev/null +++ b/azure-blob-connector-product/product.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", + "installers": [ + { + "id": "maven-import", + "data": { + "projects": [ + { + "groupId": "com.axonivy.utils.process.analyzer.demo", + "artifactId": "process-analyzer-demo", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + }, + { + "id": "maven-dependency", + "data": { + "dependencies": [ + { + "groupId": "com.axonivy.utils.process.analyzer", + "artifactId": "process-analyzer", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + }, + { + "id": "maven-dropins", + "data": { + "dependencies": [ + { + "groupId": "com.axonivy.utils.process.analyzer", + "artifactId": "process-analyzer", + "version": "${version}" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + } + ] +} diff --git a/azure-blob-connector-product/zip.xml b/azure-blob-connector-product/zip.xml new file mode 100644 index 0000000..003f06c --- /dev/null +++ b/azure-blob-connector-product/zip.xml @@ -0,0 +1,26 @@ + + zip + false + + + zip + + + + + . + + product.json + openapi.json + **/*.png + + + + target + / + + README.md + + + + diff --git a/azure-blob-connector-test/.classpath b/azure-blob-connector-test/.classpath new file mode 100644 index 0000000..a24d7cc --- /dev/null +++ b/azure-blob-connector-test/.classpath @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector-test/.gitignore b/azure-blob-connector-test/.gitignore new file mode 100644 index 0000000..1b2547b --- /dev/null +++ b/azure-blob-connector-test/.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-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..6ff2f05 --- /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.cloud.storage.azure.blob.connector.test.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector.test +ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=112000 +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..601fca9 --- /dev/null +++ b/azure-blob-connector-test/.settings/org.eclipse.wst.common.component @@ -0,0 +1,10 @@ + + + + + + + + + + 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/config/custom-fields.yaml b/azure-blob-connector-test/config/custom-fields.yaml new file mode 100644 index 0000000..bb20b70 --- /dev/null +++ b/azure-blob-connector-test/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-test/config/databases.yaml b/azure-blob-connector-test/config/databases.yaml new file mode 100644 index 0000000..10319e2 --- /dev/null +++ b/azure-blob-connector-test/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-test/config/overrides.any b/azure-blob-connector-test/config/overrides.any new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/azure-blob-connector-test/config/overrides.any @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/azure-blob-connector-test/config/persistence.xml b/azure-blob-connector-test/config/persistence.xml new file mode 100644 index 0000000..d6b96d7 --- /dev/null +++ b/azure-blob-connector-test/config/persistence.xml @@ -0,0 +1,2 @@ + + diff --git a/azure-blob-connector-test/config/rest-clients.yaml b/azure-blob-connector-test/config/rest-clients.yaml new file mode 100644 index 0000000..4bffaca --- /dev/null +++ b/azure-blob-connector-test/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-test/config/roles.xml b/azure-blob-connector-test/config/roles.xml new file mode 100644 index 0000000..59892fe --- /dev/null +++ b/azure-blob-connector-test/config/roles.xml @@ -0,0 +1,4 @@ + + + Everybody + diff --git a/azure-blob-connector-test/config/users.xml b/azure-blob-connector-test/config/users.xml new file mode 100644 index 0000000..51a6906 --- /dev/null +++ b/azure-blob-connector-test/config/users.xml @@ -0,0 +1,2 @@ + + diff --git a/azure-blob-connector-test/config/variables.yaml b/azure-blob-connector-test/config/variables.yaml new file mode 100644 index 0000000..fd14458 --- /dev/null +++ b/azure-blob-connector-test/config/variables.yaml @@ -0,0 +1,6 @@ +# == Variables == +# +# You can define here your project Variables. +# +Variables: +# myVariable: value diff --git a/azure-blob-connector-test/config/webservice-clients.yaml b/azure-blob-connector-test/config/webservice-clients.yaml new file mode 100644 index 0000000..688047a --- /dev/null +++ b/azure-blob-connector-test/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-test/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/test/Data.ivyClass b/azure-blob-connector-test/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/test/Data.ivyClass new file mode 100644 index 0000000..8d45909 --- /dev/null +++ b/azure-blob-connector-test/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/test/Data.ivyClass @@ -0,0 +1,2 @@ +Data #class +com.axonivy.cloud.storage.azure.blob.connector.test #namespace diff --git a/azure-blob-connector-test/pom.xml b/azure-blob-connector-test/pom.xml new file mode 100644 index 0000000..30e5fb0 --- /dev/null +++ b/azure-blob-connector-test/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + com.axonivy.cloud.storage + azure-blob-connector-test + 11.2.1-SNAPSHOT + iar + + + + com.axonivy.ivy.ci + project-build-plugin + 11.2.0 + true + + + + diff --git a/azure-blob-connector/.classpath b/azure-blob-connector/.classpath new file mode 100644 index 0000000..a24d7cc --- /dev/null +++ b/azure-blob-connector/.classpath @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector/.gitignore b/azure-blob-connector/.gitignore new file mode 100644 index 0000000..1b2547b --- /dev/null +++ b/azure-blob-connector/.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/.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..518d13e --- /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.cloud.storage.azure.blob.connector.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector +ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=112000 +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..9935c96 --- /dev/null +++ b/azure-blob-connector/.settings/org.eclipse.wst.common.component @@ -0,0 +1,10 @@ + + + + + + + + + + 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/pom.xml b/azure-blob-connector/pom.xml new file mode 100644 index 0000000..935aa56 --- /dev/null +++ b/azure-blob-connector/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + com.axonivy.cloud.storage + azure-blob-connector + 11.2.1-SNAPSHOT + iar + + 11.2.0 + + + + + always + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + + + src + + + com.axonivy.ivy.ci + project-build-plugin + ${build.plugin.version} + true + + + maven-assembly-plugin + 3.4.2 + + + make-assembly + package + + single + + + false + + jar.xml + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar-no-fork + + + + + + maven-release-plugin + 3.0.0-M4 + + process-analyzer-v@{project.version} + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..37e0e01 --- /dev/null +++ b/pom.xml @@ -0,0 +1,43 @@ + + 4.0.0 + com.axonivy.cloud.storage + azure-blob-connector + azure-blob-connector-modules + 11.2.1-SNAPSHOT + pom + + + ${project.name} + ${project.name}-demo + ${project.name}-test + ${project.name}-product + + + + scm:git:https://github.com/axonivy-professional-services/market-${project.name}.git + HEAD + + + + + + + maven-deploy-plugin + 3.0.0-M1 + + true + + + + org.apache.maven.plugins + maven-release-plugin + 3.0.0-M4 + + v@{project.version} + + + + + + + From ed9e9b455751f149cf4f8943816cbc120c7790cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Phan=20Th=E1=BB=8B=20Ng=C3=A2n=20H=C3=A0?= <127711713+NganHaPhan@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:15:32 +0700 Subject: [PATCH 02/13] TE-617: branch is changed --- .github/workflows/ci.yml | 12 + .github/workflows/dev.yml | 11 + .github/workflows/release.yml | 7 + Makefile | 12 + README.md | 17 +- SECURITY.md | 25 + .../.settings/ch.ivyteam.ivy.designer.prefs | 6 +- azure-blob-connector-demo/README.md | 40 + .../config/variables.yaml | 10 +- .../azure/blob/connector/demo/Data.ivyClass | 2 - .../axonivy/cloud/storage/demo/Data.ivyClass | 2 + .../cloud/storage/demo/UploadData.ivyClass | 2 + azure-blob-connector-demo/pom.xml | 44 +- .../processes/Start Processes/Upload.p.json | 71 ++ .../com/axonivy/cloud/storage/bean/Blob.java | 21 + .../cloud/storage/bean/UploadBean.java | 310 ++++++ .../storage/bean/UploadByCallSubprocess.java | 243 +++++ .../cloud/storage/utils/UploadUtils.java | 21 + .../storage/demo/Upload/Upload.rddescriptor | 7 + .../cloud/storage/demo/Upload/Upload.xhtml | 152 +++ .../storage/demo/Upload/UploadData.ivyClass | 6 + .../storage/demo/Upload/UploadProcess.p.json | 417 ++++++++ .../demo/Upload/resources/uploadDialog.css | 22 + .../UploadByCallSubprocess.rddescriptor | 7 + .../UploadByCallSubprocess.xhtml | 152 +++ .../UploadByCallSubprocessData.ivyClass | 8 + .../UploadByCallSubprocessProcess.p.json | 976 ++++++++++++++++++ .../resources/uploadDialog.css | 22 + .../webContent/layouts/basic-10.xhtml | 67 ++ .../layouts/includes/exception-details.xhtml | 109 ++ .../layouts/includes/exception.xhtml | 47 + .../webContent/layouts/includes/footer.xhtml | 18 + .../layouts/includes/progress-loader.xhtml | 15 + .../.settings/ch.ivyteam.ivy.designer.prefs | 2 +- azure-blob-connector-product/README.md | 95 ++ azure-blob-connector-product/pom.xml | 2 +- azure-blob-connector-product/product.json | 29 +- azure-blob-connector-test/.classpath | 64 +- .../.settings/ch.ivyteam.ivy.designer.prefs | 2 +- .../org.eclipse.wst.common.component | 10 + .../config/custom-fields.yaml | 22 - .../config/databases.yaml | 2 - .../config/overrides.any | 1 - .../config/persistence.xml | 2 - .../config/rest-clients.yaml | 2 - azure-blob-connector-test/config/roles.xml | 4 - azure-blob-connector-test/config/users.xml | 2 - .../config/variables.yaml | 6 - .../config/webservice-clients.yaml | 2 - .../azure/blob/connector/test/Data.ivyClass | 2 - azure-blob-connector-test/pom.xml | 64 +- .../resource_test/picture/singapore.png | Bin 0 -> 9197 bytes .../integration/AbstractIntegrationTest.java | 52 + .../AzureBlobStorageServiceTest.java | 131 +++ azure-blob-connector/.classpath | 3 +- azure-blob-connector/.gitignore | 1 + .../.settings/ch.ivyteam.ivy.designer.prefs | 2 +- .../org.eclipse.wst.common.component | 10 +- azure-blob-connector/README.md | 36 + .../blob/connector/BlobStorageData.ivyClass | 13 + azure-blob-connector/pom.xml | 93 +- .../processes/BlobStorage.p.json | 424 ++++++++ .../connector/BlobServiceClientHelper.java | 66 ++ .../azure/blob/connector/StorageService.java | 120 +++ .../internal/AzureBlobStorageService.java | 248 +++++ .../internal/helper/BlobSASHelper.java | 32 + local/.env | 1 + local/docker-compose.yml | 11 + pom.xml | 2 +- 69 files changed, 4234 insertions(+), 205 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/dev.yml create mode 100644 .github/workflows/release.yml create mode 100644 Makefile create mode 100644 SECURITY.md create mode 100644 azure-blob-connector-demo/README.md delete mode 100644 azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/demo/Data.ivyClass create mode 100644 azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/Data.ivyClass create mode 100644 azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/UploadData.ivyClass create mode 100644 azure-blob-connector-demo/processes/Start Processes/Upload.p.json create mode 100644 azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/Blob.java create mode 100644 azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java create mode 100644 azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java create mode 100644 azure-blob-connector-demo/src/com/axonivy/cloud/storage/utils/UploadUtils.java create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.rddescriptor create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.xhtml create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadData.ivyClass create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadProcess.p.json create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/resources/uploadDialog.css create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/resources/uploadDialog.css create mode 100644 azure-blob-connector-demo/webContent/layouts/basic-10.xhtml create mode 100644 azure-blob-connector-demo/webContent/layouts/includes/exception-details.xhtml create mode 100644 azure-blob-connector-demo/webContent/layouts/includes/exception.xhtml create mode 100644 azure-blob-connector-demo/webContent/layouts/includes/footer.xhtml create mode 100644 azure-blob-connector-demo/webContent/layouts/includes/progress-loader.xhtml delete mode 100644 azure-blob-connector-test/config/custom-fields.yaml delete mode 100644 azure-blob-connector-test/config/databases.yaml delete mode 100644 azure-blob-connector-test/config/overrides.any delete mode 100644 azure-blob-connector-test/config/persistence.xml delete mode 100644 azure-blob-connector-test/config/rest-clients.yaml delete mode 100644 azure-blob-connector-test/config/roles.xml delete mode 100644 azure-blob-connector-test/config/users.xml delete mode 100644 azure-blob-connector-test/config/variables.yaml delete mode 100644 azure-blob-connector-test/config/webservice-clients.yaml delete mode 100644 azure-blob-connector-test/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/test/Data.ivyClass create mode 100644 azure-blob-connector-test/resource_test/picture/singapore.png create mode 100644 azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AbstractIntegrationTest.java create mode 100644 azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AzureBlobStorageServiceTest.java create mode 100644 azure-blob-connector/README.md create mode 100644 azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass create mode 100644 azure-blob-connector/processes/BlobStorage.p.json create mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/BlobServiceClientHelper.java create mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java create mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java create mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/helper/BlobSASHelper.java create mode 100644 local/.env create mode 100644 local/docker-compose.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1bc3496 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,12 @@ +name: CI-Build + +on: + push: + pull_request: + schedule: + - cron: '21 21 * * *' + workflow_dispatch: + +jobs: + build: + uses: axonivy-market/github-workflows/.github/workflows/ci.yml@v2 diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..a27a639 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,11 @@ +name: Dev-Build + +on: + push: + schedule: + - cron: '21 21 * * *' + workflow_dispatch: + +jobs: + build: + uses: axonivy-market/github-workflows/.github/workflows/dev.yml@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..128a183 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,7 @@ +name: Release-Build + +on: workflow_dispatch + +jobs: + build: + uses: axonivy-market/github-workflows/.github/workflows/release.yml@v2 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 b87f833..b7dc1a8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,8 @@ -# Advanced Process Analyzer +# Azure Blob Connector -[![CI Build](https://github.com/axonivy-professional-services/market-process-analyzer/actions/workflows/ci.yml/badge.svg)](https://github.com/axonivy-professional-services/market-process-analyzer/actions/workflows/ci.yml) +[![CI Build](https://github.com/axonivy-professional-services/market-azure-blob-connector/actions/workflows/ci.yml/badge.svg)](https://github.com/axonivy-professional-services/market-market-azure-blob-connector/actions/workflows/ci.yml) -- Configure needed information directly in the process model - - Default duration of a task for multiple use cases. Each task can have multiple named default durations. - - Different “happy path” flows. It’s possible to set multiple named process paths. -- Possibilities to override settings of the process model - - Override duration - - Override default path for the gateways -- Create a list of all tasks in the process. -- Get configured duration for a task. -- Get all upcoming tasks on a configured process path with expected start timestamp for each task. +- Upload file to Azure blob +- Get temporary download link -Read our [documentation](process-analyzer-product/README.md). +Read our [documentation](azure-blob-connector-product/README.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1d4c06f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +## Reporting a Vulnerability + +At Axon Ivy, we take security seriously. If you believe you've found a security vulnerability in our software, we encourage you to let us know right away. We investigate all reported vulnerabilities promptly. + +To report a vulnerability, please send an email to [security@axonivy.com](mailto:security@axonivy.com) with the following information: + +- Description of the vulnerability +- Steps to reproduce the vulnerability +- Any additional information or context that may be helpful + +Please refrain from publicly disclosing the vulnerability until it has been addressed by our team. + +## Response Time + +We strive to respond to security vulnerability reports as quickly as possible. Upon receiving your report, we will acknowledge it within 72 hours and we will release a patch as soon as possible depending on complexity, but historically within a few days. +Please report (suspected) security vulnerabilities at https://support.axonivy.com/. + + +## Responsible Disclosure + +We encourage responsible disclosure of security vulnerabilities. We believe that working together with security researchers and the broader community helps us improve the security of our software for everyone. + +## Contact + +For any questions or concerns regarding security, please contact us at [security@axonivy.com](mailto:security@axonivy.com). diff --git a/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs index 1e40de3..6ea0e31 100644 --- a/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs +++ b/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs @@ -1,5 +1,5 @@ -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.cloud.storage.azure.blob.connector.demo.Data -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector.demo +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.cloud.storage.demo.Data +ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.demo ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 -ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=112000 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=100000 eclipse.preferences.version=1 diff --git a/azure-blob-connector-demo/README.md b/azure-blob-connector-demo/README.md new file mode 100644 index 0000000..1091673 --- /dev/null +++ b/azure-blob-connector-demo/README.md @@ -0,0 +1,40 @@ +# Azure Blob Connector Demo + +To run the demo module, you need to provide some information to create credential authenticates. + +## Run directly with Azure Portal + +In the project, you only add the dependency in your pom.xml and call public APIs + +** Below is an example for connect by client secret ** +```yaml +Variables: + CLIENT_ID: 'value' + CLIENT_SECRET: 'value' + TENANT_ID: 'value' + END_POINT: 'https://.blob.core.windows.net/' + TEST_CONTAINTER: 'containt_name' +``` + +If you want to create credential authenticates by account name, account key, .. You need to define variable names in variables.yaml +Then you need to get it from Ivy.var in {@link UploadBean} and create BlobServiceClient +```java + private static final String ACCOUNT_NAME = Ivy.var().get("ACCOUNT_NAME"); + private static final String ACCOUNT_KEY = Ivy.var().get("ACCOUNT_KEY"); + private static final String END_POINT = Ivy.var().get("END_POINT"); + private static final String TEST_CONTAINTER = Ivy.var().get("TEST_CONTAINTER"); + ... + + BlobServiceClient blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(ACCOUNT_NAME, ACCOUNT_KEY, END_POINT); +``` + +## Run with Azurite at local + +Start docker local: Read our [documentation](../azure-blob-connector/README.md). +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) +```yaml +Variables: + ACCOUNT_NAME: 'devstoreaccount1' + ACCOUNT_KEY: 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==' +``` + diff --git a/azure-blob-connector-demo/config/variables.yaml b/azure-blob-connector-demo/config/variables.yaml index fd14458..c052340 100644 --- a/azure-blob-connector-demo/config/variables.yaml +++ b/azure-blob-connector-demo/config/variables.yaml @@ -3,4 +3,12 @@ # You can define here your project Variables. # Variables: -# myVariable: value + CLIENT_ID: '' + + CLIENT_SECRET: '' + + TENANT_ID: '' + + END_POINT: '' + + TEST_CONTAINTER: '' \ No newline at end of file diff --git a/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/demo/Data.ivyClass b/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/demo/Data.ivyClass deleted file mode 100644 index 78024a7..0000000 --- a/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/demo/Data.ivyClass +++ /dev/null @@ -1,2 +0,0 @@ -Data #class -com.axonivy.cloud.storage.azure.blob.connector.demo #namespace diff --git a/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/Data.ivyClass b/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/Data.ivyClass new file mode 100644 index 0000000..b21cf46 --- /dev/null +++ b/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/Data.ivyClass @@ -0,0 +1,2 @@ +Data #class +com.axonivy.cloud.storage.demo #namespace diff --git a/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/UploadData.ivyClass b/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/UploadData.ivyClass new file mode 100644 index 0000000..9f1cc9a --- /dev/null +++ b/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/UploadData.ivyClass @@ -0,0 +1,2 @@ +UploadData #class +com.axonivy.cloud.storage.demo #namespace diff --git a/azure-blob-connector-demo/pom.xml b/azure-blob-connector-demo/pom.xml index 89ae415..5b90914 100644 --- a/azure-blob-connector-demo/pom.xml +++ b/azure-blob-connector-demo/pom.xml @@ -1,19 +1,29 @@ - - 4.0.0 - com.axonivy.cloud.storage - azure-blob-connector-demo - 11.2.1-SNAPSHOT - iar - - - - com.axonivy.ivy.ci - project-build-plugin - 11.2.0 - true - - - + + 4.0.0 + com.axonivy.cloud.storage + azure-blob-connector-demo + 10.0.21-SNAPSHOT + iar + + 10.0.16 + + + + com.axonivy.cloud.storage + azure-blob-connector + ${project.version} + iar + + + + + + com.axonivy.ivy.ci + project-build-plugin + ${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..34fb7c5 --- /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.cloud.storage.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.cloud.storage.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.cloud.storage.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/cloud/storage/bean/Blob.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/Blob.java new file mode 100644 index 0000000..b423b4c --- /dev/null +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/Blob.java @@ -0,0 +1,21 @@ +package com.axonivy.cloud.storage.bean; + +import com.azure.storage.blob.models.BlobItem; + +public class Blob { + private BlobItem bi; + private String linkDownLoad; + + public BlobItem getBi() { + return bi; + } + public void setBi(BlobItem bi) { + this.bi = bi; + } + public String getLinkDownLoad() { + return linkDownLoad; + } + public void setLinkDownLoad(String linkDownLoad) { + this.linkDownLoad = linkDownLoad; + } +} diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java new file mode 100644 index 0000000..7f00ce0 --- /dev/null +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java @@ -0,0 +1,310 @@ +package com.axonivy.cloud.storage.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.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.primefaces.model.file.UploadedFile; + +import com.axonivy.cloud.storage.azure.blob.connector.BlobServiceClientHelper; +import com.axonivy.cloud.storage.azure.blob.connector.StorageService; +import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService; +import com.axonivy.cloud.storage.utils.UploadUtils; +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; + private static final String CLIENT_ID = Ivy.var().get("CLIENT_ID"); + private static final String CLIENT_SECRET = Ivy.var().get("CLIENT_SECRET"); + private static final String TENANT_ID = Ivy.var().get("TENANT_ID"); + private static final String END_POINT = Ivy.var().get("END_POINT"); + private static final String TEST_CONTAINTER = Ivy.var().get("TEST_CONTAINTER"); + + public void init() { + blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, CLIENT_SECRET, TENANT_ID, END_POINT); + storageService = new AzureBlobStorageService(blobServiceClient, TEST_CONTAINTER); + getBlobs(storageService.getBlobs()); + 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)) { + getBlobs(storageService.getBlobs()); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Uploaded blobs successfully", null)); + } + } 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)) { + getBlobs(storageService.getBlobs()); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Uploaded blobs successfully", null)); + } + } 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 (StringUtils.isNotEmpty(name)) { + getBlobs(storageService.getBlobs()); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Uploaded blobs successfully", null)); + } + } else { + isFileAlreadyExistPath = true; + } + } + + public void deleteBlobs(Date date) { + storageService.delete(date); + getBlobs(storageService.getBlobs()); + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Delete blobs successfully", null)); + } + + public void deleteBlob(String blobName) { + if (storageService.delete(blobName)) { + getBlobs(storageService.getBlobs()); + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Delete blobs successfully", null)); + } + } + + public void undeleteBlob(String blobName) { + storageService.restore(blobName); + getBlobs(storageService.getBlobs()); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Undelete blobs successfully", null)); + + } + + private void getBlobs(List bis) { + blobs = new ArrayList<>(); + for(BlobItem item : bis) { + Blob b = new Blob(); + b.setBi(item); + b.setLinkDownLoad(storageService.getDownloadLink(item.getName())); + blobs.add(b); + } + } + + private Boolean checkFileAlreadyExist(String name) { + return storageService.getBlobs().stream().anyMatch(b -> b.getName().equalsIgnoreCase(name)); + } +} + diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java new file mode 100644 index 0000000..79c3322 --- /dev/null +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java @@ -0,0 +1,243 @@ +package com.axonivy.cloud.storage.bean; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.primefaces.model.file.UploadedFile; + +import com.axonivy.cloud.storage.azure.blob.connector.BlobServiceClientHelper; +import com.axonivy.cloud.storage.azure.blob.connector.StorageService; +import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.models.BlobItem; + +import ch.ivyteam.ivy.environment.Ivy; + +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; + + private StorageService storageService = null; + private static BlobServiceClient blobServiceClient = null; + private static final String CLIENT_ID = Ivy.var().get("CLIENT_ID"); + private static final String CLIENT_SECRET = Ivy.var().get("CLIENT_SECRET"); + private static final String TENANT_ID = Ivy.var().get("TENANT_ID"); + private static final String END_POINT = Ivy.var().get("END_POINT"); + private static final String TEST_CONTAINTER = Ivy.var().get("TEST_CONTAINTER"); + + public void init() { + blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, CLIENT_SECRET, TENANT_ID, END_POINT); + storageService = new AzureBlobStorageService(blobServiceClient, TEST_CONTAINTER); + 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 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 getBlobs(List bis) { + blobs = new ArrayList<>(); + for(BlobItem item : bis) { + Blob b = new Blob(); + b.setBi(item); + b.setLinkDownLoad(storageService.getDownloadLink(item.getName())); + blobs.add(b); + } + } + + public Boolean checkFileAlreadyExist(String name) { + return storageService.getBlobs().stream().anyMatch(b -> b.getName().equalsIgnoreCase(name)); + } +} + diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/utils/UploadUtils.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/utils/UploadUtils.java new file mode 100644 index 0000000..415f94b --- /dev/null +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/utils/UploadUtils.java @@ -0,0 +1,21 @@ +package com.axonivy.cloud.storage.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); + } + return StringUtils.EMPTY; + } +} diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.rddescriptor b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.rddescriptor new file mode 100644 index 0000000..ae605f0 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.rddescriptor @@ -0,0 +1,7 @@ + + + + viewTechnology + JSF + + diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.xhtml b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.xhtml new file mode 100644 index 0000000..40f3e49 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.xhtml @@ -0,0 +1,152 @@ + + + + + UploadDialog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + +
+
+
+
+ + + + diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadData.ivyClass b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadData.ivyClass new file mode 100644 index 0000000..5ff172a --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadData.ivyClass @@ -0,0 +1,6 @@ +UploadData #class +com.axonivy.cloud.storage.demo.Upload #namespace +bean com.axonivy.cloud.storage.bean.UploadBean #field +bean PERSISTENT #fieldModifier +link String #field +link PERSISTENT #fieldModifier diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadProcess.p.json b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadProcess.p.json new file mode 100644 index 0000000..bd2df6c --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadProcess.p.json @@ -0,0 +1,417 @@ +{ + "format" : "10.0.0", + "id" : "19010B658CC93485", + "kind" : "HTML_DIALOG", + "config" : { + "data" : "com.axonivy.cloud.storage.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 } + } + } ] +} \ No newline at end of file diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/resources/uploadDialog.css b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/resources/uploadDialog.css new file mode 100644 index 0000000..52d5522 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/resources/uploadDialog.css @@ -0,0 +1,22 @@ +.width-100-percent { + width : 100%; +} +.float-right { + float : right; +} + +.margin-top-10-px { + margin-top : 10px; +} + +.margin-top-15px { + margin-top : 15px !important; +} + +body .ui-fileupload .ui-fileupload-buttonbar , body .ui-fileupload .ui-fileupload-content{ + border : none !important; +} + +.color-red { + color: red ; +} \ No newline at end of file diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor new file mode 100644 index 0000000..ae605f0 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor @@ -0,0 +1,7 @@ + + + + viewTechnology + JSF + + diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml new file mode 100644 index 0000000..40f3e49 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml @@ -0,0 +1,152 @@ + + + + + UploadDialog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + +
+
+
+
+ + + + diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass new file mode 100644 index 0000000..20e861d --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass @@ -0,0 +1,8 @@ +UploadByCallSubprocessData #class +com.axonivy.cloud.storage.demo.UploadByCallSubprocess #namespace +bean com.axonivy.cloud.storage.bean.UploadByCallSubprocess #field +bean PERSISTENT #fieldModifier +link String #field +link PERSISTENT #fieldModifier +isSuccess Boolean #field +isSuccess PERSISTENT #fieldModifier diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json new file mode 100644 index 0000000..4853c4f --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json @@ -0,0 +1,976 @@ +{ + "format" : "10.0.0", + "id" : "190719BB1F7DBAD8", + "kind" : "HTML_DIALOG", + "config" : { + "data" : "com.axonivy.cloud.storage.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" : "f17", "to" : "f14" } + }, { + "id" : "f15", + "type" : "HtmlDialogEnd", + "visual" : { + "at" : { "x" : 936, "y" : 352 } + } + }, { + "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" : 504 } + }, + "connect" : { "id" : "f20", "to" : "f19" } + }, { + "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" : 1040, "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" : 1048, "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,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "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" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.content" : "in.bean.inputStreamContent", + "param.blobName" : "in.bean.fileName", + "param.uploadToFolder" : "in.bean.uploadToFolderByPrimefaces", + "param.isOverwriteFile" : "in.bean.isOverwriteFile", + "param.azureBlobStorageService" : "in.bean.storageService" + } + } + }, + "visual" : { + "at" : { "x" : 384, "y" : 352 } + }, + "connect" : { "id" : "f67", "to" : "f66" } + }, { + "id" : "f14", + "type" : "Alternative", + "name" : "is exist file ?", + "visual" : { + "at" : { "x" : 200, "y" : 352 }, + "labelOffset" : { "x" : 16, "y" : -24 } + }, + "connect" : [ + { "id" : "f62", "to" : "f61", "condition" : "in.bean.isOverwriteFile || !in.bean.checkFileAlreadyExist(in.bean.fileName)" }, + { "id" : "f64", "to" : "f63" } + ] + }, { + "id" : "f63", + "type" : "Script", + "name" : "update value isFileAlreadyExist", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExist = true;" + } + }, + "visual" : { + "at" : { "x" : 200, "y" : 424 } + }, + "connect" : { "id" : "f65", "to" : "f15", "via" : [ { "x" : 936, "y" : 424 } ] } + }, { + "id" : "f66", + "type" : "Alternative", + "name" : [ + "is blob name ", + "not null ?" + ], + "visual" : { + "at" : { "x" : 512, "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" : 512, "y" : 280 }, { "x" : 936, "y" : 280 } ], "label" : { + "name" : "no", + "segment" : 1.47 + } } + ] + }, { + "id" : "f68", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + }, + "call" : { + "params" : [ + { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.azureBlobStorage" : "in.bean.storageService" + } + } + }, + "visual" : { + "at" : { "x" : 640, "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" : 800, "y" : 352 } + }, + "connect" : { "id" : "f16", "to" : "f15" } + }, { + "id" : "f73", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + }, + "call" : { + "params" : [ + { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.azureBlobStorage" : "in.bean.storageService" + } + } + }, + "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,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "output" : { + "map" : { + "out" : "in", + "out.isSuccess" : "result.isSucess" + } + }, + "call" : { + "params" : [ + { "name" : "date", "type" : "java.util.Date" }, + { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.date" : "in.bean.startDate", + "param.azureBlobStorage" : "in.bean.storageService" + } + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 1072 } + }, + "connect" : { "id" : "f88", "to" : "f87" } + }, { + "id" : "f79", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + }, + "call" : { + "params" : [ + { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.azureBlobStorage" : "in.bean.storageService" + } + } + }, + "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,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "output" : { + "map" : { + "out" : "in", + "out.isSuccess" : "result.isSuccess" + } + }, + "call" : { + "params" : [ + { "name" : "blobName", "type" : "String" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.blobName" : "in.bean.blobName", + "param.azureBlobStorageService" : "in.bean.storageService" + } + } + }, + "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,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "output" : { + "map" : { + "out" : "in", + "out.isSuccess" : "result.isSuccess" + } + }, + "call" : { + "params" : [ + { "name" : "blobName", "type" : "String" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.blobName" : "in.bean.blobName", + "param.azureBlobStorageService" : "in.bean.storageService" + } + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 1280 } + }, + "connect" : { "id" : "f45", "to" : "f42" } + }, { + "id" : "f44", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + }, + "call" : { + "params" : [ + { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.azureBlobStorage" : "in.bean.storageService" + } + } + }, + "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" : 360, "y" : 624 }, + "labelOffset" : { "x" : 16, "y" : -24 } + }, + "connect" : [ + { "id" : "f99", "to" : "f92", "condition" : "in.bean.isOverwriteFilePath || !in.bean.checkFileAlreadyExist(in.bean.fileNamePath)" }, + { "id" : "f96", "to" : "f91" } + ] + }, { + "id" : "f91", + "type" : "Script", + "name" : "update value isFileAlreadyExistPath", + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExistPath = true;" + } + }, + "visual" : { + "at" : { "x" : 360, "y" : 712 } + }, + "connect" : { "id" : "f22", "to" : "f23", "via" : [ { "x" : 1040, "y" : 712 } ] } + }, { + "id" : "f92", + "type" : "SubProcessCall", + "name" : "save blob", + "config" : { + "processCall" : "BlobStorage:uploadFromFile(String,String,String,Boolean,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "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" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.localPath" : "in.bean.localPath", + "param.blobName" : "in.bean.fileNamePath", + "param.uploadToFolder" : "in.bean.uploadToFolderByLocalPath", + "param.isOverwriteFile" : "in.bean.isOverwriteFilePath", + "param.azureBlobStorageService" : "in.bean.storageService" + } + } + }, + "visual" : { + "at" : { "x" : 480, "y" : 624 } + }, + "connect" : { "id" : "f100", "to" : "f93" } + }, { + "id" : "f93", + "type" : "Alternative", + "name" : [ + "is blob name ", + "not null ?" + ], + "visual" : { + "at" : { "x" : 616, "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" : 616, "y" : 552 }, { "x" : 1040, "y" : 552 } ], "label" : { + "name" : "no", + "segment" : 1.48 + } } + ] + }, { + "id" : "f94", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + }, + "call" : { + "params" : [ + { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.azureBlobStorage" : "in.bean.storageService" + } + } + }, + "visual" : { + "at" : { "x" : 752, "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" : 912, "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);" + ] + } + }, + "visual" : { + "at" : { "x" : 232, "y" : 624 } + }, + "connect" : { "id" : "f101", "to" : "f90" } + }, { + "id" : "f29", + "type" : "Script", + "name" : "get file name", + "config" : { + "output" : { + "code" : [ + "import com.axonivy.cloud.storage.utils.UploadUtils;", + "", + "in.bean.fileNameURL = UploadUtils.getFileNameFromUrl(in.bean.url);" + ] + } + }, + "visual" : { + "at" : { "x" : 232, "y" : 840 } + }, + "connect" : { "id" : "f110", "to" : "f104" } + }, { + "id" : "f104", + "type" : "Alternative", + "name" : "is exist file ?", + "visual" : { + "at" : { "x" : 360, "y" : 840 }, + "labelOffset" : { "x" : 16, "y" : -24 } + }, + "connect" : [ + { "id" : "f111", "to" : "f106", "condition" : "in.bean.isOverwriteFileURL || !in.bean.checkFileAlreadyExist(in.bean.fileNameURL)" }, + { "id" : "f112", "to" : "f105" } + ] + }, { + "id" : "f105", + "type" : "Script", + "name" : [ + "update value ", + "isFileAlreadyExistURL" + ], + "config" : { + "output" : { + "code" : "in.bean.isFileAlreadyExistURL = true;" + } + }, + "visual" : { + "at" : { "x" : 360, "y" : 928 } + }, + "connect" : { "id" : "f118", "to" : "f28", "via" : [ { "x" : 1048, "y" : 928 } ] } + }, { + "id" : "f106", + "type" : "SubProcessCall", + "name" : "save blob", + "config" : { + "processCall" : "BlobStorage:uploadFromUrl(String,String,String,Boolean,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "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" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.url" : "in.bean.url", + "param.blobName" : "in.bean.fileNameURL", + "param.uploadToFolder" : "in.bean.uploadToFolderByURL", + "param.isOverwriteFile" : "in.bean.isOverwriteFileURL", + "param.azureBlobStorageService" : "in.bean.storageService" + } + } + }, + "visual" : { + "at" : { "x" : 480, "y" : 840 } + }, + "connect" : { "id" : "f115", "to" : "f107" } + }, { + "id" : "f107", + "type" : "Alternative", + "name" : [ + "is blob name ", + "not null ?" + ], + "visual" : { + "at" : { "x" : 616, "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" : 616, "y" : 776 }, { "x" : 1048, "y" : 776 } ], "label" : { + "name" : "no", + "segment" : 1.49 + } } + ] + }, { + "id" : "f108", + "type" : "SubProcessCall", + "name" : "get list blob", + "config" : { + "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "output" : { + "map" : { + "out" : "in", + "out.bean.blobItems" : "result.blobItems" + } + }, + "call" : { + "params" : [ + { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "param.azureBlobStorage" : "in.bean.storageService" + } + } + }, + "visual" : { + "at" : { "x" : 752, "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" : 912, "y" : 840 } + }, + "connect" : { "id" : "f117", "to" : "f28" } + } ] +} \ No newline at end of file diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/resources/uploadDialog.css b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/resources/uploadDialog.css new file mode 100644 index 0000000..52d5522 --- /dev/null +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/resources/uploadDialog.css @@ -0,0 +1,22 @@ +.width-100-percent { + width : 100%; +} +.float-right { + float : right; +} + +.margin-top-10-px { + margin-top : 10px; +} + +.margin-top-15px { + margin-top : 15px !important; +} + +body .ui-fileupload .ui-fileupload-buttonbar , body .ui-fileupload .ui-fileupload-content{ + border : none !important; +} + +.color-red { + color: red ; +} \ 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/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-product/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs index fdac6ba..ecc50e4 100644 --- a/azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs +++ b/azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs @@ -1,5 +1,5 @@ ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.cloud.storage.azure.blob.connector.product.Data ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector.product ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 -ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=112000 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=100000 eclipse.preferences.version=1 diff --git a/azure-blob-connector-product/README.md b/azure-blob-connector-product/README.md index 0b54ff5..021dc97 100644 --- a/azure-blob-connector-product/README.md +++ b/azure-blob-connector-product/README.md @@ -1,3 +1,98 @@ # Azure Blob Connector +Axon Ivys Azure Blob Connector helps you to connector Azure Blob Services quitly: +- Configuration to authorize access to blobs. +- Support upload content to blob with many kind of inputs. +- Support get download link with expired time. + +## Setup + +In the project, you only add the dependency in your pom.xml and call public APIs + +**1. Add dependency** +```XML + + com.axonivy.cloud.storage + azure-blob-connector + ${process.analyzer.version} + +``` + +**2. 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) +``` + +**3. 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 - 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) { +``` + + - 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) {} +``` + +**4. 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); +``` + + diff --git a/azure-blob-connector-product/pom.xml b/azure-blob-connector-product/pom.xml index 5981dcd..a3a5b59 100644 --- a/azure-blob-connector-product/pom.xml +++ b/azure-blob-connector-product/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.axonivy.cloud.storage azure-blob-connector-product - 11.2.1-SNAPSHOT + 10.0.21-SNAPSHOT pom diff --git a/azure-blob-connector-product/product.json b/azure-blob-connector-product/product.json index 074ccfb..416b995 100644 --- a/azure-blob-connector-product/product.json +++ b/azure-blob-connector-product/product.json @@ -6,8 +6,8 @@ "data": { "projects": [ { - "groupId": "com.axonivy.utils.process.analyzer.demo", - "artifactId": "process-analyzer-demo", + "groupId": "com.axonivy.cloud.storage", + "artifactId": "azure-blob-connector-demo", "version": "${version}", "type": "iar" } @@ -28,8 +28,8 @@ "data": { "dependencies": [ { - "groupId": "com.axonivy.utils.process.analyzer", - "artifactId": "process-analyzer", + "groupId": "com.axonivy.cloud.storage", + "artifactId": "azure-blob-connector", "version": "${version}", "type": "iar" } @@ -44,27 +44,6 @@ } ] } - }, - { - "id": "maven-dropins", - "data": { - "dependencies": [ - { - "groupId": "com.axonivy.utils.process.analyzer", - "artifactId": "process-analyzer", - "version": "${version}" - } - ], - "repositories": [ - { - "id": "maven.axonivy.com", - "url": "https://maven.axonivy.com", - "snapshots": { - "enabled": "true" - } - } - ] - } } ] } diff --git a/azure-blob-connector-test/.classpath b/azure-blob-connector-test/.classpath index a24d7cc..8912ca0 100644 --- a/azure-blob-connector-test/.classpath +++ b/azure-blob-connector-test/.classpath @@ -1,28 +1,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs index 6ff2f05..f2c4666 100644 --- a/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs +++ b/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs @@ -1,5 +1,5 @@ ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.cloud.storage.azure.blob.connector.test.Data ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector.test ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 -ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=112000 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=100000 eclipse.preferences.version=1 diff --git a/azure-blob-connector-test/.settings/org.eclipse.wst.common.component b/azure-blob-connector-test/.settings/org.eclipse.wst.common.component index 601fca9..8edcac9 100644 --- a/azure-blob-connector-test/.settings/org.eclipse.wst.common.component +++ b/azure-blob-connector-test/.settings/org.eclipse.wst.common.component @@ -1,10 +1,20 @@ + + + + + + + + + + diff --git a/azure-blob-connector-test/config/custom-fields.yaml b/azure-blob-connector-test/config/custom-fields.yaml deleted file mode 100644 index bb20b70..0000000 --- a/azure-blob-connector-test/config/custom-fields.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# 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-test/config/databases.yaml b/azure-blob-connector-test/config/databases.yaml deleted file mode 100644 index 10319e2..0000000 --- a/azure-blob-connector-test/config/databases.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/databases.json -Databases: diff --git a/azure-blob-connector-test/config/overrides.any b/azure-blob-connector-test/config/overrides.any deleted file mode 100644 index f59ec20..0000000 --- a/azure-blob-connector-test/config/overrides.any +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/azure-blob-connector-test/config/persistence.xml b/azure-blob-connector-test/config/persistence.xml deleted file mode 100644 index d6b96d7..0000000 --- a/azure-blob-connector-test/config/persistence.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/azure-blob-connector-test/config/rest-clients.yaml b/azure-blob-connector-test/config/rest-clients.yaml deleted file mode 100644 index 4bffaca..0000000 --- a/azure-blob-connector-test/config/rest-clients.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/rest-clients.json -RestClients: diff --git a/azure-blob-connector-test/config/roles.xml b/azure-blob-connector-test/config/roles.xml deleted file mode 100644 index 59892fe..0000000 --- a/azure-blob-connector-test/config/roles.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - Everybody - diff --git a/azure-blob-connector-test/config/users.xml b/azure-blob-connector-test/config/users.xml deleted file mode 100644 index 51a6906..0000000 --- a/azure-blob-connector-test/config/users.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/azure-blob-connector-test/config/variables.yaml b/azure-blob-connector-test/config/variables.yaml deleted file mode 100644 index fd14458..0000000 --- a/azure-blob-connector-test/config/variables.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# == Variables == -# -# You can define here your project Variables. -# -Variables: -# myVariable: value diff --git a/azure-blob-connector-test/config/webservice-clients.yaml b/azure-blob-connector-test/config/webservice-clients.yaml deleted file mode 100644 index 688047a..0000000 --- a/azure-blob-connector-test/config/webservice-clients.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# yaml-language-server: $schema=https://json-schema.axonivy.com/app/0.0.1/webservice-clients.json -WebServiceClients: diff --git a/azure-blob-connector-test/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/test/Data.ivyClass b/azure-blob-connector-test/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/test/Data.ivyClass deleted file mode 100644 index 8d45909..0000000 --- a/azure-blob-connector-test/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/test/Data.ivyClass +++ /dev/null @@ -1,2 +0,0 @@ -Data #class -com.axonivy.cloud.storage.azure.blob.connector.test #namespace diff --git a/azure-blob-connector-test/pom.xml b/azure-blob-connector-test/pom.xml index 30e5fb0..dccf789 100644 --- a/azure-blob-connector-test/pom.xml +++ b/azure-blob-connector-test/pom.xml @@ -1,19 +1,49 @@ - - 4.0.0 - com.axonivy.cloud.storage - azure-blob-connector-test - 11.2.1-SNAPSHOT - iar - - - - com.axonivy.ivy.ci - project-build-plugin - 11.2.0 - true - - - + + 4.0.0 + com.axonivy.cloud.storage + azure-blob-connector-test + 10.0.21-SNAPSHOT + iar + + 10.0.16 + 1.19.8 + + + + com.axonivy.cloud.storage + 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 + + + + src_test + + + com.axonivy.ivy.ci + project-build-plugin + ${build.plugin.version} + 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 0000000000000000000000000000000000000000..8ee3c1f47f4e632cd4e945a0ec14f9a035e5a318 GIT binary patch literal 9197 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 D#uuaK literal 0 HcmV?d00001 diff --git a/azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AbstractIntegrationTest.java b/azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AbstractIntegrationTest.java new file mode 100644 index 0000000..5320f14 --- /dev/null +++ b/azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AbstractIntegrationTest.java @@ -0,0 +1,52 @@ +package com.axonivy.cloud.storage.azure.blob.connector.test.integration; + +import java.util.UUID; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +import com.axonivy.cloud.storage.azure.blob.connector.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"; + protected static final String ACCOUNT_NAME = "devstoreaccount1"; + protected static final String END_POINT_FORMAT = "http://127.0.0.1:%s/devstoreaccount1"; + protected static final String ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; + + 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() { + final String endpoint = String.format(END_POINT_FORMAT, azure.getMappedPort(BLOB_PORT)); + return BlobServiceClientHelper.getBlobServiceClient(ACCOUNT_NAME, 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/cloud/storage/azure/blob/connector/test/integration/AzureBlobStorageServiceTest.java b/azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AzureBlobStorageServiceTest.java new file mode 100644 index 0000000..aae8c27 --- /dev/null +++ b/azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AzureBlobStorageServiceTest.java @@ -0,0 +1,131 @@ +package com.axonivy.cloud.storage.azure.blob.connector.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.cloud.storage.azure.blob.connector.StorageService; +import com.axonivy.cloud.storage.azure.blob.connector.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() { + 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 index a24d7cc..75b41fa 100644 --- a/azure-blob-connector/.classpath +++ b/azure-blob-connector/.classpath @@ -1,8 +1,9 @@ - + + diff --git a/azure-blob-connector/.gitignore b/azure-blob-connector/.gitignore index 1b2547b..86ba893 100644 --- a/azure-blob-connector/.gitignore +++ b/azure-blob-connector/.gitignore @@ -17,3 +17,4 @@ classes/ src_dataClasses/ src_wsproc/ logs/ +.azure diff --git a/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs index 518d13e..4948f7b 100644 --- a/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs +++ b/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs @@ -1,5 +1,5 @@ ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.cloud.storage.azure.blob.connector.Data ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector ch.ivyteam.ivy.project.preferences\:PRIMEFACES_VERSION=11 -ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=112000 +ch.ivyteam.ivy.project.preferences\:PROJECT_VERSION=100000 eclipse.preferences.version=1 diff --git a/azure-blob-connector/.settings/org.eclipse.wst.common.component b/azure-blob-connector/.settings/org.eclipse.wst.common.component index 9935c96..ea172dc 100644 --- a/azure-blob-connector/.settings/org.eclipse.wst.common.component +++ b/azure-blob-connector/.settings/org.eclipse.wst.common.component @@ -1,10 +1,18 @@ + + - + + + + + + + diff --git a/azure-blob-connector/README.md b/azure-blob-connector/README.md new file mode 100644 index 0000000..3f4ae8c --- /dev/null +++ b/azure-blob-connector/README.md @@ -0,0 +1,36 @@ +# How to run at local + +## Run with DockerHub + +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) \ No newline at end of file diff --git a/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass b/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass new file mode 100644 index 0000000..fc8fbdb --- /dev/null +++ b/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass @@ -0,0 +1,13 @@ +BlobStorageData #class +com.axonivy.cloud.storage.azure.blob.connector #namespace +date java.util.Date #field +azureBlobStorageService com.axonivy.cloud.storage.azure.blob.connector.StorageService #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 diff --git a/azure-blob-connector/pom.xml b/azure-blob-connector/pom.xml index 935aa56..f23ceff 100644 --- a/azure-blob-connector/pom.xml +++ b/azure-blob-connector/pom.xml @@ -1,14 +1,18 @@ - - 4.0.0 - com.axonivy.cloud.storage - azure-blob-connector - 11.2.1-SNAPSHOT - iar - - 11.2.0 + + 4.0.0 + com.axonivy.cloud.storage + azure-blob-connector + 10.0.21-SNAPSHOT + iar + + 10.0.16 + 1.2.23 + @@ -18,6 +22,38 @@ https://oss.sonatype.org/content/repositories/snapshots + + + + + 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 + + + src @@ -27,45 +63,6 @@ ${build.plugin.version} true - - maven-assembly-plugin - 3.4.2 - - - make-assembly - package - - single - - - false - - jar.xml - - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.3.0 - - - attach-sources - - jar-no-fork - - - - - - maven-release-plugin - 3.0.0-M4 - - process-analyzer-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..75ddec6 --- /dev/null +++ b/azure-blob-connector/processes/BlobStorage.p.json @@ -0,0 +1,424 @@ +{ + "format" : "10.0.0", + "id" : "1905E51E1156B82C", + "kind" : "CALLABLE_SUB", + "config" : { + "data" : "com.axonivy.cloud.storage.azure.blob.connector.BlobStorageData" + }, + "elements" : [ { + "id" : "f0", + "type" : "CallSubStart", + "name" : "getBlobs(StorageService)", + "config" : { + "callSignature" : "getBlobs", + "input" : { + "params" : [ + { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "out.azureBlobStorageService" : "param.azureBlobStorage" + } + }, + "result" : { + "params" : [ + { "name" : "blobItems", "type" : "java.util.List" } + ], + "map" : { + "result.blobItems" : "in.blobItems" + } + } + }, + "visual" : { + "at" : { "x" : 96, "y" : 64 } + }, + "connect" : { "id" : "f9", "to" : "f8" } + }, { + "id" : "f1", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 368, "y" : 64 } + } + }, { + "id" : "f3", + "type" : "CallSubStart", + "name" : "delete(Date,StorageService)", + "config" : { + "callSignature" : "delete", + "input" : { + "params" : [ + { "name" : "date", "type" : "java.util.Date" }, + { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "out.azureBlobStorageService" : "param.azureBlobStorage", + "out.date" : "param.date" + } + }, + "result" : { + "params" : [ + { "name" : "isSucess", "type" : "Boolean" } + ], + "map" : { + "result.isSucess" : "in.isSuccess" + } + } + }, + "visual" : { + "at" : { "x" : 96, "y" : 192 }, + "labelOffset" : { "x" : 9, "y" : 41 } + }, + "connect" : { "id" : "f6", "to" : "f4" } + }, { + "id" : "f5", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 368, "y" : 192 } + } + }, { + "id" : "f4", + "type" : "Script", + "name" : "delete blob by date", + "config" : { + "output" : { + "code" : [ + "in.azureBlobStorageService.delete(in.date);", + "in.isSuccess = true;" + ] + } + }, + "visual" : { + "at" : { "x" : 264, "y" : 192 } + }, + "connect" : { "id" : "f7", "to" : "f5" } + }, { + "id" : "f8", + "type" : "Script", + "name" : "get list blob", + "config" : { + "output" : { + "code" : "in.blobItems = in.azureBlobStorageService.getBlobs();" + } + }, + "visual" : { + "at" : { "x" : 256, "y" : 64 } + }, + "connect" : { "id" : "f2", "to" : "f1" } + }, { + "id" : "f10", + "type" : "CallSubStart", + "name" : "detete(String,StorageService)", + "config" : { + "callSignature" : "detete", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "out.azureBlobStorageService" : "param.azureBlobStorageService", + "out.blobName" : "param.blobName" + } + }, + "result" : { + "params" : [ + { "name" : "isSuccess", "type" : "Boolean" } + ], + "map" : { + "result.isSuccess" : "in.isSuccess" + } + } + }, + "visual" : { + "at" : { "x" : 96, "y" : 296 } + }, + "connect" : { "id" : "f13", "to" : "f12" } + }, { + "id" : "f11", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 368, "y" : 296 } + } + }, { + "id" : "f12", + "type" : "Script", + "name" : "delete blob by name", + "config" : { + "output" : { + "code" : "in.isSuccess = in.azureBlobStorageService.delete(in.blobName);" + } + }, + "visual" : { + "at" : { "x" : 264, "y" : 296 } + }, + "connect" : { "id" : "f14", "to" : "f11" } + }, { + "id" : "f15", + "type" : "CallSubStart", + "name" : "restore(String,StorageService)", + "config" : { + "callSignature" : "restore", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "out.azureBlobStorageService" : "param.azureBlobStorageService", + "out.blobName" : "param.blobName" + } + }, + "result" : { + "params" : [ + { "name" : "isSuccess", "type" : "Boolean" } + ], + "map" : { + "result.isSuccess" : "in.isSuccess" + } + } + }, + "visual" : { + "at" : { "x" : 96, "y" : 392 } + }, + "connect" : { "id" : "f18", "to" : "f16" } + }, { + "id" : "f16", + "type" : "Script", + "name" : "restore", + "config" : { + "output" : { + "code" : [ + "in.azureBlobStorageService.restore(in.blobName);", + "in.isSuccess = true;" + ] + } + }, + "visual" : { + "at" : { "x" : 264, "y" : 392 } + }, + "connect" : { "id" : "f19", "to" : "f17" } + }, { + "id" : "f17", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 376, "y" : 392 } + } + }, { + "id" : "f20", + "type" : "CallSubStart", + "name" : "getLinkDownload(String,StorageService)", + "config" : { + "callSignature" : "getLinkDownload", + "input" : { + "params" : [ + { "name" : "blobName", "type" : "String" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "out.azureBlobStorageService" : "param.azureBlobStorageService", + "out.blobName" : "param.blobName" + } + }, + "result" : { + "params" : [ + { "name" : "linkDownload", "type" : "String" } + ], + "map" : { + "result.linkDownload" : "in.linkDownload" + } + } + }, + "visual" : { + "at" : { "x" : 88, "y" : 496 }, + "labelOffset" : { "x" : 49, "y" : 57 } + }, + "connect" : { "id" : "f24", "to" : "f22" } + }, { + "id" : "f21", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 384, "y" : 496 } + } + }, { + "id" : "f22", + "type" : "Script", + "name" : "get link", + "config" : { + "output" : { + "code" : "in.linkDownload = in.azureBlobStorageService.getDownloadLink(in.blobName);" + } + }, + "visual" : { + "at" : { "x" : 264, "y" : 496 } + }, + "connect" : { "id" : "f23", "to" : "f21" } + }, { + "id" : "f25", + "type" : "CallSubStart", + "name" : "uploadFromFile(String,String,String,Boolean,StorageService)", + "config" : { + "callSignature" : "uploadFromFile", + "input" : { + "params" : [ + { "name" : "localPath", "type" : "String" }, + { "name" : "blobName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" }, + { "name" : "isOverwriteFile", "type" : "Boolean" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "out.azureBlobStorageService" : "param.azureBlobStorageService", + "out.blobName" : "param.blobName", + "out.isOverwriteFile" : "param.isOverwriteFile", + "out.localPath" : "param.localPath", + "out.uploadToFolder" : "param.uploadToFolder" + } + }, + "result" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "result.blobName" : "in.blobName" + } + } + }, + "visual" : { + "at" : { "x" : 88, "y" : 608 }, + "labelOffset" : { "x" : 25, "y" : 57 } + }, + "connect" : { "id" : "f28", "to" : "f27" } + }, { + "id" : "f26", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 384, "y" : 608 } + } + }, { + "id" : "f27", + "type" : "Script", + "name" : "upload", + "config" : { + "output" : { + "code" : "in.blobName = in.azureBlobStorageService.uploadFromFile(in.localPath, in.uploadToFolder, in.isOverwriteFile);" + } + }, + "visual" : { + "at" : { "x" : 272, "y" : 608 } + }, + "connect" : { "id" : "f29", "to" : "f26" } + }, { + "id" : "f30", + "type" : "CallSubStart", + "name" : "uploadFromUrl(String,String,String,Boolean,StorageService)", + "config" : { + "callSignature" : "uploadFromUrl", + "input" : { + "params" : [ + { "name" : "url", "type" : "String" }, + { "name" : "blobName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" }, + { "name" : "isOverwriteFile", "type" : "Boolean" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "out.azureBlobStorageService" : "param.azureBlobStorageService", + "out.blobName" : "param.blobName", + "out.isOverwriteFile" : "param.isOverwriteFile", + "out.uploadToFolder" : "param.uploadToFolder", + "out.url" : "param.url" + } + }, + "result" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "result.blobName" : "in.blobName" + } + } + }, + "visual" : { + "at" : { "x" : 88, "y" : 736 }, + "labelOffset" : { "x" : 25, "y" : 57 } + }, + "connect" : { "id" : "f33", "to" : "f31" } + }, { + "id" : "f31", + "type" : "Script", + "name" : "upload", + "config" : { + "output" : { + "code" : "in.blobName = in.azureBlobStorageService.uploadFromUrl(in.url, in.uploadToFolder, in.isOverwriteFile);" + } + }, + "visual" : { + "at" : { "x" : 272, "y" : 736 } + }, + "connect" : { "id" : "f34", "to" : "f32" } + }, { + "id" : "f32", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 384, "y" : 736 } + } + }, { + "id" : "f35", + "type" : "CallSubStart", + "name" : "uploadFromFile(InputStream,String,String,Boolean,StorageService)", + "config" : { + "callSignature" : "uploadFromFile", + "input" : { + "params" : [ + { "name" : "content", "type" : "java.io.InputStream" }, + { "name" : "blobName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" }, + { "name" : "isOverwriteFile", "type" : "Boolean" }, + { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + ], + "map" : { + "out.azureBlobStorageService" : "param.azureBlobStorageService", + "out.blobName" : "param.blobName", + "out.content" : "param.content", + "out.isOverwriteFile" : "param.isOverwriteFile", + "out.uploadToFolder" : "param.uploadToFolder" + } + }, + "result" : { + "params" : [ + { "name" : "blobName", "type" : "String" } + ], + "map" : { + "result.blobName" : "in.blobName" + } + } + }, + "visual" : { + "at" : { "x" : 88, "y" : 888 }, + "labelOffset" : { "x" : 25, "y" : 57 } + }, + "connect" : { "id" : "f39", "to" : "f36" } + }, { + "id" : "f36", + "type" : "Script", + "name" : "upload", + "config" : { + "output" : { + "code" : [ + "import org.apache.commons.io.IOUtils;", + "", + "in.blobName = in.azureBlobStorageService.upload(IOUtils.toByteArray(in.content), in.blobName, in.uploadToFolder, in.isOverwriteFile);" + ] + } + }, + "visual" : { + "at" : { "x" : 272, "y" : 888 } + }, + "connect" : { "id" : "f38", "to" : "f37" } + }, { + "id" : "f37", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 384, "y" : 888 } + } + } ] +} \ No newline at end of file diff --git a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/BlobServiceClientHelper.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/BlobServiceClientHelper.java new file mode 100644 index 0000000..99d9c37 --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/BlobServiceClientHelper.java @@ -0,0 +1,66 @@ +package com.axonivy.cloud.storage.azure.blob.connector; + +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/cloud/storage/azure/blob/connector/StorageService.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java new file mode 100644 index 0000000..37675ca --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java @@ -0,0 +1,120 @@ +package com.axonivy.cloud.storage.azure.blob.connector; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Date; +import java.util.List; + +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 - The blob name + * */ + 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); +} diff --git a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java new file mode 100644 index 0000000..b59b477 --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java @@ -0,0 +1,248 @@ +package com.axonivy.cloud.storage.azure.blob.connector.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.cloud.storage.azure.blob.connector.StorageService; +import com.axonivy.cloud.storage.azure.blob.connector.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_MG = (5 * 1024 * 1024); + + + /** + * @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_MG) { + 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); + } + } + + private void downloadFileWithLargeSize(String blobName, String filePath) { + BlockBlobClient blockBlobClient = getBlobClient(blobName).getBlockBlobClient(); + ParallelTransferOptions parallelTransferOptions = new ParallelTransferOptions() + .setBlockSizeLong((long) (4 * 1024 * 1024)) // 4 MiB block size + .setMaxConcurrency(4); + + 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 BlobClient getBlobClient(String blobName) { + return this.destinationContainer.getBlobClient(blobName); + } + + 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 bis = 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 -> bis.add(blob)); + } + return bis; + } + + @Override + public void delete(Date d) { + List bi = destinationContainer.listBlobs().stream().filter(b -> isSameDate(b, d)).collect(Collectors.toList()); + bi.forEach(blob -> delete(blob.getName())); + } + + private boolean isSameDate(BlobItem bi, Date date) { + String creationTime2String = bi.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/cloud/storage/azure/blob/connector/internal/helper/BlobSASHelper.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/helper/BlobSASHelper.java new file mode 100644 index 0000000..571a5f7 --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/helper/BlobSASHelper.java @@ -0,0 +1,32 @@ +package com.axonivy.cloud.storage.azure.blob.connector.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/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 37e0e01..3b4e465 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.axonivy.cloud.storage azure-blob-connector azure-blob-connector-modules - 11.2.1-SNAPSHOT + 10.0.21-SNAPSHOT pom From 988022bc01818d5e7ebfeea79e00d573909e1776 Mon Sep 17 00:00:00 2001 From: Phan Thi Ngan Ha Date: Thu, 11 Jul 2024 15:02:15 +0700 Subject: [PATCH 03/13] TE-617: updated name variable --- azure-blob-connector-demo/README.md | 30 ++++--- .../config/variables.yaml | 20 +++-- .../cloud/storage/bean/UploadBean.java | 12 +-- .../storage/bean/UploadByCallSubprocess.java | 12 +-- .../processes/BlobStorage.p.json | 88 +++++++++++++++++-- 5 files changed, 121 insertions(+), 41 deletions(-) diff --git a/azure-blob-connector-demo/README.md b/azure-blob-connector-demo/README.md index 1091673..095e415 100644 --- a/azure-blob-connector-demo/README.md +++ b/azure-blob-connector-demo/README.md @@ -9,23 +9,30 @@ In the project, you only add the dependency in your pom.xml and call public APIs ** Below is an example for connect by client secret ** ```yaml Variables: - CLIENT_ID: 'value' - CLIENT_SECRET: 'value' - TENANT_ID: 'value' - END_POINT: 'https://.blob.core.windows.net/' - TEST_CONTAINTER: 'containt_name' + 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: '' ``` If you want to create credential authenticates by account name, account key, .. You need to define variable names in variables.yaml Then you need to get it from Ivy.var in {@link UploadBean} and create BlobServiceClient ```java - private static final String ACCOUNT_NAME = Ivy.var().get("ACCOUNT_NAME"); - private static final String ACCOUNT_KEY = Ivy.var().get("ACCOUNT_KEY"); - private static final String END_POINT = Ivy.var().get("END_POINT"); - private static final String TEST_CONTAINTER = Ivy.var().get("TEST_CONTAINTER"); + private static final String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); + private static final String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); + private static final String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); + private static final String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); + private static final String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); ... - BlobServiceClient blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(ACCOUNT_NAME, ACCOUNT_KEY, END_POINT); + BlobServiceClient blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, CLIENT_SECRET, END_POINT); ``` ## Run with Azurite at local @@ -36,5 +43,4 @@ Provide the account name and account key in varibles.yaml with [Well Known Stora Variables: ACCOUNT_NAME: 'devstoreaccount1' ACCOUNT_KEY: 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==' -``` - +``` \ No newline at end of file diff --git a/azure-blob-connector-demo/config/variables.yaml b/azure-blob-connector-demo/config/variables.yaml index c052340..245f493 100644 --- a/azure-blob-connector-demo/config/variables.yaml +++ b/azure-blob-connector-demo/config/variables.yaml @@ -3,12 +3,14 @@ # You can define here your project Variables. # Variables: - CLIENT_ID: '' - - CLIENT_SECRET: '' - - TENANT_ID: '' - - END_POINT: '' - - TEST_CONTAINTER: '' \ No newline at end of file + 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/src/com/axonivy/cloud/storage/bean/UploadBean.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java index 7f00ce0..f000ad1 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java @@ -47,15 +47,15 @@ public class UploadBean { private StorageService storageService = null; private static BlobServiceClient blobServiceClient = null; - private static final String CLIENT_ID = Ivy.var().get("CLIENT_ID"); - private static final String CLIENT_SECRET = Ivy.var().get("CLIENT_SECRET"); - private static final String TENANT_ID = Ivy.var().get("TENANT_ID"); - private static final String END_POINT = Ivy.var().get("END_POINT"); - private static final String TEST_CONTAINTER = Ivy.var().get("TEST_CONTAINTER"); + private static final String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); + private static final String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); + private static final String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); + private static final String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); + private static final String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); public void init() { blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, CLIENT_SECRET, TENANT_ID, END_POINT); - storageService = new AzureBlobStorageService(blobServiceClient, TEST_CONTAINTER); + storageService = new AzureBlobStorageService(blobServiceClient, CONTAINTER_NAME); getBlobs(storageService.getBlobs()); isFileAlreadyExist = false; isFileAlreadyExistURL = false; diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java index 79c3322..9f10008 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java @@ -42,15 +42,15 @@ public class UploadByCallSubprocess { private StorageService storageService = null; private static BlobServiceClient blobServiceClient = null; - private static final String CLIENT_ID = Ivy.var().get("CLIENT_ID"); - private static final String CLIENT_SECRET = Ivy.var().get("CLIENT_SECRET"); - private static final String TENANT_ID = Ivy.var().get("TENANT_ID"); - private static final String END_POINT = Ivy.var().get("END_POINT"); - private static final String TEST_CONTAINTER = Ivy.var().get("TEST_CONTAINTER"); + private static final String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); + private static final String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); + private static final String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); + private static final String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); + private static final String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); public void init() { blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, CLIENT_SECRET, TENANT_ID, END_POINT); - storageService = new AzureBlobStorageService(blobServiceClient, TEST_CONTAINTER); + storageService = new AzureBlobStorageService(blobServiceClient, CONTAINTER_NAME); isFileAlreadyExist = false; isFileAlreadyExistURL = false; isFileAlreadyExistPath = false; diff --git a/azure-blob-connector/processes/BlobStorage.p.json b/azure-blob-connector/processes/BlobStorage.p.json index 75ddec6..9a723de 100644 --- a/azure-blob-connector/processes/BlobStorage.p.json +++ b/azure-blob-connector/processes/BlobStorage.p.json @@ -29,7 +29,14 @@ } }, "visual" : { - "at" : { "x" : 96, "y" : 64 } + "at" : { "x" : 96, "y" : 64 }, + "description" : [ + "Description: Get all blobs", + "Parammeter:", + "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", + "Result:", + "- blobItems: java.util.List" + ] }, "connect" : { "id" : "f9", "to" : "f8" } }, { @@ -65,7 +72,15 @@ }, "visual" : { "at" : { "x" : 96, "y" : 192 }, - "labelOffset" : { "x" : 9, "y" : 41 } + "labelOffset" : { "x" : 9, "y" : 41 }, + "description" : [ + "Description: delete blob by date", + "Parammeter:", + "- date: java.util.Date", + "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", + "Result:", + "- isSucess: java.lang.Boolean" + ] }, "connect" : { "id" : "f6", "to" : "f4" } }, { @@ -129,7 +144,15 @@ } }, "visual" : { - "at" : { "x" : 96, "y" : 296 } + "at" : { "x" : 96, "y" : 296 }, + "description" : [ + "Description: delete blob by blob name", + "Parammeter:", + "- blobName: java.lang.String", + "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", + "Result:", + "- isSucess: java.lang.Boolean" + ] }, "connect" : { "id" : "f13", "to" : "f12" } }, { @@ -177,7 +200,15 @@ } }, "visual" : { - "at" : { "x" : 96, "y" : 392 } + "at" : { "x" : 96, "y" : 392 }, + "description" : [ + "Description: restore blob by blob name", + "Parammeter:", + "- blobName: java.lang.String", + "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", + "Result:", + "- isSucess: java.lang.Boolean" + ] }, "connect" : { "id" : "f18", "to" : "f16" } }, { @@ -229,7 +260,15 @@ }, "visual" : { "at" : { "x" : 88, "y" : 496 }, - "labelOffset" : { "x" : 49, "y" : 57 } + "labelOffset" : { "x" : 49, "y" : 57 }, + "description" : [ + "Description: get link download by blob name", + "Parammeter:", + "- blobName: java.lang.String", + "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", + "Result:", + "- linkDownload: java.lang.String" + ] }, "connect" : { "id" : "f24", "to" : "f22" } }, { @@ -284,7 +323,18 @@ }, "visual" : { "at" : { "x" : 88, "y" : 608 }, - "labelOffset" : { "x" : 25, "y" : 57 } + "labelOffset" : { "x" : 25, "y" : 57 }, + "description" : [ + "Description: upload from file", + "Parammeter:", + "- localPath: java.lang.String", + "- blobName: java.lang.String", + "- uploadToFolder: java.lang.String", + "- isOverwriteFile: java.lang.Boolean", + "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", + "Result:", + "- blobName: java.lang.String" + ] }, "connect" : { "id" : "f28", "to" : "f27" } }, { @@ -339,7 +389,18 @@ }, "visual" : { "at" : { "x" : 88, "y" : 736 }, - "labelOffset" : { "x" : 25, "y" : 57 } + "labelOffset" : { "x" : 25, "y" : 57 }, + "description" : [ + "Description: upload from file", + "Parammeter:", + "- url: java.lang.String", + "- blobName: java.lang.String", + "- uploadToFolder: java.lang.String", + "- isOverwriteFile: java.lang.Boolean", + "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", + "Result:", + "- blobName: java.lang.String" + ] }, "connect" : { "id" : "f33", "to" : "f31" } }, { @@ -394,7 +455,18 @@ }, "visual" : { "at" : { "x" : 88, "y" : 888 }, - "labelOffset" : { "x" : 25, "y" : 57 } + "labelOffset" : { "x" : 25, "y" : 57 }, + "description" : [ + "Description: upload from inputStream", + "Parammeter:", + "- content: java.io.InputStream", + "- blobName: java.lang.String", + "- uploadToFolder: java.lang.String", + "- isOverwriteFile: java.lang.Boolean", + "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", + "Result:", + "- blobName: java.lang.String" + ] }, "connect" : { "id" : "f39", "to" : "f36" } }, { From 8b827fd7e2a3577ba8e297bd6ca08ccd794239eb Mon Sep 17 00:00:00 2001 From: Phan Thi Ngan Ha Date: Thu, 11 Jul 2024 15:07:58 +0700 Subject: [PATCH 04/13] clean code --- azure-blob-connector-demo/README.md | 14 -------------- .../com/axonivy/cloud/storage/bean/UploadBean.java | 12 +++++++----- .../cloud/storage/bean/UploadByCallSubprocess.java | 10 +++++----- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/azure-blob-connector-demo/README.md b/azure-blob-connector-demo/README.md index 095e415..8657be2 100644 --- a/azure-blob-connector-demo/README.md +++ b/azure-blob-connector-demo/README.md @@ -21,20 +21,6 @@ Variables: # Your container name. ContainterName: '' ``` - -If you want to create credential authenticates by account name, account key, .. You need to define variable names in variables.yaml -Then you need to get it from Ivy.var in {@link UploadBean} and create BlobServiceClient -```java - private static final String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); - private static final String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); - private static final String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); - private static final String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); - private static final String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); - ... - - BlobServiceClient blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, CLIENT_SECRET, END_POINT); -``` - ## Run with Azurite at local Start docker local: Read our [documentation](../azure-blob-connector/README.md). diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java index f000ad1..5fda29c 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java @@ -47,13 +47,15 @@ public class UploadBean { private StorageService storageService = null; private static BlobServiceClient blobServiceClient = null; - private static final String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); - private static final String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); - private static final String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); - private static final String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); - private static final String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); + public void init() { + String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); + String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); + String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); + String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); + String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); + blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, CLIENT_SECRET, TENANT_ID, END_POINT); storageService = new AzureBlobStorageService(blobServiceClient, CONTAINTER_NAME); getBlobs(storageService.getBlobs()); diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java index 9f10008..b1350c1 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java @@ -42,13 +42,13 @@ public class UploadByCallSubprocess { private StorageService storageService = null; private static BlobServiceClient blobServiceClient = null; - private static final String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); - private static final String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); - private static final String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); - private static final String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); - private static final String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); public void init() { + String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); + String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); + String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); + String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); + String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, CLIENT_SECRET, TENANT_ID, END_POINT); storageService = new AzureBlobStorageService(blobServiceClient, CONTAINTER_NAME); isFileAlreadyExist = false; From 22c17669996cc041463c63a0e0ecb56d91b5be2f Mon Sep 17 00:00:00 2001 From: Phan Thi Ngan Ha Date: Thu, 11 Jul 2024 15:17:05 +0700 Subject: [PATCH 05/13] update name --- .../axonivy/cloud/storage/bean/UploadBean.java | 14 +++++++------- .../storage/bean/UploadByCallSubprocess.java | 16 +++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java index 5fda29c..f29a374 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java @@ -50,14 +50,14 @@ public class UploadBean { public void init() { - String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); - String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); - String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); - String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); - String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); + 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(CLIENT_ID, CLIENT_SECRET, TENANT_ID, END_POINT); - storageService = new AzureBlobStorageService(blobServiceClient, CONTAINTER_NAME); + blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(clientId, clientSecret, tenantId, endPoint); + storageService = new AzureBlobStorageService(blobServiceClient, containerName); getBlobs(storageService.getBlobs()); isFileAlreadyExist = false; isFileAlreadyExistURL = false; diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java index b1350c1..d8b9a26 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java @@ -44,13 +44,15 @@ public class UploadByCallSubprocess { private static BlobServiceClient blobServiceClient = null; public void init() { - String CLIENT_ID = Ivy.var().get("AzureBlob.ClientId"); - String CLIENT_SECRET = Ivy.var().get("AzureBlob.ClientSecret"); - String TENANT_ID = Ivy.var().get("AzureBlob.TenantId"); - String END_POINT = Ivy.var().get("AzureBlob.EndPoint"); - String CONTAINTER_NAME = Ivy.var().get("AzureBlob.ContainterName"); - blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(CLIENT_ID, CLIENT_SECRET, TENANT_ID, END_POINT); - storageService = new AzureBlobStorageService(blobServiceClient, CONTAINTER_NAME); + 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); + isFileAlreadyExist = false; isFileAlreadyExistURL = false; isFileAlreadyExistPath = false; From f9b341881ec1190273a57a3a071c20955fe2d2ad Mon Sep 17 00:00:00 2001 From: Phan Thi Ngan Ha Date: Thu, 25 Jul 2024 10:03:28 +0700 Subject: [PATCH 06/13] TE-638: updated some feedback. --- .../config/variables.yaml | 11 - .../storage/bean/UploadByCallSubprocess.java | 51 +-- .../UploadByCallSubprocess.xhtml | 2 +- .../UploadByCallSubprocessData.ivyClass | 2 + .../UploadByCallSubprocessProcess.p.json | 292 +++++++++------- azure-blob-connector-product/README.md | 15 + .../config/custom-fields.yaml | 22 ++ azure-blob-connector/config/databases.yaml | 2 + azure-blob-connector/config/overrides.any | 1 + azure-blob-connector/config/persistence.xml | 2 + azure-blob-connector/config/rest-clients.yaml | 2 + azure-blob-connector/config/roles.xml | 4 + azure-blob-connector/config/users.xml | 2 + azure-blob-connector/config/variables.yaml | 16 + .../config/webservice-clients.yaml | 2 + .../blob/connector/BlobStorageData.ivyClass | 4 +- .../processes/BlobStorage.p.json | 327 ++++++++++++------ .../azure/blob/connector/StorageService.java | 10 +- .../internal/AzureBlobStorageService.java | 20 +- .../webContent/icon/azure-blob-icon.png | Bin 0 -> 2140 bytes 20 files changed, 499 insertions(+), 288 deletions(-) create mode 100644 azure-blob-connector/config/custom-fields.yaml create mode 100644 azure-blob-connector/config/databases.yaml create mode 100644 azure-blob-connector/config/overrides.any create mode 100644 azure-blob-connector/config/persistence.xml create mode 100644 azure-blob-connector/config/rest-clients.yaml create mode 100644 azure-blob-connector/config/roles.xml create mode 100644 azure-blob-connector/config/users.xml create mode 100644 azure-blob-connector/config/variables.yaml create mode 100644 azure-blob-connector/config/webservice-clients.yaml create mode 100644 azure-blob-connector/webContent/icon/azure-blob-icon.png diff --git a/azure-blob-connector-demo/config/variables.yaml b/azure-blob-connector-demo/config/variables.yaml index 245f493..99a327a 100644 --- a/azure-blob-connector-demo/config/variables.yaml +++ b/azure-blob-connector-demo/config/variables.yaml @@ -3,14 +3,3 @@ # 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/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java index d8b9a26..72fd052 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java @@ -5,16 +5,11 @@ import java.util.Date; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.primefaces.model.file.UploadedFile; -import com.axonivy.cloud.storage.azure.blob.connector.BlobServiceClientHelper; -import com.axonivy.cloud.storage.azure.blob.connector.StorageService; -import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService; -import com.azure.storage.blob.BlobServiceClient; import com.azure.storage.blob.models.BlobItem; -import ch.ivyteam.ivy.environment.Ivy; - public class UploadByCallSubprocess { private String url; private String localPath; @@ -33,49 +28,43 @@ public class UploadByCallSubprocess { 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); - 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; } @@ -196,14 +185,6 @@ 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; } @@ -227,19 +208,13 @@ public String getUploadToFolderByURL() { public void setUploadToFolderByURL(String uploadToFolderByURL) { this.uploadToFolderByURL = uploadToFolderByURL; } - + public void getBlobs(List bis) { blobs = new ArrayList<>(); - for(BlobItem item : bis) { + for (BlobItem item : bis) { Blob b = new Blob(); b.setBi(item); - b.setLinkDownLoad(storageService.getDownloadLink(item.getName())); blobs.add(b); } } - - public Boolean checkFileAlreadyExist(String name) { - return storageService.getBlobs().stream().anyMatch(b -> b.getName().equalsIgnoreCase(name)); - } } - diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml index 40f3e49..79dfb32 100644 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml @@ -122,7 +122,7 @@ diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass index 20e861d..9faf426 100644 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass @@ -6,3 +6,5 @@ link String #field link PERSISTENT #fieldModifier isSuccess Boolean #field isSuccess PERSISTENT #fieldModifier +isExist Boolean #field +isExist PERSISTENT #fieldModifier diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json index 4853c4f..ecea95e 100644 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json @@ -109,12 +109,12 @@ "visual" : { "at" : { "x" : 96, "y" : 352 } }, - "connect" : { "id" : "f17", "to" : "f14" } + "connect" : { "id" : "f119", "to" : "f30" } }, { "id" : "f15", "type" : "HtmlDialogEnd", "visual" : { - "at" : { "x" : 936, "y" : 352 } + "at" : { "x" : 1088, "y" : 352 } } }, { "id" : "f18", @@ -124,10 +124,10 @@ "callSignature" : "getLink", "input" : { "params" : [ - { "name" : "link", "type" : "String" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "out.link" : "param.link" + "out.bean.blobName" : "param.blobName" } }, "guid" : "19010CAC30297BE7" @@ -135,7 +135,7 @@ "visual" : { "at" : { "x" : 96, "y" : 504 } }, - "connect" : { "id" : "f20", "to" : "f19" } + "connect" : { "id" : "f125", "to" : "f124" } }, { "id" : "f19", "type" : "HtmlDialogEnd", @@ -157,7 +157,7 @@ "id" : "f23", "type" : "HtmlDialogEnd", "visual" : { - "at" : { "x" : 1040, "y" : 624 } + "at" : { "x" : 1200, "y" : 624 } } }, { "id" : "f26", @@ -174,7 +174,7 @@ "id" : "f28", "type" : "HtmlDialogEnd", "visual" : { - "at" : { "x" : 1048, "y" : 840 } + "at" : { "x" : 1208, "y" : 840 } } }, { "id" : "f31", @@ -334,7 +334,7 @@ "type" : "SubProcessCall", "name" : "save blob", "config" : { - "processCall" : "BlobStorage:uploadFromFile(java.io.InputStream,String,String,Boolean,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:uploadFromFile(java.io.InputStream,String,String,Boolean)", "output" : { "map" : { "out" : "in", @@ -346,20 +346,18 @@ { "name" : "content", "type" : "java.io.InputStream" }, { "name" : "blobName", "type" : "String" }, { "name" : "uploadToFolder", "type" : "String" }, - { "name" : "isOverwriteFile", "type" : "Boolean" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "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", - "param.azureBlobStorageService" : "in.bean.storageService" + "param.isOverwriteFile" : "in.bean.isOverwriteFile" } } }, "visual" : { - "at" : { "x" : 384, "y" : 352 } + "at" : { "x" : 536, "y" : 352 } }, "connect" : { "id" : "f67", "to" : "f66" } }, { @@ -367,12 +365,16 @@ "type" : "Alternative", "name" : "is exist file ?", "visual" : { - "at" : { "x" : 200, "y" : 352 }, + "at" : { "x" : 352, "y" : 352 }, "labelOffset" : { "x" : 16, "y" : -24 } }, "connect" : [ - { "id" : "f62", "to" : "f61", "condition" : "in.bean.isOverwriteFile || !in.bean.checkFileAlreadyExist(in.bean.fileName)" }, - { "id" : "f64", "to" : "f63" } + { "id" : "f62", "to" : "f61", "label" : { + "name" : "no" + }, "condition" : "in.bean.isOverwriteFile || !in.isExist" }, + { "id" : "f64", "to" : "f63", "label" : { + "name" : "yes" + } } ] }, { "id" : "f63", @@ -384,9 +386,9 @@ } }, "visual" : { - "at" : { "x" : 200, "y" : 424 } + "at" : { "x" : 352, "y" : 432 } }, - "connect" : { "id" : "f65", "to" : "f15", "via" : [ { "x" : 936, "y" : 424 } ] } + "connect" : { "id" : "f65", "to" : "f15", "via" : [ { "x" : 1088, "y" : 432 } ] } }, { "id" : "f66", "type" : "Alternative", @@ -395,14 +397,14 @@ "not null ?" ], "visual" : { - "at" : { "x" : 512, "y" : 352 }, + "at" : { "x" : 664, "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" : 512, "y" : 280 }, { "x" : 936, "y" : 280 } ], "label" : { + { "id" : "f70", "to" : "f15", "via" : [ { "x" : 664, "y" : 280 }, { "x" : 1088, "y" : 280 } ], "label" : { "name" : "no", "segment" : 1.47 } } @@ -412,24 +414,16 @@ "type" : "SubProcessCall", "name" : "get list blob", "config" : { - "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:getBlobs()", "output" : { "map" : { "out" : "in", "out.bean.blobItems" : "result.blobItems" } - }, - "call" : { - "params" : [ - { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } - ], - "map" : { - "param.azureBlobStorage" : "in.bean.storageService" - } } }, "visual" : { - "at" : { "x" : 640, "y" : 352 } + "at" : { "x" : 792, "y" : 352 } }, "connect" : { "id" : "f72", "to" : "f71" } }, { @@ -448,7 +442,7 @@ } }, "visual" : { - "at" : { "x" : 800, "y" : 352 } + "at" : { "x" : 952, "y" : 352 } }, "connect" : { "id" : "f16", "to" : "f15" } }, { @@ -456,20 +450,12 @@ "type" : "SubProcessCall", "name" : "get list blob", "config" : { - "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:getBlobs()", "output" : { "map" : { "out" : "in", "out.bean.blobItems" : "result.blobItems" } - }, - "call" : { - "params" : [ - { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } - ], - "map" : { - "param.azureBlobStorage" : "in.bean.storageService" - } } }, "visual" : { @@ -494,7 +480,7 @@ "type" : "SubProcessCall", "name" : "delete by date", "config" : { - "processCall" : "BlobStorage:delete(java.util.Date,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:delete(java.util.Date)", "output" : { "map" : { "out" : "in", @@ -503,12 +489,10 @@ }, "call" : { "params" : [ - { "name" : "date", "type" : "java.util.Date" }, - { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "date", "type" : "java.util.Date" } ], "map" : { - "param.date" : "in.bean.startDate", - "param.azureBlobStorage" : "in.bean.storageService" + "param.date" : "in.bean.startDate" } } }, @@ -521,20 +505,12 @@ "type" : "SubProcessCall", "name" : "get list blob", "config" : { - "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:getBlobs()", "output" : { "map" : { "out" : "in", "out.bean.blobItems" : "result.blobItems" } - }, - "call" : { - "params" : [ - { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } - ], - "map" : { - "param.azureBlobStorage" : "in.bean.storageService" - } } }, "visual" : { @@ -565,7 +541,7 @@ "type" : "SubProcessCall", "name" : "delete by name", "config" : { - "processCall" : "BlobStorage:detete(String,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:detete(String)", "output" : { "map" : { "out" : "in", @@ -574,12 +550,10 @@ }, "call" : { "params" : [ - { "name" : "blobName", "type" : "String" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "param.blobName" : "in.bean.blobName", - "param.azureBlobStorageService" : "in.bean.storageService" + "param.blobName" : "in.bean.blobName" } } }, @@ -592,7 +566,7 @@ "type" : "SubProcessCall", "name" : "undelete", "config" : { - "processCall" : "BlobStorage:restore(String,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:restore(String)", "output" : { "map" : { "out" : "in", @@ -601,12 +575,10 @@ }, "call" : { "params" : [ - { "name" : "blobName", "type" : "String" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "param.blobName" : "in.bean.blobName", - "param.azureBlobStorageService" : "in.bean.storageService" + "param.blobName" : "in.bean.blobName" } } }, @@ -619,20 +591,12 @@ "type" : "SubProcessCall", "name" : "get list blob", "config" : { - "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:getBlobs()", "output" : { "map" : { "out" : "in", "out.bean.blobItems" : "result.blobItems" } - }, - "call" : { - "params" : [ - { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } - ], - "map" : { - "param.azureBlobStorage" : "in.bean.storageService" - } } }, "visual" : { @@ -697,11 +661,11 @@ "type" : "Alternative", "name" : "is exist file ?", "visual" : { - "at" : { "x" : 360, "y" : 624 }, + "at" : { "x" : 520, "y" : 624 }, "labelOffset" : { "x" : 16, "y" : -24 } }, "connect" : [ - { "id" : "f99", "to" : "f92", "condition" : "in.bean.isOverwriteFilePath || !in.bean.checkFileAlreadyExist(in.bean.fileNamePath)" }, + { "id" : "f99", "to" : "f92", "condition" : "in.bean.isOverwriteFilePath || !in.isExist" }, { "id" : "f96", "to" : "f91" } ] }, { @@ -714,15 +678,15 @@ } }, "visual" : { - "at" : { "x" : 360, "y" : 712 } + "at" : { "x" : 520, "y" : 712 } }, - "connect" : { "id" : "f22", "to" : "f23", "via" : [ { "x" : 1040, "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,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:uploadFromFile(String,String,String,Boolean)", "output" : { "map" : { "out" : "in", @@ -734,20 +698,18 @@ { "name" : "localPath", "type" : "String" }, { "name" : "blobName", "type" : "String" }, { "name" : "uploadToFolder", "type" : "String" }, - { "name" : "isOverwriteFile", "type" : "Boolean" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "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", - "param.azureBlobStorageService" : "in.bean.storageService" + "param.isOverwriteFile" : "in.bean.isOverwriteFilePath" } } }, "visual" : { - "at" : { "x" : 480, "y" : 624 } + "at" : { "x" : 640, "y" : 624 } }, "connect" : { "id" : "f100", "to" : "f93" } }, { @@ -758,14 +720,14 @@ "not null ?" ], "visual" : { - "at" : { "x" : 616, "y" : 624 }, + "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" : 616, "y" : 552 }, { "x" : 1040, "y" : 552 } ], "label" : { + { "id" : "f102", "to" : "f23", "via" : [ { "x" : 776, "y" : 552 }, { "x" : 1200, "y" : 552 } ], "label" : { "name" : "no", "segment" : 1.48 } } @@ -775,24 +737,16 @@ "type" : "SubProcessCall", "name" : "get list blob", "config" : { - "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:getBlobs()", "output" : { "map" : { "out" : "in", "out.bean.blobItems" : "result.blobItems" } - }, - "call" : { - "params" : [ - { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } - ], - "map" : { - "param.azureBlobStorage" : "in.bean.storageService" - } } }, "visual" : { - "at" : { "x" : 752, "y" : 624 } + "at" : { "x" : 912, "y" : 624 } }, "connect" : { "id" : "f98", "to" : "f95" } }, { @@ -811,7 +765,7 @@ } }, "visual" : { - "at" : { "x" : 912, "y" : 624 } + "at" : { "x" : 1072, "y" : 624 } }, "connect" : { "id" : "f24", "to" : "f23" } }, { @@ -829,7 +783,7 @@ "visual" : { "at" : { "x" : 232, "y" : 624 } }, - "connect" : { "id" : "f101", "to" : "f90" } + "connect" : { "id" : "f121", "to" : "f120" } }, { "id" : "f29", "type" : "Script", @@ -846,17 +800,17 @@ "visual" : { "at" : { "x" : 232, "y" : 840 } }, - "connect" : { "id" : "f110", "to" : "f104" } + "connect" : { "id" : "f123", "to" : "f122" } }, { "id" : "f104", "type" : "Alternative", "name" : "is exist file ?", "visual" : { - "at" : { "x" : 360, "y" : 840 }, + "at" : { "x" : 520, "y" : 840 }, "labelOffset" : { "x" : 16, "y" : -24 } }, "connect" : [ - { "id" : "f111", "to" : "f106", "condition" : "in.bean.isOverwriteFileURL || !in.bean.checkFileAlreadyExist(in.bean.fileNameURL)" }, + { "id" : "f111", "to" : "f106", "condition" : "in.bean.isOverwriteFileURL || !in.isExist" }, { "id" : "f112", "to" : "f105" } ] }, { @@ -872,15 +826,15 @@ } }, "visual" : { - "at" : { "x" : 360, "y" : 928 } + "at" : { "x" : 520, "y" : 928 } }, - "connect" : { "id" : "f118", "to" : "f28", "via" : [ { "x" : 1048, "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,com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:uploadFromUrl(String,String,String,Boolean)", "output" : { "map" : { "out" : "in", @@ -892,20 +846,18 @@ { "name" : "url", "type" : "String" }, { "name" : "blobName", "type" : "String" }, { "name" : "uploadToFolder", "type" : "String" }, - { "name" : "isOverwriteFile", "type" : "Boolean" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "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", - "param.azureBlobStorageService" : "in.bean.storageService" + "param.isOverwriteFile" : "in.bean.isOverwriteFileURL" } } }, "visual" : { - "at" : { "x" : 480, "y" : 840 } + "at" : { "x" : 640, "y" : 840 } }, "connect" : { "id" : "f115", "to" : "f107" } }, { @@ -916,14 +868,14 @@ "not null ?" ], "visual" : { - "at" : { "x" : 616, "y" : 840 }, + "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" : 616, "y" : 776 }, { "x" : 1048, "y" : 776 } ], "label" : { + { "id" : "f27", "to" : "f28", "via" : [ { "x" : 776, "y" : 776 }, { "x" : 1208, "y" : 776 } ], "label" : { "name" : "no", "segment" : 1.49 } } @@ -933,24 +885,16 @@ "type" : "SubProcessCall", "name" : "get list blob", "config" : { - "processCall" : "BlobStorage:getBlobs(com.axonivy.cloud.storage.azure.blob.connector.StorageService)", + "processCall" : "BlobStorage:getBlobs()", "output" : { "map" : { "out" : "in", "out.bean.blobItems" : "result.blobItems" } - }, - "call" : { - "params" : [ - { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } - ], - "map" : { - "param.azureBlobStorage" : "in.bean.storageService" - } } }, "visual" : { - "at" : { "x" : 752, "y" : 840 } + "at" : { "x" : 912, "y" : 840 } }, "connect" : { "id" : "f113", "to" : "f109" } }, { @@ -969,8 +913,114 @@ } }, "visual" : { - "at" : { "x" : 912, "y" : 840 } + "at" : { "x" : 1072, "y" : 840 } }, "connect" : { "id" : "f117", "to" : "f28" } + }, { + "id" : "f30", + "type" : "SubProcessCall", + "name" : "check blob is exist", + "config" : { + "processCall" : "BlobStorage:checkBlobIsExist(String,String)", + "output" : { + "map" : { + "out" : "in", + "out.isExist" : "result.isExist" + } + }, + "call" : { + "params" : [ + { "name" : "fileName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" } + ], + "map" : { + "param.fileName" : "in.bean.fileName", + "param.uploadToFolder" : "in.bean.uploadToFolderByPrimefaces" + } + } + }, + "visual" : { + "at" : { "x" : 224, "y" : 352 } + }, + "connect" : { "id" : "f17", "to" : "f14" } + }, { + "id" : "f120", + "type" : "SubProcessCall", + "name" : "check blob is exist", + "config" : { + "processCall" : "BlobStorage:checkBlobIsExist(String,String)", + "output" : { + "map" : { + "out" : "in", + "out.isExist" : "result.isExist" + } + }, + "call" : { + "params" : [ + { "name" : "fileName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" } + ], + "map" : { + "param.fileName" : "in.bean.fileNamePath", + "param.uploadToFolder" : "in.bean.uploadToFolderByLocalPath" + } + } + }, + "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,String)", + "output" : { + "map" : { + "out" : "in", + "out.isExist" : "result.isExist" + } + }, + "call" : { + "params" : [ + { "name" : "fileName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" } + ], + "map" : { + "param.fileName" : "in.bean.fileNameURL", + "param.uploadToFolder" : "in.bean.uploadToFolderByURL" + } + } + }, + "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" } } ] } \ No newline at end of file diff --git a/azure-blob-connector-product/README.md b/azure-blob-connector-product/README.md index 021dc97..070fb9a 100644 --- a/azure-blob-connector-product/README.md +++ b/azure-blob-connector-product/README.md @@ -17,6 +17,21 @@ In the project, you only add the dependency in your pom.xml and call public APIs ${process.analyzer.version} ``` +** 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: '' +``` **2. 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 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/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass b/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass index fc8fbdb..ad6cfd7 100644 --- a/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass +++ b/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass @@ -1,7 +1,7 @@ BlobStorageData #class com.axonivy.cloud.storage.azure.blob.connector #namespace date java.util.Date #field -azureBlobStorageService com.axonivy.cloud.storage.azure.blob.connector.StorageService #field +azureBlobStorageService com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService #field isSuccess Boolean #field blobItems java.util.List #field blobName String #field @@ -11,3 +11,5 @@ 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/processes/BlobStorage.p.json b/azure-blob-connector/processes/BlobStorage.p.json index 9a723de..6684421 100644 --- a/azure-blob-connector/processes/BlobStorage.p.json +++ b/azure-blob-connector/processes/BlobStorage.p.json @@ -8,15 +8,12 @@ "elements" : [ { "id" : "f0", "type" : "CallSubStart", - "name" : "getBlobs(StorageService)", + "name" : "getBlobs", "config" : { "callSignature" : "getBlobs", "input" : { - "params" : [ - { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } - ], "map" : { - "out.azureBlobStorageService" : "param.azureBlobStorage" + "out.functionName" : "\"getBlobs\"" } }, "result" : { @@ -26,39 +23,32 @@ "map" : { "result.blobItems" : "in.blobItems" } - } + }, + "tags" : "connector" }, "visual" : { "at" : { "x" : 96, "y" : 64 }, "description" : [ "Description: Get all blobs", - "Parammeter:", - "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", "Result:", "- blobItems: java.util.List" - ] + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" }, - "connect" : { "id" : "f9", "to" : "f8" } - }, { - "id" : "f1", - "type" : "CallSubEnd", - "visual" : { - "at" : { "x" : 368, "y" : 64 } - } + "connect" : { "id" : "f41", "to" : "f40" } }, { "id" : "f3", "type" : "CallSubStart", - "name" : "delete(Date,StorageService)", + "name" : "delete(Date)", "config" : { "callSignature" : "delete", "input" : { "params" : [ - { "name" : "date", "type" : "java.util.Date" }, - { "name" : "azureBlobStorage", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "date", "type" : "java.util.Date" } ], "map" : { - "out.azureBlobStorageService" : "param.azureBlobStorage", - "out.date" : "param.date" + "out.date" : "param.date", + "out.functionName" : "\"deleteByDate\"" } }, "result" : { @@ -68,26 +58,27 @@ "map" : { "result.isSucess" : "in.isSuccess" } - } + }, + "tags" : "connector" }, "visual" : { - "at" : { "x" : 96, "y" : 192 }, + "at" : { "x" : 96, "y" : 136 }, "labelOffset" : { "x" : 9, "y" : 41 }, "description" : [ "Description: delete blob by date", "Parammeter:", "- date: java.util.Date", - "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", "Result:", "- isSucess: java.lang.Boolean" - ] + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" }, - "connect" : { "id" : "f6", "to" : "f4" } + "connect" : { "id" : "f42", "to" : "f40", "via" : [ { "x" : 240, "y" : 136 } ] } }, { "id" : "f5", "type" : "CallSubEnd", "visual" : { - "at" : { "x" : 368, "y" : 192 } + "at" : { "x" : 616, "y" : 144 } } }, { "id" : "f4", @@ -96,42 +87,31 @@ "config" : { "output" : { "code" : [ + "import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService;", + "", + "in.azureBlobStorageService = new AzureBlobStorageService();", "in.azureBlobStorageService.delete(in.date);", "in.isSuccess = true;" ] } }, "visual" : { - "at" : { "x" : 264, "y" : 192 } + "at" : { "x" : 488, "y" : 144 } }, "connect" : { "id" : "f7", "to" : "f5" } - }, { - "id" : "f8", - "type" : "Script", - "name" : "get list blob", - "config" : { - "output" : { - "code" : "in.blobItems = in.azureBlobStorageService.getBlobs();" - } - }, - "visual" : { - "at" : { "x" : 256, "y" : 64 } - }, - "connect" : { "id" : "f2", "to" : "f1" } }, { "id" : "f10", "type" : "CallSubStart", - "name" : "detete(String,StorageService)", + "name" : "detete(String)", "config" : { "callSignature" : "detete", "input" : { "params" : [ - { "name" : "blobName", "type" : "String" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "out.azureBlobStorageService" : "param.azureBlobStorageService", - "out.blobName" : "param.blobName" + "out.blobName" : "param.blobName", + "out.functionName" : "\"deleteByName\"" } }, "result" : { @@ -141,25 +121,26 @@ "map" : { "result.isSuccess" : "in.isSuccess" } - } + }, + "tags" : "connector" }, "visual" : { - "at" : { "x" : 96, "y" : 296 }, + "at" : { "x" : 96, "y" : 200 }, "description" : [ "Description: delete blob by blob name", "Parammeter:", "- blobName: java.lang.String", - "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", "Result:", "- isSucess: java.lang.Boolean" - ] + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" }, - "connect" : { "id" : "f13", "to" : "f12" } + "connect" : { "id" : "f45", "to" : "f40", "via" : [ { "x" : 240, "y" : 200 } ] } }, { "id" : "f11", "type" : "CallSubEnd", "visual" : { - "at" : { "x" : 368, "y" : 296 } + "at" : { "x" : 616, "y" : 224 } } }, { "id" : "f12", @@ -171,23 +152,22 @@ } }, "visual" : { - "at" : { "x" : 264, "y" : 296 } + "at" : { "x" : 488, "y" : 224 } }, "connect" : { "id" : "f14", "to" : "f11" } }, { "id" : "f15", "type" : "CallSubStart", - "name" : "restore(String,StorageService)", + "name" : "restore(String)", "config" : { "callSignature" : "restore", "input" : { "params" : [ - { "name" : "blobName", "type" : "String" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "out.azureBlobStorageService" : "param.azureBlobStorageService", - "out.blobName" : "param.blobName" + "out.blobName" : "param.blobName", + "out.functionName" : "\"restore\"" } }, "result" : { @@ -197,20 +177,21 @@ "map" : { "result.isSuccess" : "in.isSuccess" } - } + }, + "tags" : "connector" }, "visual" : { - "at" : { "x" : 96, "y" : 392 }, + "at" : { "x" : 96, "y" : 264 }, "description" : [ "Description: restore blob by blob name", "Parammeter:", "- blobName: java.lang.String", - "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", "Result:", "- isSucess: java.lang.Boolean" - ] + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" }, - "connect" : { "id" : "f18", "to" : "f16" } + "connect" : { "id" : "f46", "to" : "f40", "via" : [ { "x" : 240, "y" : 264 } ] } }, { "id" : "f16", "type" : "Script", @@ -224,29 +205,28 @@ } }, "visual" : { - "at" : { "x" : 264, "y" : 392 } + "at" : { "x" : 488, "y" : 304 } }, "connect" : { "id" : "f19", "to" : "f17" } }, { "id" : "f17", "type" : "CallSubEnd", "visual" : { - "at" : { "x" : 376, "y" : 392 } + "at" : { "x" : 616, "y" : 304 } } }, { "id" : "f20", "type" : "CallSubStart", - "name" : "getLinkDownload(String,StorageService)", + "name" : "getLinkDownload(String)", "config" : { "callSignature" : "getLinkDownload", "input" : { "params" : [ - { "name" : "blobName", "type" : "String" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "out.azureBlobStorageService" : "param.azureBlobStorageService", - "out.blobName" : "param.blobName" + "out.blobName" : "param.blobName", + "out.functionName" : "\"getLink\"" } }, "result" : { @@ -256,26 +236,27 @@ "map" : { "result.linkDownload" : "in.linkDownload" } - } + }, + "tags" : "connector" }, "visual" : { - "at" : { "x" : 88, "y" : 496 }, - "labelOffset" : { "x" : 49, "y" : 57 }, + "at" : { "x" : 96, "y" : 328 }, + "labelOffset" : { "x" : 33, "y" : 33 }, "description" : [ "Description: get link download by blob name", "Parammeter:", "- blobName: java.lang.String", - "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", "Result:", "- linkDownload: java.lang.String" - ] + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" }, - "connect" : { "id" : "f24", "to" : "f22" } + "connect" : { "id" : "f47", "to" : "f40", "via" : [ { "x" : 240, "y" : 328 } ] } }, { "id" : "f21", "type" : "CallSubEnd", "visual" : { - "at" : { "x" : 384, "y" : 496 } + "at" : { "x" : 616, "y" : 384 } } }, { "id" : "f22", @@ -287,13 +268,13 @@ } }, "visual" : { - "at" : { "x" : 264, "y" : 496 } + "at" : { "x" : 488, "y" : 384 } }, "connect" : { "id" : "f23", "to" : "f21" } }, { "id" : "f25", "type" : "CallSubStart", - "name" : "uploadFromFile(String,String,String,Boolean,StorageService)", + "name" : "uploadFromFile(String,String,String,Boolean)", "config" : { "callSignature" : "uploadFromFile", "input" : { @@ -301,12 +282,11 @@ { "name" : "localPath", "type" : "String" }, { "name" : "blobName", "type" : "String" }, { "name" : "uploadToFolder", "type" : "String" }, - { "name" : "isOverwriteFile", "type" : "Boolean" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "isOverwriteFile", "type" : "Boolean" } ], "map" : { - "out.azureBlobStorageService" : "param.azureBlobStorageService", "out.blobName" : "param.blobName", + "out.functionName" : "\"uploadByPath\"", "out.isOverwriteFile" : "param.isOverwriteFile", "out.localPath" : "param.localPath", "out.uploadToFolder" : "param.uploadToFolder" @@ -319,11 +299,12 @@ "map" : { "result.blobName" : "in.blobName" } - } + }, + "tags" : "connector" }, "visual" : { - "at" : { "x" : 88, "y" : 608 }, - "labelOffset" : { "x" : 25, "y" : 57 }, + "at" : { "x" : 96, "y" : 392 }, + "labelOffset" : { "x" : 9, "y" : 33 }, "description" : [ "Description: upload from file", "Parammeter:", @@ -331,17 +312,17 @@ "- blobName: java.lang.String", "- uploadToFolder: java.lang.String", "- isOverwriteFile: java.lang.Boolean", - "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", "Result:", "- blobName: java.lang.String" - ] + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" }, - "connect" : { "id" : "f28", "to" : "f27" } + "connect" : { "id" : "f48", "to" : "f40", "via" : [ { "x" : 240, "y" : 392 } ] } }, { "id" : "f26", "type" : "CallSubEnd", "visual" : { - "at" : { "x" : 384, "y" : 608 } + "at" : { "x" : 616, "y" : 464 } } }, { "id" : "f27", @@ -353,13 +334,13 @@ } }, "visual" : { - "at" : { "x" : 272, "y" : 608 } + "at" : { "x" : 488, "y" : 464 } }, "connect" : { "id" : "f29", "to" : "f26" } }, { "id" : "f30", "type" : "CallSubStart", - "name" : "uploadFromUrl(String,String,String,Boolean,StorageService)", + "name" : "uploadFromUrl(String,String,String,Boolean)", "config" : { "callSignature" : "uploadFromUrl", "input" : { @@ -367,12 +348,11 @@ { "name" : "url", "type" : "String" }, { "name" : "blobName", "type" : "String" }, { "name" : "uploadToFolder", "type" : "String" }, - { "name" : "isOverwriteFile", "type" : "Boolean" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "isOverwriteFile", "type" : "Boolean" } ], "map" : { - "out.azureBlobStorageService" : "param.azureBlobStorageService", "out.blobName" : "param.blobName", + "out.functionName" : "\"uploadByURL\"", "out.isOverwriteFile" : "param.isOverwriteFile", "out.uploadToFolder" : "param.uploadToFolder", "out.url" : "param.url" @@ -385,11 +365,12 @@ "map" : { "result.blobName" : "in.blobName" } - } + }, + "tags" : "connector" }, "visual" : { - "at" : { "x" : 88, "y" : 736 }, - "labelOffset" : { "x" : 25, "y" : 57 }, + "at" : { "x" : 96, "y" : 456 }, + "labelOffset" : { "x" : 25, "y" : 33 }, "description" : [ "Description: upload from file", "Parammeter:", @@ -397,12 +378,12 @@ "- blobName: java.lang.String", "- uploadToFolder: java.lang.String", "- isOverwriteFile: java.lang.Boolean", - "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", "Result:", "- blobName: java.lang.String" - ] + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" }, - "connect" : { "id" : "f33", "to" : "f31" } + "connect" : { "id" : "f49", "to" : "f40", "via" : [ { "x" : 240, "y" : 456 } ] } }, { "id" : "f31", "type" : "Script", @@ -413,19 +394,19 @@ } }, "visual" : { - "at" : { "x" : 272, "y" : 736 } + "at" : { "x" : 488, "y" : 544 } }, "connect" : { "id" : "f34", "to" : "f32" } }, { "id" : "f32", "type" : "CallSubEnd", "visual" : { - "at" : { "x" : 384, "y" : 736 } + "at" : { "x" : 616, "y" : 544 } } }, { "id" : "f35", "type" : "CallSubStart", - "name" : "uploadFromFile(InputStream,String,String,Boolean,StorageService)", + "name" : "uploadFromFile(InputStream,String,String,Boolean)", "config" : { "callSignature" : "uploadFromFile", "input" : { @@ -433,13 +414,12 @@ { "name" : "content", "type" : "java.io.InputStream" }, { "name" : "blobName", "type" : "String" }, { "name" : "uploadToFolder", "type" : "String" }, - { "name" : "isOverwriteFile", "type" : "Boolean" }, - { "name" : "azureBlobStorageService", "type" : "com.axonivy.cloud.storage.azure.blob.connector.StorageService" } + { "name" : "isOverwriteFile", "type" : "Boolean" } ], "map" : { - "out.azureBlobStorageService" : "param.azureBlobStorageService", "out.blobName" : "param.blobName", "out.content" : "param.content", + "out.functionName" : "\"uploadByFile\"", "out.isOverwriteFile" : "param.isOverwriteFile", "out.uploadToFolder" : "param.uploadToFolder" } @@ -451,11 +431,12 @@ "map" : { "result.blobName" : "in.blobName" } - } + }, + "tags" : "connector" }, "visual" : { - "at" : { "x" : 88, "y" : 888 }, - "labelOffset" : { "x" : 25, "y" : 57 }, + "at" : { "x" : 96, "y" : 512 }, + "labelOffset" : { "x" : 17, "y" : 33 }, "description" : [ "Description: upload from inputStream", "Parammeter:", @@ -463,12 +444,12 @@ "- blobName: java.lang.String", "- uploadToFolder: java.lang.String", "- isOverwriteFile: java.lang.Boolean", - "- azureBlobStorage: com.axonivy.cloud.storage.azure.blob.connector.StorageService", "Result:", "- blobName: java.lang.String" - ] + ], + "icon" : "res:/webContent/icon/azure-blob-icon.png?small" }, - "connect" : { "id" : "f39", "to" : "f36" } + "connect" : { "id" : "f50", "to" : "f40", "via" : [ { "x" : 240, "y" : 512 } ] } }, { "id" : "f36", "type" : "Script", @@ -483,14 +464,132 @@ } }, "visual" : { - "at" : { "x" : 272, "y" : 888 } + "at" : { "x" : 488, "y" : 624 } }, "connect" : { "id" : "f38", "to" : "f37" } }, { "id" : "f37", "type" : "CallSubEnd", "visual" : { - "at" : { "x" : 384, "y" : 888 } + "at" : { "x" : 616, "y" : 624 } + } + }, { + "id" : "f8", + "type" : "Script", + "name" : "get list blob", + "config" : { + "output" : { + "code" : [ + "import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService;", + "", + "in.azureBlobStorageService = new AzureBlobStorageService();", + "in.blobItems = in.azureBlobStorageService.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.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService;", + "", + "in.azureBlobStorageService = new AzureBlobStorageService();" + ] + } + }, + "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,String)", + "config" : { + "callSignature" : "checkBlobIsExist", + "input" : { + "params" : [ + { "name" : "fileName", "type" : "String" }, + { "name" : "uploadToFolder", "type" : "String" } + ], + "map" : { + "out.blobName" : "param.fileName", + "out.functionName" : "\"checkBlobExist\"", + "out.uploadToFolder" : "param.uploadToFolder" + } + }, + "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:", + "- fileName: java.lang.String", + "- uploadToFolder: 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.azureBlobStorageService.checkBlobIsExist(in.blobName, in.uploadToFolder);" + } + }, + "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/cloud/storage/azure/blob/connector/StorageService.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java index 37675ca..87bce7d 100644 --- a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java +++ b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java @@ -70,7 +70,7 @@ public interface StorageService { /** * The API to delete blob * @param blob name - * @return - The blob name + * @return - boolean * */ public boolean delete(String blobName); @@ -117,4 +117,12 @@ public interface StorageService { * @return - The url for download */ public String getDownloadLink(String blobName); + + /** + * The API to check blob is already exist + * @param blobName - The blob name + * @param uploadToFolder - The folder name + * @return boolean + * */ + public boolean checkBlobIsExist(String blobName, String uploadToFolder); } diff --git a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java index b59b477..cf9898d 100644 --- a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java +++ b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; +import com.axonivy.cloud.storage.azure.blob.connector.BlobServiceClientHelper; import com.axonivy.cloud.storage.azure.blob.connector.StorageService; import com.axonivy.cloud.storage.azure.blob.connector.internal.helper.BlobSASHelper; import com.azure.core.http.rest.PagedResponse; @@ -64,7 +65,18 @@ public AzureBlobStorageService(BlobServiceClient blobServiceClient, String conta this.destinationContainer = getBlobContainerClient(this.blobServiceClient, container); this.downloadLinkLiveTime = downloadLinkLiveTime; } - + + public AzureBlobStorageService() { + 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); + destinationContainer = getBlobContainerClient(blobServiceClient, containerName); + } + @Override public String upload(String content, String fileName) { BlockBlobClient blockBlobClient = getBlobClient(fileName).getBlockBlobClient(); @@ -239,10 +251,16 @@ public void delete(Date d) { List bi = destinationContainer.listBlobs().stream().filter(b -> isSameDate(b, d)).collect(Collectors.toList()); bi.forEach(blob -> delete(blob.getName())); } + + @Override + public boolean checkBlobIsExist(String fileName, String uploadToFolder) { + return getBlobClient(createBlobPath(uploadToFolder, fileName)).exists(); + } private boolean isSameDate(BlobItem bi, Date date) { String creationTime2String = bi.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/webContent/icon/azure-blob-icon.png b/azure-blob-connector/webContent/icon/azure-blob-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d94371448c610492afc354a236ae1754c36f48b9 GIT binary patch literal 2140 zcmd5;c{JNu8@?g7sv5OqG*m}AMNNb1Am~_Xt)`aI`DO&897;`VB9_qciE&ya9X%~- zrm^p;gi&jVRM06J8GLAxqOlJ_s`l;sMLXY|`D6b7?m73|``r8f&Ut_D^E~glDQ=e? z6%HLc1OQNQcCzyT00wPgKvo)Byu-_a#E9VGXbY;?8gl^1syW-)coMUOug56f*VJW} zXU-ebd9P3MuHZblgBz&pl_jb>?lz&tWP}gb!;5E8EU{ZO1{pCkdazllEyR_mTcJC!QsuOe?VbGnr<#T#r&Zv1$;Q;4N-r{@rf zo)bT@G^o-D8D? zT}-TOD@~q?(wAKGN;R`0SMq%K+F7)v2=-(QgqttDzj)J&3C6fZaChoIKrjpbC~7}Om0>LUlyoRM_`j#^v9C3 z2}i)`Qy|Y@*^wk00D8}A0?$jpx|1(OBF{m`65wi$94;=i7K_(p`WYrC$|yQqEMHl_Q?DSM__dc8d2@5qEb?;GBGjPLQ4jD;ZcB-Er^nq;766q&gQ2?J-ZIm!rFTBin;A>U9FZIkW-Ya=pU zR<;(RL8Y(}1fSFFEwf3u2S0ri-uQ)TrZb0}5llu_P;fUI6szXx@DXK4`JL|&2$0lo z$S`U+N|CB}sYbFiM9be}z8Ffc({r**Kv~;$WQTlmGq=si_hqJFvaID=IcuspU%gB7 zrL8xjym8H{fYS;??a_0&njxPoe*7@=k)b{r2_r44G#G!x(fm_4E%G4Z!Ux5m4x}P> zdQE{NY1ni~z%Qn>4uO9K;0^DxWU7{gyQP(HwD;wg-%?8_N@>UMt)7X83%I72u$F82 zyosKq(XnVfp2{#RtF=+xDt3xy6b^f!UX$@$v@-0Qg6diNqXpSGog=r=k1YM{-$DKI zrLJq@;!829RAgKxxFz@&IUkoCT4B|lkMnFQJ zTheRu=I{%A!m=A4Wqlu)7`GaymC`p#Bfe>1S}AXKJHE!53OCq8S{D=xR1Y%18_I&KlQP?Jp5)?Vb_2xR79jw_9| z?(c2K$21v?tmf|K#;v_VVFxt0!uf=XOahzrW_dDh{q_1iMyU8v@Q;IJz9aLicZ4v7 SVbm`Ua<;#0S7qz><9`9f9HPqr literal 0 HcmV?d00001 From c6fe0438afbb855067de1f586438b1093f72b52c Mon Sep 17 00:00:00 2001 From: Phan Thi Ngan Ha Date: Thu, 25 Jul 2024 14:29:44 +0700 Subject: [PATCH 07/13] Clean code --- .../storage/bean/UploadByCallSubprocess.java | 5 ++ .../UploadByCallSubprocess.xhtml | 4 +- .../UploadByCallSubprocessData.ivyClass | 2 + .../UploadByCallSubprocessProcess.p.json | 66 +++++++++++-------- .../blob/connector/BlobStorageData.ivyClass | 2 +- .../processes/BlobStorage.p.json | 39 +++++------ .../azure/blob/connector/StorageService.java | 8 +-- .../internal/AzureBlobStorageService.java | 26 ++------ .../internal/bean/AzureBlobStorageBean.java | 36 ++++++++++ 9 files changed, 109 insertions(+), 79 deletions(-) create mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/bean/AzureBlobStorageBean.java diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java index 72fd052..a988404 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java +++ b/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java @@ -217,4 +217,9 @@ public void getBlobs(List bis) { blobs.add(b); } } + + public String createBlobPath(String folderName, String fileName) { + String path = StringUtils.isNotBlank(folderName) ? folderName + "/" : StringUtils.EMPTY; + return path + fileName; + } } diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml index 79dfb32..15ce07d 100644 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml @@ -31,7 +31,7 @@ - + @@ -58,7 +58,7 @@ - + diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass index 9faf426..cdfe8cc 100644 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass @@ -8,3 +8,5 @@ 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/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json index ecea95e..eba5575 100644 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json +++ b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json @@ -109,12 +109,12 @@ "visual" : { "at" : { "x" : 96, "y" : 352 } }, - "connect" : { "id" : "f119", "to" : "f30" } + "connect" : { "id" : "f127", "to" : "f126" } }, { "id" : "f15", "type" : "HtmlDialogEnd", "visual" : { - "at" : { "x" : 1088, "y" : 352 } + "at" : { "x" : 1240, "y" : 352 } } }, { "id" : "f18", @@ -357,7 +357,7 @@ } }, "visual" : { - "at" : { "x" : 536, "y" : 352 } + "at" : { "x" : 688, "y" : 352 } }, "connect" : { "id" : "f67", "to" : "f66" } }, { @@ -365,7 +365,7 @@ "type" : "Alternative", "name" : "is exist file ?", "visual" : { - "at" : { "x" : 352, "y" : 352 }, + "at" : { "x" : 504, "y" : 352 }, "labelOffset" : { "x" : 16, "y" : -24 } }, "connect" : [ @@ -386,9 +386,9 @@ } }, "visual" : { - "at" : { "x" : 352, "y" : 432 } + "at" : { "x" : 504, "y" : 432 } }, - "connect" : { "id" : "f65", "to" : "f15", "via" : [ { "x" : 1088, "y" : 432 } ] } + "connect" : { "id" : "f65", "to" : "f15", "via" : [ { "x" : 1240, "y" : 432 } ] } }, { "id" : "f66", "type" : "Alternative", @@ -397,14 +397,14 @@ "not null ?" ], "visual" : { - "at" : { "x" : 664, "y" : 352 }, + "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" : 664, "y" : 280 }, { "x" : 1088, "y" : 280 } ], "label" : { + { "id" : "f70", "to" : "f15", "via" : [ { "x" : 816, "y" : 280 }, { "x" : 1240, "y" : 280 } ], "label" : { "name" : "no", "segment" : 1.47 } } @@ -423,7 +423,7 @@ } }, "visual" : { - "at" : { "x" : 792, "y" : 352 } + "at" : { "x" : 944, "y" : 352 } }, "connect" : { "id" : "f72", "to" : "f71" } }, { @@ -442,7 +442,7 @@ } }, "visual" : { - "at" : { "x" : 952, "y" : 352 } + "at" : { "x" : 1104, "y" : 352 } }, "connect" : { "id" : "f16", "to" : "f15" } }, { @@ -776,7 +776,9 @@ "output" : { "code" : [ "import org.apache.commons.io.FilenameUtils;", - "in.bean.fileNamePath = FilenameUtils.getName(in.bean.localPath);" + "", + "in.bean.fileNamePath = FilenameUtils.getName(in.bean.localPath);", + "in.combineFileNameAndFolder = in.bean.createBlobPath(in.bean.uploadToFolderByLocalPath, in.bean.fileNamePath);" ] } }, @@ -793,7 +795,8 @@ "code" : [ "import com.axonivy.cloud.storage.utils.UploadUtils;", "", - "in.bean.fileNameURL = UploadUtils.getFileNameFromUrl(in.bean.url);" + "in.bean.fileNameURL = UploadUtils.getFileNameFromUrl(in.bean.url);", + "in.combineFileNameAndFolder = in.bean.createBlobPath(in.bean.uploadToFolderByURL, in.bean.fileNameURL);" ] } }, @@ -921,7 +924,7 @@ "type" : "SubProcessCall", "name" : "check blob is exist", "config" : { - "processCall" : "BlobStorage:checkBlobIsExist(String,String)", + "processCall" : "BlobStorage:checkBlobIsExist(String)", "output" : { "map" : { "out" : "in", @@ -930,17 +933,15 @@ }, "call" : { "params" : [ - { "name" : "fileName", "type" : "String" }, - { "name" : "uploadToFolder", "type" : "String" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "param.fileName" : "in.bean.fileName", - "param.uploadToFolder" : "in.bean.uploadToFolderByPrimefaces" + "param.blobName" : "in.combineFileNameAndFolder" } } }, "visual" : { - "at" : { "x" : 224, "y" : 352 } + "at" : { "x" : 384, "y" : 352 } }, "connect" : { "id" : "f17", "to" : "f14" } }, { @@ -948,7 +949,7 @@ "type" : "SubProcessCall", "name" : "check blob is exist", "config" : { - "processCall" : "BlobStorage:checkBlobIsExist(String,String)", + "processCall" : "BlobStorage:checkBlobIsExist(String)", "output" : { "map" : { "out" : "in", @@ -957,12 +958,10 @@ }, "call" : { "params" : [ - { "name" : "fileName", "type" : "String" }, - { "name" : "uploadToFolder", "type" : "String" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "param.fileName" : "in.bean.fileNamePath", - "param.uploadToFolder" : "in.bean.uploadToFolderByLocalPath" + "param.blobName" : "in.combineFileNameAndFolder" } } }, @@ -975,7 +974,7 @@ "type" : "SubProcessCall", "name" : "check blob is exist", "config" : { - "processCall" : "BlobStorage:checkBlobIsExist(String,String)", + "processCall" : "BlobStorage:checkBlobIsExist(String)", "output" : { "map" : { "out" : "in", @@ -984,12 +983,10 @@ }, "call" : { "params" : [ - { "name" : "fileName", "type" : "String" }, - { "name" : "uploadToFolder", "type" : "String" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "param.fileName" : "in.bean.fileNameURL", - "param.uploadToFolder" : "in.bean.uploadToFolderByURL" + "param.blobName" : "in.combineFileNameAndFolder" } } }, @@ -1022,5 +1019,18 @@ "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" } } ] } \ No newline at end of file diff --git a/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass b/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass index ad6cfd7..ee5ff6b 100644 --- a/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass +++ b/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass @@ -1,7 +1,7 @@ BlobStorageData #class com.axonivy.cloud.storage.azure.blob.connector #namespace date java.util.Date #field -azureBlobStorageService com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService #field +azureBlobStorageBean com.axonivy.cloud.storage.azure.blob.connector.internal.bean.AzureBlobStorageBean #field isSuccess Boolean #field blobItems java.util.List #field blobName String #field diff --git a/azure-blob-connector/processes/BlobStorage.p.json b/azure-blob-connector/processes/BlobStorage.p.json index 6684421..1553ba7 100644 --- a/azure-blob-connector/processes/BlobStorage.p.json +++ b/azure-blob-connector/processes/BlobStorage.p.json @@ -87,10 +87,8 @@ "config" : { "output" : { "code" : [ - "import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService;", "", - "in.azureBlobStorageService = new AzureBlobStorageService();", - "in.azureBlobStorageService.delete(in.date);", + "in.azureBlobStorageBean.getAzureBlobStorageService().delete(in.date);", "in.isSuccess = true;" ] } @@ -148,7 +146,7 @@ "name" : "delete blob by name", "config" : { "output" : { - "code" : "in.isSuccess = in.azureBlobStorageService.delete(in.blobName);" + "code" : "in.isSuccess = in.azureBlobStorageBean.getAzureBlobStorageService().delete(in.blobName);" } }, "visual" : { @@ -199,7 +197,7 @@ "config" : { "output" : { "code" : [ - "in.azureBlobStorageService.restore(in.blobName);", + "in.azureBlobStorageBean.getAzureBlobStorageService().restore(in.blobName);", "in.isSuccess = true;" ] } @@ -264,7 +262,7 @@ "name" : "get link", "config" : { "output" : { - "code" : "in.linkDownload = in.azureBlobStorageService.getDownloadLink(in.blobName);" + "code" : "in.linkDownload = in.azureBlobStorageBean.getAzureBlobStorageService().getDownloadLink(in.blobName);" } }, "visual" : { @@ -330,7 +328,7 @@ "name" : "upload", "config" : { "output" : { - "code" : "in.blobName = in.azureBlobStorageService.uploadFromFile(in.localPath, in.uploadToFolder, in.isOverwriteFile);" + "code" : "in.blobName = in.azureBlobStorageBean.getAzureBlobStorageService().uploadFromFile(in.localPath, in.uploadToFolder, in.isOverwriteFile);" } }, "visual" : { @@ -390,7 +388,7 @@ "name" : "upload", "config" : { "output" : { - "code" : "in.blobName = in.azureBlobStorageService.uploadFromUrl(in.url, in.uploadToFolder, in.isOverwriteFile);" + "code" : "in.blobName = in.azureBlobStorageBean.getAzureBlobStorageService().uploadFromUrl(in.url, in.uploadToFolder, in.isOverwriteFile);" } }, "visual" : { @@ -459,7 +457,7 @@ "code" : [ "import org.apache.commons.io.IOUtils;", "", - "in.blobName = in.azureBlobStorageService.upload(IOUtils.toByteArray(in.content), in.blobName, in.uploadToFolder, in.isOverwriteFile);" + "in.blobName = in.azureBlobStorageBean.getAzureBlobStorageService().upload(IOUtils.toByteArray(in.content), in.blobName, in.uploadToFolder, in.isOverwriteFile);" ] } }, @@ -480,10 +478,8 @@ "config" : { "output" : { "code" : [ - "import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService;", "", - "in.azureBlobStorageService = new AzureBlobStorageService();", - "in.blobItems = in.azureBlobStorageService.getBlobs();" + "in.blobItems = in.azureBlobStorageBean.getAzureBlobStorageService().getBlobs();" ] } }, @@ -504,9 +500,9 @@ "config" : { "output" : { "code" : [ - "import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService;", + "import com.axonivy.cloud.storage.azure.blob.connector.internal.bean.AzureBlobStorageBean;", "", - "in.azureBlobStorageService = new AzureBlobStorageService();" + "in.azureBlobStorageBean = new AzureBlobStorageBean();" ] } }, @@ -536,18 +532,16 @@ }, { "id" : "f51", "type" : "CallSubStart", - "name" : "checkBlobIsExist(String,String)", + "name" : "checkBlobIsExist(String)", "config" : { "callSignature" : "checkBlobIsExist", "input" : { "params" : [ - { "name" : "fileName", "type" : "String" }, - { "name" : "uploadToFolder", "type" : "String" } + { "name" : "blobName", "type" : "String" } ], "map" : { - "out.blobName" : "param.fileName", - "out.functionName" : "\"checkBlobExist\"", - "out.uploadToFolder" : "param.uploadToFolder" + "out.blobName" : "param.blobName", + "out.functionName" : "\"checkBlobExist\"" } }, "result" : { @@ -564,8 +558,7 @@ "description" : [ "Description: check blob is exist by blob name", "Parammeter:", - "- fileName: java.lang.String", - "- uploadToFolder: java.lang.String", + "- blobName: java.lang.String", "Result:", "- isExist: java.lang.Boolean" ], @@ -578,7 +571,7 @@ "name" : "check blob exist", "config" : { "output" : { - "code" : "in.isExist = in.azureBlobStorageService.checkBlobIsExist(in.blobName, in.uploadToFolder);" + "code" : "in.isExist = in.azureBlobStorageBean.isBlobExist(in.blobName);" } }, "visual" : { diff --git a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java index 87bce7d..8e2e284 100644 --- a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java +++ b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java @@ -5,6 +5,7 @@ import java.util.Date; import java.util.List; +import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.models.BlobItem; public interface StorageService { @@ -119,10 +120,9 @@ public interface StorageService { public String getDownloadLink(String blobName); /** - * The API to check blob is already exist + * The API to get blob client * @param blobName - The blob name - * @param uploadToFolder - The folder name - * @return boolean + * @return BlobClient * */ - public boolean checkBlobIsExist(String blobName, String uploadToFolder); + public BlobClient getBlobClient(String blobName); } diff --git a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java index cf9898d..6a7d34d 100644 --- a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java +++ b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java @@ -66,17 +66,6 @@ public AzureBlobStorageService(BlobServiceClient blobServiceClient, String conta this.downloadLinkLiveTime = downloadLinkLiveTime; } - public AzureBlobStorageService() { - 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); - destinationContainer = getBlobContainerClient(blobServiceClient, containerName); - } - @Override public String upload(String content, String fileName) { BlockBlobClient blockBlobClient = getBlobClient(fileName).getBlockBlobClient(); @@ -167,6 +156,11 @@ public ByteArrayOutputStream downloadStream(String blobName) throws IOException } } + @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() @@ -188,10 +182,6 @@ private BlobContainerClient getBlobContainerClient(BlobServiceClient blobService return blobContainerClient; } - private BlobClient getBlobClient(String blobName) { - return this.destinationContainer.getBlobClient(blobName); - } - private static String getFileNameFromUrl(String url) { try { return Paths.get(new URI(url).getPath()).getFileName().toString(); @@ -251,16 +241,10 @@ public void delete(Date d) { List bi = destinationContainer.listBlobs().stream().filter(b -> isSameDate(b, d)).collect(Collectors.toList()); bi.forEach(blob -> delete(blob.getName())); } - - @Override - public boolean checkBlobIsExist(String fileName, String uploadToFolder) { - return getBlobClient(createBlobPath(uploadToFolder, fileName)).exists(); - } private boolean isSameDate(BlobItem bi, Date date) { String creationTime2String = bi.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/cloud/storage/azure/blob/connector/internal/bean/AzureBlobStorageBean.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/bean/AzureBlobStorageBean.java new file mode 100644 index 0000000..0e075bc --- /dev/null +++ b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/bean/AzureBlobStorageBean.java @@ -0,0 +1,36 @@ +package com.axonivy.cloud.storage.azure.blob.connector.internal.bean; + +import com.axonivy.cloud.storage.azure.blob.connector.BlobServiceClientHelper; +import com.axonivy.cloud.storage.azure.blob.connector.StorageService; +import com.axonivy.cloud.storage.azure.blob.connector.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(); + } +} From f0ec3a76af5bcf5247c3cabf17cf8555af918d14 Mon Sep 17 00:00:00 2001 From: Trung Mai Date: Mon, 29 Jul 2024 15:44:14 +0700 Subject: [PATCH 08/13] TE-641: Update document for process element --- azure-blob-connector-product/.classpath | 28 ------------------ azure-blob-connector-product/.gitignore | 20 +------------ .../org.eclipse.wst.common.component | 10 +++++++ azure-blob-connector-product/README.md | 25 ++++++++++++---- .../images/AddBlobStorageAndCallFunction.png | Bin 0 -> 37094 bytes .../images/BlobStorageFunctions.png | Bin 0 -> 55252 bytes .../images/ElementInExtensions.png | Bin 0 -> 24507 bytes .../internal/AzureBlobStorageService.java | 1 - 8 files changed, 31 insertions(+), 53 deletions(-) delete mode 100644 azure-blob-connector-product/.classpath create mode 100644 azure-blob-connector-product/images/AddBlobStorageAndCallFunction.png create mode 100644 azure-blob-connector-product/images/BlobStorageFunctions.png create mode 100644 azure-blob-connector-product/images/ElementInExtensions.png diff --git a/azure-blob-connector-product/.classpath b/azure-blob-connector-product/.classpath deleted file mode 100644 index a24d7cc..0000000 --- a/azure-blob-connector-product/.classpath +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/azure-blob-connector-product/.gitignore b/azure-blob-connector-product/.gitignore index 1b2547b..ae3c172 100644 --- a/azure-blob-connector-product/.gitignore +++ b/azure-blob-connector-product/.gitignore @@ -1,19 +1 @@ -# general -Thumbs.db -.DS_Store -*~ -*.log - -# java -*.class -hs_err_pid* - -# maven -target/ -lib/mvn-deps/ - -# ivy -classes/ -src_dataClasses/ -src_wsproc/ -logs/ +/bin/ diff --git a/azure-blob-connector-product/.settings/org.eclipse.wst.common.component b/azure-blob-connector-product/.settings/org.eclipse.wst.common.component index d8c5a11..7168897 100644 --- a/azure-blob-connector-product/.settings/org.eclipse.wst.common.component +++ b/azure-blob-connector-product/.settings/org.eclipse.wst.common.component @@ -1,10 +1,20 @@ + + + + + + + + + + diff --git a/azure-blob-connector-product/README.md b/azure-blob-connector-product/README.md index 070fb9a..82b12a1 100644 --- a/azure-blob-connector-product/README.md +++ b/azure-blob-connector-product/README.md @@ -17,7 +17,9 @@ In the project, you only add the dependency in your pom.xml and call public APIs ${process.analyzer.version} ``` -** Below is an example for connect by client secret ** +**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: @@ -33,7 +35,20 @@ Variables: ContainterName: '' ``` -**2. 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.** +## 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. @@ -41,7 +56,7 @@ Variables: public AzureBlobStorageService(BlobServiceClient blobServiceClient, String container) ``` -**3. Application requests to Azure Blob Storage must be authorized. You must to create a BlobServiceClient.** +**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 @@ -60,7 +75,7 @@ Variables: ```java /** * Create client to a storage account. - * @param connectionString - onnection string of the 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 */ @@ -79,7 +94,7 @@ Variables: public static BlobServiceClient getBlobServiceClient(String accountName, String accountKey, String endpoint) {} ``` -**4. You can call `uploadFromUrl` to upload a file from url, `getDownloadLink` to get download link of a blob.** +**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 diff --git a/azure-blob-connector-product/images/AddBlobStorageAndCallFunction.png b/azure-blob-connector-product/images/AddBlobStorageAndCallFunction.png new file mode 100644 index 0000000000000000000000000000000000000000..c9ea09e48f51b523f92577bd4c806228741cb17c GIT binary patch literal 37094 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 CRdYN5 literal 0 HcmV?d00001 diff --git a/azure-blob-connector-product/images/BlobStorageFunctions.png b/azure-blob-connector-product/images/BlobStorageFunctions.png new file mode 100644 index 0000000000000000000000000000000000000000..b47f3fb81a5ccc1399c135f3c98c872892691167 GIT binary patch literal 55252 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 literal 0 HcmV?d00001 diff --git a/azure-blob-connector-product/images/ElementInExtensions.png b/azure-blob-connector-product/images/ElementInExtensions.png new file mode 100644 index 0000000000000000000000000000000000000000..9696ef98fe4917ea2426070f95efee1238fd86bb GIT binary patch literal 24507 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 Date: Tue, 30 Jul 2024 10:05:40 +0700 Subject: [PATCH 09/13] TE-641: Fix Dev-Build --- .github/workflows/ci.yml | 2 +- .github/workflows/dev.yml | 2 +- .github/workflows/release.yml | 2 +- azure-blob-connector-demo/pom.xml | 6 +++--- azure-blob-connector-test/pom.xml | 22 ++++++++++++++++++++-- azure-blob-connector/pom.xml | 31 ++++++++++++++----------------- 6 files changed, 40 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bc3496..87d78f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,4 +9,4 @@ on: jobs: build: - uses: axonivy-market/github-workflows/.github/workflows/ci.yml@v2 + uses: axonivy-market/github-workflows/.github/workflows/ci.yml@v4 diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index a27a639..c2fee37 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -8,4 +8,4 @@ on: jobs: build: - uses: axonivy-market/github-workflows/.github/workflows/dev.yml@v2 + uses: axonivy-market/github-workflows/.github/workflows/dev.yml@v4 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/azure-blob-connector-demo/pom.xml b/azure-blob-connector-demo/pom.xml index 5b90914..16e68d8 100644 --- a/azure-blob-connector-demo/pom.xml +++ b/azure-blob-connector-demo/pom.xml @@ -5,8 +5,8 @@ azure-blob-connector-demo 10.0.21-SNAPSHOT iar - - 10.0.16 + + 10.0.16 @@ -21,7 +21,7 @@ com.axonivy.ivy.ci project-build-plugin - ${build.plugin.version} + ${project.build.plugin.version} true diff --git a/azure-blob-connector-test/pom.xml b/azure-blob-connector-test/pom.xml index dccf789..bbb701d 100644 --- a/azure-blob-connector-test/pom.xml +++ b/azure-blob-connector-test/pom.xml @@ -6,7 +6,7 @@ 10.0.21-SNAPSHOT iar - 10.0.16 + 10.0.16 1.19.8 @@ -35,13 +35,31 @@ 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 - ${build.plugin.version} + ${project.build.plugin.version} true diff --git a/azure-blob-connector/pom.xml b/azure-blob-connector/pom.xml index f23ceff..bfc8cdd 100644 --- a/azure-blob-connector/pom.xml +++ b/azure-blob-connector/pom.xml @@ -1,28 +1,15 @@ - + 4.0.0 com.axonivy.cloud.storage azure-blob-connector 10.0.21-SNAPSHOT iar - - 10.0.16 + + 10.0.16 1.2.23 - - - - always - - sonatype - https://oss.sonatype.org/content/repositories/snapshots - - - @@ -54,13 +41,23 @@ + + + + always + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + + src com.axonivy.ivy.ci project-build-plugin - ${build.plugin.version} + ${project.build.plugin.version} true From f6ed3ca1de62d589b202ae1dde92492c5b21d661 Mon Sep 17 00:00:00 2001 From: Trung Mai Date: Tue, 30 Jul 2024 10:05:40 +0700 Subject: [PATCH 10/13] TE-641: Fix Dev-Build --- .github/workflows/ci.yml | 2 +- .github/workflows/dev.yml | 2 +- .github/workflows/release.yml | 2 +- azure-blob-connector-demo/pom.xml | 6 ++--- azure-blob-connector-test/pom.xml | 34 +++++++++++++++++++++++++-- azure-blob-connector/pom.xml | 38 +++++++++++++++++-------------- 6 files changed, 59 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bc3496..87d78f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,4 +9,4 @@ on: jobs: build: - uses: axonivy-market/github-workflows/.github/workflows/ci.yml@v2 + uses: axonivy-market/github-workflows/.github/workflows/ci.yml@v4 diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index a27a639..c2fee37 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -8,4 +8,4 @@ on: jobs: build: - uses: axonivy-market/github-workflows/.github/workflows/dev.yml@v2 + uses: axonivy-market/github-workflows/.github/workflows/dev.yml@v4 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/azure-blob-connector-demo/pom.xml b/azure-blob-connector-demo/pom.xml index 5b90914..16e68d8 100644 --- a/azure-blob-connector-demo/pom.xml +++ b/azure-blob-connector-demo/pom.xml @@ -5,8 +5,8 @@ azure-blob-connector-demo 10.0.21-SNAPSHOT iar - - 10.0.16 + + 10.0.16 @@ -21,7 +21,7 @@ com.axonivy.ivy.ci project-build-plugin - ${build.plugin.version} + ${project.build.plugin.version} true diff --git a/azure-blob-connector-test/pom.xml b/azure-blob-connector-test/pom.xml index dccf789..45acdd5 100644 --- a/azure-blob-connector-test/pom.xml +++ b/azure-blob-connector-test/pom.xml @@ -6,7 +6,7 @@ 10.0.21-SNAPSHOT iar - 10.0.16 + 10.0.16 1.19.8 @@ -35,15 +35,45 @@ 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 - ${build.plugin.version} + ${project.build.plugin.version} true + + + + maven-deploy-plugin + 3.0.0-M1 + + true + + + + + diff --git a/azure-blob-connector/pom.xml b/azure-blob-connector/pom.xml index f23ceff..07458cc 100644 --- a/azure-blob-connector/pom.xml +++ b/azure-blob-connector/pom.xml @@ -1,28 +1,15 @@ - + 4.0.0 com.axonivy.cloud.storage azure-blob-connector 10.0.21-SNAPSHOT iar - - 10.0.16 + + 10.0.16 1.2.23 - - - - always - - sonatype - https://oss.sonatype.org/content/repositories/snapshots - - - @@ -54,15 +41,32 @@ + + + + always + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + + src com.axonivy.ivy.ci project-build-plugin - ${build.plugin.version} + ${project.build.plugin.version} true + + maven-release-plugin + 3.0.0-M4 + + azure-blob-connector-v@{project.version} + + From d892d7d73ca55610b30057c8c32bb61605f2c922 Mon Sep 17 00:00:00 2001 From: Trung Mai <138564881+trungmaihova@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:35:19 +0700 Subject: [PATCH 11/13] TE-644: Fix review code to release first version * TE-644: Fix review code to release first version --------- Co-authored-by: Dinh Nguyen --- .github/ISSUE_TEMPLATE/bug_report.md | 40 +++ .github/ISSUE_TEMPLATE/feature_request.md | 22 ++ .github/workflows/ci.yml | 2 + .github/workflows/dev.yml | 2 + CODE_OF_CONDUCT.md | 24 ++ README.md | 2 +- .../.settings/ch.ivyteam.ivy.designer.prefs | 4 +- .../config/variables.yaml | 11 + .../axonivy/cloud/storage/demo/Data.ivyClass | 2 - .../cloud/storage/demo/UploadData.ivyClass | 2 - .../connector/azure/blob/demo/Data.ivyClass | 2 + .../azure/blob/demo/UploadData.ivyClass | 2 + azure-blob-connector-demo/pom.xml | 55 ++-- .../processes/Start Processes/Upload.p.json | 6 +- .../azure/blob/demo}/bean/Blob.java | 12 +- .../azure/blob/demo}/bean/UploadBean.java | 64 +++-- .../demo}/bean/UploadByCallSubprocess.java | 16 +- .../azure/blob/demo}/utils/UploadUtils.java | 4 +- .../storage/demo/Upload/UploadData.ivyClass | 6 - .../demo/Upload/resources/uploadDialog.css | 22 -- .../resources/uploadDialog.css | 22 -- .../blob}/demo/Upload/Upload.rddescriptor | 0 .../azure/blob}/demo/Upload/Upload.xhtml | 54 ++-- .../blob/demo/Upload/UploadData.ivyClass | 6 + .../blob}/demo/Upload/UploadProcess.p.json | 32 ++- .../UploadByCallSubprocess.rddescriptor | 0 .../UploadByCallSubprocess.xhtml | 46 ++-- .../UploadByCallSubprocessData.ivyClass | 4 +- .../UploadByCallSubprocessProcess.p.json | 35 ++- .../layouts/frame-10-full-width.xhtml | 59 ++++ .../webContent/layouts/styles/upload-view.css | 36 +++ azure-blob-connector-product/.classpath | 22 ++ .../.settings/ch.ivyteam.ivy.designer.prefs | 4 +- .../org.eclipse.wst.common.component | 28 +- azure-blob-connector-product/README.md | 58 +++- azure-blob-connector-product/README_DE.md | 168 ++++++++++++ .../images/DevAccountKey.png | Bin 0 -> 8290 bytes azure-blob-connector-product/pom.xml | 130 +++++---- azure-blob-connector-product/product.json | 10 +- azure-blob-connector-test/.gitignore | 1 + .../.settings/ch.ivyteam.ivy.designer.prefs | 4 +- azure-blob-connector-test/pom.xml | 16 +- .../resource_test/picture/singapore.png | Bin 9197 -> 7317 bytes .../integration/AbstractIntegrationTest.java | 119 +++++---- .../AzureBlobStorageServiceTest.java | 8 +- .../.settings/ch.ivyteam.ivy.designer.prefs | 4 +- .../azure/blob}/BlobStorageData.ivyClass | 4 +- azure-blob-connector/pom.xml | 4 +- .../processes/BlobStorage.p.json | 4 +- .../azure/blob/BlobServiceClientHelper.java | 66 +++++ .../connector/azure/blob/StorageService.java | 128 +++++++++ .../internal/AzureBlobStorageService.java | 252 ++++++++++++++++++ .../internal/bean/AzureBlobStorageBean.java | 36 +++ .../blob/internal/helper/BlobSASHelper.java | 32 +++ .../webContent/icon/azure-blob-icon.png | Bin 2140 -> 2040 bytes pom.xml | 6 +- 56 files changed, 1358 insertions(+), 340 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/Data.ivyClass delete mode 100644 azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/UploadData.ivyClass create mode 100644 azure-blob-connector-demo/dataclasses/com/axonivy/connector/azure/blob/demo/Data.ivyClass create mode 100644 azure-blob-connector-demo/dataclasses/com/axonivy/connector/azure/blob/demo/UploadData.ivyClass rename azure-blob-connector-demo/src/com/axonivy/{cloud/storage => connector/azure/blob/demo}/bean/Blob.java (55%) rename azure-blob-connector-demo/src/com/axonivy/{cloud/storage => connector/azure/blob/demo}/bean/UploadBean.java (81%) rename azure-blob-connector-demo/src/com/axonivy/{cloud/storage => connector/azure/blob/demo}/bean/UploadByCallSubprocess.java (92%) rename azure-blob-connector-demo/src/com/axonivy/{cloud/storage => connector/azure/blob/demo}/utils/UploadUtils.java (78%) delete mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadData.ivyClass delete mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/resources/uploadDialog.css delete mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/resources/uploadDialog.css rename azure-blob-connector-demo/src_hd/com/axonivy/{cloud/storage => connector/azure/blob}/demo/Upload/Upload.rddescriptor (100%) rename azure-blob-connector-demo/src_hd/com/axonivy/{cloud/storage => connector/azure/blob}/demo/Upload/Upload.xhtml (78%) create mode 100644 azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadData.ivyClass rename azure-blob-connector-demo/src_hd/com/axonivy/{cloud/storage => connector/azure/blob}/demo/Upload/UploadProcess.p.json (92%) rename azure-blob-connector-demo/src_hd/com/axonivy/{cloud/storage => connector/azure/blob}/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor (100%) rename azure-blob-connector-demo/src_hd/com/axonivy/{cloud/storage => connector/azure/blob}/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml (79%) rename azure-blob-connector-demo/src_hd/com/axonivy/{cloud/storage => connector/azure/blob}/demo/UploadByCallSubprocess/UploadByCallSubprocessData.ivyClass (68%) rename azure-blob-connector-demo/src_hd/com/axonivy/{cloud/storage => connector/azure/blob}/demo/UploadByCallSubprocess/UploadByCallSubprocessProcess.p.json (96%) create mode 100644 azure-blob-connector-demo/webContent/layouts/frame-10-full-width.xhtml create mode 100644 azure-blob-connector-demo/webContent/layouts/styles/upload-view.css create mode 100644 azure-blob-connector-product/.classpath create mode 100644 azure-blob-connector-product/README_DE.md create mode 100644 azure-blob-connector-product/images/DevAccountKey.png rename azure-blob-connector-test/src_test/com/axonivy/{cloud/storage/azure/blob/connector => connector/azure/blob}/test/integration/AbstractIntegrationTest.java (61%) rename azure-blob-connector-test/src_test/com/axonivy/{cloud/storage/azure/blob/connector => connector/azure/blob}/test/integration/AzureBlobStorageServiceTest.java (93%) rename azure-blob-connector/dataclasses/com/axonivy/{cloud/storage/azure/blob/connector => connector/azure/blob}/BlobStorageData.ivyClass (69%) create mode 100644 azure-blob-connector/src/com/axonivy/connector/azure/blob/BlobServiceClientHelper.java create mode 100644 azure-blob-connector/src/com/axonivy/connector/azure/blob/StorageService.java create mode 100644 azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/AzureBlobStorageService.java create mode 100644 azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/bean/AzureBlobStorageBean.java create mode 100644 azure-blob-connector/src/com/axonivy/connector/azure/blob/internal/helper/BlobSASHelper.java diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..1d8d93f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +Dear @ivy-sgi, we have found the following bug: + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..3285507 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +Dear @ivy-sgi, it would be cool to have the following feature in the market place: + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87d78f0..596401f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,3 +10,5 @@ on: jobs: build: 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 c2fee37..b672da0 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -9,3 +9,5 @@ on: jobs: build: 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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ec0fe32 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,24 @@ +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. +As part of the Ricoh Group, Axon Ivy is guided by [The spirit of the three loves](https://www.ricoh.com/about/ricoh-way): + +- **Love your neighbor** 🤝 +We love to get in touch with people and are willing to help others when we are aware of their issues and ideas. Everyone who participates as a user or contributor in this repository is our neighbor. + +- **Love your country** 🗺 +We love the place we’re located at and enjoy the nature around us. We take care of the environment and are eager to learn from cultures around the globe. + +- **Love your work** 👷‍♂️ +We are passionate developers, eager to work with new technologies, and are happy to be part of the digital transformation. We love to be creative at work and see our visions accomplished. + +## Our Guidelines + +This repository is intended to facilitate a friendly and inspiring exchange in which we focus on technical content. + +- Be friendly and patient. +- Be welcoming. +- Be considerate. +- Be respectful. +- Be careful in the words that you choose. +- When we disagree, try to understand why. diff --git a/README.md b/README.md index b7dc1a8..ff60a5a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Azure Blob Connector -[![CI Build](https://github.com/axonivy-professional-services/market-azure-blob-connector/actions/workflows/ci.yml/badge.svg)](https://github.com/axonivy-professional-services/market-market-azure-blob-connector/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) - Upload file to Azure blob - Get temporary download link diff --git a/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs index 6ea0e31..e93fcf9 100644 --- a/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs +++ b/azure-blob-connector-demo/.settings/ch.ivyteam.ivy.designer.prefs @@ -1,5 +1,5 @@ -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.cloud.storage.demo.Data -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.demo +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/config/variables.yaml b/azure-blob-connector-demo/config/variables.yaml index 99a327a..245f493 100644 --- a/azure-blob-connector-demo/config/variables.yaml +++ b/azure-blob-connector-demo/config/variables.yaml @@ -3,3 +3,14 @@ # 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/dataclasses/com/axonivy/cloud/storage/demo/Data.ivyClass b/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/Data.ivyClass deleted file mode 100644 index b21cf46..0000000 --- a/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/Data.ivyClass +++ /dev/null @@ -1,2 +0,0 @@ -Data #class -com.axonivy.cloud.storage.demo #namespace diff --git a/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/UploadData.ivyClass b/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/UploadData.ivyClass deleted file mode 100644 index 9f1cc9a..0000000 --- a/azure-blob-connector-demo/dataclasses/com/axonivy/cloud/storage/demo/UploadData.ivyClass +++ /dev/null @@ -1,2 +0,0 @@ -UploadData #class -com.axonivy.cloud.storage.demo #namespace 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 index 16e68d8..a632b99 100644 --- a/azure-blob-connector-demo/pom.xml +++ b/azure-blob-connector-demo/pom.xml @@ -1,29 +1,30 @@ - - 4.0.0 - com.axonivy.cloud.storage - azure-blob-connector-demo - 10.0.21-SNAPSHOT - iar - - 10.0.16 - - - - com.axonivy.cloud.storage - azure-blob-connector - ${project.version} - iar - - - - - - com.axonivy.ivy.ci - project-build-plugin - ${project.build.plugin.version} - true - - - + + 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 index 34fb7c5..1c02aa5 100644 --- a/azure-blob-connector-demo/processes/Start Processes/Upload.p.json +++ b/azure-blob-connector-demo/processes/Start Processes/Upload.p.json @@ -2,7 +2,7 @@ "format" : "10.0.0", "id" : "19010D5E49BD2F7F", "config" : { - "data" : "com.axonivy.cloud.storage.demo.UploadData" + "data" : "com.axonivy.connector.azure.blob.demo.UploadData" }, "elements" : [ { "id" : "f0", @@ -28,7 +28,7 @@ "type" : "DialogCall", "name" : "Upload", "config" : { - "dialogId" : "com.axonivy.cloud.storage.demo.Upload", + "dialogId" : "com.axonivy.connector.azure.blob.demo.Upload", "startMethod" : "start()" }, "visual" : { @@ -54,7 +54,7 @@ "type" : "DialogCall", "name" : "Upload", "config" : { - "dialogId" : "com.axonivy.cloud.storage.demo.UploadByCallSubprocess", + "dialogId" : "com.axonivy.connector.azure.blob.demo.UploadByCallSubprocess", "startMethod" : "start()" }, "visual" : { diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/Blob.java b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/Blob.java similarity index 55% rename from azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/Blob.java rename to azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/Blob.java index b423b4c..3c4f2e0 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/Blob.java +++ b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/Blob.java @@ -1,16 +1,16 @@ -package com.axonivy.cloud.storage.bean; +package com.axonivy.connector.azure.blob.demo.bean; import com.azure.storage.blob.models.BlobItem; public class Blob { - private BlobItem bi; + private BlobItem blobItem; private String linkDownLoad; - public BlobItem getBi() { - return bi; + public BlobItem getBlobItem() { + return blobItem; } - public void setBi(BlobItem bi) { - this.bi = bi; + public void setBlobItem(BlobItem BlobItem) { + this.blobItem = BlobItem; } public String getLinkDownLoad() { return linkDownLoad; diff --git a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadBean.java similarity index 81% rename from azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java rename to azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadBean.java index f29a374..a3cb4f8 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadBean.java +++ b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadBean.java @@ -1,4 +1,6 @@ -package com.axonivy.cloud.storage.bean; +package com.axonivy.connector.azure.blob.demo.bean; + +import static org.apache.commons.lang3.StringUtils.isNotEmpty; import java.io.InputStream; import java.util.ArrayList; @@ -12,10 +14,10 @@ import org.apache.commons.lang3.StringUtils; import org.primefaces.model.file.UploadedFile; -import com.axonivy.cloud.storage.azure.blob.connector.BlobServiceClientHelper; -import com.axonivy.cloud.storage.azure.blob.connector.StorageService; -import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService; -import com.axonivy.cloud.storage.utils.UploadUtils; +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; @@ -58,7 +60,7 @@ public void init() { blobServiceClient = BlobServiceClientHelper.getBlobServiceClient(clientId, clientSecret, tenantId, endPoint); storageService = new AzureBlobStorageService(blobServiceClient, containerName); - getBlobs(storageService.getBlobs()); + this.blobs = getAllBlobs(); isFileAlreadyExist = false; isFileAlreadyExistURL = false; isFileAlreadyExistPath = false; @@ -232,9 +234,7 @@ public void upload() throws Exception { try { String name = storageService.upload(content, fileName, uploadToFolderByPrimefaces, isOverwriteFile); if (StringUtils.isNotEmpty(name)) { - getBlobs(storageService.getBlobs()); - FacesContext.getCurrentInstance().addMessage(null, - new FacesMessage(FacesMessage.SEVERITY_INFO, "Uploaded blobs successfully", null)); + fetchAllBlobsAndAddMessage("Uploaded blobs successfully"); } } catch (Exception e) { throw new Exception("upload file error " + e.getMessage(), e); @@ -250,9 +250,7 @@ public void uploadFromURL() { if (!isExistFile || isOverwriteFileURL) { String name = storageService.uploadFromUrl(url, uploadToFolderByURL, isOverwriteFileURL); if (StringUtils.isNotEmpty(name)) { - getBlobs(storageService.getBlobs()); - FacesContext.getCurrentInstance().addMessage(null, - new FacesMessage(FacesMessage.SEVERITY_INFO, "Uploaded blobs successfully", null)); + fetchAllBlobsAndAddMessage("Uploaded blobs successfully"); } } else { isFileAlreadyExistURL = true; @@ -264,10 +262,8 @@ public void uploadFromPath() { boolean isExistFile = checkFileAlreadyExist(fileNamePath); if (!isExistFile || isOverwriteFilePath) { String name = storageService.uploadFromFile(localPath, uploadToFolderByLocalPath, isOverwriteFilePath); - if (StringUtils.isNotEmpty(name)) { - getBlobs(storageService.getBlobs()); - FacesContext.getCurrentInstance().addMessage(null, - new FacesMessage(FacesMessage.SEVERITY_INFO, "Uploaded blobs successfully", null)); + if (isNotEmpty(name)) { + fetchAllBlobsAndAddMessage("Uploaded blobs successfully"); } } else { isFileAlreadyExistPath = true; @@ -276,37 +272,47 @@ public void uploadFromPath() { public void deleteBlobs(Date date) { storageService.delete(date); - getBlobs(storageService.getBlobs()); - FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Delete blobs successfully", null)); + fetchAllBlobsAndAddMessage("Delete blobs successfully"); } public void deleteBlob(String blobName) { if (storageService.delete(blobName)) { - getBlobs(storageService.getBlobs()); - FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Delete blobs successfully", null)); + fetchAllBlobsAndAddMessage("Delete blobs successfully"); } } public void undeleteBlob(String blobName) { storageService.restore(blobName); - getBlobs(storageService.getBlobs()); - FacesContext.getCurrentInstance().addMessage(null, - new FacesMessage(FacesMessage.SEVERITY_INFO, "Undelete blobs successfully", null)); - + fetchAllBlobsAndAddMessage("Undelete blobs successfully"); + } + + public void showCopiedMessage() { + doAddInfoMessageForInstance("Download link is copied"); } - private void getBlobs(List bis) { - blobs = new ArrayList<>(); - for(BlobItem item : bis) { + 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.setBi(item); + 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().equalsIgnoreCase(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/cloud/storage/bean/UploadByCallSubprocess.java b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadByCallSubprocess.java similarity index 92% rename from azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java rename to azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadByCallSubprocess.java index a988404..54910fb 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/bean/UploadByCallSubprocess.java +++ b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/bean/UploadByCallSubprocess.java @@ -1,10 +1,13 @@ -package com.axonivy.cloud.storage.bean; +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; @@ -209,11 +212,11 @@ public void setUploadToFolderByURL(String uploadToFolderByURL) { this.uploadToFolderByURL = uploadToFolderByURL; } - public void getBlobs(List bis) { + public void getBlobs(List blobItems) { blobs = new ArrayList<>(); - for (BlobItem item : bis) { + for (BlobItem item : blobItems) { Blob b = new Blob(); - b.setBi(item); + b.setBlobItem(item); blobs.add(b); } } @@ -222,4 +225,9 @@ 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/cloud/storage/utils/UploadUtils.java b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/utils/UploadUtils.java similarity index 78% rename from azure-blob-connector-demo/src/com/axonivy/cloud/storage/utils/UploadUtils.java rename to azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/utils/UploadUtils.java index 415f94b..30f9404 100644 --- a/azure-blob-connector-demo/src/com/axonivy/cloud/storage/utils/UploadUtils.java +++ b/azure-blob-connector-demo/src/com/axonivy/connector/azure/blob/demo/utils/UploadUtils.java @@ -1,4 +1,4 @@ -package com.axonivy.cloud.storage.utils; +package com.axonivy.connector.azure.blob.demo.utils; import java.net.URI; import java.net.URISyntaxException; @@ -14,7 +14,7 @@ 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); + 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/cloud/storage/demo/Upload/UploadData.ivyClass b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadData.ivyClass deleted file mode 100644 index 5ff172a..0000000 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadData.ivyClass +++ /dev/null @@ -1,6 +0,0 @@ -UploadData #class -com.axonivy.cloud.storage.demo.Upload #namespace -bean com.axonivy.cloud.storage.bean.UploadBean #field -bean PERSISTENT #fieldModifier -link String #field -link PERSISTENT #fieldModifier diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/resources/uploadDialog.css b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/resources/uploadDialog.css deleted file mode 100644 index 52d5522..0000000 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/resources/uploadDialog.css +++ /dev/null @@ -1,22 +0,0 @@ -.width-100-percent { - width : 100%; -} -.float-right { - float : right; -} - -.margin-top-10-px { - margin-top : 10px; -} - -.margin-top-15px { - margin-top : 15px !important; -} - -body .ui-fileupload .ui-fileupload-buttonbar , body .ui-fileupload .ui-fileupload-content{ - border : none !important; -} - -.color-red { - color: red ; -} \ No newline at end of file diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/resources/uploadDialog.css b/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/resources/uploadDialog.css deleted file mode 100644 index 52d5522..0000000 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/resources/uploadDialog.css +++ /dev/null @@ -1,22 +0,0 @@ -.width-100-percent { - width : 100%; -} -.float-right { - float : right; -} - -.margin-top-10-px { - margin-top : 10px; -} - -.margin-top-15px { - margin-top : 15px !important; -} - -body .ui-fileupload .ui-fileupload-buttonbar , body .ui-fileupload .ui-fileupload-content{ - border : none !important; -} - -.color-red { - color: red ; -} \ No newline at end of file diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.rddescriptor b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.rddescriptor similarity index 100% rename from azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.rddescriptor rename to azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.rddescriptor diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.xhtml b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.xhtml similarity index 78% rename from azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.xhtml rename to azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.xhtml index 40f3e49..17c6e5a 100644 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/Upload.xhtml +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/Upload.xhtml @@ -7,13 +7,12 @@ xmlns:p="http://primefaces.org/ui" xmlns:pe="http://primefaces.org/ui/extensions"> - + UploadDialog - - - - + @@ -97,7 +96,7 @@ - + @@ -109,38 +108,45 @@
- - - + + - - + + - - + + - + - - + +
- - - -
+ appendTo="@(body)" visible="false" header="Link Download" + width="500"> +
+ + + + + +
+ +
+ process="@this" oncomplete="PF('linkDownloadDialog').hide()" + icon="fa fa-check">
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/cloud/storage/demo/Upload/UploadProcess.p.json b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadProcess.p.json similarity index 92% rename from azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadProcess.p.json rename to azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadProcess.p.json index bd2df6c..30aadea 100644 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/Upload/UploadProcess.p.json +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/Upload/UploadProcess.p.json @@ -3,7 +3,7 @@ "id" : "19010B658CC93485", "kind" : "HTML_DIALOG", "config" : { - "data" : "com.axonivy.cloud.storage.demo.Upload.UploadData" + "data" : "com.axonivy.connector.azure.blob.demo.Upload.UploadData" }, "elements" : [ { "id" : "f0", @@ -413,5 +413,35 @@ "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/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor similarity index 100% rename from azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor rename to azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.rddescriptor diff --git a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml similarity index 79% rename from azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml rename to azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml index 15ce07d..568b87f 100644 --- a/azure-blob-connector-demo/src_hd/com/axonivy/cloud/storage/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml +++ b/azure-blob-connector-demo/src_hd/com/axonivy/connector/azure/blob/demo/UploadByCallSubprocess/UploadByCallSubprocess.xhtml @@ -7,13 +7,12 @@ xmlns:p="http://primefaces.org/ui" xmlns:pe="http://primefaces.org/ui/extensions"> - + UploadDialog - - + @@ -42,7 +41,7 @@ - - - + @@ -109,24 +108,24 @@
- - - + + - - + + - - + + - + - - + +
@@ -134,9 +133,14 @@ - - - +
+ + + + + +

+ + + + + + + + + <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/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/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs index ecc50e4..3767d0e 100644 --- a/azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs +++ b/azure-blob-connector-product/.settings/ch.ivyteam.ivy.designer.prefs @@ -1,5 +1,5 @@ -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.cloud.storage.azure.blob.connector.product.Data -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector.product +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.wst.common.component b/azure-blob-connector-product/.settings/org.eclipse.wst.common.component index 7168897..f435d2a 100644 --- a/azure-blob-connector-product/.settings/org.eclipse.wst.common.component +++ b/azure-blob-connector-product/.settings/org.eclipse.wst.common.component @@ -1,20 +1,28 @@ - + + - + + - + + - + + - + + - - + + - + + - - + + + + diff --git a/azure-blob-connector-product/README.md b/azure-blob-connector-product/README.md index 82b12a1..87086b3 100644 --- a/azure-blob-connector-product/README.md +++ b/azure-blob-connector-product/README.md @@ -1,9 +1,11 @@ -# Azure Blob Connector +# Azure Blob Storage -Axon Ivys Azure Blob Connector helps you to connector Azure Blob Services quitly: -- Configuration to authorize access to blobs. -- Support upload content to blob with many kind of inputs. -- Support get download link with expired time. +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 @@ -12,7 +14,7 @@ In the project, you only add the dependency in your pom.xml and call public APIs **1. Add dependency** ```XML - com.axonivy.cloud.storage + com.axonivy.connector azure-blob-connector ${process.analyzer.version} @@ -35,7 +37,7 @@ Variables: ContainterName: '' ``` -## For Process GUI +### For Process GUI **1. What is support in BlobStorage Callable Sub Process?** ![azure-blob-connector](images/BlobStorageFunctions.png) @@ -47,7 +49,7 @@ Variables: ![azure-blob-connector](images/AddBlobStorageAndCallFunction.png) -## For Java Developer +### 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 /** @@ -111,7 +113,7 @@ Variables: public String getDownloadLink(String blobName); ``` -## Example +### Example Below is a simple example for upload a file from url and get temporary download link. ``` @@ -124,5 +126,43 @@ Below is a simple example for upload a file from url and get temporary download 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/DevAccountKey.png b/azure-blob-connector-product/images/DevAccountKey.png new file mode 100644 index 0000000000000000000000000000000000000000..c0b4721905c64a569a897e7f54718d40bf64b6f2 GIT binary patch literal 8290 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 - 4.0.0 - com.axonivy.cloud.storage - azure-blob-connector-product - 10.0.21-SNAPSHOT - pom - - - ../workflow-estimator/config/variables.yaml - - - - - - org.apache.maven.plugins - maven-assembly-plugin - 3.3.0 - - - package - - single - - - false - - zip.xml - - - - - - - maven-antrun-plugin - 1.7 - - - generate-sources - - ${skip-readme} - - - - - - - - - - - run - - - - - - - - - org.apache.maven.plugins - maven-deploy-plugin - 3.0.0-M1 - - - - + + + 4.0.0 + com.axonivy.connector.azure.blob + azure-blob-connector-product + 10.0.22-SNAPSHOT + pom + + ../azure-blob-connector/config/variables.yaml + + + + + + maven-deploy-plugin + 3.0.0-M1 + + + + + + maven-assembly-plugin + 3.3.0 + + + package + + single + + + false + + zip.xml + + + + + + + maven-antrun-plugin + 1.7 + + + generate-sources + + run + + + ${skip-readme} + + + + + + + + + + + + + + diff --git a/azure-blob-connector-product/product.json b/azure-blob-connector-product/product.json index 416b995..3c0edd7 100644 --- a/azure-blob-connector-product/product.json +++ b/azure-blob-connector-product/product.json @@ -6,10 +6,16 @@ "data": { "projects": [ { - "groupId": "com.axonivy.cloud.storage", + "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" } ], "repositories": [ @@ -28,7 +34,7 @@ "data": { "dependencies": [ { - "groupId": "com.axonivy.cloud.storage", + "groupId": "com.axonivy.connector.azure.blob", "artifactId": "azure-blob-connector", "version": "${version}", "type": "iar" diff --git a/azure-blob-connector-test/.gitignore b/azure-blob-connector-test/.gitignore index 1b2547b..064fd4a 100644 --- a/azure-blob-connector-test/.gitignore +++ b/azure-blob-connector-test/.gitignore @@ -17,3 +17,4 @@ classes/ src_dataClasses/ src_wsproc/ logs/ +credentials.properties diff --git a/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs index f2c4666..bcba9f6 100644 --- a/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs +++ b/azure-blob-connector-test/.settings/ch.ivyteam.ivy.designer.prefs @@ -1,5 +1,5 @@ -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.cloud.storage.azure.blob.connector.test.Data -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector.test +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/pom.xml b/azure-blob-connector-test/pom.xml index 45acdd5..de909fe 100644 --- a/azure-blob-connector-test/pom.xml +++ b/azure-blob-connector-test/pom.xml @@ -1,9 +1,9 @@ 4.0.0 - com.axonivy.cloud.storage + com.axonivy.connector.azure.blob azure-blob-connector-test - 10.0.21-SNAPSHOT + 10.0.22-SNAPSHOT iar 10.0.16 @@ -11,7 +11,7 @@ - com.axonivy.cloud.storage + com.axonivy.connector.azure.blob azure-blob-connector ${project.version} iar @@ -62,6 +62,16 @@ ${project.build.plugin.version} true + + maven-surefire-plugin + 3.0.0-M5 + + + ${azureblob.account} + ${azureblob.key} + + + diff --git a/azure-blob-connector-test/resource_test/picture/singapore.png b/azure-blob-connector-test/resource_test/picture/singapore.png index 8ee3c1f47f4e632cd4e945a0ec14f9a035e5a318..ab8ffa9eb2fcea6eb3c301395e31df4c7c0426a1 100644 GIT binary patch literal 7317 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 Mvoj|Oe{;F>Uv+pHwg3PC literal 9197 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 D#uuaK diff --git a/azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AbstractIntegrationTest.java b/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AbstractIntegrationTest.java similarity index 61% rename from azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AbstractIntegrationTest.java rename to azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AbstractIntegrationTest.java index 5320f14..a62a177 100644 --- a/azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AbstractIntegrationTest.java +++ b/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AbstractIntegrationTest.java @@ -1,52 +1,67 @@ -package com.axonivy.cloud.storage.azure.blob.connector.test.integration; - -import java.util.UUID; - -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.utility.DockerImageName; - -import com.axonivy.cloud.storage.azure.blob.connector.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"; - protected static final String ACCOUNT_NAME = "devstoreaccount1"; - protected static final String END_POINT_FORMAT = "http://127.0.0.1:%s/devstoreaccount1"; - protected static final String ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; - - 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() { - final String endpoint = String.format(END_POINT_FORMAT, azure.getMappedPort(BLOB_PORT)); - return BlobServiceClientHelper.getBlobServiceClient(ACCOUNT_NAME, 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); - } -} +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/cloud/storage/azure/blob/connector/test/integration/AzureBlobStorageServiceTest.java b/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AzureBlobStorageServiceTest.java similarity index 93% rename from azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AzureBlobStorageServiceTest.java rename to azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AzureBlobStorageServiceTest.java index aae8c27..d613652 100644 --- a/azure-blob-connector-test/src_test/com/axonivy/cloud/storage/azure/blob/connector/test/integration/AzureBlobStorageServiceTest.java +++ b/azure-blob-connector-test/src_test/com/axonivy/connector/azure/blob/test/integration/AzureBlobStorageServiceTest.java @@ -1,4 +1,4 @@ -package com.axonivy.cloud.storage.azure.blob.connector.test.integration; +package com.axonivy.connector.azure.blob.test.integration; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -20,8 +20,8 @@ import org.junit.jupiter.api.TestMethodOrder; import org.testcontainers.junit.jupiter.Testcontainers; -import com.axonivy.cloud.storage.azure.blob.connector.StorageService; -import com.axonivy.cloud.storage.azure.blob.connector.internal.AzureBlobStorageService; +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; @@ -34,7 +34,7 @@ public class AzureBlobStorageServiceTest extends AbstractIntegrationTest { private static String blobNameOfFileUpload = null; @BeforeAll - public static void setup() { + public static void setup() throws IOException { blobServiceClient = getBlobServiceClient(); storageService = new AzureBlobStorageService(blobServiceClient, TEST_CONTAINTER); } diff --git a/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs b/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs index 4948f7b..3bfd371 100644 --- a/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs +++ b/azure-blob-connector/.settings/ch.ivyteam.ivy.designer.prefs @@ -1,5 +1,5 @@ -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_DATA_CLASS=com.axonivy.cloud.storage.azure.blob.connector.Data -ch.ivyteam.ivy.designer.preferences.DataClassPreferencePage\:DEFAULT_NAMESPACE=com.axonivy.cloud.storage.azure.blob.connector +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/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass b/azure-blob-connector/dataclasses/com/axonivy/connector/azure/blob/BlobStorageData.ivyClass similarity index 69% rename from azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass rename to azure-blob-connector/dataclasses/com/axonivy/connector/azure/blob/BlobStorageData.ivyClass index ee5ff6b..62ff5a2 100644 --- a/azure-blob-connector/dataclasses/com/axonivy/cloud/storage/azure/blob/connector/BlobStorageData.ivyClass +++ b/azure-blob-connector/dataclasses/com/axonivy/connector/azure/blob/BlobStorageData.ivyClass @@ -1,7 +1,7 @@ BlobStorageData #class -com.axonivy.cloud.storage.azure.blob.connector #namespace +com.axonivy.connector.azure.blob #namespace date java.util.Date #field -azureBlobStorageBean com.axonivy.cloud.storage.azure.blob.connector.internal.bean.AzureBlobStorageBean #field +azureBlobStorageBean com.axonivy.connector.azure.blob.internal.bean.AzureBlobStorageBean #field isSuccess Boolean #field blobItems java.util.List #field blobName String #field diff --git a/azure-blob-connector/pom.xml b/azure-blob-connector/pom.xml index 07458cc..22cd148 100644 --- a/azure-blob-connector/pom.xml +++ b/azure-blob-connector/pom.xml @@ -1,9 +1,9 @@ 4.0.0 - com.axonivy.cloud.storage + com.axonivy.connector.azure.blob azure-blob-connector - 10.0.21-SNAPSHOT + 10.0.22-SNAPSHOT iar 10.0.16 diff --git a/azure-blob-connector/processes/BlobStorage.p.json b/azure-blob-connector/processes/BlobStorage.p.json index 1553ba7..4d8f9b5 100644 --- a/azure-blob-connector/processes/BlobStorage.p.json +++ b/azure-blob-connector/processes/BlobStorage.p.json @@ -3,7 +3,7 @@ "id" : "1905E51E1156B82C", "kind" : "CALLABLE_SUB", "config" : { - "data" : "com.axonivy.cloud.storage.azure.blob.connector.BlobStorageData" + "data" : "com.axonivy.connector.azure.blob.BlobStorageData" }, "elements" : [ { "id" : "f0", @@ -500,7 +500,7 @@ "config" : { "output" : { "code" : [ - "import com.axonivy.cloud.storage.azure.blob.connector.internal.bean.AzureBlobStorageBean;", + "import com.axonivy.connector.azure.blob.internal.bean.AzureBlobStorageBean;", "", "in.azureBlobStorageBean = new AzureBlobStorageBean();" ] 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 index d94371448c610492afc354a236ae1754c36f48b9..0130ca2c37b58dc83b4f8ccba6d7298668bb78fa 100644 GIT binary patch literal 2040 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 SVbm`Ua<;#0S7qz><9`9f9HPqr diff --git a/pom.xml b/pom.xml index 3b4e465..cc3cfbd 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,9 @@ 4.0.0 - com.axonivy.cloud.storage + com.axonivy.connector.azure.blob azure-blob-connector azure-blob-connector-modules - 10.0.21-SNAPSHOT + 10.0.22-SNAPSHOT pom @@ -14,7 +14,7 @@ - scm:git:https://github.com/axonivy-professional-services/market-${project.name}.git + scm:git:https://github.com/axonivy-market/${project.name}.git HEAD From 357676d6f2e799b4e19c098fdae6af7c159292d8 Mon Sep 17 00:00:00 2001 From: Dinh Nguyen Date: Thu, 15 Aug 2024 16:48:30 +0700 Subject: [PATCH 12/13] MARP-846 approval for release --- MY-PRODUCT-NAME-product/README.md | 32 --- MY-PRODUCT-NAME-product/pom.xml | 67 ----- MY-PRODUCT-NAME-product/product.json | 70 ----- MY-PRODUCT-NAME-product/zip.xml | 26 -- azure-blob-connector/README.md | 36 --- .../connector/BlobServiceClientHelper.java | 66 ----- .../azure/blob/connector/StorageService.java | 128 --------- .../internal/AzureBlobStorageService.java | 249 ------------------ .../internal/bean/AzureBlobStorageBean.java | 36 --- .../internal/helper/BlobSASHelper.java | 32 --- 10 files changed, 742 deletions(-) delete mode 100644 MY-PRODUCT-NAME-product/README.md delete mode 100644 MY-PRODUCT-NAME-product/pom.xml delete mode 100644 MY-PRODUCT-NAME-product/product.json delete mode 100644 MY-PRODUCT-NAME-product/zip.xml delete mode 100644 azure-blob-connector/README.md delete mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/BlobServiceClientHelper.java delete mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java delete mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java delete mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/bean/AzureBlobStorageBean.java delete mode 100644 azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/helper/BlobSASHelper.java 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/MY-PRODUCT-NAME-product/pom.xml b/MY-PRODUCT-NAME-product/pom.xml deleted file mode 100644 index dd59011..0000000 --- a/MY-PRODUCT-NAME-product/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - 4.0.0 - com.axonivy.market - MY-PRODUCT-NAME-product - 10.0.0-SNAPSHOT - pom - - - ../MY-PRODUCT-NAME/config/variables.yaml - - - - - - org.apache.maven.plugins - maven-assembly-plugin - 3.3.0 - - - package - - single - - - false - - zip.xml - - - - - - - maven-antrun-plugin - 1.7 - - - generate-sources - - ${skip-readme} - - - - - - - - - - - run - - - - - - - - - org.apache.maven.plugins - maven-deploy-plugin - 3.0.0-M1 - - - - - diff --git a/MY-PRODUCT-NAME-product/product.json b/MY-PRODUCT-NAME-product/product.json deleted file mode 100644 index a5a4b33..0000000 --- a/MY-PRODUCT-NAME-product/product.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", - "installers": [ - { - "id": "maven-import", - "data": { - "projects": [ - { - "groupId": "MY-GROUP-ID", - "artifactId": "MY-PRODUCT-NAME-demo", - "version": "${version}", - "type": "iar" - } - ], - "repositories": [ - { - "id": "maven.axonivy.com", - "url": "https://maven.axonivy.com", - "snapshots": { - "enabled": "true" - } - } - ] - } - }, - { - "id": "maven-dependency", - "data": { - "dependencies": [ - { - "groupId": "MY-GROUP-ID", - "artifactId": "MY-PRODUCT-NAME", - "version": "${version}", - "type": "iar" - } - ], - "repositories": [ - { - "id": "maven.axonivy.com", - "url": "https://maven.axonivy.com", - "snapshots": { - "enabled": "true" - } - } - ] - } - }, - { - "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/MY-PRODUCT-NAME-product/zip.xml deleted file mode 100644 index 003f06c..0000000 --- a/MY-PRODUCT-NAME-product/zip.xml +++ /dev/null @@ -1,26 +0,0 @@ - - zip - false - - - zip - - - - - . - - product.json - openapi.json - **/*.png - - - - target - / - - README.md - - - - diff --git a/azure-blob-connector/README.md b/azure-blob-connector/README.md deleted file mode 100644 index 3f4ae8c..0000000 --- a/azure-blob-connector/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# How to run at local - -## Run with DockerHub - -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) \ No newline at end of file diff --git a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/BlobServiceClientHelper.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/BlobServiceClientHelper.java deleted file mode 100644 index 99d9c37..0000000 --- a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/BlobServiceClientHelper.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.axonivy.cloud.storage.azure.blob.connector; - -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/cloud/storage/azure/blob/connector/StorageService.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java deleted file mode 100644 index 8e2e284..0000000 --- a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/StorageService.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.axonivy.cloud.storage.azure.blob.connector; - -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/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java deleted file mode 100644 index 937b516..0000000 --- a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/AzureBlobStorageService.java +++ /dev/null @@ -1,249 +0,0 @@ -package com.axonivy.cloud.storage.azure.blob.connector.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.cloud.storage.azure.blob.connector.StorageService; -import com.axonivy.cloud.storage.azure.blob.connector.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_MG = (5 * 1024 * 1024); - - - /** - * @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_MG) { - 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((long) (4 * 1024 * 1024)) // 4 MiB block size - .setMaxConcurrency(4); - - 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 bis = 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 -> bis.add(blob)); - } - return bis; - } - - @Override - public void delete(Date d) { - List bi = destinationContainer.listBlobs().stream().filter(b -> isSameDate(b, d)).collect(Collectors.toList()); - bi.forEach(blob -> delete(blob.getName())); - } - - private boolean isSameDate(BlobItem bi, Date date) { - String creationTime2String = bi.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/cloud/storage/azure/blob/connector/internal/bean/AzureBlobStorageBean.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/bean/AzureBlobStorageBean.java deleted file mode 100644 index 0e075bc..0000000 --- a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/bean/AzureBlobStorageBean.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.axonivy.cloud.storage.azure.blob.connector.internal.bean; - -import com.axonivy.cloud.storage.azure.blob.connector.BlobServiceClientHelper; -import com.axonivy.cloud.storage.azure.blob.connector.StorageService; -import com.axonivy.cloud.storage.azure.blob.connector.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/cloud/storage/azure/blob/connector/internal/helper/BlobSASHelper.java b/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/helper/BlobSASHelper.java deleted file mode 100644 index 571a5f7..0000000 --- a/azure-blob-connector/src/com/axonivy/cloud/storage/azure/blob/connector/internal/helper/BlobSASHelper.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.axonivy.cloud.storage.azure.blob.connector.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; - } -} From beab5f73c6776c99c5baa5e96ed0f544bd3c48c3 Mon Sep 17 00:00:00 2001 From: Dinh Nguyen Date: Thu, 15 Aug 2024 16:52:11 +0700 Subject: [PATCH 13/13] MARP-846 approval for release --- azure-blob-connector-demo/README.md | 32 ----------------------------- 1 file changed, 32 deletions(-) delete mode 100644 azure-blob-connector-demo/README.md diff --git a/azure-blob-connector-demo/README.md b/azure-blob-connector-demo/README.md deleted file mode 100644 index 8657be2..0000000 --- a/azure-blob-connector-demo/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Azure Blob Connector Demo - -To run the demo module, you need to provide some information to create credential authenticates. - -## Run directly with Azure Portal - -In the project, you only add the dependency in your pom.xml and call public APIs - -** 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: '' -``` -## Run with Azurite at local - -Start docker local: Read our [documentation](../azure-blob-connector/README.md). -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) -```yaml -Variables: - ACCOUNT_NAME: 'devstoreaccount1' - ACCOUNT_KEY: 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==' -``` \ No newline at end of file