├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .gitignore ├── src ├── test │ ├── resources │ │ └── logback-test.xml │ └── java │ │ └── com │ │ └── rabbitmq │ │ └── actions │ │ ├── UtilsTest.java │ │ ├── JsonTest.java │ │ ├── HttpTest.java │ │ └── PackagecloudDeletePackageActionTest.java └── main │ └── java │ └── com │ └── rabbitmq │ └── actions │ ├── LogUtils.java │ ├── Domain.java │ ├── Utils.java │ ├── PackagecloudLogic.java │ ├── PackagecloudDeletePackageAction.java │ └── ComparableVersion.java ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── Makefile ├── README.md ├── Dockerfile ├── pom.xml ├── mvnw.cmd ├── mvnw └── LICENSE /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabbitmq/delete-packagecloud-package-action/main/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .sw? 3 | .*.sw? 4 | *.beam 5 | *.class 6 | *.dat 7 | *.dump 8 | *.iml 9 | *.ipr 10 | *.iws 11 | .DS_Store 12 | \#~ 13 | /.idea/ 14 | /deps/ 15 | /target/ 16 | /.classpath 17 | /.project 18 | /.settings 19 | 20 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | version: 2 4 | updates: 5 | - package-ecosystem: "maven" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | open-pull-requests-limit: 20 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up JDK 21 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '21' 20 | distribution: 'zulu' 21 | cache: maven 22 | - name: Test 23 | run: ./mvnw test 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash# we want bash behaviour in all shell invocations 2 | 3 | .DEFAULT_GOAL = package 4 | 5 | ### VARIABLES ### 6 | # 7 | 8 | ### TARGETS ### 9 | # 10 | 11 | .PHONY: package 12 | package: clean ## Build the binary distribution 13 | ./mvnw package -Dmaven.test.skip 14 | 15 | .PHONY: docker-image 16 | docker-image: ## Build Docker image 17 | @docker build --tag pivotalrabbitmq/delete-packagecloud-package-action:latest . 18 | 19 | .PHONY: push-docker-image 20 | push-docker-image: docker-image ## Push Docker image 21 | @docker push pivotalrabbitmq/delete-packagecloud-package-action:latest 22 | 23 | .PHONY: clean 24 | clean: ## Clean all build artefacts 25 | ./mvnw clean 26 | 27 | .PHONY: compile 28 | compile: ## Compile the source code 29 | ./mvnw compile 30 | 31 | .PHONY: test 32 | test: ## Run tests 33 | ./mvnw test -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/4.0.0-alpha-9/apache-maven-4.0.0-alpha-9-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /src/main/java/com/rabbitmq/actions/LogUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | abstract class LogUtils { 9 | 10 | private LogUtils() {} 11 | 12 | static void logGreen(String message) { 13 | log(green(message)); 14 | } 15 | 16 | static String green(String message) { 17 | return "\u001B[32m" + message + "\u001B[0m"; 18 | } 19 | 20 | static void logYellow(String message) { 21 | log(yellow(message)); 22 | } 23 | 24 | static String yellow(String message) { 25 | return "\u001B[33m" + message + "\u001B[0m"; 26 | } 27 | 28 | static void logRed(String message) { 29 | log(red(message)); 30 | } 31 | 32 | static String red(String message) { 33 | return "\u001B[31m" + message + "\u001B[0m"; 34 | } 35 | 36 | static void log(String message, Object... args) { 37 | System.out.printf((message) + "%n", args); 38 | } 39 | 40 | static void newLine() { 41 | log(""); 42 | } 43 | 44 | static void logIndent(String message) { 45 | log(indent(message)); 46 | } 47 | 48 | static String indent(String message) { 49 | return " " + message; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/rabbitmq/actions/UtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | import static com.rabbitmq.actions.Utils.globPredicate; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | public class UtilsTest { 14 | 15 | @Test 16 | void globsPredicateShouldFilterFiles() { 17 | assertThat(globPredicate("*.rpm,*.dat").test(p("erlang-20.3.8.25-1.el7.x86_64.rpm"))).isTrue(); 18 | assertThat(globPredicate("*.rpm,*.dat").test(p("erlang-20.3.8.25-1.el7.x86_64.txt"))).isFalse(); 19 | assertThat(globPredicate("*.rpm,*.dat").test(p("erlang-20.3.8.25-1.el7.x86_64.dat"))).isTrue(); 20 | assertThat(globPredicate("*.rpm,*.dat").test(p("erlang-20.3.8.25-1.el7.x86_64.bar"))).isFalse(); 21 | 22 | assertThat(globPredicate("erlang-*el7*.rpm").test(p("erlang-20.3.8.25-1.el7.x86_64.rpm"))) 23 | .isTrue(); 24 | assertThat(globPredicate("erlang-*20.*el7*.rpm").test(p("erlang-20.3.8.25-1.el7.x86_64.rpm"))) 25 | .isTrue(); 26 | assertThat(globPredicate("erlang-*21.*el7*.rpm").test(p("erlang-20.3.8.25-1.el7.x86_64.rpm"))) 27 | .isFalse(); 28 | assertThat(globPredicate("erlang-*el7*.rpm").test(p("erlang-20.3.8.25-1.el8.x86_64.rpm"))) 29 | .isFalse(); 30 | } 31 | 32 | private static Domain.Package p(String name) { 33 | Domain.Package p = new Domain.Package(); 34 | p.setFilename(name); 35 | return p; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is a GitHub Action to delete packages on [Packagecloud](https://packagecloud.io/). 4 | The action deletes older packages and lets a given number of packages left, based on version or creation time. 5 | 6 | # Usage 7 | 8 | ## Configuration 9 | 10 | * `username`: *Required.* The username for the Packagecloud account. 11 | * `repository`: *Required.* The repository name. 12 | * `token`: *Required.* Token used for all requests. 13 | * `type`: *Optional*. Type of the packages in the repository (`deb`, `rpm`). 14 | * `globs`: *Optional.* Comma-separated list of globs for files that will be uploaded/downloaded. 15 | * `do_delete`: *Optional*. Flag to enforce deletion. 16 | Allows to preview deleted packages with dry runs. 17 | Default is `false` (do not delete). 18 | * `version_filter`: *Optional*. Java regular expression to select the packages. 19 | * `keep_last_n`: *Optional*. Number of versions to keep. 20 | Default is 0. 21 | * `keep_last_minor_patches`: *Optional*. 22 | Do not delete last patch versions of identified minors. 23 | Default is `false`. 24 | * `order_by`: *Optional. One of [version, time]*. 25 | Whether to sort packages by version (the default) or by time. 26 | 27 | ## Example 28 | 29 | ```yaml 30 | - name: Delete old packages 31 | uses: docker://pivotalrabbitmq/delete-packagecloud-package-action:latest 32 | with: 33 | username: ${{ secrets.PACKAGECLOUD_USERNAME }} 34 | repository: erlang 35 | token: ${{ secrets.PACKAGECLOUD_TOKEN }} 36 | globs: erlang-* 37 | version_filter: '^25\.*' 38 | keep_last_n: 2 39 | keep_last_minor_patches: true 40 | do_delete: true 41 | ``` 42 | 43 | # License and Copyright 44 | 45 | (c) 2023 Broadcom. All Rights Reserved. 46 | The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. 47 | 48 | This package, the Packagecloud Delete Package GitHub Action, is licensed under the Mozilla Public License 2.0 ("MPL"). 49 | 50 | See [LICENSE](./LICENSE). 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 as builder 2 | 3 | RUN set -eux; \ 4 | apt-get update; \ 5 | apt-get -y upgrade; \ 6 | apt-get install --yes --no-install-recommends \ 7 | ca-certificates \ 8 | wget \ 9 | jq 10 | 11 | ARG JAVA_VERSION="21" 12 | 13 | RUN ARCH="x86"; BUNDLE="jdk"; \ 14 | wget "https://api.azul.com/zulu/download/community/v1.0/bundles/latest/?java_version=$JAVA_VERSION&ext=tar.gz&os=linux&arch=$ARCH&hw_bitness=64&release_status=ga&bundle_type=$BUNDLE" -O jdk-info.json 15 | 16 | RUN wget --progress=bar:force:noscroll -O "jdk.tar.gz" $(cat jdk-info.json | jq --raw-output .url) 17 | RUN echo "$(cat jdk-info.json | jq --raw-output .sha256_hash) *jdk.tar.gz" | sha256sum --check --strict - 18 | 19 | ENV JAVA_HOME="/usr/lib/jdk-$JAVA_VERSION" 20 | 21 | RUN set -eux; \ 22 | mkdir $JAVA_HOME && \ 23 | tar --extract --file jdk.tar.gz --directory "$JAVA_HOME" --strip-components 1; \ 24 | $JAVA_HOME/bin/jlink --compress=zip-6 --output /jre --add-modules java.base,jdk.crypto.cryptoki,java.net.http; \ 25 | /jre/bin/java -version \ 26 | ; \ 27 | mkdir -p /app 28 | 29 | RUN mkdir -p /project/src /project/.mvn 30 | COPY src /project/src 31 | COPY .mvn /project/.mvn 32 | COPY pom.xml /project 33 | COPY mvnw /project 34 | 35 | WORKDIR /project 36 | RUN set -eux; \ 37 | ./mvnw package -Dmaven.test.skip --no-transfer-progress 38 | 39 | FROM ubuntu:22.04 40 | 41 | RUN set -eux; \ 42 | \ 43 | apt-get update; \ 44 | apt-get -y upgrade; \ 45 | apt-get install --yes --no-install-recommends \ 46 | ca-certificates \ 47 | ; \ 48 | rm -rf /var/lib/apt/lists/* 49 | 50 | ENV LANG en_US.UTF-8 51 | ENV LANGUAGE en_US:en 52 | 53 | ENV JAVA_HOME=/usr/lib/jvm/java-21-openjdk/jre 54 | RUN mkdir -p $JAVA_HOME 55 | COPY --from=builder /jre $JAVA_HOME/ 56 | RUN ln -svT $JAVA_HOME/bin/java /usr/local/bin/java 57 | 58 | RUN mkdir -p /app 59 | 60 | COPY --from=builder /project/target/delete-packagecloud-package-action.jar /app 61 | RUN set -eux; \ 62 | java -jar /app/delete-packagecloud-package-action.jar test 63 | 64 | RUN groupadd --gid 1000 github 65 | RUN useradd --uid 1000 --gid github --comment "github user" github 66 | 67 | USER github:github 68 | 69 | ENTRYPOINT ["java", "-jar", "/app/delete-packagecloud-package-action.jar"] 70 | -------------------------------------------------------------------------------- /src/main/java/com/rabbitmq/actions/Domain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | import java.time.ZonedDateTime; 9 | import java.util.Objects; 10 | 11 | abstract class Domain { 12 | 13 | private Domain() {} 14 | 15 | static class Package { 16 | 17 | private String name; 18 | private ZonedDateTime created_at; 19 | private String destroy_url; 20 | private String package_url; 21 | private String filename; 22 | private String version; 23 | 24 | Package() {} 25 | 26 | public void setFilename(String filename) { 27 | this.filename = filename; 28 | } 29 | 30 | public void setVersion(String version) { 31 | this.version = version; 32 | } 33 | 34 | String filename() { 35 | return this.filename; 36 | } 37 | 38 | String version() { 39 | return this.version; 40 | } 41 | 42 | public String destroy_url() { 43 | return destroy_url; 44 | } 45 | 46 | public ZonedDateTime created_at() { 47 | return created_at; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "Package{" 53 | + "name='" 54 | + name 55 | + '\'' 56 | + ", filename='" 57 | + filename 58 | + '\'' 59 | + ", version='" 60 | + version 61 | + '\'' 62 | + ", created_at='" 63 | + created_at 64 | + '\'' 65 | + ", destroy_url='" 66 | + destroy_url 67 | + '\'' 68 | + '}'; 69 | } 70 | 71 | @Override 72 | public boolean equals(Object o) { 73 | if (this == o) return true; 74 | if (o == null || getClass() != o.getClass()) return false; 75 | Package aPackage = (Package) o; 76 | return Objects.equals(package_url, aPackage.package_url); 77 | } 78 | 79 | @Override 80 | public int hashCode() { 81 | return Objects.hash(package_url); 82 | } 83 | } 84 | 85 | static class PackageVersion { 86 | 87 | final String version; 88 | ZonedDateTime lastPackageDate; 89 | 90 | PackageVersion(String version) { 91 | this.version = version; 92 | } 93 | 94 | void consider(Domain.Package p) { 95 | if (lastPackageDate == null) { 96 | lastPackageDate = p.created_at(); 97 | } else { 98 | lastPackageDate = 99 | lastPackageDate.isBefore(p.created_at()) ? p.created_at() : lastPackageDate; 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/rabbitmq/actions/JsonTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | import java.time.ZonedDateTime; 11 | import java.time.format.DateTimeFormatter; 12 | import org.junit.jupiter.api.Test; 13 | 14 | public class JsonTest { 15 | 16 | private static final String JSON = 17 | "{\n" 18 | + " \"name\":\"erlang\",\n" 19 | + " \"distro_version\":\"el/7\",\n" 20 | + " \"created_at\":\"2020-01-24T05:35:34.000Z\",\n" 21 | + " \"version\":\"20.3.8.25\",\n" 22 | + " \"release\":\"1.el7\",\n" 23 | + " \"epoch\":0,\n" 24 | + " \"scope\":null,\n" 25 | + " \"private\":false,\n" 26 | + " \"type\":\"rpm\",\n" 27 | + " \"filename\":\"erlang-20.3.8.25-1.el7.x86_64.rpm\",\n" 28 | + " \"uploader_name\":\"rabbitmq\",\n" 29 | + " \"indexed\":true,\n" 30 | + " \"sha256sum\":\"09c2b01168517a7992aab81c6eeaad4be08396b97315b41866f60832ff165de4\",\n" 31 | + " \"repository_html_url\":\"/rabbitmq/erlang\",\n" 32 | + " \"package_url\":\"/api/v1/repos/rabbitmq/erlang/package/rpm/el/7/erlang/x86_64/20.3.8.25/1.el7.json\",\n" 33 | + " \"downloads_detail_url\":\"/api/v1/repos/rabbitmq/erlang/package/rpm/el/7/erlang/x86_64/20.3.8.25/1.el7/stats/downloads/detail.json\",\n" 34 | + " \"downloads_series_url\":\"/api/v1/repos/rabbitmq/erlang/package/rpm/el/7/erlang/x86_64/20.3.8.25/1.el7/stats/downloads/series/daily.json\",\n" 35 | + " \"downloads_count_url\":\"/api/v1/repos/rabbitmq/erlang/package/rpm/el/7/erlang/x86_64/20.3.8.25/1.el7/stats/downloads/count.json\",\n" 36 | + " \"package_html_url\":\"/rabbitmq/erlang/packages/el/7/erlang-20.3.8.25-1.el7.x86_64.rpm\",\n" 37 | + " \"download_url\":\"https://packagecloud.io/rabbitmq/erlang/packages/el/7/erlang-20.3.8.25-1.el7.x86_64.rpm/download.rpm?distro_version_id=140\",\n" 38 | + " \"promote_url\":\"/api/v1/repos/rabbitmq/erlang/el/7/erlang-20.3.8.25-1.el7.x86_64.rpm/promote.json\",\n" 39 | + " \"destroy_url\":\"/api/v1/repos/rabbitmq/erlang/el/7/erlang-20.3.8.25-1.el7.x86_64.rpm\"\n" 40 | + " }"; 41 | 42 | @Test 43 | void jsonPackageShouldDeserialized() { 44 | Domain.Package p = Utils.GSON.fromJson(JSON, Domain.Package.class); 45 | assertThat(p.filename()).isEqualTo("erlang-20.3.8.25-1.el7.x86_64.rpm"); 46 | assertThat(p.destroy_url()) 47 | .isEqualTo("/api/v1/repos/rabbitmq/erlang/el/7/erlang-20.3.8.25-1.el7.x86_64.rpm"); 48 | assertThat(p.version()).isEqualTo("20.3.8.25"); 49 | assertThat(p.created_at()) 50 | .isEqualTo( 51 | ZonedDateTime.parse("2020-01-24T05:35:34.000Z", DateTimeFormatter.ISO_ZONED_DATE_TIME)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/rabbitmq/actions/HttpTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 9 | import static com.github.tomakehurst.wiremock.client.WireMock.exactly; 10 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 11 | import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; 12 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 13 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 14 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; 15 | import static com.github.tomakehurst.wiremock.client.WireMock.verify; 16 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | import com.github.tomakehurst.wiremock.WireMockServer; 20 | import com.github.tomakehurst.wiremock.client.WireMock; 21 | import com.rabbitmq.actions.PackagecloudLogic.PackagecloudPackageAccess; 22 | import java.util.List; 23 | import java.util.function.IntFunction; 24 | import org.junit.jupiter.api.AfterEach; 25 | import org.junit.jupiter.api.BeforeEach; 26 | import org.junit.jupiter.api.Test; 27 | 28 | public class HttpTest { 29 | 30 | WireMockServer wireMockServer; 31 | 32 | @BeforeEach 33 | public void startMockServer() { 34 | wireMockServer = new WireMockServer(wireMockConfig().dynamicPort()); 35 | wireMockServer.start(); 36 | WireMock.configureFor(wireMockServer.port()); 37 | } 38 | 39 | @AfterEach 40 | public void stopMockServer() { 41 | wireMockServer.stop(); 42 | } 43 | 44 | @Test 45 | void findForDelete() { 46 | stubFor(get(urlPathMatching("/.*")).willReturn(aResponse().withBody("[]"))); 47 | PackagecloudPackageAccess access = access(); 48 | access.list(); 49 | verify(exactly(1), getRequestedFor(urlEqualTo("/rabbitmq/erlang/packages.json?filter=deb"))); 50 | } 51 | 52 | @Test 53 | void pagination() { 54 | String response = "[{\"filename\": \"some-package.deb\"}]"; 55 | IntFunction nextHeader = 56 | page -> "<" + baseUrl() + "/rabbitmq/erlang/page-" + page + ">; rel=\"next\""; 57 | stubFor( 58 | get(urlPathMatching("/rabbitmq/erlang/packages.*")) 59 | .willReturn(aResponse().withBody(response).withHeader("Link", nextHeader.apply(2)))); 60 | stubFor( 61 | get(urlPathMatching("/rabbitmq/erlang/page-2")) 62 | .willReturn(aResponse().withBody(response).withHeader("Link", nextHeader.apply(3)))); 63 | stubFor( 64 | get(urlPathMatching("/rabbitmq/erlang/page-3")).willReturn(aResponse().withBody(response))); 65 | PackagecloudPackageAccess access = access(); 66 | List packages = access.list(); 67 | assertThat(packages).hasSize(3); 68 | } 69 | 70 | PackagecloudPackageAccess access() { 71 | String username = "rabbitmq"; 72 | String repository = "erlang"; 73 | String token = "abcde"; 74 | String type = "deb"; 75 | String globs = null; 76 | String versionFilter = null; 77 | return new PackagecloudPackageAccess( 78 | baseUrl(), username, repository, token, type, globs, versionFilter); 79 | } 80 | 81 | String baseUrl() { 82 | return "http://localhost:" + wireMockServer.port(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/rabbitmq/actions/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | import com.google.gson.*; 9 | import java.lang.reflect.Type; 10 | import java.net.URI; 11 | import java.net.URLEncoder; 12 | import java.net.http.HttpClient; 13 | import java.net.http.HttpRequest; 14 | import java.net.http.HttpResponse; 15 | import java.nio.charset.StandardCharsets; 16 | import java.nio.file.FileSystems; 17 | import java.nio.file.Path; 18 | import java.time.Duration; 19 | import java.time.ZonedDateTime; 20 | import java.time.format.DateTimeFormatter; 21 | import java.util.Arrays; 22 | import java.util.Base64; 23 | import java.util.function.Consumer; 24 | import java.util.function.Predicate; 25 | import java.util.regex.Pattern; 26 | 27 | abstract class Utils { 28 | 29 | static final Gson GSON = 30 | new GsonBuilder() 31 | .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeDeserializer()) 32 | .create(); 33 | 34 | private Utils() {} 35 | 36 | static String encodeHttpParameter(String value) { 37 | return URLEncoder.encode(value, StandardCharsets.UTF_8); 38 | } 39 | 40 | static String base64(String in) { 41 | return Base64.getEncoder().encodeToString(in.getBytes(StandardCharsets.UTF_8)); 42 | } 43 | 44 | static Predicate globPredicate(String globs) { 45 | globs = globs == null || globs.isBlank() ? "*" : globs; 46 | return Arrays.stream(globs.split(",")) 47 | .map(String::trim) 48 | .map(g -> "glob:" + g) 49 | .map(g -> FileSystems.getDefault().getPathMatcher(g)) 50 | .map( 51 | pathMatcher -> 52 | (Predicate) 53 | p -> pathMatcher.matches(Path.of(p.filename()).getFileName())) 54 | .reduce(aPackage -> false, Predicate::or); 55 | } 56 | 57 | static Predicate versionPredicate(String version) { 58 | Pattern pattern = Pattern.compile(version, Pattern.CASE_INSENSITIVE); 59 | return p -> pattern.matcher(p.version()).find(); 60 | } 61 | 62 | static void testSequence() { 63 | Consumer display = m -> LogUtils.logGreen(m); 64 | String message; 65 | int exitCode = 0; 66 | try { 67 | String testUri = "https://www.wikipedia.org/"; 68 | LogUtils.logYellow("Starting test sequence, trying to reach " + testUri); 69 | HttpRequest request = HttpRequest.newBuilder().uri(new URI(testUri)).GET().build(); 70 | HttpResponse response = 71 | HttpClient.newBuilder() 72 | .connectTimeout(Duration.ofSeconds(60)) 73 | .build() 74 | .send(request, HttpResponse.BodyHandlers.discarding()); 75 | int statusClass = response.statusCode() - response.statusCode() % 100; 76 | message = "Response code is " + response.statusCode(); 77 | if (statusClass != 200) { 78 | display = LogUtils::logRed; 79 | exitCode = 1; 80 | } 81 | } catch (Exception e) { 82 | message = "Error during test sequence: " + e.getMessage(); 83 | display = LogUtils::logRed; 84 | exitCode = 1; 85 | } 86 | display.accept(message); 87 | System.exit(exitCode); 88 | } 89 | 90 | static class ZonedDateTimeDeserializer implements JsonDeserializer { 91 | 92 | @Override 93 | public ZonedDateTime deserialize( 94 | JsonElement json, Type typeOfT, JsonDeserializationContext context) 95 | throws JsonParseException { 96 | return ZonedDateTime.parse(json.getAsString(), DateTimeFormatter.ISO_ZONED_DATE_TIME); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.rabbitmq 8 | delete-packagecloud-package-action 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | MPL 2.0 14 | https://www.mozilla.org/en-US/MPL/2.0/ 15 | repo 16 | 17 | 18 | 19 | 20 | 21 | info@rabbitmq.com 22 | Team RabbitMQ 23 | Broadcom Inc. and/or its subsidiaries. 24 | https://rabbitmq.com 25 | 26 | 27 | 28 | 29 | UTF-8 30 | UTF-8 31 | 2.11.0 32 | 5.11.0 33 | 3.3.3 34 | 3.26.3 35 | 5.13.0 36 | 3.9.1 37 | 2.0.16 38 | 1.5.7 39 | 2.43.0 40 | 1.21.0 41 | 3.13.0 42 | 3.5.0 43 | 3.4.2 44 | 21 45 | 21 46 | 47 | 48 | 49 | 50 | 51 | com.google.code.gson 52 | gson 53 | ${gson.version} 54 | 55 | 56 | 57 | org.junit.jupiter 58 | junit-jupiter-engine 59 | ${junit.jupiter.version} 60 | test 61 | 62 | 63 | 64 | org.assertj 65 | assertj-core 66 | ${assertj.version} 67 | test 68 | 69 | 70 | 71 | org.mockito 72 | mockito-core 73 | ${mockito.version} 74 | test 75 | 76 | 77 | 78 | org.wiremock 79 | wiremock 80 | ${wiremock.version} 81 | test 82 | 83 | 84 | 85 | org.slf4j 86 | slf4j-api 87 | ${slf4j.version} 88 | test 89 | 90 | 91 | 92 | ch.qos.logback 93 | logback-classic 94 | ${logback.version} 95 | test 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ${project.artifactId} 104 | 105 | 106 | 107 | 108 | org.apache.maven.plugins 109 | maven-compiler-plugin 110 | ${maven.compiler.plugin.version} 111 | 112 | 21 113 | 21 114 | 115 | 116 | 117 | 118 | maven-surefire-plugin 119 | ${maven-surefire-plugin.version} 120 | 121 | 122 | 123 | org.springframework.boot 124 | spring-boot-maven-plugin 125 | ${spring-boot-maven-plugin.version} 126 | 127 | 128 | 129 | repackage 130 | 131 | 132 | 133 | 134 | com.rabbitmq.actions.PackagecloudDeletePackageAction 135 | 136 | 137 | 138 | 139 | com.diffplug.spotless 140 | spotless-maven-plugin 141 | ${spotless.version} 142 | 143 | 144 | 145 | ${google-java-format.version} 146 | 147 | 148 | 149 | 150 | /* 151 | * This Source Code Form is subject to the terms of the Mozilla Public 152 | * License, v. 2.0. If a copy of the MPL was not distributed with this 153 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 154 | */ 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /src/main/java/com/rabbitmq/actions/PackagecloudLogic.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | import static com.rabbitmq.actions.LogUtils.logIndent; 9 | import static com.rabbitmq.actions.LogUtils.yellow; 10 | import static java.util.stream.Collectors.toList; 11 | 12 | import com.google.gson.reflect.TypeToken; 13 | import java.lang.reflect.Type; 14 | import java.net.URI; 15 | import java.net.http.HttpClient; 16 | import java.net.http.HttpRequest; 17 | import java.net.http.HttpResponse; 18 | import java.time.Duration; 19 | import java.util.*; 20 | import java.util.function.Predicate; 21 | import java.util.stream.Collectors; 22 | 23 | abstract class PackagecloudLogic { 24 | 25 | private static final String API_URL = "https://packagecloud.io/api/v1/repos"; 26 | 27 | private PackagecloudLogic() {} 28 | 29 | interface PackageAccess { 30 | 31 | List list(); 32 | 33 | void delete(Domain.Package p); 34 | } 35 | 36 | static class PackagecloudPackageAccess implements PackageAccess { 37 | 38 | private final HttpClient client = 39 | HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(60)).build(); 40 | 41 | private final String baseUrl; 42 | private final String username, repository, token, type, globs, version; 43 | 44 | PackagecloudPackageAccess( 45 | String username, 46 | String repository, 47 | String token, 48 | String type, 49 | String globs, 50 | String version) { 51 | this(API_URL, username, repository, token, type, globs, version); 52 | } 53 | 54 | PackagecloudPackageAccess( 55 | String baseUrl, 56 | String username, 57 | String repository, 58 | String token, 59 | String type, 60 | String globs, 61 | String version) { 62 | this.baseUrl = baseUrl; 63 | this.username = username; 64 | this.repository = repository; 65 | this.token = token; 66 | this.type = type; 67 | this.globs = globs; 68 | this.version = version; 69 | } 70 | 71 | static String nextLink(String linkHeader) { 72 | String nextLink = null; 73 | for (String link : linkHeader.split(",")) { 74 | // e.g. 75 | // ; rel="next" 76 | String[] urlRel = link.split(";"); 77 | if ("rel=\"next\"".equals(urlRel[1].trim())) { 78 | String url = urlRel[0].trim(); 79 | // removing the < and > 80 | nextLink = url.substring(1, url.length() - 1); 81 | } 82 | } 83 | return nextLink; 84 | } 85 | 86 | @Override 87 | public List list() { 88 | Map queryParameters = new LinkedHashMap<>(); 89 | if (this.type != null) { 90 | queryParameters.put("filter", this.type); 91 | } 92 | 93 | String path = "packages.json"; 94 | 95 | if (!queryParameters.isEmpty()) { 96 | String parameters = 97 | queryParameters.entrySet().stream() 98 | .map(e -> e.getKey() + "=" + Utils.encodeHttpParameter(e.getValue())) 99 | .collect(Collectors.joining("&")); 100 | path = path + "?" + parameters; 101 | } 102 | 103 | HttpRequest request = requestBuilder(path).GET().build(); 104 | try { 105 | Type type = TypeToken.getParameterized(List.class, Domain.Package.class).getType(); 106 | List packages = new ArrayList<>(); 107 | boolean hasMore = true; 108 | while (hasMore) { 109 | HttpResponse response = 110 | client.send(request, HttpResponse.BodyHandlers.ofString()); 111 | packages.addAll(Utils.GSON.fromJson(response.body(), type)); 112 | Optional link = response.headers().firstValue("link"); 113 | String nextLink; 114 | if (link.isPresent() && (nextLink = nextLink(link.get())) != null) { 115 | request = requestBuilder().uri(URI.create(nextLink)).GET().build(); 116 | } else { 117 | hasMore = false; 118 | } 119 | } 120 | return filter(this.version, this.globs, packages); 121 | } catch (Exception e) { 122 | throw new RuntimeException(e); 123 | } 124 | } 125 | 126 | static List filter( 127 | String version, String globs, List packages) { 128 | Predicate filter = null; 129 | if (globs != null) { 130 | filter = Utils.globPredicate(globs); 131 | packages = packages.stream().filter(Utils.globPredicate(globs)).collect(toList()); 132 | } 133 | 134 | if (version != null) { 135 | filter = 136 | filter == null 137 | ? Utils.versionPredicate(version) 138 | : filter.and(Utils.versionPredicate(version)); 139 | } 140 | 141 | if (filter != null) { 142 | packages = packages.stream().filter(filter).collect(toList()); 143 | } 144 | return packages; 145 | } 146 | 147 | @Override 148 | public void delete(Domain.Package p) { 149 | // getting just the host to add it to the destroy URL 150 | String base = this.baseUrl.replace(URI.create(this.baseUrl).getPath(), ""); 151 | HttpRequest request = 152 | requestBuilder().DELETE().uri(URI.create(base + p.destroy_url())).build(); 153 | try { 154 | HttpResponse response = client.send(request, HttpResponse.BodyHandlers.discarding()); 155 | if (response.statusCode() != 200) { 156 | logIndent(yellow("Unexpected response code:" + response.statusCode())); 157 | } 158 | } catch (Exception e) { 159 | throw new RuntimeException(e); 160 | } 161 | } 162 | 163 | private HttpRequest.Builder requestBuilder() { 164 | return auth(HttpRequest.newBuilder()); 165 | } 166 | 167 | private HttpRequest.Builder requestBuilder(String path) { 168 | String url = String.format("%s/%s/%s/%s", this.baseUrl, this.username, this.repository, path); 169 | return auth(HttpRequest.newBuilder().uri(URI.create(url))); 170 | } 171 | 172 | private HttpRequest.Builder auth(HttpRequest.Builder builder) { 173 | return builder.setHeader("Authorization", "Basic " + Utils.base64(this.token + ":")); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 168 | " exit 1;"^ 169 | "}"^ 170 | "}" 171 | if ERRORLEVEL 1 goto error 172 | ) 173 | 174 | @REM Provide a "standardized" way to retrieve the CLI args that will 175 | @REM work with both Windows and non-Windows executions. 176 | set MAVEN_CMD_LINE_ARGS=%* 177 | 178 | %MAVEN_JAVA_EXE% ^ 179 | %JVM_CONFIG_MAVEN_PROPS% ^ 180 | %MAVEN_OPTS% ^ 181 | %MAVEN_DEBUG_OPTS% ^ 182 | -classpath %WRAPPER_JAR% ^ 183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 185 | if ERRORLEVEL 1 goto error 186 | goto end 187 | 188 | :error 189 | set ERROR_CODE=1 190 | 191 | :end 192 | @endlocal & set ERROR_CODE=%ERROR_CODE% 193 | 194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 198 | :skipRcPost 199 | 200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 202 | 203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 204 | 205 | cmd /C exit /B %ERROR_CODE% 206 | -------------------------------------------------------------------------------- /src/main/java/com/rabbitmq/actions/PackagecloudDeletePackageAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | import static com.rabbitmq.actions.LogUtils.*; 9 | import static java.util.stream.Collectors.*; 10 | 11 | import com.rabbitmq.actions.Domain.PackageVersion; 12 | import java.time.format.DateTimeFormatter; 13 | import java.util.*; 14 | import java.util.Map.Entry; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | import java.util.function.Function; 17 | 18 | public class PackagecloudDeletePackageAction { 19 | 20 | public static void main(String[] args) { 21 | if (args.length == 1 && "test".equals(args[0])) { 22 | Utils.testSequence(); 23 | } 24 | Map envArguments = new LinkedHashMap<>(); 25 | envArguments.put("INPUT_USERNAME", "username"); 26 | envArguments.put("INPUT_REPOSITORY", "repository"); 27 | envArguments.put("INPUT_TOKEN", "token"); 28 | 29 | for (Entry entry : envArguments.entrySet()) { 30 | try { 31 | checkParameter(entry.getKey(), entry.getValue()); 32 | } catch (IllegalArgumentException e) { 33 | logRed(e.getMessage()); 34 | System.exit(1); 35 | } 36 | } 37 | String username = System.getenv("INPUT_USERNAME"); 38 | String repository = System.getenv("INPUT_REPOSITORY"); 39 | String token = System.getenv("INPUT_TOKEN"); 40 | 41 | String type = System.getenv("INPUT_TYPE"); 42 | String globs = System.getenv("INPUT_GLOBS"); 43 | String versionFilter = System.getenv("INPUT_VERSION_FILTER"); 44 | String orderBy = System.getenv("INPUT_ORDER_BY"); 45 | String keepLastNStr = System.getenv("INPUT_KEEP_LAST_N"); 46 | String keepLastMinorPatchesStr = System.getenv("INPUT_KEEP_LAST_MINOR_PATCHES"); 47 | String doDeleteStr = System.getenv("INPUT_DO_DELETE"); 48 | 49 | boolean orderByVersion = true; 50 | if ("time".equals(orderBy)) { 51 | orderByVersion = false; 52 | } 53 | 54 | int keepLastN = 0; 55 | if (keepLastNStr != null) { 56 | try { 57 | keepLastN = Integer.parseInt(keepLastNStr); 58 | } catch (Exception e) { 59 | logYellow("Incorrect value for keep_last_n: " + keepLastNStr); 60 | logYellow("Using default value instead (" + keepLastN + ")."); 61 | } 62 | } 63 | 64 | boolean keepLastMinorPatches = false; 65 | if (keepLastMinorPatchesStr != null) { 66 | keepLastMinorPatches = Boolean.parseBoolean(keepLastMinorPatchesStr); 67 | } 68 | 69 | boolean doDeleteTemp = false; 70 | if (doDeleteStr != null) { 71 | doDeleteTemp = Boolean.parseBoolean(doDeleteStr); 72 | } 73 | boolean doDelete = doDeleteTemp; 74 | 75 | PackagecloudLogic.PackageAccess access = 76 | new PackagecloudLogic.PackagecloudPackageAccess( 77 | username, repository, token, type, globs, versionFilter); 78 | 79 | List packages = access.list(); 80 | 81 | Map versions = 82 | packages.stream() 83 | .reduce( 84 | new HashMap<>(), 85 | (packageVersions, aPackage) -> { 86 | PackageVersion packageVersion = 87 | packageVersions.computeIfAbsent( 88 | aPackage.version(), 89 | version -> { 90 | PackageVersion pv = new PackageVersion(version); 91 | pv.consider(aPackage); 92 | return pv; 93 | }); 94 | packageVersion.consider(aPackage); 95 | return packageVersions; 96 | }, 97 | (stringPackageVersionHashMap, stringPackageVersionHashMap2) -> { 98 | stringPackageVersionHashMap.putAll(stringPackageVersionHashMap2); 99 | return stringPackageVersionHashMap; 100 | }); 101 | 102 | List versionsToDelete = filterForDeletion(versions.values(), keepLastN, orderByVersion); 103 | 104 | Collection deletionExceptions = Collections.emptySet(); 105 | if (keepLastMinorPatches) { 106 | String latestMinor = 107 | latestMinor(versions.values().stream().map(v -> v.version).collect(toList())); 108 | deletionExceptions = lastMinorPatches(latestMinor, versionsToDelete); 109 | if (!orderByVersion) { 110 | logYellow("Warning: keep_last_minor_patches should only be used with order_by:version"); 111 | } 112 | } 113 | 114 | DateTimeFormatter dateTimeFormatter = 115 | DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mmO", Locale.ENGLISH); 116 | Function formatVersion = 117 | version -> { 118 | PackageVersion packageVersion = versions.get(version); 119 | return String.format( 120 | "%s [%s]", 121 | packageVersion.version, dateTimeFormatter.format(packageVersion.lastPackageDate)); 122 | }; 123 | 124 | log( 125 | green("Version(s) detected: ") 126 | + versions.values().stream() 127 | .map(pv -> formatVersion.apply(pv.version)) 128 | .collect(joining(", "))); 129 | log( 130 | green("Version(s) to delete: ") 131 | + versionsToDelete.stream().map(formatVersion).collect(joining(", "))); 132 | 133 | if (keepLastMinorPatches && !deletionExceptions.isEmpty()) { 134 | log( 135 | green("Deletion exception(s) (last minor patches): ") 136 | + deletionExceptions.stream().map(formatVersion).collect(joining(", "))); 137 | } 138 | 139 | Collection exceptionsToDeletion = new HashSet<>(deletionExceptions); 140 | 141 | newLine(); 142 | 143 | List versionsToKeep = 144 | versions.values().stream() 145 | .filter( 146 | pv -> 147 | !versionsToDelete.contains(pv.version) 148 | || exceptionsToDeletion.contains(pv.version)) 149 | .map(pv -> formatVersion.apply(pv.version)) 150 | .toList(); 151 | 152 | if (!versionsToKeep.isEmpty()) { 153 | log(green("Version(s) to keep: ") + String.join(", ", versionsToKeep)); 154 | } 155 | 156 | newLine(); 157 | 158 | AtomicInteger deletedCount = new AtomicInteger(); 159 | logGreen("Packages:"); 160 | packages.forEach( 161 | p -> { 162 | boolean shouldBeDeleted = 163 | versionsToDelete.contains(p.version()) && !exceptionsToDeletion.contains(p.version()); 164 | if (shouldBeDeleted) { 165 | deletedCount.incrementAndGet(); 166 | } 167 | if (shouldBeDeleted && doDelete) { 168 | try { 169 | access.delete(p); 170 | logIndent(red("deleting " + p.filename())); 171 | } catch (Exception e) { 172 | logRed("Error while trying to delete " + p.destroy_url() + ": " + e.getMessage()); 173 | } 174 | } else { 175 | boolean isDeletionException = exceptionsToDeletion.contains(p.version()); 176 | logIndent( 177 | shouldBeDeleted 178 | ? (red("deleting " + p.filename()) + yellow(" (skipped)")) 179 | : "keeping " 180 | + p.filename() 181 | + (isDeletionException ? " (latest minor patch)" : "")); 182 | } 183 | }); 184 | 185 | newLine(); 186 | logGreen("Deleted " + deletedCount.get() + " file(s)"); 187 | } 188 | 189 | private static void checkParameter(String env, String arg) { 190 | if (System.getenv(env) == null) { 191 | throw new IllegalArgumentException("Parameter " + arg + " must be set"); 192 | } 193 | } 194 | 195 | static List filterForDeletion( 196 | Collection versions, int keepLastN, boolean orderByVersion) { 197 | if (versions.isEmpty()) { 198 | return Collections.emptyList(); 199 | } else if (keepLastN <= 0) { 200 | // do not want to keep any, return all 201 | return versions.stream().map(v -> v.version).collect(toList()); 202 | } else if (keepLastN >= versions.size()) { 203 | // we want to keep more than we have, so nothing to delete 204 | return Collections.emptyList(); 205 | } else { 206 | class VersionWrapper { 207 | 208 | private final PackageVersion version; 209 | private final ComparableVersion comparableVersion; 210 | 211 | VersionWrapper(PackageVersion version) { 212 | this.version = version; 213 | this.comparableVersion = 214 | new ComparableVersion( 215 | version.version.startsWith("1:") 216 | ? version.version.substring(2) 217 | : version.version); 218 | } 219 | } 220 | Comparator comparator = 221 | orderByVersion 222 | ? Comparator.comparing(packageVersion -> packageVersion.comparableVersion) 223 | : Comparator.comparing(packageVersion -> packageVersion.version.lastPackageDate); 224 | return versions.stream() 225 | .map(VersionWrapper::new) 226 | .sorted(comparator) 227 | .limit(versions.size() - keepLastN) 228 | .map(w -> w.version.version) 229 | .collect(toList()); 230 | } 231 | } 232 | 233 | static String latestMinor(List versions) { 234 | if (versions == null || versions.isEmpty()) { 235 | return null; 236 | } else { 237 | return versions.stream() 238 | .map(PackagecloudDeletePackageAction::extractMinor) 239 | .distinct() 240 | .map(ComparableVersion::new) 241 | .max(Comparator.naturalOrder()) 242 | .get() 243 | .toString(); 244 | } 245 | } 246 | 247 | static String extractMinor(String version) { 248 | // e.g. 1:22.3.4.3-1, removing 1: 249 | String curatedVersion = version.startsWith("1:") ? version.substring(2) : version; 250 | // e.g. 22.3-1, removing -1 251 | curatedVersion = 252 | curatedVersion.contains("-") 253 | ? curatedVersion.substring(0, curatedVersion.lastIndexOf("-")) 254 | : curatedVersion; 255 | String[] digits = curatedVersion.split("\\."); 256 | if (digits == null || digits.length <= 1) { 257 | return curatedVersion; 258 | } else { 259 | return digits[0] + "." + digits[1]; 260 | } 261 | } 262 | 263 | static List lastMinorPatches(String minorToIgnore, List versions) { 264 | if (versions == null || versions.isEmpty()) { 265 | return Collections.emptyList(); 266 | } 267 | class VersionWrapper { 268 | 269 | private final String version; 270 | private final ComparableVersion comparableVersion; 271 | private final String minor; 272 | 273 | VersionWrapper(String version) { 274 | this.version = version; 275 | // e.g. 1:22.3.4.3-1, removing 1: 276 | String curatedVersion = version.startsWith("1:") ? version.substring(2) : version; 277 | this.comparableVersion = new ComparableVersion(curatedVersion); 278 | this.minor = extractMinor(version); 279 | } 280 | } 281 | 282 | Map> minors = 283 | versions.stream() 284 | .map(VersionWrapper::new) 285 | .filter(v -> !v.minor.equals(minorToIgnore)) 286 | .collect(groupingBy(versionWrapper -> versionWrapper.minor)); 287 | 288 | Comparator comparator = 289 | Comparator.comparing(wrapper -> wrapper.comparableVersion); 290 | return minors.values().stream() 291 | .map( 292 | patches -> { 293 | if (patches.size() == 1) { 294 | return patches.get(0).version; 295 | } else { 296 | return Collections.max(patches, comparator).version; 297 | } 298 | }) 299 | .collect(toList()); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.2.0 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=$(java-config --jre-home) 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && 89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="$(which javac)" 94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=$(which readlink) 97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 98 | if $darwin ; then 99 | javaHome="$(dirname "\"$javaExecutable\"")" 100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" 101 | else 102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")" 103 | fi 104 | javaHome="$(dirname "\"$javaExecutable\"")" 105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=$(cd "$wdir/.." || exit 1; pwd) 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | # Remove \r in case we run on Windows within Git Bash 164 | # and check out the repository with auto CRLF management 165 | # enabled. Otherwise, we may read lines that are delimited with 166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 167 | # splitting rules. 168 | tr -s '\r\n' ' ' < "$1" 169 | fi 170 | } 171 | 172 | log() { 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | printf '%s\n' "$1" 175 | fi 176 | } 177 | 178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 179 | if [ -z "$BASE_DIR" ]; then 180 | exit 1; 181 | fi 182 | 183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 184 | log "$MAVEN_PROJECTBASEDIR" 185 | 186 | ########################################################################################## 187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 188 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 189 | ########################################################################################## 190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 191 | if [ -r "$wrapperJarPath" ]; then 192 | log "Found $wrapperJarPath" 193 | else 194 | log "Couldn't find $wrapperJarPath, downloading it ..." 195 | 196 | if [ -n "$MVNW_REPOURL" ]; then 197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 198 | else 199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 200 | fi 201 | while IFS="=" read -r key value; do 202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) 203 | safeValue=$(echo "$value" | tr -d '\r') 204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; 205 | esac 206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 207 | log "Downloading from: $wrapperUrl" 208 | 209 | if $cygwin; then 210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 211 | fi 212 | 213 | if command -v wget > /dev/null; then 214 | log "Found wget ... using wget" 215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 218 | else 219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 220 | fi 221 | elif command -v curl > /dev/null; then 222 | log "Found curl ... using curl" 223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 228 | fi 229 | else 230 | log "Falling back to using Java to download" 231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 233 | # For Cygwin, switch paths to Windows format before running javac 234 | if $cygwin; then 235 | javaSource=$(cygpath --path --windows "$javaSource") 236 | javaClass=$(cygpath --path --windows "$javaClass") 237 | fi 238 | if [ -e "$javaSource" ]; then 239 | if [ ! -e "$javaClass" ]; then 240 | log " - Compiling MavenWrapperDownloader.java ..." 241 | ("$JAVA_HOME/bin/javac" "$javaSource") 242 | fi 243 | if [ -e "$javaClass" ]; then 244 | log " - Running MavenWrapperDownloader.java ..." 245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 246 | fi 247 | fi 248 | fi 249 | fi 250 | ########################################################################################## 251 | # End of extension 252 | ########################################################################################## 253 | 254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 255 | wrapperSha256Sum="" 256 | while IFS="=" read -r key value; do 257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; 258 | esac 259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 260 | if [ -n "$wrapperSha256Sum" ]; then 261 | wrapperSha256Result=false 262 | if command -v sha256sum > /dev/null; then 263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then 264 | wrapperSha256Result=true 265 | fi 266 | elif command -v shasum > /dev/null; then 267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then 268 | wrapperSha256Result=true 269 | fi 270 | else 271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." 272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." 273 | exit 1 274 | fi 275 | if [ $wrapperSha256Result = false ]; then 276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 279 | exit 1 280 | fi 281 | fi 282 | 283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 284 | 285 | # For Cygwin, switch paths to Windows format before running java 286 | if $cygwin; then 287 | [ -n "$JAVA_HOME" ] && 288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 289 | [ -n "$CLASSPATH" ] && 290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 291 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 293 | fi 294 | 295 | # Provide a "standardized" way to retrieve the CLI args that will 296 | # work with both Windows and non-Windows executions. 297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 298 | export MAVEN_CMD_LINE_ARGS 299 | 300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 301 | 302 | # shellcheck disable=SC2086 # safe args 303 | exec "$JAVACMD" \ 304 | $MAVEN_OPTS \ 305 | $MAVEN_DEBUG_OPTS \ 306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 309 | -------------------------------------------------------------------------------- /src/test/java/com/rabbitmq/actions/PackagecloudDeletePackageActionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | // import static com.rabbitmq.actions.PackagecloudDeletePackageAction.filterByTag; 9 | // import static com.rabbitmq.actions.PackagecloudDeletePackageAction.filterForDeletion; 10 | import static com.rabbitmq.actions.PackagecloudDeletePackageAction.*; 11 | import static com.rabbitmq.actions.PackagecloudLogic.PackagecloudPackageAccess.filter; 12 | import static java.util.Arrays.asList; 13 | import static java.util.Collections.shuffle; 14 | import static java.util.stream.Collectors.toList; 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | import com.rabbitmq.actions.Domain.PackageVersion; 18 | import java.time.ZonedDateTime; 19 | import java.time.format.DateTimeFormatter; 20 | import java.util.Arrays; 21 | import java.util.Collections; 22 | import java.util.List; 23 | import org.junit.jupiter.api.Test; 24 | 25 | public class PackagecloudDeletePackageActionTest { 26 | 27 | @Test 28 | void filterPackagesVersion() { 29 | List packages = 30 | asList( 31 | p(null, "25.0"), 32 | p(null, "25.0.1"), 33 | p(null, "25.1.1"), 34 | p(null, "25.2.0"), 35 | p(null, "26.0"), 36 | p(null, "26.0.1"), 37 | p(null, "26.1.1"), 38 | p(null, "26.2.0")); 39 | 40 | assertThat(filter("^25\\.*", null, packages)) 41 | .hasSize(4) 42 | .allMatch(p -> p.version().startsWith("25")); 43 | assertThat(filter("^25\\.1\\.*", null, packages)) 44 | .hasSize(1) 45 | .allMatch(p -> p.version().startsWith("25.1")); 46 | } 47 | 48 | @Test 49 | void filterPackagesGlobs() { 50 | List packages = 51 | asList( 52 | p("erlang-25.0.1-1.el9.x86_64.rpm", null), 53 | p("erlang-debuginfo-25.0.1-1.el9.x86_64.rpm", null), 54 | p("erlang-25.0.1-1.el8.x86_64.rpm", null), 55 | p("erlang-debuginfo-25.0.1-1.el8.x86_64.rpm", null)); 56 | 57 | assertThat(filter(null, "erlang-*-*.el8*.rpm", packages)) 58 | .hasSize(2) 59 | .allMatch(p -> p.filename().contains("el8")); 60 | } 61 | 62 | @Test 63 | void filterVersionGlobs() { 64 | List packages = 65 | asList( 66 | p("erlang-25.0-1.el8.x86_64.rpm", "25.0"), 67 | p("erlang-debuginfo-25.0-1.el8.x86_64.rpm", "25.0"), 68 | p("erlang-25.0-1.el9.x86_64.rpm", "25.0"), 69 | p("erlang-debuginfo-25.0-1.el9.x86_64.rpm", "25.0"), 70 | p("erlang-25.0.1-1.el8.x86_64.rpm", "25.0.1"), 71 | p("erlang-debuginfo-25.0-1.el8.x86_64.rpm", "25.0.1"), 72 | p("erlang-25.0.1-1.el9.x86_64.rpm", "25.0.1"), 73 | p("erlang-debuginfo-25.0-1.el9.x86_64.rpm", "25.0.1"), 74 | p("erlang-25.1-1.el8.x86_64.rpm", "25.1"), 75 | p("erlang-debuginfo-25.1-1.el8.x86_64.rpm", "25.1"), 76 | p("erlang-25.1-1.el9.x86_64.rpm", "25.1"), 77 | p("erlang-debuginfo-25.1-1.el9.x86_64.rpm", "25.1"), 78 | p("erlang-25.1.1-1.el8.x86_64.rpm", "25.1.1"), 79 | p("erlang-debuginfo-25.1.1-1.el8.x86_64.rpm", "25.1.1"), 80 | p("erlang-25.1.1-1.el9.x86_64.rpm", "25.1.1"), 81 | p("erlang-debuginfo-25.1.1-1.el9.x86_64.rpm", "25.1.1"), 82 | p("erlang-26.0-1.el8.x86_64.rpm", "26.0"), 83 | p("erlang-debuginfo-26.0-1.el8.x86_64.rpm", "26.0"), 84 | p("erlang-26.0-1.el9.x86_64.rpm", "26.0"), 85 | p("erlang-debuginfo-26.0-1.el9.x86_64.rpm", "26.0")); 86 | 87 | assertThat(filter("^25\\.*", "erlang-*-*.el8*.rpm", packages)) 88 | .hasSize(8) 89 | .allMatch(p -> p.filename().contains("el8")); 90 | 91 | assertThat(filter("^25\\.1\\.*", "erlang-*-*.el8*.rpm", packages)) 92 | .hasSize(4) 93 | .allMatch(p -> p.filename().contains("el8")); 94 | 95 | assertThat(filter("^26\\.*", "erlang-*-*.el9*.rpm", packages)) 96 | .hasSize(2) 97 | .allMatch(p -> p.filename().contains("el9")); 98 | } 99 | 100 | @Test 101 | void filterForDeletionShouldReturnVersionsToDelete() { 102 | List versions = 103 | asList( 104 | "1:22.0-1", 105 | "1:22.3-1", 106 | "1:22.3.4-1", 107 | "1:22.3.4.1-1", 108 | "1:22.3.4.1-2", 109 | "1:22.3.4.10-1", 110 | "1:22.3.4.11-1", 111 | "1:22.3.4.12-1", 112 | "1:22.3.4.13-1", 113 | "1:22.3.4.14-1", 114 | "1:22.3.4.15-1", 115 | "1:22.3.4.16-1", // latest 116 | "1:22.3.4.2-1", 117 | "1:22.3.4.3-1", 118 | "1:22.3.4.4-1", 119 | "1:22.3.4.5-1", 120 | "1:22.3.4.6-1", 121 | "1:22.3.4.7-1", 122 | "1:22.3.4.8-1", 123 | "1:22.3.4.9-1") 124 | .stream() 125 | .map(Domain.PackageVersion::new) 126 | .collect(toList()); 127 | 128 | shuffle(versions); 129 | assertThat(filterForDeletion(versions, 2, true)) 130 | .hasSize(versions.size() - 2) 131 | .containsExactlyInAnyOrder( 132 | "1:22.0-1", 133 | "1:22.3-1", 134 | "1:22.3.4-1", 135 | "1:22.3.4.1-1", 136 | "1:22.3.4.1-2", 137 | "1:22.3.4.10-1", 138 | "1:22.3.4.11-1", 139 | "1:22.3.4.12-1", 140 | "1:22.3.4.13-1", 141 | "1:22.3.4.14-1", 142 | "1:22.3.4.2-1", 143 | "1:22.3.4.3-1", 144 | "1:22.3.4.4-1", 145 | "1:22.3.4.5-1", 146 | "1:22.3.4.6-1", 147 | "1:22.3.4.7-1", 148 | "1:22.3.4.8-1", 149 | "1:22.3.4.9-1") 150 | .doesNotContain("1:22.3.4.15-1", "1:22.3.4.16-1"); 151 | 152 | assertThat(filterForDeletion(versions, versions.size() - 1, true)) 153 | .hasSize(1) 154 | .containsExactly("1:22.0-1"); 155 | 156 | assertThat(filterForDeletion(versions, 0, true)) 157 | .hasSameSizeAs(versions) 158 | .hasSameElementsAs(versions.stream().map(v -> v.version).collect(toList())); 159 | 160 | assertThat(filterForDeletion(versions, versions.size() + 1, true)).isEmpty(); 161 | 162 | versions = 163 | asList( 164 | "22.2.4-1.el8", 165 | "22.3-1.el8", 166 | "22.3.4-1.el8", 167 | "22.3.4.1-1.el8", 168 | "22.3.4.10-1.el8", 169 | "22.3.4.11-1.el8", 170 | "22.3.4.12-1.el8", 171 | "22.3.4.16-1.el8", // latest 172 | "22.3.4.2-1.el8", 173 | "22.3.4.3-1.el8", 174 | "22.3.4.4-1.el8", 175 | "22.3.4.5-1.el8", 176 | "22.3.4.6-1.el8", 177 | "22.3.4.7-1.el8") 178 | .stream() 179 | .map(Domain.PackageVersion::new) 180 | .collect(toList()); 181 | shuffle(versions); 182 | assertThat(filterForDeletion(versions, 2, true)) 183 | .hasSize(versions.size() - 2) 184 | .containsExactlyInAnyOrder( 185 | "22.2.4-1.el8", 186 | "22.3-1.el8", 187 | "22.3.4-1.el8", 188 | "22.3.4.1-1.el8", 189 | "22.3.4.10-1.el8", 190 | "22.3.4.11-1.el8", 191 | "22.3.4.2-1.el8", 192 | "22.3.4.3-1.el8", 193 | "22.3.4.4-1.el8", 194 | "22.3.4.5-1.el8", 195 | "22.3.4.6-1.el8", 196 | "22.3.4.7-1.el8") 197 | .doesNotContain("22.3.4.12-1.el8", "22.3.4.16-1.el8"); 198 | 199 | assertThat(filterForDeletion(versions, versions.size() - 1, true)) 200 | .hasSize(1) 201 | .containsExactly("22.2.4-1.el8"); 202 | 203 | assertThat(filterForDeletion(versions, 0, true)) 204 | .hasSameSizeAs(versions) 205 | .hasSameElementsAs(versions.stream().map(v -> v.version).collect(toList())); 206 | 207 | assertThat(filterForDeletion(versions, versions.size() + 1, true)).isEmpty(); 208 | } 209 | 210 | @Test 211 | void filterForDeletionShouldReturnVersionsToDeleteWhenUsingUploadingDate() { 212 | List versions = 213 | asList( 214 | pv("1.1", "2021-04-01"), 215 | pv("1.2", "2021-04-02"), 216 | pv("1.3", "2021-04-03"), 217 | pv("1.4", "2021-04-04")); 218 | 219 | shuffle(versions); 220 | 221 | assertThat(filterForDeletion(versions, 2, false)) 222 | .hasSize(versions.size() - 2) 223 | .containsExactly("1.1", "1.2"); 224 | 225 | assertThat(filterForDeletion(versions, 1, false)) 226 | .hasSize(versions.size() - 1) 227 | .containsExactly("1.1", "1.2", "1.3"); 228 | 229 | versions = 230 | asList( 231 | pv("1.1", "2021-04-01"), 232 | pv("1.3", "2021-04-02"), 233 | pv("1.2", "2021-04-03"), // uploaded after 1.3 234 | pv("1.4", "2021-04-04")); 235 | 236 | shuffle(versions); 237 | 238 | assertThat(filterForDeletion(versions, 2, false)) 239 | .hasSize(versions.size() - 2) 240 | .containsExactly("1.1", "1.3"); 241 | 242 | assertThat(filterForDeletion(versions, 2, true)) 243 | .hasSize(versions.size() - 2) 244 | .containsExactly("1.1", "1.2"); 245 | } 246 | 247 | @Test 248 | void testLastMinorPatches() { 249 | List versions = 250 | asList( 251 | "1:22.0-1", 252 | "1:22.1.5-1", 253 | "1:22.1.4-1", 254 | "1:22.1.7-1", 255 | "1:22.1.6-1", 256 | "1:22.3-1", 257 | "1:22.3.4-1", 258 | "1:22.3.4.1-1", 259 | "1:22.3.4.1-2", 260 | "1:22.3.4.2-1", 261 | "1:22.3.4.3-1") 262 | .stream() 263 | .collect(toList()); 264 | 265 | assertThat(lastMinorPatches("22.3", versions)) 266 | .hasSize(2) 267 | .containsExactlyInAnyOrder("1:22.1.7-1", "1:22.0-1"); 268 | 269 | versions = 270 | asList( 271 | "1:24.0.2-1", 272 | "1:24.0.3-1", 273 | "1:24.0.4-1", 274 | "1:24.0.5-1", 275 | "1:24.0.6-1", 276 | "1:24.1-1", 277 | "1:24.1.1-1", 278 | "1:24.1.2-1", 279 | "1:24.1.3-1", 280 | "1:24.1.4-1", 281 | "1:24.1.5-1", 282 | "1:24.1.6-1", 283 | "1:24.1.7-1", 284 | "1:24.2-1", 285 | "1:24.2.1-1", 286 | "1:24.2.2-1", 287 | "1:24.3-1", 288 | "1:24.3.1-1"); 289 | 290 | assertThat(lastMinorPatches("24.3", versions)) 291 | .hasSize(3) 292 | .containsExactlyInAnyOrder("1:24.2.2-1", "1:24.1.7-1", "1:24.0.6-1"); 293 | } 294 | 295 | @Test 296 | void testLatestMinor() { 297 | List versions = 298 | asList( 299 | "1:24.1.1-1", 300 | "1:24.1.3-1", 301 | "1:24.1.2-1", 302 | "1:24.3.1-1", 303 | "1:24.3.3-1", 304 | "1:24.3.2-1", 305 | "1:24.1.5-1", 306 | "1:24.1.4-1", 307 | "1:24.1.7-1", 308 | "1:24.1.6-1", 309 | "1:24.0.4-1", 310 | "1:24.0.3-1", 311 | "1:24.0.2-1", 312 | "1:24.3-1", 313 | "1:24.2-1", 314 | "1:24.2.2-1", 315 | "1:24.1-1", 316 | "1:24.2.1-1", 317 | "1:24.0.6-1", 318 | "1:24.0.5-1"); 319 | shuffle(versions); 320 | assertThat(latestMinor(versions)).isEqualTo("24.3"); 321 | } 322 | 323 | @Test 324 | void extractLatestMinorThenGetLastMinorPatches() { 325 | List detected = 326 | versions( 327 | "1:24.1.1-1, 1:24.1.3-1, 1:24.1.2-1, 1:24.3.1-1, 1:24.3.3-1, " 328 | + "1:24.3.2-1, 1:24.1.5-1, 1:24.1.4-1, 1:24.1.7-1, 1:24.1.6-1, " 329 | + "1:24.0.4-1, 1:24.0.3-1, 1:24.0.2-1, 1:24.3-1, 1:24.2-1, " 330 | + "1:24.2.2-1, 1:24.1-1, 1:24.2.1-1, 1:24.0.6-1, 1:24.0.5-1"); 331 | List toDelete = 332 | versions( 333 | "1:24.0.2-1, 1:24.0.3-1, 1:24.0.4-1, 1:24.0.5-1, 1:24.0.6-1, 1:24.1-1, " 334 | + "1:24.1.1-1, 1:24.1.2-1, 1:24.1.3-1, 1:24.1.4-1, 1:24.1.5-1, " 335 | + "1:24.1.6-1, 1:24.1.7-1, 1:24.2-1, 1:24.2.1-1, 1:24.2.2-1, 1:24.3-1, 1:24.3.1-1"); 336 | 337 | String latestMinor = latestMinor(detected); 338 | assertThat(latestMinor).isEqualTo("24.3"); 339 | assertThat(lastMinorPatches(latestMinor, toDelete)) 340 | .containsExactlyInAnyOrder("1:24.0.6-1", "1:24.1.7-1", "1:24.2.2-1"); 341 | 342 | detected = 343 | versions( 344 | "1:24.1.3-1, 1:24.3.1-1, 1:24.3.3-1, 1:24.3.4.1-1, 1:24.3.2-1, 1:24.3.4.3-1, " 345 | + "1:24.3.4-1, 1:24.3.4.2-1, 1:24.1.5-1, 1:24.1.4-1, 1:24.1.7-1, " 346 | + "1:24.1.6-1, 1:24.3-1, 1:24.2-1, 1:24.2.2-1, 1:24.2.1-1"); 347 | toDelete = 348 | versions( 349 | "1:24.1.3-1, 1:24.1.4-1, 1:24.1.5-1, 1:24.1.6-1, 1:24.1.7-1, 1:24.2-1, " 350 | + "1:24.2.1-1, 1:24.2.2-1, 1:24.3-1, 1:24.3.1-1, 1:24.3.2-1, 1:24.3.3-1, " 351 | + "1:24.3.4-1, 1:24.3.4.1-1"); 352 | 353 | latestMinor = latestMinor(detected); 354 | assertThat(latestMinor).isEqualTo("24.3"); 355 | assertThat(lastMinorPatches(latestMinor, toDelete)) 356 | .containsExactlyInAnyOrder("1:24.1.7-1", "1:24.2.2-1"); 357 | 358 | detected = 359 | versions( 360 | "1:24.3.4.8-1, 1:24.3.4.5-1, 1:24.3.4.4-1, 1:24.3.4.7-1, 1:24.3.4.6-1, " 361 | + "1:24.3.4.1-1, 1:24.3.4.3-1, 1:24.3.4-1, 1:24.3.4.2-1"); 362 | toDelete = 363 | versions( 364 | "1:24.3.4-1, 1:24.3.4.1-1, 1:24.3.4.2-1, 1:24.3.4.3-1, 1:24.3.4.4-1, 1:24.3.4.5-1, 1:24.3.4.6-1"); 365 | 366 | latestMinor = latestMinor(detected); 367 | assertThat(latestMinor).isEqualTo("24.3"); 368 | assertThat(lastMinorPatches(latestMinor, toDelete)).isEmpty(); 369 | 370 | detected = 371 | versions( 372 | "1:25.2-1, 1:25.2.2-1, 1:25.0.4-1, 1:25.1-1, 1:25.0-1, 1:25.0.1-1, " 373 | + "1:25.0.3-1, 1:25.0.2-1, 1:25.1.2-1, 1:25.1.1-1, 1:25.2.1-1"); 374 | toDelete = 375 | versions( 376 | "1:25.0-1, 1:25.0.1-1, 1:25.0.2-1, 1:25.0.3-1, 1:25.0.4-1, 1:25.1-1, 1:25.1.1-1, " 377 | + "1:25.1.2-1, 1:25.2-1"); 378 | 379 | latestMinor = latestMinor(detected); 380 | assertThat(latestMinor).isEqualTo("25.2"); 381 | assertThat(lastMinorPatches(latestMinor, toDelete)) 382 | .containsExactlyInAnyOrder("1:25.0.4-1", "1:25.1.2-1"); 383 | 384 | detected = 385 | versions( 386 | "25.1-1.el8, 25.1.1-1.el8, 25.0.1-1.el8, 25.0.2-1.el8, 25.1.2-1.el8, " 387 | + "25.0.3-1.el8, 25.0-1.el8, 25.0.4-1.el8, 25.1.1-2.el8"); 388 | toDelete = 389 | versions( 390 | "25.0-1.el8, 25.0.1-1.el8, 25.0.2-1.el8, 25.0.3-1.el8, 25.0.4-1.el8, " 391 | + "25.1-1.el8, 25.1.1-1.el8, 25.1.1-2.el8"); 392 | 393 | latestMinor = latestMinor(detected); 394 | assertThat(latestMinor).isEqualTo("25.1"); 395 | assertThat(lastMinorPatches(latestMinor, toDelete)).containsExactlyInAnyOrder("25.0.4-1.el8"); 396 | 397 | detected = 398 | versions( 399 | "25.1-1.el8, 25.1.1-1.el8, 25.0.1-1.el8, 25.0.2-1.el8, 25.1.2-1.el8, " 400 | + "25.0.3-1.el8, 25.0-1.el8, 25.0.4-1.el8, 25.2-1.el8, 25.1.1-2.el8"); 401 | toDelete = 402 | versions( 403 | "25.0-1.el8, 25.0.1-1.el8, 25.0.2-1.el8, 25.0.3-1.el8, 25.0.4-1.el8, " 404 | + "25.1-1.el8, 25.1.1-1.el8, 25.1.1-2.el8"); 405 | 406 | latestMinor = latestMinor(detected); 407 | assertThat(latestMinor).isEqualTo("25.2"); 408 | assertThat(lastMinorPatches(latestMinor, toDelete)) 409 | .containsExactlyInAnyOrder("25.0.4-1.el8", "25.1.1-2.el8"); 410 | } 411 | 412 | static List versions(String line) { 413 | List versions = Arrays.stream(line.split(",")).map(String::trim).collect(toList()); 414 | Collections.shuffle(versions); 415 | return versions; 416 | } 417 | 418 | static PackageVersion pv(String version, String date) { 419 | PackageVersion pv = new PackageVersion(version); 420 | pv.lastPackageDate = 421 | ZonedDateTime.parse(date + "T12:58:11.418817Z", DateTimeFormatter.ISO_ZONED_DATE_TIME); 422 | return pv; 423 | } 424 | 425 | private static Domain.Package p(String filename, String version) { 426 | Domain.Package p = new Domain.Package(); 427 | p.setFilename(filename); 428 | p.setVersion(version); 429 | return p; 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/main/java/com/rabbitmq/actions/ComparableVersion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | */ 6 | package com.rabbitmq.actions; 7 | 8 | // from 9 | // https://github.com/apache/maven/blob/master/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java 10 | 11 | /* 12 | * Licensed to the Apache Software Foundation (ASF) under one 13 | * or more contributor license agreements. See the NOTICE file 14 | * distributed with this work for additional information 15 | * regarding copyright ownership. The ASF licenses this file 16 | * to you under the Apache License, Version 2.0 (the 17 | * "License"); you may not use this file except in compliance 18 | * with the License. You may obtain a copy of the License at 19 | * 20 | * http://www.apache.org/licenses/LICENSE-2.0 21 | * 22 | * Unless required by applicable law or agreed to in writing, 23 | * software distributed under the License is distributed on an 24 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 25 | * KIND, either express or implied. See the License for the 26 | * specific language governing permissions and limitations 27 | * under the License. 28 | */ 29 | 30 | import java.math.BigInteger; 31 | import java.util.ArrayDeque; 32 | import java.util.ArrayList; 33 | import java.util.Arrays; 34 | import java.util.Deque; 35 | import java.util.Iterator; 36 | import java.util.List; 37 | import java.util.Locale; 38 | import java.util.Properties; 39 | 40 | /** 41 | * Generic implementation of version comparison. Features: 42 | * 43 | *
    44 | *
  • mixing of '-' (hyphen) and '.' (dot) separators, 45 | *
  • transition between characters and digits also constitutes a separator: 46 | * 1.0alpha1 => [1, 0, alpha, 1] 47 | *
  • unlimited number of version components, 48 | *
  • version components in the text can be digits or strings, 49 | *
  • strings are checked for well-known qualifiers and the qualifier ordering is used for 50 | * version ordering. Well-known qualifiers (case insensitive) are: 51 | *
      52 | *
    • alpha or a 53 | *
    • beta or b 54 | *
    • milestone or m 55 | *
    • rc or cr 56 | *
    • snapshot 57 | *
    • (the empty string) or ga or final 58 | *
    • sp 59 | *
    60 | * Unknown qualifiers are considered after known qualifiers, with lexical order (always case 61 | * insensitive), 62 | *
  • a hyphen usually precedes a qualifier, and is always less important than something preceded 63 | * with a dot. 64 | *
