├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── RELEASES.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── how-to-release.md ├── pics └── picture.jpg ├── settings.gradle └── src ├── functionalTest └── groovy │ └── com │ └── github │ └── qwazer │ └── markdown │ └── confluence │ └── ConfluenceTaskSpecification.groovy ├── integrationTest └── java │ └── com │ └── github │ └── qwazer │ └── markdown │ └── confluence │ └── core │ ├── AbstractIT.java │ └── service │ ├── ConfluenceServiceIT.java │ └── PageServiceIT.java ├── main └── java │ └── com │ └── github │ └── qwazer │ └── markdown │ └── confluence │ ├── core │ ├── ConfluenceException.java │ ├── HttpHeader.java │ ├── NotFoundException.java │ ├── OkHttpUtils.java │ ├── Utils.java │ ├── model │ │ ├── ConfluencePage.java │ │ └── ConfluenceSpace.java │ ├── service │ │ ├── AttachmentService.java │ │ ├── ConfluenceService.java │ │ ├── MarkdownService.java │ │ ├── MarkdownServiceCommonmark.java │ │ ├── MarkdownServicePegdown.java │ │ └── PageService.java │ └── ssl │ │ └── SslUtil.java │ └── gradle │ └── plugin │ ├── AuthenticationType.java │ ├── ConfluenceExtension.java │ ├── ConfluenceGradlePlugin.java │ └── ConfluenceGradleTask.java └── test ├── how-to-test.md ├── java └── com │ └── github │ └── qwazer │ └── markdown │ └── confluence │ ├── core │ └── service │ │ ├── AttachmentServiceTest.java │ │ ├── ConfluenceServiceTest.java │ │ ├── MarkdownServiceCommonmarkTest.java │ │ ├── MarkdownServicePegdownTest.java │ │ └── PageServiceTest.java │ └── gradle │ └── plugin │ └── ConfluenceExtensionTest.java └── resources ├── com └── github │ └── qwazer │ └── markdown │ └── confluence │ └── core │ └── service │ ├── PageServiceTest1.md │ ├── PageServiceTest2.md │ └── images │ └── image.png └── test.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | ### JetBrains template 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | .idea/ 8 | 9 | ## File-based project format: 10 | *.iws 11 | 12 | ## Plugin-specific files: 13 | 14 | # IntelliJ 15 | /out/ 16 | 17 | # mpeltonen/sbt-idea plugin 18 | .idea_modules/ 19 | 20 | # JIRA plugin 21 | atlassian-ide-plugin.xml 22 | 23 | # Crashlytics plugin (for Android Studio and IntelliJ) 24 | com_crashlytics_export_strings.xml 25 | crashlytics.properties 26 | crashlytics-build.properties 27 | fabric.properties 28 | 29 | ### Gradle template 30 | .gradle 31 | build/ 32 | 33 | # Ignore Gradle GUI config 34 | gradle-app.setting 35 | 36 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 37 | !gradle-wrapper.jar 38 | 39 | # Cache of project 40 | .gradletasknamecache 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | 4 | install: echo "skip 'gradle assemble' step" 5 | script: 6 | - ./gradlew build 7 | 8 | deploy: 9 | provider: script 10 | script: ./gradlew bintrayUpload 11 | skip_cleanup: true 12 | on: 13 | branch: master 14 | tags: true 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | https://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/qwazer/markdown-confluence-gradle-plugin.svg?branch=master)](https://travis-ci.org/qwazer/markdown-confluence-gradle-plugin) 2 | 3 | # markdown-confluence-gradle-plugin 4 | 5 | Gradle plugin to publish markdown pages to confluence. 6 | 7 | ## Usage and sample configuration. 8 | 9 | You can define the plugin in ``settings.gradle`` 10 | 11 | ```groovy 12 | pluginManagement { 13 | repositories { 14 | maven { url "https://jitpack.io" } 15 | } 16 | 17 | plugins { 18 | id "com.github.qwazer.markdown-confluence" version "$VERSION" 19 | } 20 | } 21 | ``` 22 | 23 | And then apply the plugin in your ``build.gradle`` - if the version is specified in the ``settings.gradle``, you don't 24 | need to repeat it in the ``build.gradle``. 25 | 26 | ```groovy 27 | plugins { 28 | id "com.github.qwazer.markdown-confluence" 29 | } 30 | ``` 31 | 32 | Sample config. 33 | 34 | ```groovy 35 | confluence { 36 | authenticationType = AuthenticationType.BASIC 37 | authentication = 'username:password'.bytes.encodeBase64().toString() 38 | restApiUrl = 'https://confluence.acme.com/rest/api/' 39 | spaceKey = 'SAMPLE' 40 | sslTrustAll = true 41 | pageVariables = ['project.name': project.name] 42 | parserType = 'commonmark' 43 | 44 | pages { 45 | "Releases" { 46 | parentTitle = "Home" 47 | srcFile = file("RELEASES.md") 48 | labels = ["release-notes", "${project.version}"] 49 | } 50 | "${project.name}" { 51 | parentTitle = "Releases" 52 | srcFile = file("README.md") 53 | } 54 | } 55 | } 56 | ``` 57 | 58 | It's possible to define multiple pages with groovy closures. 59 | For example, the below code snippet defines all ``*.md`` files inside the ``src`` directory as confluence pages 60 | titled the same as underlying files (minus file path and extension) and having ``parentTitle`` equal to ``Parent Page``. 61 | 62 | ```groovy 63 | pages { 64 | fileTree("src") 65 | .include("**/*.md") 66 | .collect { file -> 67 | def title = file.name.take(file.name.lastIndexOf('.')) 68 | "${title}" { 69 | parentTitle = "Parent Page" 70 | srcFile = file 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ## Inline images. 77 | 78 | The below Markdown code: 79 | 80 | ```text 81 | ![Picture Alt Text](pics/picture.jpg "Extra title") 82 | ``` 83 | 84 | is translated to the following Confluence wiki: 85 | 86 | ```text 87 | !picture.jpg|Picture Alt Text! 88 | ``` 89 | 90 | Plus the **pics/picture.jpg** file (relative to the Markdown file) is added as an attachment (named **picture.jpg**) 91 | to the generated Confluence page and correctly linked. 92 | 93 | 94 | ### Description of config parameters 95 | 96 | | parameter | datatype | optional | description | 97 | |:-------------------------|:---------------------------|:---------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 98 | | authenticationType | Enum | yes | Authentication type to use when calling Confluence APIs, one of: BASIC, PAT (Personal Access Token). Defaults to BASIC when not specified explicitly. | 99 | | authenticationTypeString | String | yes | Overrides the above configuration using a string constant, one of: `BASIC`, `PAT` (Personal Access Token). Defaults to empty string when not specified explicitly. | 100 | | authentication | String | no | 'user:pass'.bytes.encodeBase64().toString() when `authenticationType` is `BASIC`, or token string when `authenticationType` is `PAT`. | 101 | | restApiUrl | String | no | Confluence REST API URL. | 102 | | spaceKey | String | no | Confluence space key. | 103 | | sslTrustAll | Boolean | yes | Setting to ignore self-signed and unknown certificate errors. Useful in some corporate environments. | 104 | | pageVariables | Map | yes | Map of page variables. For example, if a Markdown file to be published contains `${myVar}` placeholder, and the `pageVariable` map has an entry with the `myVar` key, then the placeholder in the Markdown file in will be substituted by value of the map entry having the `myVar` key. | 105 | | parserType | String | yes | Markdown to Confluence wiki parser to use. One of: `commonmark` (default if not specified), `pegdown`. | 106 | | pages | Closure | no | Collection of NamedDomainObjectContainer. If this collection contains multiple pages, they will be ordered according their parent-child relationship. | 107 | | page | NamedDomainObjectContainer | no | Name of the container is the title of the page. Check [Declaring DSL configuration container](https://docs.gradle.org/current/userguide/implementing_gradle_plugins.html#declaring_a_dsl_configuration_container). | 108 | | page.parentTitle | String | no | The title of the parent page under which this page should be published. It is used to resolve target page ancestorId in Confluence. | 109 | | page.srcFile | File | no | The Markdown file to be published as Confluence wiki page (can be mixed with [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html)). | 110 | | page.labels | Collection | yes | Collection of labels to be added to the generated Confluence page. | 111 | 112 | ### Run the build 113 | 114 | To trigger the generation of the page(s) configured in your build.gradle(.kts), just run the below command 115 | 116 | ```bash 117 | ./gradlew confluence 118 | ``` 119 | 120 | When the build completes successfully the configured pages will be in Confluence under the specified space. 121 | 122 | 123 | ## Thanks 124 | 125 | * Inspired by [maven-confluence-plugin](https://github.com/bsorrentino/maven-confluence-plugin) by bsorrentino. 126 | * [Swagger Confluence](https://gitlab.slkdev.net/starlightknight/swagger-confluence) 127 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | ## Release Notes 2 | 3 | This document provides an overview of the changes and improvements in each version of the ``com.github.qwazer.markdown-confluence`` plugin. 4 | It is recommended to review the release notes before upgrading to a new version. 5 | 6 | ### Version 0.10.0 7 | 8 | - Dropped the dependency on the Spring Framework. 9 | - OkHttpClient is now used to interact with Confluence REST APIs. 10 | - Jackson is the only JSON processing library. 11 | - The jvm-test-suite plugin has been applied to separate unit & integration tests. 12 | - The Gradle publish-plugin has been updated to the latest version (1.2.0). 13 | - Gradle's integration tests for the confluence task first publish the current SNAPSHOT version of the plugin to mavenLocal, so it's no longer necessary to do it manually before running integration tests. 14 | - Using built-in Java utils for reading and writing files. 15 | - The ``=`` sign is now required when assigning a value to a property. This is because 16 | the underlying way the plugin's configuration is applied has changed to make the plugin future-proof. 17 | Part of that change was replacing the previously used ``ConfluenceConfig`` class with the ``ConfluenceExtension`` class. 18 | The new class also provides the ``pages`` method, but the argument to that method is now 19 | ``Action>`` instead of ``Closure``, and the most notable consequence of this 20 | change is that now the ``pageTitle`` is the name of the page configuration container as shown in the example below. 21 | 22 | ```groovy 23 | confluence { 24 | authenticationType = AuthenticationType.BASIC 25 | authentication = 'username:password'.bytes.encodeBase64().toString() 26 | restApiUrl = 'https://confluence.acme.com/rest/api/' 27 | spaceKey = 'SAMPLE' 28 | sslTrustAll = true 29 | pageVariables = ['project.name': project.name] 30 | parseTimeout = 2000L 31 | 32 | pages { 33 | "Releases" { 34 | parentTitle = "Home" 35 | srcFile = file("RELEASES.md") 36 | labels = ["release-notes", "${project.version}"] 37 | } 38 | "${project.name}" { 39 | parentTitle = "Releases" 40 | srcFile = file("README.md") 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | - 47 | 48 | ### Version 0.9.3 49 | 50 | - Added support for Confluence Personal Access Token (PAT) authentication. 51 | 52 | ### Version 0.9.2 53 | 54 | - Added support for publishing of inline images referenced in Markdown documents. Since this version, any local image referenced in your markdown file will be uploaded as an 55 | attachment to the generated Confluence page and correctly linked. Check [README.md](README.md) for details/example. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy' 3 | id 'java-gradle-plugin' 4 | id 'jvm-test-suite' 5 | id 'com.gradle.plugin-publish' version '1.2.0' 6 | id 'com.github.ben-manes.versions' version '0.40.0' 7 | } 8 | 9 | group 'com.github.qwazer' 10 | description 'A plugin for publishing markdown or confluence wiki files to an Atlassian Confluence server' 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | java { 17 | toolchain { 18 | languageVersion.set(JavaLanguageVersion.of(8)) 19 | } 20 | compileJava.options.encoding = 'UTF-8' 21 | compileTestJava.options.encoding = 'UTF-8' 22 | } 23 | 24 | dependencies { 25 | api 'com.squareup.okhttp3:okhttp:4.12.0' 26 | api 'com.fasterxml.jackson.core:jackson-databind:2.15.2' 27 | api 'commons-codec:commons-codec:1.16.0' 28 | api 'org.bsc.maven:confluence-markdown-processor-commonmark:7.13' 29 | api 'org.bsc.maven:confluence-markdown-processor-pegdown:6.20' 30 | } 31 | 32 | testing { 33 | suites { 34 | withType(JvmTestSuite).matching { it.name in ['test', 'integrationTest'] }.configureEach { 35 | useJUnit() 36 | dependencies { 37 | implementation 'org.mockito:mockito-core:4.11.0' 38 | implementation 'com.squareup.okhttp3:mockwebserver:4.11.0' 39 | } 40 | } 41 | 42 | integrationTest(JvmTestSuite) { 43 | testType = TestSuiteType.INTEGRATION_TEST 44 | dependencies { 45 | implementation project() 46 | } 47 | } 48 | 49 | functionalTest(JvmTestSuite) { 50 | testType = TestSuiteType.FUNCTIONAL_TEST 51 | useSpock() 52 | dependencies { 53 | implementation gradleTestKit() 54 | implementation project() 55 | } 56 | } 57 | } 58 | } 59 | 60 | gradlePlugin { 61 | website = 'https://github.com/qwazer/markdown-confluence-gradle-plugin' 62 | vcsUrl = 'https://github.com/qwazer/markdown-confluence-gradle-plugin.git' 63 | plugins { 64 | markdownConfluenceGradlePlugin { 65 | id = 'com.github.qwazer.markdown-confluence' 66 | implementationClass = 'com.github.qwazer.markdown.confluence.gradle.plugin.ConfluenceGradlePlugin' 67 | description = project.description 68 | displayName = 'Markdown Confluence' 69 | tags.set(['markdown', 'confluence']) 70 | } 71 | } 72 | testSourceSets(testing.suites.functionalTest.sources) 73 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=0.15.0-SNAPSHOT 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwazer/markdown-confluence-gradle-plugin/1d7e2067449405adc1003cf3a8a719da7365a605/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon May 25 21:24:42 BST 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /how-to-release.md: -------------------------------------------------------------------------------- 1 | # How to release markdown-confluence-gradle-plugin 2 | 3 | 1. update [gradle.properties](gradle.properties) 4 | 2. git commit 5 | 3. git tag 6 | 4. git push 7 | 5. verify if the [jitpack](https://jitpack.io/#qwazer/markdown-confluence-gradle-plugin) release build matching the latest release tag completed successfully 8 | 6. publish to https://plugins.gradle.org via `gradle publishPlugins` task (`gradle login` may be required from fresh workstation) 9 | 7. update & run test `prev_release_availability.gradle` 10 | 8. update [README](README.md) 11 | -------------------------------------------------------------------------------- /pics/picture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwazer/markdown-confluence-gradle-plugin/1d7e2067449405adc1003cf3a8a719da7365a605/pics/picture.jpg -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'markdown-confluence-gradle-plugin' 2 | 3 | -------------------------------------------------------------------------------- /src/functionalTest/groovy/com/github/qwazer/markdown/confluence/ConfluenceTaskSpecification.groovy: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence 2 | 3 | import com.github.qwazer.markdown.confluence.core.OkHttpUtils 4 | import com.github.qwazer.markdown.confluence.core.service.ConfluenceService 5 | import com.github.qwazer.markdown.confluence.gradle.plugin.AuthenticationType 6 | import okhttp3.OkHttpClient 7 | import org.gradle.testkit.runner.GradleRunner 8 | import org.gradle.testkit.runner.TaskOutcome 9 | import spock.lang.Specification 10 | import spock.lang.TempDir 11 | 12 | import java.nio.file.Files 13 | import java.nio.file.Paths 14 | 15 | class ConfluenceTaskSpecification extends Specification { 16 | 17 | static final def CONFLUENCE_BASE_URL = 'http://localhost:8090/rest/api' 18 | static final def CONFLUENCE_SPACE_KEY = 'SN' 19 | static final def CONFLUENCE_AUTHENTICATION_TYPE = AuthenticationType.BASIC 20 | static final def CONFLUENCE_AUTHENTICATION = 'admin:admin'.bytes.encodeBase64().toString() 21 | 22 | // used to assert builds outcomes 23 | ConfluenceService confluenceService 24 | 25 | @TempDir 26 | File testProjectDir 27 | File buildFile 28 | File settingsFile 29 | File markdownFile 30 | GradleRunner gradleRunner 31 | 32 | def setup() { 33 | def authorizationHeaderValue = 34 | CONFLUENCE_AUTHENTICATION_TYPE.getAuthorizationHeader(CONFLUENCE_AUTHENTICATION) 35 | def httpClient = new OkHttpClient.Builder() 36 | .addInterceptor(OkHttpUtils.getAuthorizationInterceptor(authorizationHeaderValue)) 37 | .build() 38 | confluenceService = new ConfluenceService(CONFLUENCE_BASE_URL, CONFLUENCE_SPACE_KEY, httpClient) 39 | confluenceService.getOrCreateSpace(CONFLUENCE_SPACE_KEY) 40 | 41 | settingsFile = new File(testProjectDir, "settings.gradle") 42 | settingsFile << """\ 43 | rootProject.name = 'markdown-confluence-gradle-plugin' 44 | """.stripIndent() 45 | 46 | buildFile = new File(testProjectDir, "build.gradle") 47 | buildFile << """\ 48 | plugins { 49 | id 'com.github.qwazer.markdown-confluence' 50 | } 51 | 52 | allprojects { 53 | pluginManager.withPlugin('com.github.qwazer.markdown-confluence') { 54 | confluence { 55 | authenticationTypeString = '${CONFLUENCE_AUTHENTICATION_TYPE.name()}' 56 | authentication = '${CONFLUENCE_AUTHENTICATION}' 57 | restApiUrl = '${CONFLUENCE_BASE_URL}' 58 | spaceKey = '${CONFLUENCE_SPACE_KEY}' 59 | sslTrustAll = true 60 | } 61 | } 62 | } 63 | 64 | """.stripIndent() 65 | 66 | markdownFile = new File(testProjectDir, "README.md") 67 | 68 | gradleRunner = GradleRunner.create() 69 | .withPluginClasspath() 70 | .withProjectDir(testProjectDir) 71 | .withArguments("confluence", "--stacktrace") 72 | .forwardOutput() 73 | } 74 | 75 | def "Page variables should be substituted before the page is published to Confluence"() { 76 | 77 | given: 78 | def pageTitle = "Page With Substituted Variables" 79 | 80 | markdownFile << """\ 81 | # Header 1\nThe project name is \${project.name} and project version is: \${project.version} 82 | """.stripIndent() 83 | 84 | // this overrides the common build file created in the setup method 85 | buildFile << """\ 86 | confluence { 87 | pageVariables = ['project.name': project.name, 'project.version': project.version] 88 | pages { 89 | "${pageTitle}" { 90 | parentTitle = "Home" 91 | srcFile = file("README.md") 92 | } 93 | } 94 | } 95 | """ 96 | 97 | when: 98 | def result = gradleRunner 99 | .withArguments("confluence", "-Pversion=1.0", "--stacktrace") 100 | .build() 101 | 102 | then: 103 | assert result.task(":confluence").outcome == TaskOutcome.SUCCESS 104 | 105 | and: 106 | def publishedPage = confluenceService.findPageByTitle(pageTitle) 107 | assert publishedPage != null 108 | def publishedPageContent = publishedPage.content 109 | assert publishedPageContent.contains("The project name is markdown-confluence-gradle-plugin and project version is: 1.0") 110 | 111 | } 112 | 113 | def "Project README file should be published - Commonmark"() { 114 | 115 | given: 116 | buildFile << """ 117 | confluence { 118 | parserType = "commonmark" 119 | pages { 120 | "\${project.name}-commonmark" { 121 | parentTitle = "Home" 122 | srcFile = file("README.md") 123 | } 124 | } 125 | } 126 | """.stripIndent() 127 | // using project's README.md markdown file as the file to publish to Confluence 128 | markdownFile << new File('README.md').text 129 | 130 | // project's README file refers to the pics/picture.jpg picture and hence we need to copy it to the project's 131 | // test directory retaining the directory structure 132 | Files.createDirectories(testProjectDir.toPath().resolve("pics")) 133 | Files.copy(Paths.get("pics", "picture.jpg"), testProjectDir.toPath().resolve("pics/picture.jpg")) 134 | 135 | when: 136 | def result = gradleRunner.build() 137 | 138 | then: 139 | assert result.task(":confluence").outcome == TaskOutcome.SUCCESS 140 | 141 | and: 142 | def publishedPageTitle = "markdown-confluence-gradle-plugin-commonmark" 143 | def publishedPage = confluenceService.findPageByTitle(publishedPageTitle) 144 | assert publishedPage != null 145 | 146 | } 147 | 148 | def "Project README file should be published - Pegdown"() { 149 | 150 | given: 151 | buildFile << """ 152 | confluence { 153 | parserType = "pegdown" 154 | pages { 155 | "\${project.name}-pegdown" { 156 | parentTitle = "Home" 157 | srcFile = file("README.md") 158 | } 159 | } 160 | } 161 | """.stripIndent() 162 | // using project's README.md markdown file as the file to publish to Confluence 163 | markdownFile << new File('README.md').text 164 | 165 | // project's README file refers to the pics/picture.jpg picture and hence we need to copy it to the project's 166 | // test directory retaining the directory structure 167 | Files.createDirectories(testProjectDir.toPath().resolve("pics")) 168 | Files.copy(Paths.get("pics", "picture.jpg"), testProjectDir.toPath().resolve("pics/picture.jpg")) 169 | 170 | when: 171 | def result = gradleRunner.build() 172 | 173 | then: 174 | assert result.task(":confluence").outcome == TaskOutcome.SUCCESS 175 | 176 | and: 177 | def publishedPageTitle = "markdown-confluence-gradle-plugin-pegdown" 178 | def publishedPage = confluenceService.findPageByTitle(publishedPageTitle) 179 | assert publishedPage != null 180 | 181 | } 182 | 183 | def "Image referred in the markdown should be uploaded to the published Confluence page - Commonmark"() { 184 | 185 | given: 186 | def pageTitle = "Page With An Image (Commonmark)" 187 | buildFile << """ 188 | confluence { 189 | pages { 190 | "${pageTitle}" { 191 | parentTitle = "Home" 192 | srcFile = file("README.md") 193 | } 194 | } 195 | } 196 | """.stripIndent() 197 | // using project's README.md markdown file as the file to publish to Confluence 198 | markdownFile << "![This is a picture](pics/picture.jpg \"Extra title\")" 199 | 200 | Files.createDirectories(testProjectDir.toPath().resolve("pics")) 201 | Files.copy(Paths.get("pics", "picture.jpg"), testProjectDir.toPath().resolve("pics/picture.jpg")) 202 | 203 | when: 204 | def result = gradleRunner.build() 205 | 206 | then: 207 | assert result.task(":confluence").outcome == TaskOutcome.SUCCESS 208 | 209 | and: 210 | def publishedPage = confluenceService.findPageByTitle(pageTitle) 211 | assert publishedPage != null 212 | assert publishedPage.content.contains("ac:image> 335 | writer.write("""\ 336 | rootProject.name = '${rootProjectName}' 337 | 338 | include 'subproject1' 339 | include 'subproject2' 340 | """.stripIndent() 341 | ) 342 | } 343 | 344 | def subproject1 = Files.createDirectories(testProjectDir.toPath().resolve("subproject1")) 345 | def subproject1BuildFile = new File(subproject1.toFile(), "build.gradle") 346 | subproject1BuildFile << """\ 347 | plugins { 348 | id 'com.github.qwazer.markdown-confluence' 349 | } 350 | 351 | confluence { 352 | pageVariables = ['project.name': project.name, 'rootProject.name': project.rootProject.name] 353 | pages { 354 | "\${project.rootProject.name} - \${project.name}" { 355 | parentTitle = "${rootProjectName}" 356 | srcFile = file("README.md") 357 | } 358 | } 359 | } 360 | """.stripIndent() 361 | 362 | def subproject1MarkdownFile = new File(subproject1.toFile(), "README.md") 363 | subproject1MarkdownFile << """\ 364 | # \${project.name} 365 | 366 | This is a page documenting the the subproject named \${project.name}. 367 | 368 | This subproject is a child of a \${rootProject.name} project. 369 | """.stripIndent() 370 | 371 | def subproject2 = Files.createDirectories(testProjectDir.toPath().resolve("subproject2")) 372 | def subproject2BuildFile = new File(subproject2.toFile(), "build.gradle") 373 | subproject2BuildFile << """\ 374 | plugins { 375 | id 'com.github.qwazer.markdown-confluence' 376 | } 377 | 378 | confluence { 379 | pageVariables = ['project.name': project.name, 'rootProject.name': project.rootProject.name] 380 | pages { 381 | "\${project.rootProject.name} - \${project.name}" { 382 | parentTitle = "${rootProjectName}" 383 | srcFile = file("README.md") 384 | } 385 | } 386 | } 387 | """.stripIndent() 388 | 389 | def subproject2MarkdownFile = new File(subproject2.toFile(), "README.md") 390 | subproject2MarkdownFile << """\ 391 | # \${project.name} 392 | 393 | This is a page documenting the the subproject named \${project.name}. 394 | 395 | This subproject is a child of a \${rootProject.name} project. 396 | """.stripIndent() 397 | 398 | // root project's page to be published - this page is a parent page of the pages configured in subprojects 399 | buildFile << """\ 400 | confluence { 401 | parserType = 'pegdown' 402 | pageVariables = ['project.name': project.name] 403 | pages { 404 | "\${project.name}" { 405 | parentTitle = "Home" 406 | srcFile = file("README.md") 407 | } 408 | } 409 | } 410 | """.stripIndent() 411 | 412 | markdownFile << """\ 413 | # \${project.name} 414 | 415 | This is a page documenting a root project of a multi-module Gradle project named \${project.name}. 416 | 417 | Child pages: 418 | 419 | {children:depth=1} 420 | """.stripIndent() 421 | 422 | when: 423 | def result = gradleRunner.build() 424 | 425 | then: 426 | assert result.task(":confluence").outcome == TaskOutcome.SUCCESS 427 | def rootPage = confluenceService.findPageByTitle(rootProjectName) 428 | assert rootPage != null 429 | def rootPageContent = rootPage.content 430 | // this assertion fails when using the Commonmark parser, hence the Pegdown parser is specified 431 | // in the configuration 432 | assert rootPageContent.contains("ac:structured-macro ac:name=\"children\"") 433 | 434 | and: 435 | def subproject1Page = confluenceService.findPageByTitle("multi-module-project - subproject1") 436 | assert subproject1Page != null 437 | assert subproject1Page.ancestorId == rootPage.id 438 | def subproject1PageContent = subproject1Page.content 439 | assert subproject1PageContent.contains("This is a page documenting the the subproject named subproject1.") 440 | 441 | and: 442 | def subproject2Page = confluenceService.findPageByTitle("multi-module-project - subproject2") 443 | assert subproject2Page != null 444 | assert subproject2Page.ancestorId == rootPage.id 445 | def subproject2PageContent = subproject2Page.content 446 | assert subproject2PageContent.contains("This is a page documenting the the subproject named subproject2.") 447 | 448 | } 449 | 450 | def "Confluence task should fail when page title is the same as parent page title"() { 451 | 452 | given: 453 | markdownFile << """\ 454 | # README 455 | """.stripIndent() 456 | 457 | buildFile << """\ 458 | confluence { 459 | pages { 460 | "Releases" { 461 | parentTitle = "Releases" 462 | srcFile = file("README.md") 463 | labels = ["\${project.name}"] 464 | } 465 | } 466 | } 467 | """.stripIndent() 468 | 469 | when: 470 | def result = gradleRunner.buildAndFail() 471 | 472 | then: 473 | assert result.task(":confluence").outcome == TaskOutcome.FAILED 474 | assert result.output.contains("Page title cannot be the same as page parent title") 475 | 476 | } 477 | 478 | def "Confluence task should fail when authentication is not set"() { 479 | 480 | given: 481 | markdownFile << """\ 482 | # README 483 | """.stripIndent() 484 | 485 | // this overrides the common build file created in the setup method 486 | buildFile.withWriter { writer -> 487 | writer.write("""\ 488 | plugins { 489 | id 'com.github.qwazer.markdown-confluence' 490 | } 491 | 492 | confluence { 493 | authenticationTypeString = '${CONFLUENCE_AUTHENTICATION_TYPE.name()}' 494 | restApiUrl = '${CONFLUENCE_BASE_URL}' 495 | spaceKey = '${CONFLUENCE_SPACE_KEY}' 496 | } 497 | """.stripIndent() 498 | ) 499 | } 500 | 501 | when: 502 | def result = gradleRunner.buildAndFail() 503 | 504 | then: 505 | assert result.task(":confluence").outcome == TaskOutcome.FAILED 506 | assert result.output.contains("Cannot query the value of extension 'confluence' property 'authentication' because it has no value available") 507 | 508 | } 509 | 510 | def "Confluence task should fail when restApiUrl is invalid"() { 511 | 512 | given: 513 | markdownFile << """\ 514 | # README 515 | """.stripIndent() 516 | 517 | buildFile << """\ 518 | confluence { 519 | restApiUrl = 'Not a valid URL' 520 | } 521 | """.stripIndent() 522 | 523 | when: 524 | def result = gradleRunner.buildAndFail() 525 | 526 | then: 527 | assert result.task(":confluence").outcome == TaskOutcome.FAILED 528 | assert result.output.contains("Invalid restApiUrl value supplied") 529 | 530 | } 531 | 532 | def "Confluence task should fail when restApiUrl is not set"() { 533 | 534 | given: 535 | markdownFile << """\ 536 | # README 537 | """.stripIndent() 538 | 539 | // this overrides the common build file created in the setup method 540 | buildFile.withWriter { writer -> 541 | writer.write("""\ 542 | plugins { 543 | id 'com.github.qwazer.markdown-confluence' 544 | } 545 | 546 | confluence { 547 | authenticationTypeString = '${CONFLUENCE_AUTHENTICATION_TYPE.name()}' 548 | authentication = '${CONFLUENCE_AUTHENTICATION}' 549 | spaceKey = '${CONFLUENCE_SPACE_KEY}' 550 | } 551 | """.stripIndent() 552 | ) 553 | } 554 | 555 | when: 556 | def result = gradleRunner.buildAndFail() 557 | 558 | then: 559 | assert result.task(":confluence").outcome == TaskOutcome.FAILED 560 | assert result.output.contains("Cannot query the value of extension 'confluence' property 'restApiUrl' because it has no value available") 561 | 562 | } 563 | 564 | def "Confluence task should fail when blank spaceKey is set"() { 565 | 566 | given: 567 | markdownFile << """\ 568 | # README 569 | """.stripIndent() 570 | 571 | buildFile << """\ 572 | confluence { 573 | spaceKey = ' ' 574 | } 575 | """.stripIndent() 576 | 577 | when: 578 | def result = gradleRunner.buildAndFail() 579 | 580 | then: 581 | assert result.task(":confluence").outcome == TaskOutcome.FAILED 582 | assert result.output.contains("Confluence space key cannot be blank/empty") 583 | 584 | } 585 | 586 | def "Confluence task should fail when markdown file to be published does not exist"() { 587 | 588 | given: 589 | buildFile << """\ 590 | confluence { 591 | pages { 592 | "Releases" { 593 | parentTitle = "Releases" 594 | srcFile = file("NoSuchFile.md") 595 | labels = ["\${project.name}"] 596 | } 597 | } 598 | } 599 | """.stripIndent() 600 | 601 | when: 602 | def result = gradleRunner.buildAndFail() 603 | 604 | then: 605 | assert result.task(":confluence").outcome == TaskOutcome.FAILED 606 | assert result.output.contains("File not found:") 607 | assert result.output.contains("NoSuchFile.md") 608 | 609 | } 610 | 611 | def "Confluence task should fail when authenticationTypeString is invalid"() { 612 | 613 | given: 614 | markdownFile << """\ 615 | # README 616 | """.stripIndent() 617 | 618 | buildFile << """\ 619 | confluence { 620 | authenticationTypeString = 'wrong' 621 | } 622 | """.stripIndent() 623 | 624 | when: 625 | def result = gradleRunner.buildAndFail() 626 | 627 | then: 628 | assert result.task(":confluence").outcome == TaskOutcome.FAILED 629 | assert result.output.contains("No enum constant") 630 | 631 | } 632 | 633 | def "Confluence task should fail when parserType value is invalid"() { 634 | 635 | given: 636 | def parserType = "fancyParser" 637 | markdownFile << """\ 638 | # README 639 | """.stripIndent() 640 | 641 | buildFile << """\ 642 | confluence { 643 | parserType = '${parserType}' 644 | } 645 | """.stripIndent() 646 | 647 | when: 648 | def result = gradleRunner.buildAndFail() 649 | 650 | then: 651 | assert result.task(":confluence").outcome == TaskOutcome.FAILED 652 | def expectedOutput = "Unknown parser type specified: " + parserType.toLowerCase() 653 | assert result.output.contains(expectedOutput) 654 | 655 | } 656 | 657 | def "Confluence task should fail when using unknown Confluence macro with the Pegdown parser"() { 658 | 659 | given: 660 | def pageTitle = "Page With Unknown Macro" 661 | 662 | markdownFile << """\ 663 | # Page with unknown macro 664 | 665 | Markdown file with {unknown_macro}. 666 | """.stripIndent() 667 | 668 | // this overrides the common build file created in the setup method 669 | buildFile << """\ 670 | confluence { 671 | parserType = 'pegdown' 672 | pages { 673 | "${pageTitle}" { 674 | parentTitle = "Home" 675 | srcFile = file("README.md") 676 | } 677 | } 678 | } 679 | """ 680 | 681 | when: 682 | def result = gradleRunner.buildAndFail() 683 | 684 | then: 685 | assert result.task(":confluence").outcome == TaskOutcome.FAILED 686 | 687 | } 688 | 689 | } 690 | -------------------------------------------------------------------------------- /src/integrationTest/java/com/github/qwazer/markdown/confluence/core/AbstractIT.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core; 2 | 3 | import com.github.qwazer.markdown.confluence.core.model.ConfluenceSpace; 4 | import com.github.qwazer.markdown.confluence.core.service.ConfluenceService; 5 | import com.github.qwazer.markdown.confluence.gradle.plugin.AuthenticationType; 6 | import okhttp3.OkHttpClient; 7 | import org.junit.Before; 8 | 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.Base64; 11 | 12 | public class AbstractIT { 13 | 14 | public static final String CONFLUENCE_BASE_URL = "http://localhost:8090/rest/api"; 15 | public static final String CONFLUENCE_SPACE_KEY = "SN"; 16 | public static final AuthenticationType CONFLUENCE_AUTHENTICATION_TYPE = AuthenticationType.BASIC; 17 | public static final String CONFLUENCE_AUTHENTICATION = 18 | Base64.getEncoder().encodeToString("admin:admin".getBytes(StandardCharsets.UTF_8)); 19 | 20 | protected ConfluenceService confluenceService; 21 | protected ConfluenceSpace confluenceSpace; 22 | 23 | @Before 24 | public void abstractBefore() { 25 | final String authorizationHeaderValue = 26 | CONFLUENCE_AUTHENTICATION_TYPE.getAuthorizationHeader(CONFLUENCE_AUTHENTICATION); 27 | final OkHttpClient httpClient = new OkHttpClient.Builder() 28 | .addInterceptor(OkHttpUtils.getAuthorizationInterceptor(authorizationHeaderValue)) 29 | .build(); 30 | confluenceService = new ConfluenceService(CONFLUENCE_BASE_URL, CONFLUENCE_SPACE_KEY, httpClient); 31 | confluenceSpace = confluenceService.getOrCreateSpace(CONFLUENCE_SPACE_KEY); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/integrationTest/java/com/github/qwazer/markdown/confluence/core/service/ConfluenceServiceIT.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import com.github.qwazer.markdown.confluence.core.AbstractIT; 4 | import com.github.qwazer.markdown.confluence.core.ConfluenceException; 5 | import com.github.qwazer.markdown.confluence.core.model.ConfluencePage; 6 | import org.junit.Test; 7 | 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.Arrays; 11 | import java.util.UUID; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertNotNull; 15 | import static org.junit.Assert.assertTrue; 16 | 17 | /** 18 | * Created by Anton Reshetnikov on 24 Nov 2016. 19 | */ 20 | public class ConfluenceServiceIT extends AbstractIT { 21 | 22 | @Test 23 | public void testFindAncestorId() { 24 | final Long id = confluenceService.findAncestorId(confluenceSpace.getKey()); 25 | assertNotNull(id); 26 | } 27 | 28 | @Test 29 | public void testCreatePage() { 30 | final ConfluencePage page = new ConfluencePage(); 31 | page.setTitle("Basic Test Page - " + UUID.randomUUID()); 32 | page.setContent("Hello, Confluence!"); 33 | confluenceService.createPage(page); 34 | 35 | final ConfluencePage createdPage = confluenceService.findPageByTitle(page.getTitle()); 36 | assertNotNull(createdPage); 37 | assertTrue(createdPage.getContent().contains(page.getContent())); 38 | } 39 | 40 | @Test 41 | public void testUpdatePage() { 42 | 43 | final ConfluencePage initialPage = new ConfluencePage(); 44 | initialPage.setTitle("Update Page Test - " + UUID.randomUUID()); 45 | initialPage.setContent("Hello, Confluence!"); 46 | confluenceService.createPage(initialPage); 47 | 48 | final ConfluencePage savedPage = confluenceService.findPageByTitle(initialPage.getTitle()); 49 | assertNotNull(savedPage); 50 | 51 | final ConfluencePage updatedPage = new ConfluencePage(); 52 | updatedPage.setId(savedPage.getId()); 53 | updatedPage.setTitle(savedPage.getTitle()); 54 | updatedPage.setContent("Hello, Confluence after update!"); 55 | updatedPage.setLabels(savedPage.getLabels()); 56 | updatedPage.setVersion(savedPage.getVersion()); 57 | confluenceService.updatePage(updatedPage); 58 | 59 | final ConfluencePage savedUpdatedPage = confluenceService.findPageByTitle(initialPage.getTitle()); 60 | assertNotNull(savedUpdatedPage); 61 | assertTrue(savedUpdatedPage.getContent().contains(updatedPage.getContent())); 62 | } 63 | 64 | @Test 65 | public void testCreatePageWithAttachment() { 66 | final ConfluencePage page = new ConfluencePage(); 67 | page.setTitle("Test Page With Attachment - " + UUID.randomUUID()); 68 | page.setContent("Hello, Confluence!"); 69 | final Long pageId = confluenceService.createPage(page); 70 | final Path attachmentPath = Paths.get("pics/picture.jpg"); 71 | confluenceService.createAttachment(pageId, attachmentPath.toString()); 72 | 73 | final String attachmentId = 74 | confluenceService.getAttachmentId(pageId, "picture.jpg"); 75 | assertNotNull(attachmentId); 76 | } 77 | 78 | @Test(expected = ConfluenceException.class) 79 | public void testCreatePageWithUnknownMacro() { 80 | ConfluencePage page = new ConfluencePage(); 81 | page.setTitle("Test Page With Invalid Macro - " + UUID.randomUUID()); 82 | page.setContent("Hello, {unknown_macro}!"); 83 | confluenceService.createPage(page); 84 | } 85 | 86 | @Test 87 | public void testAddLabel() { 88 | final ConfluencePage page = new ConfluencePage(); 89 | page.setTitle("Test Page With Labels - " + UUID.randomUUID()); 90 | page.setContent("This is a very simple Confluence page with a labels."); 91 | 92 | final Long pageId = confluenceService.createPage(page); 93 | 94 | confluenceService.addLabels(pageId, Arrays.asList("label1", "label2")); 95 | 96 | final ConfluencePage savedPage = 97 | confluenceService.findPageByTitle(page.getTitle()); 98 | assertNotNull(savedPage); 99 | assertEquals(2, savedPage.getLabels().size()); 100 | assertTrue(savedPage.getLabels().contains("label1")); 101 | assertTrue(savedPage.getLabels().contains("label2")); 102 | } 103 | 104 | 105 | } -------------------------------------------------------------------------------- /src/integrationTest/java/com/github/qwazer/markdown/confluence/core/service/PageServiceIT.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import com.github.qwazer.markdown.confluence.core.AbstractIT; 4 | import com.github.qwazer.markdown.confluence.gradle.plugin.ConfluenceExtension; 5 | import kotlin.Pair; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.mockito.Mockito; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.nio.file.Path; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | import static org.junit.Assert.assertFalse; 17 | import static org.junit.Assert.assertTrue; 18 | 19 | /** 20 | * Created by Anton Reshetnikov on 15 Nov 2016. 21 | */ 22 | public class PageServiceIT extends AbstractIT { 23 | 24 | private PageService pageService; 25 | 26 | @Before 27 | public void before() { 28 | pageService = new PageService(confluenceService); 29 | } 30 | 31 | @Test 32 | public void testPrepareWikiTextWithInlineImage() throws IOException { 33 | 34 | ConfluenceExtension.Page page = Mockito.mock(); 35 | Mockito.when(page.getName()).thenReturn("Page with invalid macro"); 36 | Mockito.when(page.getTitle()).thenCallRealMethod(); 37 | Mockito.when(page.getParentTitle()).thenReturn("Home"); 38 | Mockito.when(page.getSrcFile()).thenReturn(new File("README.md")); 39 | final String markdown = "# Page with inline image\n\nThis is an image: ![Cool Picture](pics/picture.jpg \"Cool Picture's Title\")"; 40 | Mockito.when(page.getContent()).thenReturn(markdown); 41 | 42 | final Pair> pair = pageService.prepareWikiText(page); 43 | final String wikiText = pair.getFirst(); 44 | final List attachments = pair.getSecond(); 45 | 46 | assertTrue(wikiText.contains("!picture.jpg|Cool Picture!")); 47 | assertFalse(attachments.isEmpty()); 48 | } 49 | 50 | @Test 51 | public void testPublishingPageWithUnknownMacro() throws IOException { 52 | // Should escape unknown macros. 53 | 54 | ConfluenceExtension.Page page = Mockito.mock(); 55 | Mockito.when(page.getName()).thenReturn("Page with invalid macro"); 56 | Mockito.when(page.getTitle()).thenCallRealMethod(); 57 | Mockito.when(page.getParentTitle()).thenReturn("Home"); 58 | Mockito.when(page.getContent()).thenReturn("{no_such_macro}"); 59 | 60 | final Pair> pair = pageService.prepareWikiText(page); 61 | final String wikiText = pair.getFirst(); 62 | 63 | assertTrue(wikiText.contains("\\{no_such_macro\\}")); 64 | 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/ConfluenceException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Aaron Knight 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.qwazer.markdown.confluence.core; 17 | 18 | public class ConfluenceException extends RuntimeException { 19 | 20 | public ConfluenceException(String message) { 21 | super(message); 22 | } 23 | 24 | public ConfluenceException(String message, Throwable cause) { 25 | super(message, cause); 26 | } 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/HttpHeader.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core; 2 | 3 | public interface HttpHeader { 4 | 5 | String ACCEPT = "Accept"; 6 | String AUTHORIZATION = "Authorization"; 7 | String CONTENT_TYPE = "Content-Type"; 8 | String X_ATLASSIAN_TOKEN = "X-Atlassian-Token"; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core; 2 | 3 | import com.github.qwazer.markdown.confluence.core.ConfluenceException; 4 | 5 | public class NotFoundException extends ConfluenceException { 6 | 7 | 8 | public NotFoundException(String message) { 9 | super(message); 10 | } 11 | 12 | public NotFoundException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/OkHttpUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core; 2 | 3 | import okhttp3.Interceptor; 4 | import okhttp3.Request; 5 | 6 | public class OkHttpUtils { 7 | 8 | public static Interceptor getAuthorizationInterceptor(String authorizationHeader) { 9 | return chain -> { 10 | final Request request = chain.request() 11 | .newBuilder() 12 | .header(HttpHeader.ACCEPT, "application/json") 13 | .header(HttpHeader.AUTHORIZATION, authorizationHeader) 14 | .build(); 15 | return chain.proceed(request); 16 | }; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/Utils.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core; 2 | 3 | public class Utils { 4 | 5 | private Utils() {} 6 | 7 | public static void require(boolean condition, String message) { 8 | if (!condition) { 9 | throw new IllegalArgumentException(message); 10 | } 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/model/ConfluencePage.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.Objects; 6 | 7 | /** 8 | * Created by Anton Reshetnikov on 15 Nov 2016. 9 | */ 10 | public class ConfluencePage { 11 | 12 | private Long ancestorId; 13 | private String title; 14 | private Long id; 15 | private Integer version; 16 | private String content; 17 | private Collection labels = new ArrayList<>(); 18 | 19 | public Long getAncestorId() { 20 | return ancestorId; 21 | } 22 | 23 | public void setAncestorId(Long ancestorId) { 24 | this.ancestorId = ancestorId; 25 | } 26 | 27 | public String getTitle() { 28 | return title; 29 | } 30 | 31 | public void setTitle(String title) { 32 | this.title = title; 33 | } 34 | 35 | public Long getId() { 36 | return id; 37 | } 38 | 39 | public void setId(Long id) { 40 | this.id = id; 41 | } 42 | 43 | 44 | public Integer getVersion() { 45 | return version; 46 | } 47 | 48 | public void setVersion(Integer version) { 49 | this.version = version; 50 | } 51 | 52 | public String getContent() { 53 | return content; 54 | } 55 | 56 | public void setContent(String content) { 57 | this.content = content; 58 | } 59 | 60 | public Collection getLabels() { 61 | return labels; 62 | } 63 | 64 | public void setLabels(Collection labels) { 65 | Objects.requireNonNull(labels); 66 | this.labels = labels; 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/model/ConfluenceSpace.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import java.util.Objects; 7 | 8 | public class ConfluenceSpace { 9 | 10 | private final Long id; 11 | private final String key; 12 | private final String name; 13 | private final String type; 14 | 15 | public ConfluenceSpace(String key) { 16 | this(null, key, key, null); 17 | } 18 | 19 | @JsonCreator 20 | public ConfluenceSpace( 21 | @JsonProperty("id") Long id, 22 | @JsonProperty("key") String key, 23 | @JsonProperty("name") String name, 24 | @JsonProperty("type") String type 25 | ) { 26 | this.id = id; 27 | this.key = key; 28 | this.name = name; 29 | this.type = type; 30 | } 31 | 32 | public Long getId() { 33 | return id; 34 | } 35 | 36 | public String getKey() { 37 | return key; 38 | } 39 | 40 | public String getName() { 41 | return name; 42 | } 43 | 44 | public String getType() { 45 | return type; 46 | } 47 | 48 | @Override 49 | public boolean equals(Object o) { 50 | if (this == o) return true; 51 | if (o == null || getClass() != o.getClass()) return false; 52 | ConfluenceSpace that = (ConfluenceSpace) o; 53 | return Objects.equals(id, that.id) && Objects.equals(key, that.key) && Objects.equals(name, that.name) && Objects.equals(type, that.type); 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | return Objects.hash(id, key, name, type); 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "ConfluenceSpace{" + 64 | "id=" + id + 65 | ", key='" + key + '\'' + 66 | ", name='" + name + '\'' + 67 | ", type='" + type + '\'' + 68 | '}'; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/service/AttachmentService.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.nio.file.Path; 7 | 8 | public class AttachmentService { 9 | 10 | private static final Logger LOG = LoggerFactory.getLogger(AttachmentService.class); 11 | 12 | private final ConfluenceService confluenceService; 13 | 14 | public AttachmentService(final ConfluenceService confluenceService) { 15 | this.confluenceService = confluenceService; 16 | } 17 | 18 | public void postAttachmentToPage(Long pageId, Path filePath) { 19 | 20 | LOG.info("Posting attachment {} to page {} in Confluence...", filePath.toString(), pageId); 21 | 22 | String attachmentId = confluenceService.getAttachmentId(pageId, filePath.getFileName().toString()); 23 | if (attachmentId == null) { 24 | LOG.info("Create new attachment"); 25 | confluenceService.createAttachment(pageId, filePath.toString()); 26 | } else { 27 | LOG.info("Update existing attachment"); 28 | confluenceService.updateAttachment(pageId, attachmentId, filePath.toString()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/service/ConfluenceService.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.node.ArrayNode; 8 | import com.fasterxml.jackson.databind.node.ObjectNode; 9 | import com.github.qwazer.markdown.confluence.core.ConfluenceException; 10 | import com.github.qwazer.markdown.confluence.core.HttpHeader; 11 | import com.github.qwazer.markdown.confluence.core.NotFoundException; 12 | import com.github.qwazer.markdown.confluence.core.model.ConfluencePage; 13 | import com.github.qwazer.markdown.confluence.core.model.ConfluenceSpace; 14 | import okhttp3.HttpUrl; 15 | import okhttp3.MediaType; 16 | import okhttp3.MultipartBody; 17 | import okhttp3.OkHttpClient; 18 | import okhttp3.Request; 19 | import okhttp3.RequestBody; 20 | import okhttp3.Response; 21 | import okhttp3.ResponseBody; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import javax.annotation.Nonnull; 26 | import javax.annotation.Nullable; 27 | import java.io.IOException; 28 | import java.nio.file.Files; 29 | import java.nio.file.Path; 30 | import java.nio.file.Paths; 31 | import java.util.Collection; 32 | import java.util.List; 33 | import java.util.Objects; 34 | import java.util.stream.Collectors; 35 | 36 | /** 37 | * Created by Anton Reshetnikov on 24 Nov 2016. 38 | */ 39 | public class ConfluenceService { 40 | 41 | private static final Logger LOG = LoggerFactory.getLogger(ConfluenceService.class); 42 | 43 | private static final String BODY = "body"; 44 | private static final String EXPAND = "expand"; 45 | private static final String ID = "id"; 46 | private static final String SPACE = "space"; 47 | private static final String SPACE_KEY = "spaceKey"; 48 | private static final String TITLE = "title"; 49 | private static final String TYPE = "type"; 50 | private static final String PAGE_VERSION = "version"; 51 | private static final String VERSION_NUMBER = "number"; 52 | 53 | private final HttpUrl baseUrl; 54 | private final String spaceKey; 55 | 56 | private final OkHttpClient httpClient; 57 | private final ObjectMapper mapper; 58 | 59 | public ConfluenceService( 60 | @Nonnull final String baseUrl, 61 | @Nonnull final String spaceKey, 62 | @Nonnull final OkHttpClient httpClient 63 | ) { 64 | this(baseUrl, spaceKey, httpClient, new ObjectMapper()); 65 | } 66 | 67 | public ConfluenceService( 68 | @Nonnull final String baseUrl, 69 | @Nonnull final String spaceKey, 70 | @Nonnull final OkHttpClient httpClient, 71 | @Nonnull final ObjectMapper mapper 72 | ) { 73 | this.baseUrl = HttpUrl.parse(baseUrl); 74 | this.spaceKey = spaceKey; 75 | this.httpClient = httpClient; 76 | this.mapper = mapper 77 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 78 | .setSerializationInclusion(JsonInclude.Include.NON_NULL); 79 | } 80 | 81 | public Request getSpaceRequest(final String spaceKey) { 82 | final HttpUrl url = baseUrl 83 | .newBuilder() 84 | .addPathSegments(String.format("space/%s", spaceKey)) 85 | .build(); 86 | 87 | return new Request.Builder() 88 | .get() 89 | .url(url) 90 | .build(); 91 | } 92 | 93 | @Nullable 94 | public ConfluenceSpace getSpace(final String spaceKey) { 95 | try { 96 | return mapper.convertValue(executeRequest(getSpaceRequest(spaceKey)), ConfluenceSpace.class); 97 | } catch (IllegalArgumentException e) { 98 | throw new ConfluenceException(e.getMessage(), e); 99 | } catch (NotFoundException e) { 100 | return null; 101 | } 102 | } 103 | 104 | public ConfluenceSpace getOrCreateSpace(final String spaceKey) { 105 | ConfluenceSpace existingSpace = getSpace(spaceKey); 106 | if (existingSpace == null) { 107 | return createSpace(new ConfluenceSpace(spaceKey)); 108 | } else { 109 | return existingSpace; 110 | } 111 | } 112 | 113 | public Request createSpaceRequest(final ConfluenceSpace space) { 114 | final HttpUrl url = baseUrl 115 | .newBuilder() 116 | .addPathSegment("space") 117 | .build(); 118 | 119 | return new Request.Builder() 120 | .url(url) 121 | .post(RequestBody.create(mapper.convertValue(space, JsonNode.class).toString(), MediaType.parse("application/json"))) 122 | .build(); 123 | } 124 | 125 | public ConfluenceSpace createSpace(final ConfluenceSpace space) { 126 | return mapper.convertValue(executeRequest(createSpaceRequest(space)), ConfluenceSpace.class); 127 | } 128 | 129 | public Request findPageByTitleRequest(final String title) { 130 | 131 | final HttpUrl url = baseUrl 132 | .newBuilder() 133 | .addPathSegment("content") 134 | .setQueryParameter(SPACE_KEY, spaceKey) 135 | .setQueryParameter(TITLE, title) 136 | .setQueryParameter(EXPAND, "body.storage,version,ancestors,metadata.labels") 137 | .build(); 138 | 139 | return new Request.Builder() 140 | .get() 141 | .url(url) 142 | .build(); 143 | } 144 | 145 | @Nullable // if the page with the given title does not exist 146 | public ConfluencePage findPageByTitle(final String title) { 147 | try { 148 | return parseResponseEntityToConfluencePage(executeRequest(findPageByTitleRequest(title))); 149 | } catch (NotFoundException e) { 150 | LOG.debug("Page \"{}\" not found in space \"{}\"", title, spaceKey); 151 | return null; 152 | } 153 | } 154 | 155 | public Request findSpaceHomePageRequest() { 156 | 157 | final HttpUrl url = baseUrl 158 | .newBuilder() 159 | .addPathSegment("content") 160 | .setQueryParameter(SPACE_KEY, spaceKey) 161 | .setQueryParameter(EXPAND, "body.storage,version,ancestors") 162 | .build(); 163 | 164 | return new Request.Builder() 165 | .get() 166 | .url(url) 167 | .build(); 168 | } 169 | 170 | public ConfluencePage findSpaceHomePage() { 171 | return parseResponseEntityToConfluencePage(executeRequest(findSpaceHomePageRequest())); 172 | } 173 | 174 | public void updatePage(final ConfluencePage page) { 175 | executeRequest(updatePageRequest(page)); 176 | } 177 | 178 | public Request updatePageRequest(final ConfluencePage page) { 179 | final HttpUrl url = baseUrl 180 | .newBuilder() 181 | .addPathSegments(String.format("content/%s", page.getId())) 182 | .build(); 183 | 184 | final ObjectNode requestBody = buildPostBody(page); 185 | requestBody.put(ID, page.getId()); 186 | requestBody.set(PAGE_VERSION, mapper.createObjectNode().put(VERSION_NUMBER, page.getVersion() + 1)); 187 | 188 | return new Request.Builder() 189 | .url(url) 190 | .put(RequestBody.create(requestBody.toString(), MediaType.parse("application/json"))) 191 | .build(); 192 | } 193 | 194 | private JsonNode executeRequest(final Request request) { 195 | try (final Response response = httpClient.newCall(request).execute()) { 196 | if (!response.isSuccessful()) { 197 | if (response.code() == 404) { 198 | throw new NotFoundException(String.format("The requested resource %s was not found", request.url())); 199 | } 200 | final String message = String.format( 201 | "%s: %s", 202 | response, 203 | response.body() != null ? response.body().string() : "EMPTY BODY" 204 | ); 205 | throw new ConfluenceException(message); 206 | } 207 | try (final ResponseBody responseBody = response.body()) { 208 | if (responseBody == null) { 209 | throw new ConfluenceException(String.format("%s has null body", response)); 210 | } 211 | return mapper.readTree(responseBody.bytes()); 212 | } catch (IOException e) { 213 | throw new ConfluenceException("Could not parse Confluence REST API response", e); 214 | } 215 | 216 | } catch (IOException e) { 217 | throw new ConfluenceException("Could not process response", e); 218 | } 219 | } 220 | 221 | public Request createPageRequest(final ConfluencePage page) { 222 | 223 | final HttpUrl url = baseUrl 224 | .newBuilder() 225 | .addPathSegment("content") 226 | .build(); 227 | 228 | final ObjectNode requestBody = buildPostBody(page); 229 | return new Request.Builder() 230 | .url(url) 231 | .post(RequestBody.create(requestBody.toString(), MediaType.parse("application/json"))) 232 | .build(); 233 | } 234 | 235 | public Long createPage(final ConfluencePage page) { 236 | return parsePageIdFromResponse(executeRequest(createPageRequest(page))); 237 | } 238 | 239 | 240 | public Request addLabelsRequest(Long pageId, @Nonnull String labelName) { 241 | final HttpUrl url = baseUrl 242 | .newBuilder() 243 | .addPathSegments(String.format("content/%d/label", pageId)) 244 | .build(); 245 | 246 | final JsonNode requestBody = buildAddLabelPostBody(labelName); 247 | return new Request.Builder() 248 | .url(url) 249 | .post(RequestBody.create(requestBody.toString(), MediaType.parse("application/json"))) 250 | .build(); 251 | } 252 | 253 | public void addLabels(Long pageId, @Nonnull Collection labels) { 254 | Objects.requireNonNull(labels); 255 | if (labels.isEmpty()) 256 | return; 257 | 258 | labels.forEach(label -> executeRequest(addLabelsRequest(pageId, label))); 259 | } 260 | 261 | 262 | private JsonNode buildAddLabelPostBody(@Nonnull String labelName) { 263 | final ObjectNode label = mapper.createObjectNode() 264 | .put("name", labelName.replaceAll("\\.", "-")) 265 | .put("prefix", "global"); 266 | return mapper.createArrayNode().add(label); 267 | } 268 | 269 | 270 | protected static ConfluencePage parseResponseEntityToConfluencePage(JsonNode responseBody) { 271 | if (responseBody == null) { 272 | return null; 273 | } 274 | 275 | final JsonNode results = responseBody.get("results"); 276 | if (results != null) { 277 | final JsonNode first = results.get(0); 278 | if (first != null) { 279 | final ConfluencePage confluencePage = new ConfluencePage(); 280 | confluencePage.setId(first.get("id").asLong()); 281 | confluencePage.setVersion(first.get("version").get("number").asInt()); 282 | confluencePage.setTitle(first.get("title").asText()); 283 | 284 | final JsonNode bodyNode = first.get("body"); 285 | if (bodyNode != null) { 286 | final JsonNode storageNode = bodyNode.get("storage"); 287 | if (storageNode != null) { 288 | JsonNode valueNode = storageNode.get("value"); 289 | if (valueNode != null) { 290 | confluencePage.setContent(valueNode.asText()); 291 | } 292 | } 293 | } 294 | 295 | final JsonNode metadataNode = first.get("metadata"); 296 | if (metadataNode != null) { 297 | final JsonNode labelsNode = metadataNode.get("labels"); 298 | if (labelsNode != null) { 299 | final List labels = labelsNode.findValues("name").stream() 300 | .map(JsonNode::asText) 301 | .collect(Collectors.toList()); 302 | confluencePage.setLabels(labels); 303 | } 304 | } 305 | 306 | final JsonNode ancestors = first.get("ancestors"); 307 | if (ancestors instanceof ArrayNode) { 308 | final ArrayNode arrayNode = (ArrayNode) ancestors; 309 | if (!arrayNode.isEmpty()) { 310 | final Long ancestorId = arrayNode.get(arrayNode.size() - 1).get(ID).asLong(); 311 | LOG.debug("ancestors: {} : {}, choose -> {}", ancestors.getClass().getName(), ancestors, ancestorId); 312 | confluencePage.setAncestorId(ancestorId); 313 | } 314 | } 315 | return confluencePage; 316 | } 317 | } 318 | 319 | return null; 320 | } 321 | 322 | 323 | private ObjectNode buildPostBody(ConfluencePage confluencePage) { 324 | 325 | final ObjectNode spaceNode = mapper.createObjectNode(); 326 | spaceNode.put("key", spaceKey); 327 | 328 | final ObjectNode storageData = mapper.createObjectNode(); 329 | storageData.put("value", confluencePage.getContent()); 330 | storageData.put("representation", "wiki"); 331 | 332 | final ObjectNode storageNode = mapper.createObjectNode(); 333 | storageNode.set("storage", storageData); 334 | 335 | final ObjectNode bodyNode = mapper.createObjectNode(); 336 | bodyNode.put(TYPE, "page"); 337 | bodyNode.put(TITLE, confluencePage.getTitle()); 338 | bodyNode.set(SPACE, spaceNode); 339 | bodyNode.set(BODY, storageNode); 340 | 341 | if (confluencePage.getAncestorId() != null) { 342 | final ObjectNode ancestor = mapper.createObjectNode(); 343 | ancestor.put("type", "page"); 344 | ancestor.put(ID, confluencePage.getAncestorId()); 345 | 346 | final ArrayNode ancestors = mapper.createArrayNode(); 347 | ancestors.add(ancestor); 348 | 349 | bodyNode.set("ancestors", ancestors); 350 | } 351 | 352 | return bodyNode; 353 | } 354 | 355 | public Long findAncestorId(String title) { 356 | LOG.info("Looking up ancestor id by title {}", title); 357 | ConfluencePage page = findPageByTitle(title); 358 | if (page != null) { 359 | return page.getId(); 360 | } else { 361 | LOG.info("Using page home id ({}) as ancestorId", spaceKey); 362 | return findSpaceHomePage().getId(); 363 | } 364 | } 365 | 366 | private static Long parsePageIdFromResponse(final JsonNode responseEntity) { 367 | try { 368 | return responseEntity.get(ID).asLong(); 369 | } catch (Exception e) { 370 | throw new ConfluenceException("Error Parsing JSON Response from Confluence!", e); 371 | } 372 | } 373 | 374 | private static String parseAttachmentIdFromResponse(final JsonNode responseEntity) { 375 | final JsonNode results = responseEntity.get("results"); 376 | if (results != null) { 377 | final JsonNode first = results.get(0); 378 | if (first != null) { 379 | final JsonNode id = first.get("id"); 380 | if (id != null) { 381 | return id.asText(); 382 | } 383 | } 384 | } 385 | return null; 386 | } 387 | 388 | public Request getAttachmentIdRequest(Long pageId, String attachmentFilename) { 389 | final HttpUrl url = baseUrl 390 | .newBuilder() 391 | .addPathSegments(String.format("content/%d/child/attachment", pageId)) 392 | .addQueryParameter("filename", attachmentFilename) 393 | .build(); 394 | 395 | return new Request.Builder() 396 | .get() 397 | .url(url) 398 | .build(); 399 | } 400 | 401 | public String getAttachmentId(Long pageId, String attachmentFilename) { 402 | return parseAttachmentIdFromResponse(executeRequest(getAttachmentIdRequest(pageId, attachmentFilename))); 403 | } 404 | 405 | public Request createAttachmentRequest(@Nonnull Long pageId, @Nonnull String filePath) { 406 | Objects.requireNonNull(pageId); 407 | Objects.requireNonNull(filePath); 408 | final Path path = Paths.get(filePath); 409 | if (Files.notExists(path)) { 410 | throw new IllegalArgumentException("File not found: " + filePath); 411 | } 412 | 413 | final HttpUrl url = baseUrl 414 | .newBuilder() 415 | .addPathSegments(String.format("content/%d/child/attachment", pageId)) 416 | .build(); 417 | 418 | final RequestBody requestBody = new MultipartBody.Builder() 419 | .setType(MultipartBody.FORM) 420 | .addFormDataPart( 421 | "file", 422 | path.getFileName().toString(), 423 | RequestBody.create(path.toFile(), MediaType.parse("application/octet-stream")) 424 | ) 425 | .build(); 426 | 427 | return new Request.Builder() 428 | .url(url) 429 | .header(HttpHeader.X_ATLASSIAN_TOKEN, "no-check") 430 | .post(requestBody) 431 | .build(); 432 | } 433 | 434 | public void createAttachment(Long pageId, String filePath) { 435 | executeRequest(createAttachmentRequest(pageId, filePath)); 436 | } 437 | 438 | public Request updateAttachmentRequest(@Nonnull Long pageId, @Nonnull String attachmentId, @Nonnull String filePath) { 439 | Objects.requireNonNull(pageId); 440 | Objects.requireNonNull(attachmentId); 441 | Objects.requireNonNull(filePath); 442 | 443 | final Path path = Paths.get(filePath); 444 | if (Files.notExists(path)) { 445 | throw new IllegalArgumentException("File not found: " + filePath); 446 | } 447 | 448 | final RequestBody requestBody = new MultipartBody.Builder() 449 | .setType(MultipartBody.FORM) 450 | .addFormDataPart( 451 | "file", 452 | path.getFileName().toString(), 453 | RequestBody.create(path.toFile(), MediaType.parse("application/octet-stream")) 454 | ) 455 | .build(); 456 | 457 | final HttpUrl url = baseUrl 458 | .newBuilder() 459 | .addPathSegments(String.format("content/%d/child/attachment/%s/data", pageId, attachmentId)) 460 | .build(); 461 | 462 | return new Request.Builder() 463 | .url(url) 464 | .header(HttpHeader.X_ATLASSIAN_TOKEN, "no-check") 465 | .post(requestBody) 466 | .build(); 467 | } 468 | 469 | public void updateAttachment(Long pageId, String attachmentId, String filePath) { 470 | executeRequest(updateAttachmentRequest(pageId, attachmentId, filePath)); 471 | } 472 | 473 | } 474 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/service/MarkdownService.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import org.bsc.markdown.MarkdownParserContext; 4 | 5 | import java.util.Collections; 6 | import java.util.Map; 7 | 8 | public abstract class MarkdownService { 9 | 10 | public static final MarkdownParserContext DEFAULT_PARSER_CONTEXT = new MarkdownParserContext() { 11 | @Override 12 | public boolean isSkipHtml() { 13 | return false; 14 | } 15 | 16 | @Override 17 | public boolean isLinkPrefixEnabled() { 18 | return false; 19 | } 20 | }; 21 | 22 | protected final MarkdownParserContext parserContext; 23 | 24 | public MarkdownService() { 25 | this.parserContext = DEFAULT_PARSER_CONTEXT; 26 | } 27 | 28 | public MarkdownService(MarkdownParserContext parserContext) { 29 | this.parserContext = parserContext; 30 | } 31 | 32 | final String preprocessMarkdown(String markdown, Map pageVariables) { 33 | if (markdown == null || pageVariables == null || pageVariables.isEmpty()) { 34 | return markdown; 35 | } 36 | for (String key : pageVariables.keySet()) { 37 | markdown = markdown.replace("${" + key + "}", pageVariables.get(key)); 38 | } 39 | return markdown; 40 | } 41 | 42 | final String parseMarkdown(String markdown) { 43 | return parseMarkdown(markdown, Collections.emptyMap()); 44 | } 45 | 46 | final String parseMarkdown(String markdown, Map pageVariables) { 47 | final String preprocessedMarkdown = preprocessMarkdown(markdown, pageVariables); 48 | return doParseMarkdown(preprocessedMarkdown); 49 | } 50 | 51 | abstract String doParseMarkdown(String preprocessedMarkdown); 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/service/MarkdownServiceCommonmark.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import org.bsc.markdown.MarkdownParserContext; 4 | import org.bsc.markdown.commonmark.CommonmarkConfluenceWikiVisitor; 5 | 6 | public class MarkdownServiceCommonmark extends MarkdownService { 7 | 8 | public MarkdownServiceCommonmark() { 9 | super(); 10 | } 11 | 12 | public MarkdownServiceCommonmark(MarkdownParserContext parserContext) { 13 | super(parserContext); 14 | } 15 | 16 | @Override 17 | String doParseMarkdown(String preprocessedMarkdown) { 18 | return CommonmarkConfluenceWikiVisitor.parser().parseMarkdown(parserContext, preprocessedMarkdown); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/service/MarkdownServicePegdown.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import org.bsc.markdown.MarkdownParserContext; 4 | import org.bsc.markdown.pegdown.PegdownMarkdownProcessorImpl; 5 | 6 | import java.io.IOException; 7 | import java.io.UncheckedIOException; 8 | 9 | public class MarkdownServicePegdown extends MarkdownService { 10 | 11 | public MarkdownServicePegdown() { 12 | super(); 13 | } 14 | 15 | public MarkdownServicePegdown(MarkdownParserContext parserContext) { 16 | super(parserContext); 17 | } 18 | 19 | @Override 20 | String doParseMarkdown(String preprocessedMarkdown) { 21 | try { 22 | return new PegdownMarkdownProcessorImpl().processMarkdown(parserContext, preprocessedMarkdown); 23 | } catch (IOException e) { 24 | throw new UncheckedIOException(e); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/service/PageService.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import com.github.qwazer.markdown.confluence.core.ConfluenceException; 4 | import com.github.qwazer.markdown.confluence.core.model.ConfluencePage; 5 | import com.github.qwazer.markdown.confluence.gradle.plugin.ConfluenceExtension; 6 | import kotlin.Pair; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | 22 | /** 23 | * Created by Anton Reshetnikov on 14 Nov 2016. 24 | */ 25 | public class PageService { 26 | 27 | public static final Pattern INLINE_IMAGE_PATTERN = Pattern.compile("!\\[(.*)]\\((\\S+)(.*)\\)"); 28 | private static final Logger LOG = LoggerFactory.getLogger(PageService.class); 29 | 30 | private final ConfluenceService confluenceService; 31 | private final MarkdownService markdownService; 32 | private final AttachmentService attachmentService; 33 | 34 | public PageService(final ConfluenceService confluenceService) { 35 | this(confluenceService, new AttachmentService(confluenceService), new MarkdownServiceCommonmark()); 36 | } 37 | 38 | public PageService( 39 | final ConfluenceService confluenceService, 40 | final AttachmentService attachmentService, 41 | final MarkdownService markdownService 42 | ) { 43 | Objects.requireNonNull(confluenceService); 44 | Objects.requireNonNull(attachmentService); 45 | Objects.requireNonNull(markdownService); 46 | 47 | this.confluenceService = confluenceService; 48 | this.attachmentService = attachmentService; 49 | this.markdownService = markdownService; 50 | } 51 | 52 | public Pair> prepareWikiText(ConfluenceExtension.Page page) throws IOException { 53 | return prepareWikiText(page, Collections.emptyMap()); 54 | } 55 | 56 | // Any images referenced in the markdown need to be uploaded as attachments to the generated Confluence page. 57 | // An inline image in markdown looks like this: ![alt text](uri "Title") 58 | // uri can be an external reference (e.g. https://zedplanet.com/images/logo.png) or a local file path 59 | // e.g. (docs/images/logo.png or /Users/user/home/avatar.png). However, using absolute paths is typically not 60 | // portable, i.e., there's a little chance that the aforementioned path /Users/user/home/avatar.png will be 61 | // present on the build server in case you want to automate publishing of wiki pages during your CI builds. 62 | public Pair> prepareWikiText(ConfluenceExtension.Page page, Map pageVariables) throws IOException { 63 | 64 | final String markdownText = page.getContent(); 65 | 66 | final List inlineImages = new ArrayList<>(); 67 | final Matcher matcher = INLINE_IMAGE_PATTERN.matcher(markdownText); 68 | while (matcher.find()) { 69 | // group[0] is the whole matched string 70 | // group[1] is the alt text 71 | // group[2] is the uri 72 | // group[3] is the title - with quotes 73 | String uri = matcher.group(2); 74 | // Don't deal with http references or absolute file references 75 | if (!uri.toLowerCase().startsWith("http") && (!uri.startsWith("/"))) { 76 | // the path should be relative to the markdown file 77 | final String parent = page.getSrcFile().getParent(); 78 | final Path path; 79 | if (parent != null) { 80 | path = Paths.get(parent, uri); 81 | } else { 82 | path = Paths.get(uri); 83 | } 84 | if (Files.exists(path)) { 85 | inlineImages.add(path); 86 | } else { 87 | final String message = 88 | String.format("Could not find local image '%s' referenced in the '%s' markdown file", path, page.getSrcFile()); 89 | throw new ConfluenceException(message); 90 | } 91 | } 92 | } 93 | 94 | final String wikiText = 95 | markdownService.parseMarkdown(markdownText, pageVariables); 96 | 97 | return new Pair<>(wikiText, inlineImages); 98 | 99 | } 100 | 101 | public Long publishWikiPage(ConfluenceExtension.Page page) throws IOException { 102 | return publishWikiPage(page, Collections.emptyMap()); 103 | } 104 | 105 | public Long publishWikiPage(ConfluenceExtension.Page page, Map pageVariables) throws IOException { 106 | 107 | final Pair> pair = prepareWikiText(page, pageVariables); 108 | final String wikiText = pair.getFirst(); 109 | final List images = pair.getSecond(); 110 | 111 | ConfluencePage confluencePage = 112 | confluenceService.findPageByTitle(page.getName()); 113 | if (confluencePage != null) { // page exists 114 | LOG.info("Updating existing page: {}", confluencePage); 115 | confluencePage.setContent(wikiText); 116 | confluencePage.setLabels(page.getLabels()); 117 | confluenceService.updatePage(confluencePage); 118 | confluenceService.addLabels(confluencePage.getId(), page.getLabels()); 119 | } else { 120 | confluencePage = new ConfluencePage(); 121 | confluencePage.setContent(wikiText); 122 | confluencePage.setTitle(page.getTitle()); 123 | confluencePage.setLabels(page.getLabels()); 124 | final Long ancestorId = 125 | confluenceService.findAncestorId(page.getParentTitle()); 126 | confluencePage.setAncestorId(ancestorId); 127 | LOG.info("Creating new Confluence page: {}", confluencePage); 128 | final Long pageId = 129 | confluenceService.createPage(confluencePage); 130 | confluencePage.setId(pageId); 131 | confluenceService.addLabels(pageId, page.getLabels()); 132 | } 133 | 134 | for (Path imageLocalPath : images) { 135 | attachmentService.postAttachmentToPage(confluencePage.getId(), imageLocalPath); 136 | } 137 | 138 | return confluencePage.getId(); 139 | } 140 | 141 | } -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/core/ssl/SslUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.ssl; 2 | 3 | import javax.net.ssl.HostnameVerifier; 4 | import javax.net.ssl.HttpsURLConnection; 5 | import javax.net.ssl.SSLContext; 6 | import javax.net.ssl.TrustManager; 7 | import javax.net.ssl.X509TrustManager; 8 | import java.security.cert.X509Certificate; 9 | 10 | /** 11 | * Created by Anton Reshetnikov on 15 Nov 2016. 12 | */ 13 | public class SslUtil { 14 | 15 | // Not intended to be instantiated 16 | private SslUtil() {} 17 | 18 | public static final X509TrustManager INSECURE_TRUST_MANAGER = new X509TrustManager() { 19 | @Override 20 | public void checkClientTrusted(X509Certificate[] chain, String authType) { 21 | } 22 | 23 | @Override 24 | public void checkServerTrusted(X509Certificate[] chain, String authType) { 25 | } 26 | 27 | @Override 28 | public X509Certificate[] getAcceptedIssuers() { 29 | return new X509Certificate[] {}; 30 | } 31 | }; 32 | 33 | public static final HostnameVerifier INSECURE_HOSTNAME_VERIFIER = (hostname, session) -> true; 34 | 35 | public static final SSLContext INSECURE_SSL_CONTEXT; 36 | static { 37 | try { 38 | SSLContext sslContext = SSLContext.getInstance("SSL"); 39 | sslContext.init(null, new TrustManager[] { INSECURE_TRUST_MANAGER }, new java.security.SecureRandom()); 40 | INSECURE_SSL_CONTEXT = sslContext; 41 | } catch (Exception e) { 42 | throw new RuntimeException(e); 43 | } 44 | } 45 | 46 | public static void sslTrustAll() { 47 | HttpsURLConnection.setDefaultSSLSocketFactory(INSECURE_SSL_CONTEXT.getSocketFactory()); 48 | HttpsURLConnection.setDefaultHostnameVerifier(INSECURE_HOSTNAME_VERIFIER); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/gradle/plugin/AuthenticationType.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.gradle.plugin; 2 | 3 | /** 4 | * Authentication strategy to use when interacting with Confluence APIs. 5 | */ 6 | public enum AuthenticationType { 7 | /** 8 | * Use username/password combination (a.k.a. basic authentication) to authenticate Confluence API calls. 9 | */ 10 | BASIC("Basic"), 11 | /** 12 | * Use Personal Access Token (PAT) to authenticate Confluence API calls. 13 | */ 14 | PAT("Bearer"); 15 | 16 | private final String authorizationHeaderPrefix; 17 | 18 | AuthenticationType(String authorizationHeaderPrefix) { 19 | this.authorizationHeaderPrefix = authorizationHeaderPrefix; 20 | } 21 | 22 | public String getAuthorizationHeader(String authentication) { 23 | return String.format("%s %s", authorizationHeaderPrefix, authentication); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/gradle/plugin/ConfluenceExtension.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.gradle.plugin; 2 | 3 | import org.gradle.api.Action; 4 | import org.gradle.api.Named; 5 | import org.gradle.api.NamedDomainObjectContainer; 6 | import org.gradle.api.provider.MapProperty; 7 | import org.gradle.api.provider.Property; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.nio.charset.StandardCharsets; 13 | import java.nio.file.Files; 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.stream.Collectors; 19 | 20 | public abstract class ConfluenceExtension { 21 | 22 | public abstract Property getAuthenticationType(); 23 | public abstract Property getAuthentication(); 24 | public abstract Property getRestApiUrl(); 25 | public abstract Property getSpaceKey(); 26 | public abstract Property getSslTrustAll(); 27 | public abstract MapProperty getPageVariables(); 28 | abstract Property getAuthenticationTypeString(); 29 | abstract NamedDomainObjectContainer getConfiguredPages(); 30 | public abstract Property getParserType(); 31 | 32 | private List pages; 33 | 34 | @SuppressWarnings("unused") // used by Gradle 35 | public void pages(Action> action) { 36 | action.execute(getConfiguredPages()); 37 | } 38 | 39 | /** 40 | * When later creating the pages, we need to created parent pages first before we create child pages. 41 | * 42 | * @return Pages ordered such that all parents are before the children. 43 | */ 44 | public List getPages() { 45 | if (pages == null) { 46 | pages = getPages(new ArrayList<>(getConfiguredPages())); 47 | } 48 | return pages; 49 | } 50 | 51 | private List getPages(List pages) { 52 | if (pages.isEmpty()) { 53 | return Collections.emptyList(); 54 | } 55 | return pages.stream() 56 | .collect(Collectors.partitioningBy(page -> hasParent(page, pages))) 57 | .entrySet().stream() 58 | .sorted(Map.Entry.comparingByKey()) 59 | .flatMap(entry -> { 60 | if (entry.getKey()) { // pages with parent 61 | return getPages(entry.getValue()).stream(); 62 | } else { // pages with no parent 63 | return entry.getValue().stream(); 64 | } 65 | }) 66 | .collect(Collectors.toList()); 67 | } 68 | 69 | /** 70 | * @return {@code true} if {@code page} has parent in {@code pages}. 71 | */ 72 | boolean hasParent(Page page, List pages) { 73 | return pages.stream() 74 | .anyMatch(pageFromPages -> pageFromPages != page && page.getParentTitle().equals(pageFromPages.getName())); 75 | } 76 | 77 | public static abstract class Page implements Named { 78 | 79 | private final String name; 80 | private String parentTitle; 81 | private File srcFile; 82 | private List labels = new ArrayList<>(); 83 | 84 | public Page(String name) { 85 | this.name = name; 86 | } 87 | 88 | public String getContent() throws IOException { 89 | return new String(Files.readAllBytes(getSrcFile().toPath()), StandardCharsets.UTF_8); 90 | } 91 | 92 | @NotNull 93 | @Override 94 | public String getName() { 95 | return name; 96 | } 97 | 98 | @NotNull 99 | public String getTitle() { 100 | return getName(); 101 | } 102 | 103 | public boolean isTitleSet() { 104 | return !getTitle().trim().isEmpty(); 105 | } 106 | 107 | public String getParentTitle() { 108 | return parentTitle; 109 | } 110 | 111 | public void setParentTitle(String parentTitle) { 112 | this.parentTitle = parentTitle; 113 | } 114 | 115 | public boolean isParentTitleSet() { 116 | final String parentTitle = getParentTitle(); 117 | return parentTitle != null && !parentTitle.trim().isEmpty(); 118 | } 119 | 120 | public File getSrcFile() { 121 | return srcFile; 122 | } 123 | 124 | public void setSrcFile(File srcFile) { 125 | this.srcFile = srcFile; 126 | } 127 | 128 | public List getLabels() { 129 | return labels; 130 | } 131 | 132 | // for some reason unknown to me, Gradle may pass a list of GString object here, which are not Java Strings 133 | // and then ClassCastException may be thrown. 134 | public void setLabels(List labels) { 135 | if (labels == null) { 136 | throw new IllegalArgumentException("labels cannot be null"); 137 | } 138 | this.labels = labels.stream() 139 | .map(Object::toString) 140 | .collect(Collectors.toList()); 141 | } 142 | 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/gradle/plugin/ConfluenceGradlePlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Aaron Knight 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.qwazer.markdown.confluence.gradle.plugin; 17 | 18 | import org.gradle.api.Plugin; 19 | import org.gradle.api.Project; 20 | 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | @SuppressWarnings("unused") 25 | public class ConfluenceGradlePlugin implements Plugin { 26 | 27 | @Override 28 | public void apply(final Project project) { 29 | ConfluenceExtension extension = project.getExtensions().create("confluence", ConfluenceExtension.class); 30 | extension.getAuthenticationType().convention(AuthenticationType.BASIC); 31 | extension.getAuthenticationTypeString().convention(""); 32 | extension.getParserType().convention("commonmark"); 33 | 34 | final Map options = new HashMap<>(); 35 | options.put("type", ConfluenceGradleTask.class); 36 | 37 | project.task(options, "confluence"); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/main/java/com/github/qwazer/markdown/confluence/gradle/plugin/ConfluenceGradleTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Aaron Knight 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.qwazer.markdown.confluence.gradle.plugin; 17 | 18 | import com.github.qwazer.markdown.confluence.core.OkHttpUtils; 19 | import com.github.qwazer.markdown.confluence.core.Utils; 20 | import com.github.qwazer.markdown.confluence.core.service.AttachmentService; 21 | import com.github.qwazer.markdown.confluence.core.service.ConfluenceService; 22 | import com.github.qwazer.markdown.confluence.core.service.MarkdownService; 23 | import com.github.qwazer.markdown.confluence.core.service.MarkdownServiceCommonmark; 24 | import com.github.qwazer.markdown.confluence.core.service.MarkdownServicePegdown; 25 | import com.github.qwazer.markdown.confluence.core.service.PageService; 26 | import com.github.qwazer.markdown.confluence.core.ssl.SslUtil; 27 | import okhttp3.OkHttpClient; 28 | import org.gradle.api.DefaultTask; 29 | import org.gradle.api.tasks.TaskAction; 30 | 31 | import java.io.IOException; 32 | import java.net.MalformedURLException; 33 | import java.net.URL; 34 | import java.nio.file.Files; 35 | import java.time.Duration; 36 | import java.util.Collections; 37 | import java.util.Map; 38 | import java.util.Objects; 39 | 40 | public class ConfluenceGradleTask extends DefaultTask { 41 | 42 | @TaskAction 43 | public void confluence() throws IOException { 44 | 45 | // obtaining plugins configuration 46 | final ConfluenceExtension extension = 47 | Objects.requireNonNull(getProject().getExtensions().findByType(ConfluenceExtension.class)); 48 | 49 | // validating each and every page configured 50 | extension.getConfiguredPages().forEach(page -> { 51 | // the referenced markdown file must exist 52 | Utils.require(Files.exists(page.getSrcFile().toPath()), "File not found: " + page.getSrcFile()); 53 | // page title and page parentTitle must not be same 54 | Utils.require( 55 | !page.getTitle().equals(page.getParentTitle()), 56 | String.format("Page title cannot be the same as page parent title: \"%s\"", page.getTitle()) 57 | ); 58 | // page title cannot be empty/blank 59 | Utils.require(page.isTitleSet(), "Page title cannot be blank/empty"); 60 | // page parent's title cannot be empty/blank 61 | Utils.require(page.isParentTitleSet(), "Parent's title cannot be blank/empty"); 62 | }); 63 | 64 | final String authenticationTypeString = extension.getAuthenticationTypeString().get(); 65 | final AuthenticationType authenticationType = 66 | !authenticationTypeString.isEmpty() ? 67 | AuthenticationType.valueOf(authenticationTypeString) : 68 | extension.getAuthenticationType().get(); 69 | final String authentication = extension.getAuthentication().get(); 70 | final String authorizationHeader = authenticationType.getAuthorizationHeader(authentication); 71 | final OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder() 72 | .connectTimeout(Duration.ofSeconds(30)) 73 | .readTimeout(Duration.ofSeconds(60)) 74 | .writeTimeout(Duration.ofSeconds(60)) 75 | .addInterceptor(OkHttpUtils.getAuthorizationInterceptor(authorizationHeader)); 76 | 77 | final boolean sslTrustAll = extension.getSslTrustAll().getOrElse(false); 78 | if (sslTrustAll) { 79 | httpClientBuilder 80 | .sslSocketFactory(SslUtil.INSECURE_SSL_CONTEXT.getSocketFactory(), SslUtil.INSECURE_TRUST_MANAGER); 81 | httpClientBuilder.hostnameVerifier((hostname, session) -> true); 82 | } 83 | 84 | final OkHttpClient httpClient = httpClientBuilder.build(); 85 | 86 | final String restApiUrl; 87 | try { 88 | restApiUrl = new URL(extension.getRestApiUrl().get()).toString(); 89 | } catch (MalformedURLException e) { 90 | throw new IllegalArgumentException("Invalid restApiUrl value supplied: " + extension.getRestApiUrl().get()); 91 | } 92 | 93 | final String spaceKey = extension.getSpaceKey().get(); 94 | Utils.require(!spaceKey.trim().isEmpty(), "Confluence space key cannot be blank/empty"); 95 | 96 | // Creating components that implement plugin's logic 97 | final ConfluenceService confluenceService = new ConfluenceService(restApiUrl, spaceKey, httpClient); 98 | 99 | final String parserType = extension.getParserType().get().toLowerCase(); 100 | final MarkdownService markdownService; 101 | switch (parserType) { 102 | case "commonmark": { 103 | markdownService = new MarkdownServiceCommonmark(); 104 | break; 105 | } 106 | case "pegdown": { 107 | markdownService = new MarkdownServicePegdown(); 108 | break; 109 | } 110 | default: throw new RuntimeException("Unknown parser type specified: " + parserType); 111 | } 112 | 113 | final AttachmentService attachmentService = new AttachmentService(confluenceService); 114 | final PageService pageService = 115 | new PageService(confluenceService, attachmentService, markdownService); 116 | final Map pageVariables = 117 | extension.getPageVariables().getOrElse(Collections.emptyMap()); 118 | 119 | // Publishing pages 120 | extension.getPages().forEach(page -> { 121 | try { 122 | pageService.publishWikiPage(page, pageVariables); 123 | } catch (IOException e) { 124 | throw new RuntimeException(e); 125 | } 126 | }); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/test/how-to-test.md: -------------------------------------------------------------------------------- 1 | 1. Run confluence with 2 | 3 | ```bash 4 | docker run -p 8090:8090 -p 8091:8091 qwazer/atlassian-sdk-confluence 5 | ``` 6 | 7 | 2. invoke gradle :publishToMavenLocal before running of IT tests 8 | -------------------------------------------------------------------------------- /src/test/java/com/github/qwazer/markdown/confluence/core/service/AttachmentServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import com.github.qwazer.markdown.confluence.core.ConfluenceException; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.MockitoJUnitRunner; 9 | 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | 13 | import static org.mockito.ArgumentMatchers.anyLong; 14 | import static org.mockito.ArgumentMatchers.anyString; 15 | import static org.mockito.BDDMockito.given; 16 | import static org.mockito.Mockito.verify; 17 | import static org.mockito.Mockito.verifyNoInteractions; 18 | import static org.mockito.Mockito.verifyNoMoreInteractions; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class AttachmentServiceTest { 22 | 23 | @Mock 24 | private ConfluenceService confluenceService; 25 | private AttachmentService attachmentService; 26 | 27 | 28 | @Before 29 | public void before() { 30 | attachmentService = new AttachmentService(confluenceService); 31 | } 32 | 33 | @Test (expected = ConfluenceException.class) 34 | // Given confluenceService.getAttachmentId throws an HttpStatusCodeException 35 | // When postAttachmentToPage called 36 | // Then a ConfluenceException is thrown 37 | public void testGetAttachmentThrows() { 38 | given(confluenceService.getAttachmentId(anyLong(), anyString())) 39 | .willThrow(new ConfluenceException("Controlled exception")); 40 | 41 | final Long expectedPageId = 1L; 42 | final Path expectedPath = Paths.get("/a/path/file.png"); 43 | 44 | attachmentService.postAttachmentToPage(expectedPageId, expectedPath); 45 | 46 | verifyNoInteractions(confluenceService); 47 | } 48 | 49 | @Test 50 | // Given attachment does not exist 51 | // When postAttachmentToPage called 52 | // Then confluenceService.createAttachment is called 53 | public void testAttachmentDoesNotExist() { 54 | given(confluenceService.getAttachmentId(anyLong(), anyString())).willReturn(null); 55 | 56 | final Long expectedPageId = 1L; 57 | final Path expectedPath = Paths.get("/a/path/file.png"); 58 | 59 | attachmentService.postAttachmentToPage(expectedPageId, expectedPath); 60 | 61 | verify(confluenceService).getAttachmentId(expectedPageId, expectedPath.getFileName().toString()); 62 | verify(confluenceService).createAttachment(expectedPageId, expectedPath.toString()); 63 | verifyNoMoreInteractions(confluenceService); 64 | } 65 | 66 | @Test 67 | // Given attachment does exist 68 | // When postAttachmentToPage called 69 | // Then confluenceService.updateAttachment is called 70 | public void testAttachmentDoesExist() { 71 | final String attachmentId = "123"; 72 | 73 | given(confluenceService.getAttachmentId(anyLong(), anyString())).willReturn(attachmentId); 74 | 75 | final Long expectedPageId = 1L; 76 | final Path expectedPath = Paths.get("/a/path/file.png"); 77 | 78 | attachmentService.postAttachmentToPage(expectedPageId, expectedPath); 79 | 80 | verify(confluenceService).getAttachmentId(expectedPageId, expectedPath.getFileName().toString()); 81 | verify(confluenceService).updateAttachment(expectedPageId, attachmentId, expectedPath.toString()); 82 | verifyNoMoreInteractions(confluenceService); 83 | } 84 | } -------------------------------------------------------------------------------- /src/test/java/com/github/qwazer/markdown/confluence/core/service/ConfluenceServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import com.github.qwazer.markdown.confluence.core.OkHttpUtils; 4 | import com.github.qwazer.markdown.confluence.gradle.plugin.AuthenticationType; 5 | import okhttp3.HttpUrl; 6 | import okhttp3.OkHttpClient; 7 | import okhttp3.mockwebserver.Dispatcher; 8 | import okhttp3.mockwebserver.MockResponse; 9 | import okhttp3.mockwebserver.MockWebServer; 10 | import okhttp3.mockwebserver.RecordedRequest; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.junit.After; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | 16 | import java.net.URL; 17 | import java.nio.file.Path; 18 | import java.nio.file.Paths; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | import static org.junit.Assert.assertNotNull; 23 | import static org.junit.Assert.assertNull; 24 | import static org.junit.Assert.assertTrue; 25 | 26 | public class ConfluenceServiceTest { 27 | 28 | private MockWebServer webServer; 29 | private ConfluenceService confluenceService; 30 | 31 | @Before 32 | public void before() throws Exception { 33 | webServer = new MockWebServer(); 34 | webServer.start(); 35 | final String restApiUrl = 36 | String.format("http://localhost:%d/rest/api/", webServer.getPort()); 37 | final String authorization = 38 | AuthenticationType.PAT.getAuthorizationHeader("token"); 39 | final OkHttpClient httpClient = new OkHttpClient.Builder() 40 | .addInterceptor(OkHttpUtils.getAuthorizationInterceptor(authorization)) 41 | .build(); 42 | confluenceService = new ConfluenceService(restApiUrl, "SN", httpClient); 43 | } 44 | 45 | @After 46 | public void after() throws Exception { 47 | webServer.shutdown(); 48 | } 49 | 50 | // Given a call to confluence returns no attachment 51 | // When getAttachmentId called 52 | // Then the returned attachmentId is correct 53 | @Test 54 | public void testGetAttachmentId_attachment_does_not_exist() throws InterruptedException { 55 | 56 | final Long pageId = 1L; 57 | final String attachmentFilename = "file.png"; 58 | // the /rest/api part of the path comes from the base URI configured for testing 59 | final String happyPath = 60 | String.format("/rest/api/content/%d/child/attachment?filename=%s", pageId, attachmentFilename); 61 | 62 | webServer.setDispatcher(getDispatcher(happyPath, "{ \"results\": [] }")); 63 | 64 | // when 65 | final String actualAttachmentId = confluenceService.getAttachmentId(pageId, attachmentFilename); 66 | 67 | // then 68 | final RecordedRequest recordedRequest = webServer.takeRequest(3, TimeUnit.SECONDS); 69 | assertNotNull(recordedRequest); 70 | final HttpUrl httpUrl = recordedRequest.getRequestUrl(); 71 | assertNotNull(httpUrl); 72 | assertEquals(attachmentFilename, httpUrl.queryParameter("filename")); 73 | 74 | assertNull(actualAttachmentId); 75 | } 76 | 77 | // Given a call to confluence returns valid json with attachment 78 | // When getAttachmentId called 79 | // Then the returned attachmentId is correct 80 | @Test 81 | public void testGetAttachmentId_attachment_exists() throws InterruptedException { 82 | 83 | final Long pageId = 1L; 84 | final String attachmentFilename = "file.png"; 85 | final String expectedAttachmentId = "1234"; 86 | // the /rest/api part of the path comes from the base URI configured for testing 87 | final String expectedPath = 88 | String.format("/rest/api/content/%d/child/attachment?filename=%s", pageId, attachmentFilename); 89 | final String expectedBody = 90 | String.format("{ \"results\": [ { \"id\": \"%s\" } ] }", expectedAttachmentId); 91 | 92 | webServer.setDispatcher(getDispatcher(expectedPath, expectedBody)); 93 | 94 | final String actualAttachmentId = confluenceService.getAttachmentId(pageId, attachmentFilename); 95 | 96 | // then 97 | final RecordedRequest recordedRequest = webServer.takeRequest(3, TimeUnit.SECONDS); 98 | assertNotNull(recordedRequest); 99 | final HttpUrl httpUrl = recordedRequest.getRequestUrl(); 100 | assertNotNull(httpUrl); 101 | assertEquals(attachmentFilename, httpUrl.queryParameter("filename")); 102 | 103 | assertEquals(expectedAttachmentId, actualAttachmentId); 104 | } 105 | 106 | // Given a call to confluence returns with no error 107 | // When createAttachment called 108 | // Then the correct call to confluence is made 109 | @Test 110 | public void testCreateAttachment() throws Exception { 111 | 112 | final String happyPath = "/rest/api/content/1/child/attachment"; 113 | webServer.setDispatcher(getDispatcher(happyPath, "{}")); 114 | 115 | // when 116 | final Long pageId = 1L; 117 | 118 | final URL attachmentUrl = getClass().getResource("/test.md"); 119 | assertNotNull(attachmentUrl); 120 | final Path attachmentFilePath = Paths.get(attachmentUrl.toURI()); 121 | 122 | confluenceService.createAttachment(pageId, attachmentFilePath.toString()); 123 | 124 | // then 125 | final RecordedRequest recordedRequest = webServer.takeRequest(3, TimeUnit.SECONDS); 126 | assertNotNull(recordedRequest); 127 | final HttpUrl httpUrl = recordedRequest.getRequestUrl(); 128 | assertNotNull(httpUrl); 129 | 130 | // this is due to the provided ConfluenceConfig 131 | assertEquals("Bearer token", recordedRequest.getHeader("Authorization")); 132 | assertEquals("application/json", recordedRequest.getHeader("Accept")); 133 | final String contentType = recordedRequest.getHeader("Content-Type"); 134 | assertNotNull(contentType); 135 | assertTrue(contentType.startsWith("multipart/form-data; boundary=")); 136 | 137 | } 138 | 139 | @Test 140 | // Given a call to confluence returns with no error 141 | // When updateAttachment called 142 | // Then the correct call to confluence is made 143 | public void testUpdateAttachment() throws Exception { 144 | 145 | final String happyPath = "/rest/api/content/1/child/attachment/4321/data"; 146 | webServer.setDispatcher(getDispatcher(happyPath, "{}")); 147 | 148 | // when 149 | final Long pageId = 1L; 150 | final String attachmentId = "4321"; 151 | final URL attachmentUrl = getClass().getResource("/test.md"); 152 | assertNotNull(attachmentUrl); 153 | final Path attachmentFilePath = Paths.get(attachmentUrl.toURI()); 154 | 155 | confluenceService.updateAttachment(pageId, attachmentId, attachmentFilePath.toString()); 156 | 157 | // then 158 | final RecordedRequest recordedRequest = webServer.takeRequest(3, TimeUnit.SECONDS); 159 | assertNotNull(recordedRequest); 160 | final HttpUrl httpUrl = recordedRequest.getRequestUrl(); 161 | assertNotNull(httpUrl); 162 | 163 | // this is due to the provided ConfluenceConfig 164 | assertEquals("Bearer token", recordedRequest.getHeader("Authorization")); 165 | assertEquals("application/json", recordedRequest.getHeader("Accept")); 166 | final String contentType = recordedRequest.getHeader("Content-Type"); 167 | assertNotNull(contentType); 168 | assertTrue(contentType.startsWith("multipart/form-data; boundary=")); 169 | 170 | } 171 | 172 | /** 173 | * @param happyPath - request path for which to respond with HTTP 200 - OK and {@code happyBody} 174 | * @param happyBody - the body of the HTTP 200 - OK response. 175 | * @return A {@link Dispatcher} that responds with HTTP 200 - OK when the request is made to the {@code happyPath}. 176 | * The payload of the returned response is {@code happyBody}. If the request path does not match {@code happyPath} 177 | * then HTTP 404 response with no payload is returned. 178 | */ 179 | private Dispatcher getDispatcher(@NotNull final String happyPath, @NotNull final String happyBody) { 180 | return new Dispatcher() { 181 | @NotNull 182 | @Override 183 | public MockResponse dispatch(@NotNull RecordedRequest recordedRequest) { 184 | if (recordedRequest.getHeader("Authorization") == null) { 185 | return new MockResponse().setResponseCode(401).setBody("UNAUTHORIZED"); 186 | } 187 | if (happyPath.equals(recordedRequest.getPath())) { 188 | return new MockResponse().setResponseCode(200).setBody(happyBody); 189 | } 190 | return new MockResponse().setResponseCode(404); 191 | } 192 | }; 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /src/test/java/com/github/qwazer/markdown/confluence/core/service/MarkdownServiceCommonmarkTest.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertFalse; 10 | import static org.junit.Assert.assertTrue; 11 | 12 | public class MarkdownServiceCommonmarkTest { 13 | 14 | private final MarkdownService markdownService = new MarkdownServiceCommonmark(); 15 | 16 | @Test 17 | public void testReplaceProperties() { 18 | 19 | final String content = "# ${project.name}\n" + 20 | "Gradle plugin to publish markdown pages to confluence ```java\ncode;\n``` _italic_"; 21 | final String pageVarName = "project.name"; 22 | final String pageVarValue = "HELLO_PROJECT"; 23 | final Map pageVariables = new HashMap() {{ 24 | put(pageVarName, pageVarValue); 25 | }}; 26 | final String wikiText = markdownService.parseMarkdown(content, pageVariables); 27 | 28 | assertFalse(wikiText.contains("${" + pageVarName + "}")); 29 | assertTrue(wikiText.contains(pageVarValue)); 30 | } 31 | 32 | @Test 33 | public void testNotReplacePropertiesInCode() { 34 | 35 | final String markdown = "# ${project.name}\n" + 36 | "Gradle plugin to publish markdown pages to confluence ```java\n${java.code};\n``` _italic_"; 37 | final String pageVarName = "java.code"; 38 | final String pageVarValue = "String s = new String()"; 39 | final Map pageVariables = new HashMap() {{ 40 | put(pageVarName, pageVarValue); 41 | }}; 42 | final String wikiText = markdownService.parseMarkdown(markdown, pageVariables); 43 | 44 | assertFalse(wikiText.contains("${" + pageVarName + "}")); 45 | assertTrue(wikiText.contains(pageVarValue)); 46 | } 47 | 48 | @Test 49 | public void testMarkdownLinkReplace() { 50 | 51 | final String markdown = "[скачать](${url})"; 52 | final String pageVarName = "url"; 53 | final String pageVarValue = "https://localhost"; 54 | final Map pageVariables = new HashMap() {{ 55 | put(pageVarName, pageVarValue); 56 | }}; 57 | final String wikiText = markdownService.parseMarkdown(markdown, pageVariables); 58 | 59 | assertFalse(wikiText.contains("${" + pageVarName + "}")); 60 | assertTrue(wikiText.contains(pageVarValue)); 61 | } 62 | 63 | @Test 64 | public void imageLinkShouldNotContainPageTitleVariableAfterMarkdownToConfluenceWikiConversion() { 65 | final String markdown = "![Image Description](images/image.png)"; 66 | final String wikiText = markdownService.parseMarkdown(markdown); 67 | 68 | assertFalse(wikiText.contains("${page.title}")); 69 | assertEquals("!image.png|Image Description!", wikiText.trim()); 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/test/java/com/github/qwazer/markdown/confluence/core/service/MarkdownServicePegdownTest.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertFalse; 10 | import static org.junit.Assert.assertTrue; 11 | 12 | /** 13 | * Created by Anton Reshetnikov on 15 Nov 2016. 14 | */ 15 | public class MarkdownServicePegdownTest { 16 | 17 | private final MarkdownService markdownService = new MarkdownServicePegdown(); 18 | 19 | @Test 20 | public void testReplaceProperties() { 21 | 22 | final String content = "# ${project.name}\n" + 23 | "Gradle plugin to publish markdown pages to confluence ```java\ncode;\n``` _italic_"; 24 | final String pageVarName = "project.name"; 25 | final String pageVarValue = "HELLO_PROJECT"; 26 | final Map pageVariables = new HashMap() {{ 27 | put(pageVarName, pageVarValue); 28 | }}; 29 | final String wikiText = markdownService.parseMarkdown(content, pageVariables); 30 | 31 | assertFalse(wikiText.contains("${" + pageVarName + "}")); 32 | assertTrue(wikiText.contains(pageVarValue)); 33 | } 34 | 35 | @Test 36 | public void testNotReplacePropertiesInCode() { 37 | 38 | final String markdown = "# ${project.name}\n" + 39 | "Gradle plugin to publish markdown pages to confluence ```java\n${java.code};\n``` _italic_"; 40 | final String pageVarName = "java.code"; 41 | final String pageVarValue = "String s = new String()"; 42 | final Map pageVariables = new HashMap() {{ 43 | put(pageVarName, pageVarValue); 44 | }}; 45 | final String wikiText = markdownService.parseMarkdown(markdown, pageVariables); 46 | 47 | assertFalse(wikiText.contains("${" + pageVarName + "}")); 48 | assertTrue(wikiText.contains(pageVarValue)); 49 | } 50 | 51 | @Test 52 | public void testMarkdownLinkReplace() { 53 | 54 | final String markdown = "[скачать](${url})"; 55 | final String pageVarName = "url"; 56 | final String pageVarValue = "https://localhost"; 57 | final Map pageVariables = new HashMap() {{ 58 | put(pageVarName, pageVarValue); 59 | }}; 60 | final String wikiText = markdownService.parseMarkdown(markdown, pageVariables); 61 | 62 | assertFalse(wikiText.contains("${" + pageVarName + "}")); 63 | assertTrue(wikiText.contains(pageVarValue)); 64 | } 65 | 66 | @Test 67 | public void imageLinkShouldNotContainPageTitleVariableAfterMarkdownToConfluenceWikiConversion() { 68 | final String markdown = "![Image Description](images/image.png)"; 69 | final String wikiText = markdownService.parseMarkdown(markdown); 70 | 71 | assertFalse(wikiText.contains("${page.title}")); 72 | assertEquals("!image.png|Image Description!", wikiText.trim()); 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /src/test/java/com/github/qwazer/markdown/confluence/core/service/PageServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.core.service; 2 | 3 | import com.github.qwazer.markdown.confluence.core.ConfluenceException; 4 | import com.github.qwazer.markdown.confluence.gradle.plugin.ConfluenceExtension; 5 | import kotlin.Pair; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | import org.mockito.Mockito; 9 | 10 | import java.io.File; 11 | import java.net.URL; 12 | import java.nio.file.Path; 13 | import java.util.List; 14 | 15 | public class PageServiceTest { 16 | 17 | @Test 18 | public void markdownReferringToExistingLocalImageShouldBeCorrectlyConverted() throws Exception { 19 | 20 | ConfluenceService confluenceService = Mockito.mock(ConfluenceService.class); 21 | PageService pageService = new PageService(confluenceService); 22 | ConfluenceExtension.Page page = Mockito.mock(ConfluenceExtension.Page.class); 23 | Mockito.when(page.getName()).thenReturn("Test Page"); 24 | Mockito.when(page.getTitle()).thenReturn("Test Page"); 25 | Mockito.when(page.getParentTitle()).thenReturn("Parent Page"); 26 | URL testResource = getClass().getResource("PageServiceTest1.md"); 27 | Assert.assertNotNull(testResource); 28 | File markdownFile = new File(testResource.toURI()); 29 | Mockito.when(page.getSrcFile()).thenReturn(markdownFile); 30 | Mockito.when(page.getContent()).thenCallRealMethod(); 31 | 32 | Pair> result = pageService.prepareWikiText(page); 33 | String confluenceWiki = result.getFirst(); 34 | List attachments = result.getSecond(); 35 | Assert.assertEquals("!image.png|Image Description!", confluenceWiki.trim()); 36 | Assert.assertEquals("image.png", attachments.get(0).getFileName().toString()); 37 | 38 | } 39 | 40 | @Test(expected = ConfluenceException.class) 41 | public void markdownReferringToNonExistentLocalImageShouldResultInException() throws Exception { 42 | 43 | ConfluenceService confluenceService = Mockito.mock(ConfluenceService.class); 44 | PageService pageService = new PageService(confluenceService); 45 | ConfluenceExtension.Page page = Mockito.mock(ConfluenceExtension.Page.class); 46 | Mockito.when(page.getName()).thenReturn("Test Page"); 47 | Mockito.when(page.getTitle()).thenReturn("Test Page"); 48 | Mockito.when(page.getParentTitle()).thenReturn("Parent Page"); 49 | URL testResource = getClass().getResource("PageServiceTest2.md"); 50 | Assert.assertNotNull(testResource); 51 | File markdownFile = new File(testResource.toURI()); 52 | Mockito.when(page.getSrcFile()).thenReturn(markdownFile); 53 | Mockito.when(page.getContent()).thenCallRealMethod(); 54 | 55 | pageService.prepareWikiText(page); 56 | 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/test/java/com/github/qwazer/markdown/confluence/gradle/plugin/ConfluenceExtensionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.qwazer.markdown.confluence.gradle.plugin; 2 | 3 | import org.gradle.api.NamedDomainObjectContainer; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.ArgumentMatchers; 7 | import org.mockito.Mock; 8 | import org.mockito.Mockito; 9 | import org.mockito.junit.MockitoJUnitRunner; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | import static org.junit.Assert.assertFalse; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class ConfluenceExtensionTest { 19 | 20 | @Mock 21 | private NamedDomainObjectContainer configuredPages; 22 | 23 | @Test 24 | public void testGetPagesOrder() { 25 | 26 | final ConfluenceExtension.Page page1 = Mockito.mock(); 27 | Mockito.when(page1.getName()).thenReturn("Page1"); 28 | Mockito.when(page1.getTitle()).thenCallRealMethod(); 29 | Mockito.when(page1.getParentTitle()).thenReturn("Home"); 30 | 31 | final ConfluenceExtension.Page page2 = Mockito.mock(); 32 | Mockito.when(page2.getName()).thenReturn("Page2"); 33 | Mockito.when(page2.getTitle()).thenCallRealMethod(); 34 | Mockito.when(page2.getParentTitle()).thenReturn("Page1"); 35 | 36 | final ConfluenceExtension.Page page3 = Mockito.mock(); 37 | Mockito.when(page3.getName()).thenReturn("Page3"); 38 | Mockito.when(page3.getTitle()).thenCallRealMethod(); 39 | Mockito.when(page3.getParentTitle()).thenReturn("Home"); 40 | 41 | Mockito.when(configuredPages.toArray()).thenReturn(new Object[]{page1, page2, page3}); 42 | 43 | final ConfluenceExtension extension = Mockito.mock(); 44 | Mockito.when(extension.getConfiguredPages()).thenReturn(configuredPages); 45 | Mockito.when(extension.getPages()).thenCallRealMethod(); 46 | Mockito.when(extension.hasParent(ArgumentMatchers.any(), ArgumentMatchers.anyList())).thenCallRealMethod(); 47 | 48 | final List pages = extension.getPages(); 49 | assertEquals(3, pages.size()); 50 | assertEquals("Page1", pages.get(0).getTitle()); 51 | assertEquals("Page3", pages.get(1).getTitle()); 52 | assertEquals("Page2", pages.get(2).getTitle()); 53 | } 54 | 55 | @Test 56 | public void testHasParentPageTitleAndParentTitleAreTheSame() { 57 | 58 | final ConfluenceExtension.Page page = Mockito.mock(); 59 | final ConfluenceExtension extension = Mockito.mock(); 60 | Mockito.when(extension.hasParent(ArgumentMatchers.any(), ArgumentMatchers.anyList())) 61 | .thenCallRealMethod(); 62 | 63 | final List pages = new ArrayList<>(); 64 | pages.add(page); 65 | assertFalse(extension.hasParent(page, pages)); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/test/resources/com/github/qwazer/markdown/confluence/core/service/PageServiceTest1.md: -------------------------------------------------------------------------------- 1 | ![Image Description](images/image.png) -------------------------------------------------------------------------------- /src/test/resources/com/github/qwazer/markdown/confluence/core/service/PageServiceTest2.md: -------------------------------------------------------------------------------- 1 | ![Image Description](images/unknown.png) -------------------------------------------------------------------------------- /src/test/resources/com/github/qwazer/markdown/confluence/core/service/images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwazer/markdown-confluence-gradle-plugin/1d7e2067449405adc1003cf3a8a719da7365a605/src/test/resources/com/github/qwazer/markdown/confluence/core/service/images/image.png -------------------------------------------------------------------------------- /src/test/resources/test.md: -------------------------------------------------------------------------------- 1 | This is a sample markdown file for the testing purposes. 2 | 3 | 1. A numbered list. 4 | 2. Hello, world! --------------------------------------------------------------------------------