65 | * 66 | * @see "Versioning" on 67 | * Maven Wiki 68 | * @author Kenney Westerhof 69 | * @author Hervé Boutemy 70 | */ 71 | class ComparableVersion implements Comparable { 72 | private static final int MAX_INTITEM_LENGTH = 9; 73 | 74 | private static final int MAX_LONGITEM_LENGTH = 18; 75 | 76 | private String value; 77 | 78 | private String canonical; 79 | 80 | private ListItem items; 81 | 82 | private interface Item { 83 | int INT_ITEM = 3; 84 | int LONG_ITEM = 4; 85 | int BIGINTEGER_ITEM = 0; 86 | int STRING_ITEM = 1; 87 | int LIST_ITEM = 2; 88 | 89 | int compareTo(Item item); 90 | 91 | int getType(); 92 | 93 | boolean isNull(); 94 | } 95 | 96 | /** Represents a numeric item in the version item list that can be represented with an int. */ 97 | private static class IntItem implements Item { 98 | private final int value; 99 | 100 | public static final IntItem ZERO = new IntItem(); 101 | 102 | private IntItem() { 103 | this.value = 0; 104 | } 105 | 106 | IntItem(String str) { 107 | this.value = Integer.parseInt(str); 108 | } 109 | 110 | @Override 111 | public int getType() { 112 | return INT_ITEM; 113 | } 114 | 115 | @Override 116 | public boolean isNull() { 117 | return value == 0; 118 | } 119 | 120 | @Override 121 | public int compareTo(Item item) { 122 | if (item == null) { 123 | return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 124 | } 125 | 126 | switch (item.getType()) { 127 | case INT_ITEM: 128 | int itemValue = ((IntItem) item).value; 129 | return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1); 130 | case LONG_ITEM: 131 | case BIGINTEGER_ITEM: 132 | return -1; 133 | 134 | case STRING_ITEM: 135 | return 1; // 1.1 > 1-sp 136 | 137 | case LIST_ITEM: 138 | return 1; // 1.1 > 1-1 139 | 140 | default: 141 | throw new IllegalStateException("invalid item: " + item.getClass()); 142 | } 143 | } 144 | 145 | @Override 146 | public boolean equals(Object o) { 147 | if (this == o) { 148 | return true; 149 | } 150 | if (o == null || getClass() != o.getClass()) { 151 | return false; 152 | } 153 | 154 | IntItem intItem = (IntItem) o; 155 | 156 | return value == intItem.value; 157 | } 158 | 159 | @Override 160 | public int hashCode() { 161 | return value; 162 | } 163 | 164 | @Override 165 | public String toString() { 166 | return Integer.toString(value); 167 | } 168 | } 169 | 170 | /** Represents a numeric item in the version item list that can be represented with a long. */ 171 | private static class LongItem implements Item { 172 | private final long value; 173 | 174 | LongItem(String str) { 175 | this.value = Long.parseLong(str); 176 | } 177 | 178 | @Override 179 | public int getType() { 180 | return LONG_ITEM; 181 | } 182 | 183 | @Override 184 | public boolean isNull() { 185 | return value == 0; 186 | } 187 | 188 | @Override 189 | public int compareTo(Item item) { 190 | if (item == null) { 191 | return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 192 | } 193 | 194 | switch (item.getType()) { 195 | case INT_ITEM: 196 | return 1; 197 | case LONG_ITEM: 198 | long itemValue = ((LongItem) item).value; 199 | return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1); 200 | case BIGINTEGER_ITEM: 201 | return -1; 202 | 203 | case STRING_ITEM: 204 | return 1; // 1.1 > 1-sp 205 | 206 | case LIST_ITEM: 207 | return 1; // 1.1 > 1-1 208 | 209 | default: 210 | throw new IllegalStateException("invalid item: " + item.getClass()); 211 | } 212 | } 213 | 214 | @Override 215 | public boolean equals(Object o) { 216 | if (this == o) { 217 | return true; 218 | } 219 | if (o == null || getClass() != o.getClass()) { 220 | return false; 221 | } 222 | 223 | LongItem longItem = (LongItem) o; 224 | 225 | return value == longItem.value; 226 | } 227 | 228 | @Override 229 | public int hashCode() { 230 | return (int) (value ^ (value >>> 32)); 231 | } 232 | 233 | @Override 234 | public String toString() { 235 | return Long.toString(value); 236 | } 237 | } 238 | 239 | /** Represents a numeric item in the version item list. */ 240 | private static class BigIntegerItem implements Item { 241 | private final BigInteger value; 242 | 243 | BigIntegerItem(String str) { 244 | this.value = new BigInteger(str); 245 | } 246 | 247 | @Override 248 | public int getType() { 249 | return BIGINTEGER_ITEM; 250 | } 251 | 252 | @Override 253 | public boolean isNull() { 254 | return BigInteger.ZERO.equals(value); 255 | } 256 | 257 | @Override 258 | public int compareTo(Item item) { 259 | if (item == null) { 260 | return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 261 | } 262 | 263 | switch (item.getType()) { 264 | case INT_ITEM: 265 | case LONG_ITEM: 266 | return 1; 267 | 268 | case BIGINTEGER_ITEM: 269 | return value.compareTo(((BigIntegerItem) item).value); 270 | 271 | case STRING_ITEM: 272 | return 1; // 1.1 > 1-sp 273 | 274 | case LIST_ITEM: 275 | return 1; // 1.1 > 1-1 276 | 277 | default: 278 | throw new IllegalStateException("invalid item: " + item.getClass()); 279 | } 280 | } 281 | 282 | @Override 283 | public boolean equals(Object o) { 284 | if (this == o) { 285 | return true; 286 | } 287 | if (o == null || getClass() != o.getClass()) { 288 | return false; 289 | } 290 | 291 | BigIntegerItem that = (BigIntegerItem) o; 292 | 293 | return value.equals(that.value); 294 | } 295 | 296 | @Override 297 | public int hashCode() { 298 | return value.hashCode(); 299 | } 300 | 301 | public String toString() { 302 | return value.toString(); 303 | } 304 | } 305 | 306 | /** Represents a string in the version item list, usually a qualifier. */ 307 | private static class StringItem implements Item { 308 | private static final List QUALIFIERS = 309 | Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp"); 310 | 311 | private static final Properties ALIASES = new Properties(); 312 | 313 | static { 314 | ALIASES.put("ga", ""); 315 | ALIASES.put("final", ""); 316 | ALIASES.put("release", ""); 317 | ALIASES.put("cr", "rc"); 318 | } 319 | 320 | /** 321 | * A comparable value for the empty-string qualifier. This one is used to determine if a given 322 | * qualifier makes the version older than one without a qualifier, or more recent. 323 | */ 324 | private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf("")); 325 | 326 | private final String value; 327 | 328 | StringItem(String value, boolean followedByDigit) { 329 | if (followedByDigit && value.length() == 1) { 330 | // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 331 | switch (value.charAt(0)) { 332 | case 'a': 333 | value = "alpha"; 334 | break; 335 | case 'b': 336 | value = "beta"; 337 | break; 338 | case 'm': 339 | value = "milestone"; 340 | break; 341 | default: 342 | } 343 | } 344 | this.value = ALIASES.getProperty(value, value); 345 | } 346 | 347 | @Override 348 | public int getType() { 349 | return STRING_ITEM; 350 | } 351 | 352 | @Override 353 | public boolean isNull() { 354 | return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); 355 | } 356 | 357 | /** 358 | * Returns a comparable value for a qualifier. 359 | * 360 | *

This method takes into account the ordering of known qualifiers then unknown qualifiers 361 | * with lexical ordering. 362 | * 363 | *

just returning an Integer with the index here is faster, but requires a lot of 364 | * if/then/else to check for -1 or QUALIFIERS.size and then resort to lexical ordering. Most 365 | * comparisons are decided by the first character, so this is still fast. If more characters are 366 | * needed then it requires a lexical sort anyway. 367 | * 368 | * @param qualifier 369 | * @return an equivalent value that can be used with lexical comparison 370 | */ 371 | public static String comparableQualifier(String qualifier) { 372 | int i = QUALIFIERS.indexOf(qualifier); 373 | 374 | return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); 375 | } 376 | 377 | @Override 378 | public int compareTo(Item item) { 379 | if (item == null) { 380 | // 1-rc < 1, 1-ga > 1 381 | return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); 382 | } 383 | switch (item.getType()) { 384 | case INT_ITEM: 385 | case LONG_ITEM: 386 | case BIGINTEGER_ITEM: 387 | return -1; // 1.any < 1.1 ? 388 | 389 | case STRING_ITEM: 390 | return comparableQualifier(value) 391 | .compareTo(comparableQualifier(((StringItem) item).value)); 392 | 393 | case LIST_ITEM: 394 | return -1; // 1.any < 1-1 395 | 396 | default: 397 | throw new IllegalStateException("invalid item: " + item.getClass()); 398 | } 399 | } 400 | 401 | @Override 402 | public boolean equals(Object o) { 403 | if (this == o) { 404 | return true; 405 | } 406 | if (o == null || getClass() != o.getClass()) { 407 | return false; 408 | } 409 | 410 | StringItem that = (StringItem) o; 411 | 412 | return value.equals(that.value); 413 | } 414 | 415 | @Override 416 | public int hashCode() { 417 | return value.hashCode(); 418 | } 419 | 420 | public String toString() { 421 | return value; 422 | } 423 | } 424 | 425 | /** 426 | * Represents a version list item. This class is used both for the global item list and for 427 | * sub-lists (which start with '-(number)' in the version specification). 428 | */ 429 | private static class ListItem extends ArrayList implements Item { 430 | @Override 431 | public int getType() { 432 | return LIST_ITEM; 433 | } 434 | 435 | @Override 436 | public boolean isNull() { 437 | return (size() == 0); 438 | } 439 | 440 | void normalize() { 441 | for (int i = size() - 1; i >= 0; i--) { 442 | Item lastItem = get(i); 443 | 444 | if (lastItem.isNull()) { 445 | // remove null trailing items: 0, "", empty list 446 | remove(i); 447 | } else if (!(lastItem instanceof ListItem)) { 448 | break; 449 | } 450 | } 451 | } 452 | 453 | @Override 454 | public int compareTo(Item item) { 455 | if (item == null) { 456 | if (size() == 0) { 457 | return 0; // 1-0 = 1- (normalize) = 1 458 | } 459 | Item first = get(0); 460 | return first.compareTo(null); 461 | } 462 | switch (item.getType()) { 463 | case INT_ITEM: 464 | case LONG_ITEM: 465 | case BIGINTEGER_ITEM: 466 | return -1; // 1-1 < 1.0.x 467 | 468 | case STRING_ITEM: 469 | return 1; // 1-1 > 1-sp 470 | 471 | case LIST_ITEM: 472 | Iterator left = iterator(); 473 | Iterator right = ((ListItem) item).iterator(); 474 | 475 | while (left.hasNext() || right.hasNext()) { 476 | Item l = left.hasNext() ? left.next() : null; 477 | Item r = right.hasNext() ? right.next() : null; 478 | 479 | // if this is shorter, then invert the compare and mul with -1 480 | int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); 481 | 482 | if (result != 0) { 483 | return result; 484 | } 485 | } 486 | 487 | return 0; 488 | 489 | default: 490 | throw new IllegalStateException("invalid item: " + item.getClass()); 491 | } 492 | } 493 | 494 | @Override 495 | public String toString() { 496 | StringBuilder buffer = new StringBuilder(); 497 | for (Item item : this) { 498 | if (buffer.length() > 0) { 499 | buffer.append((item instanceof ListItem) ? '-' : '.'); 500 | } 501 | buffer.append(item); 502 | } 503 | return buffer.toString(); 504 | } 505 | } 506 | 507 | public ComparableVersion(String version) { 508 | parseVersion(version); 509 | } 510 | 511 | @SuppressWarnings("checkstyle:innerassignment") 512 | public final void parseVersion(String version) { 513 | this.value = version; 514 | 515 | items = new ListItem(); 516 | 517 | version = version.toLowerCase(Locale.ENGLISH); 518 | 519 | ListItem list = items; 520 | 521 | Deque stack = new ArrayDeque<>(); 522 | stack.push(list); 523 | 524 | boolean isDigit = false; 525 | 526 | int startIndex = 0; 527 | 528 | for (int i = 0; i < version.length(); i++) { 529 | char c = version.charAt(i); 530 | 531 | if (c == '.') { 532 | if (i == startIndex) { 533 | list.add(IntItem.ZERO); 534 | } else { 535 | list.add(parseItem(isDigit, version.substring(startIndex, i))); 536 | } 537 | startIndex = i + 1; 538 | } else if (c == '-') { 539 | if (i == startIndex) { 540 | list.add(IntItem.ZERO); 541 | } else { 542 | list.add(parseItem(isDigit, version.substring(startIndex, i))); 543 | } 544 | startIndex = i + 1; 545 | 546 | list.add(list = new ListItem()); 547 | stack.push(list); 548 | } else if (Character.isDigit(c)) { 549 | if (!isDigit && i > startIndex) { 550 | list.add(new StringItem(version.substring(startIndex, i), true)); 551 | startIndex = i; 552 | 553 | list.add(list = new ListItem()); 554 | stack.push(list); 555 | } 556 | 557 | isDigit = true; 558 | } else { 559 | if (isDigit && i > startIndex) { 560 | list.add(parseItem(true, version.substring(startIndex, i))); 561 | startIndex = i; 562 | 563 | list.add(list = new ListItem()); 564 | stack.push(list); 565 | } 566 | 567 | isDigit = false; 568 | } 569 | } 570 | 571 | if (version.length() > startIndex) { 572 | list.add(parseItem(isDigit, version.substring(startIndex))); 573 | } 574 | 575 | while (!stack.isEmpty()) { 576 | list = (ListItem) stack.pop(); 577 | list.normalize(); 578 | } 579 | } 580 | 581 | private static Item parseItem(boolean isDigit, String buf) { 582 | if (isDigit) { 583 | buf = stripLeadingZeroes(buf); 584 | if (buf.length() <= MAX_INTITEM_LENGTH) { 585 | // lower than 2^31 586 | return new IntItem(buf); 587 | } else if (buf.length() <= MAX_LONGITEM_LENGTH) { 588 | // lower than 2^63 589 | return new LongItem(buf); 590 | } 591 | return new BigIntegerItem(buf); 592 | } 593 | return new StringItem(buf, false); 594 | } 595 | 596 | private static String stripLeadingZeroes(String buf) { 597 | if (buf == null || buf.isEmpty()) { 598 | return "0"; 599 | } 600 | for (int i = 0; i < buf.length(); ++i) { 601 | char c = buf.charAt(i); 602 | if (c != '0') { 603 | return buf.substring(i); 604 | } 605 | } 606 | return buf; 607 | } 608 | 609 | @Override 610 | public int compareTo(ComparableVersion o) { 611 | return items.compareTo(o.items); 612 | } 613 | 614 | @Override 615 | public String toString() { 616 | return value; 617 | } 618 | 619 | public String getCanonical() { 620 | if (canonical == null) { 621 | canonical = items.toString(); 622 | } 623 | return canonical; 624 | } 625 | 626 | @Override 627 | public boolean equals(Object o) { 628 | return (o instanceof ComparableVersion) && items.equals(((ComparableVersion) o).items); 629 | } 630 | 631 | @Override 632 | public int hashCode() { 633 | return items.hashCode(); 634 | } 635 | 636 | // CHECKSTYLE_OFF: LineLength 637 | /** 638 | * Main to test version parsing and comparison. 639 | * 640 | *

To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue 641 | * 642 | *

643 |    * java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"
644 |    * 
645 | * 646 | * command to command line. Result of given command will be something like this: 647 | * 648 | *
649 |    * Display parameters as parsed by Maven (in canonical form) and comparison result:
650 |    * 1. 1.2.7 == 1.2.7
651 |    *    1.2.7 > 1.2-SNAPSHOT
652 |    * 2. 1.2-SNAPSHOT == 1.2-snapshot
653 |    * 
654 | * 655 | * @param args the version strings to parse and compare. You can pass arbitrary number of version 656 | * strings and always two adjacent will be compared 657 | */ 658 | // CHECKSTYLE_ON: LineLength 659 | public static void main(String... args) { 660 | System.out.println( 661 | "Display parameters as parsed by Maven (in canonical form) and comparison result:"); 662 | if (args.length == 0) { 663 | return; 664 | } 665 | 666 | ComparableVersion prev = null; 667 | int i = 1; 668 | for (String version : args) { 669 | ComparableVersion c = new ComparableVersion(version); 670 | 671 | if (prev != null) { 672 | int compare = prev.compareTo(c); 673 | System.out.println( 674 | " " 675 | + prev 676 | + ' ' 677 | + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">")) 678 | + ' ' 679 | + version); 680 | } 681 | 682 | System.out.println(i++ + ". " + version + " == " + c.getCanonical()); 683 | 684 | prev = c; 685 | } 686 | } 687 | } 688 | --------------------------------------------------------------------------------