diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..0587738 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,18 @@ +name: HiveVM Gradle DevOps + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Build with Gradle + run: ./gradlew build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d77f796 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +bin/ + +.settings/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d53ecaf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4aaa9b7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 HiveVM + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3664340 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +![Status](https://img.shields.io/github/actions/workflow/status/hivevm/gradleDevOps/main.yml?label=Build) +![GitHub License](https://img.shields.io/github/license/hivevm/gradleDevOps?label=License&link=https%3A%2F%2Fopensource.org%2Flicense%2Fmit) + + +# Gradle DevOps + +Gradle plugin to support DevOps. + + +## Gradle wrapper + +To generate the gradle warpper for a specific gradle version type following command. + +~~~ +./gradlew wrapper --gradle-version X.X.X +~~~ \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..651af29 --- /dev/null +++ b/build.gradle @@ -0,0 +1,84 @@ +plugins { + id 'com.gradle.plugin-publish' version '1.3.0' + id 'java-gradle-plugin' + id 'maven-publish' +} + + +// Apply the java-library plugin to add support for Java Library +apply plugin: 'java' +apply plugin: 'java-library' +apply plugin: 'java-gradle-plugin' + + +repositories { + mavenLocal() + mavenCentral() +} + + +version = '1.0.0' +group = 'org.hivevm' + + +allprojects { + tasks.withType(Javadoc).all { + enabled = false + } +} + + +dependencies { + implementation gradleApi() + + // https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit + implementation 'org.eclipse.jgit:org.eclipse.jgit:7.0.0.202409031743-r' + + // https://mvnrepository.com/artifact/com.github.docker-java/docker-java + implementation 'com.github.docker-java:docker-java:3.4.0' + + // https://mvnrepository.com/artifact/org.apache.commons/commons-compress + implementation 'org.apache.commons:commons-compress:1.27.1' + + // Use JUnit test framework + // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.3' +} + + +sourceSets { + main { + java { + srcDirs += 'src/main/gradle' + } + resources { + } + } + test { + java { + } + } +} + +jar { + manifest { + attributes 'Implementation-Title': 'HiveVM DevOps' + attributes 'Implementation-Version': project.version + } +} + + +// Configured to publish your plugin to the plugin portal +gradlePlugin { + website = 'https://www.hivevm.org' + vcsUrl = 'https://github.com/hivevm/gradleDevOps.git' + plugins { + create("gradle") { + id = 'org.hivevm.devops' + displayName = 'HiveVM DevOps' + description = 'HiveVM gradle plugin for DevOps' + implementationClass = 'org.hivevm.gradle.DevOpsPlugin' + tags.set(["hivevm", "gradle", "devops", "ci/cd"]) + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e2847c8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b810437 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,7 @@ +pluginManagement { + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} \ No newline at end of file diff --git a/src/main/gradle/org/hivevm/gradle/DevOpsConfig.java b/src/main/gradle/org/hivevm/gradle/DevOpsConfig.java new file mode 100644 index 0000000..c8526d3 --- /dev/null +++ b/src/main/gradle/org/hivevm/gradle/DevOpsConfig.java @@ -0,0 +1,71 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.gradle; + +import java.io.File; +import java.util.List; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Nested; + +/** + * The {@link DevOpsConfig} class. + */ +public abstract class DevOpsConfig { + + private final Project project; + + + public String remote; + public String username; + public String password; + public String branch; + + + private final ListProperty pipelines; + + /** + * Constructs an instance of {@link DevOpsConfig}. + * + * @param project + */ + @Inject + public DevOpsConfig(Project project) { + this.project = project; + this.pipelines = project.getObjects().listProperty(DevOpsPipeline.class).empty(); + } + + /** + * Gets the {@link Project}. + */ + public final Project getProject() { + return this.project; + } + + /** + * Gets the working directory. + */ + public final File getWorkingDir() { + return this.project.getProjectDir(); + } + + @Nested + public final List getPipelines() { + return this.pipelines.get(); + } + + public final void pipeline(Action action) { + this.pipelines.add(newInstance(DevOpsPipeline.class, action)); + } + + private C newInstance(Class clazz, Action action) { + C instance = this.project.getObjects().newInstance(clazz); + action.execute(instance); + return instance; + } +} diff --git a/src/main/gradle/org/hivevm/gradle/DevOpsPipeline.java b/src/main/gradle/org/hivevm/gradle/DevOpsPipeline.java new file mode 100644 index 0000000..209cc84 --- /dev/null +++ b/src/main/gradle/org/hivevm/gradle/DevOpsPipeline.java @@ -0,0 +1,32 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.gradle; + +import javax.inject.Inject; + +import org.gradle.api.Project; + +/** + * The {@link DevOpsPipeline} class. + */ +public class DevOpsPipeline { + + private final Project project; + + + public String name; + + + @Inject + public DevOpsPipeline(Project project) { + this.project = project; + } + + /** + * Gets the {@link Project}. + */ + public final Project getProject() { + return this.project; + } +} diff --git a/src/main/gradle/org/hivevm/gradle/DevOpsPlugin.java b/src/main/gradle/org/hivevm/gradle/DevOpsPlugin.java new file mode 100644 index 0000000..7399b92 --- /dev/null +++ b/src/main/gradle/org/hivevm/gradle/DevOpsPlugin.java @@ -0,0 +1,24 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.gradle; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.ExtensionContainer; + +/** + * The {@link DevOpsPlugin} defines the different tasks required for a smart.IO build management. + */ +public class DevOpsPlugin implements Plugin { + + private static final String CONFIG = "gradleDevOps"; + + @Override + public void apply(Project project) { + ExtensionContainer extension = project.getExtensions(); + extension.create(DevOpsPlugin.CONFIG, DevOpsConfig.class); + + // project.getTasks().register("generateParser", ParserGenerator.class); + } +} \ No newline at end of file diff --git a/src/main/java/org/hivevm/util/Builder.java b/src/main/java/org/hivevm/util/Builder.java new file mode 100644 index 0000000..183b20f --- /dev/null +++ b/src/main/java/org/hivevm/util/Builder.java @@ -0,0 +1,17 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util; + + +/** + * Builder is a creational design pattern that lets you construct complex objects step by step. The + * pattern allows you to produce different types and representations of an object using the same + * construction code. + * + * @see https://refactoring.guru/design-patterns/builder + */ +public interface Builder { + + T build(); +} diff --git a/src/main/java/org/hivevm/util/Version.java b/src/main/java/org/hivevm/util/Version.java new file mode 100644 index 0000000..80bd1d5 --- /dev/null +++ b/src/main/java/org/hivevm/util/Version.java @@ -0,0 +1,310 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The {@link Version} implements the semantic version syntax (https://semver.org/spec/v2.0.0.html). + * Depending on the software the major.minor.patch are interpreted differently. + * + * On the API the interpretation of the version number is following: + * + *
+ * - MAJOR version when you make incompatible API changes,
+ * - MINOR version when you add functionality in a backwards compatible manner, and
+ * - PATCH version when you make backwards compatible bug fixes.
+ * 
+ * + * On an released client software the version number is interpreted as following: + * + *
+ * - MAJOR defines the year of release,
+ * - MINOR defines the month of release
+ * - PATCH version when you make backwards compatible bug fixes.
+ * 
+ * + * For the interpretation of a full version text see the Backus–Naur Form Grammar from the + * specification. + * + * E.g.: + * + *
+ *   19.12
+ *   19.12.1
+ *   19.12.1-rc1
+ *   19.12-beta1+build.1.2
+ *   19.04
+ *   19.4+build.1.2
+ * 
+ */ +public class Version implements Comparable { + + public static final Version NONE = new Version(0, 0, 0, null, null); + + + private static final String PATTERN = + "(?\\d+)\\.(?\\d+)(?:\\.(?\\d+))?(?:-(?[a-zA-Z0-9.]+))?(?:\\+(?[a-zA-Z0-9.]+))?"; + + private static final Pattern PARSE = Pattern.compile(Version.PATTERN); + private static final Pattern MATCH = Pattern.compile("^" + Version.PATTERN + "$"); + private static final Pattern FORMAT = Pattern.compile("([0]+)\\.([0]+)(?:\\.([0]+))?(?:-([0]+))?(?:\\+([0]+))?"); + + + private final int major; + private final int minor; + private final int patch; + + private final String name; + private final String build; + + /** + * Constructs an instance of {@link Version}. + * + * @param major + * @param minor + * @param patch + * @param name + * @param build + */ + protected Version(int major, int minor, int patch, String name, String build) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.name = name; + this.build = build; + } + + /** + * Gets the major number. + */ + public final int getMajor() { + return this.major; + } + + /** + * Gets the minor number. + */ + public final int getMinor() { + return this.minor; + } + + /** + * Gets the patch number. + */ + public final int getPatch() { + return this.patch; + } + + /** + * Gets the pre-release name. + */ + public final String getName() { + return this.name; + } + + /** + * Gets the build text. + */ + public final String getBuild() { + return this.build; + } + + /** + * Compares this {@link Version} with the specified {@link Version} for order. + * + * @param other + */ + @Override + public int compareTo(Version other) { + if (getMajor() != other.getMajor()) { // Major version + return getMajor() > other.getMajor() ? -1 : 1; + } else if (getMinor() != other.getMinor()) { // Minor version + return getMinor() > other.getMinor() ? -1 : 1; + } else if (getPatch() != other.getPatch()) { // Patch version + return getPatch() > other.getPatch() ? -1 : 1; + } + return 0; + } + + /** + * Indicates whether some other object is "equal to" this one. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + return (obj instanceof Version) && (compareTo((Version) obj) == 0); + } + + /** + * Creates a new instance of {@link Version} using the provided build number. + * + * @param build + */ + public final Version build(String build) { + return new Version(getMajor(), getMinor(), getPatch(), getName(), build); + } + + /** + * Creates a new instance of {@link Version} using the provided build number. + * + * @param build + */ + public final Version build(long build) { + return build("" + build); + } + + /** + * Creates a new instance of {@link Version} using the provided name of the pre-release. + * + * @param name + */ + public final Version preRelease(String name) { + return new Version(getMajor(), getMinor(), getPatch(), name, getBuild()); + } + + /** + * Returns a string representation of the version. + */ + @Override + public final String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(getMajor()); + buffer.append("."); + buffer.append(getMinor()); + if (getPatch() > -1) { + buffer.append("."); + buffer.append(getPatch()); + } + if (getName() != null) { + buffer.append("-"); + buffer.append(getName()); + } + if (getBuild() != null) { + buffer.append("+"); + buffer.append(getBuild()); + } + return buffer.toString(); + } + + /** + * Returns a string representation of the version, using the provided format. + * + * @param format + */ + public final String toString(String format) { + Matcher matcher = Version.FORMAT.matcher(format); + if (!matcher.find()) { + return toString(); + } + + StringBuffer buffer = new StringBuffer(); + String text = "%0" + matcher.group(1).length() + "d.%0" + matcher.group(2).length() + "d"; + buffer.append(String.format(text, getMajor(), getMinor())); + if (matcher.group(3) != null) { + text = ".%0" + matcher.group(3).length() + "d"; + buffer.append(String.format(text, getPatch() < 0 ? 0 : getPatch())); + } + if ((matcher.group(4) != null) && (getName() != null)) { + buffer.append("-"); + buffer.append(getName()); + } + if ((matcher.group(5) != null) && (getBuild() != null)) { + buffer.append("+"); + buffer.append(getBuild()); + } + return buffer.toString(); + } + + /** + * Creates a new instance of {@link Version} + * + * @param major + * @param minor + */ + public static Version of(int major, int minor) { + return Version.of(major, minor, -1, null, null); + } + + /** + * Creates a new instance of {@link Version} + * + * @param major + * @param minor + * @param patch + */ + public static Version of(int major, int minor, int patch) { + return Version.of(major, minor, patch, null, null); + } + + /** + * Creates a new instance of {@link Version} + * + * @param major + * @param minor + * @param pre + * @param build + */ + public static Version of(int major, int minor, String pre, String build) { + return Version.of(major, minor, -1, pre, build); + } + + /** + * Creates a new instance of {@link Version} + * + * @param major + * @param minor + * @param patch + * @param pre + * @param build + */ + public static Version of(int major, int minor, int patch, String pre, String build) { + return new Version(major, minor, patch, pre, build); + } + + /** + * Parses a {@link Version} from the text. Instead of the {@link #parse(String)}, the method + * expects an exact matching of the version without any preceding and succeeding character. + * + * @param text + */ + public static Version of(String text) throws IllegalArgumentException { + return Version.parse(text, Version.MATCH); + } + + /** + * Parses a new instance of {@link Version} + * + * @param text + */ + public static Version parse(String text) throws IllegalArgumentException { + return Version.parse(text, Version.PARSE); + } + + /** + * Parses a new instance of {@link Version}. The provided pattern must contain named groups with + * the names: major, minor, patch, name, build. + * + * @param text + * @param pattern + */ + public static Version parse(String text, Pattern pattern) throws IllegalArgumentException { + if (text == null) { + return null; + } + + Matcher matcher = pattern.matcher(text); + if (!matcher.find()) { + throw new IllegalArgumentException("'" + text + "' is not a valid version"); + } + + int major = Integer.parseInt(matcher.group("major")); + int minor = Integer.parseInt(matcher.group("minor")); + int patch = (matcher.group("patch") == null) ? -1 : Integer.parseInt(matcher.group("patch")); + return Version.of(major, minor, patch, matcher.group("name"), matcher.group("build")); + } +} \ No newline at end of file diff --git a/src/main/java/org/hivevm/util/archive/Archive.java b/src/main/java/org/hivevm/util/archive/Archive.java new file mode 100644 index 0000000..cc1a9d3 --- /dev/null +++ b/src/main/java/org/hivevm/util/archive/Archive.java @@ -0,0 +1,144 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.archive; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; + +/** + * Gzipped Tar archiver which preserves + * + *
+ * - POSIX file permissions
+ * -Symbolic links (if the link target points inside the archive)
+ * -Last modification timestamp
+ * 
+ * + * In the archive as found in the filesystem for files to be archived. It uses GNU tar format + * extensions for archive entries with path length greate than 100. + */ +public abstract class Archive { + + private final File file; + private final String name; + + /** + * Creates a .tar.gz file + * + * @param file + * @param name + */ + public Archive(File file, String name) { + this.file = file; + this.name = name; + } + + /** + * Get the archive {@link File}. + */ + protected final File getFile() { + return this.file; + } + + /** + * Relativize the entry for the path. Returns null if the name doesn't match the + * path. + * + * @param name + * @param path + */ + protected final String relativize(String name, Path path) { + if (path == null) { + return name; + } + + Path entry = Paths.get(name); + if (entry.equals(path) || !entry.startsWith(path)) { + return null; + } + return path.relativize(entry).toString(); + } + + /** + * Creates a specific {@link InputStream}. + */ + protected InputStream getInputStream() throws IOException { + return new BufferedInputStream(new FileInputStream(getFile())); + } + + /** + * Creates a specific {@link OutputStream}. + */ + protected OutputStream getOutputStream() throws IOException { + return new BufferedOutputStream(new FileOutputStream(getFile())); + } + + /** + * Extract the files in a directory with the name of the file + */ + public final LocalDateTime extract() throws IOException { + return extractTo(new File(getFile().getParentFile(), this.name), null); + } + + /** + * Extract to the target directory and return the last modification time. + * + * @param target + */ + public final LocalDateTime extractTo(File target) throws IOException { + return extractTo(target, null); + } + + /** + * Extract the path from to the target directory and return the last modification time. + * + * @param target + * @param folder + */ + public abstract LocalDateTime extractTo(File target, Path folder) throws IOException; + + /** + * Creates an {@link ArchiveBuilder} to adding new files to the archive. + */ + public abstract ArchiveBuilder builder() throws IOException; + + /** + * Archive the files. + * + * @param file + */ + public static Archive of(File file) throws IOException { + String filename = file.getName(); + String name = filename.toLowerCase(); + + if (name.endsWith(".tar")) { + return new ArchiveTar(file, filename.substring(0, filename.length() - 4)); + } else if (name.endsWith(".tar.gz")) { + return new ArchiveTarGz(file, filename.substring(0, filename.length() - 7)); + } else if (name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".war")) { + return new ArchiveZip(file, filename.substring(0, filename.length() - 4)); + } + + throw new UnsupportedEncodingException("Unsupported compression format for " + file.getName()); + } + + /** + * Creates an {@link Archive} {@link ArchiveBuilder} for the file. + * + * @param file + */ + public static ArchiveBuilder builder(File file) throws IOException { + return Archive.of(file).builder(); + } +} \ No newline at end of file diff --git a/src/main/java/org/hivevm/util/archive/ArchiveBuilder.java b/src/main/java/org/hivevm/util/archive/ArchiveBuilder.java new file mode 100644 index 0000000..d7ad3e0 --- /dev/null +++ b/src/main/java/org/hivevm/util/archive/ArchiveBuilder.java @@ -0,0 +1,114 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.archive; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.List; + +/** + * Gzipped Tar archiver which preserves + * + *
+ * 
    + *
  • POSIX file permissions
  • + *
  • Symbolic links (if the link target points inside the archive)
  • + *
  • Last modification timestamp
  • +
+ *
+ * + * In the archive as found in the filesystem for files to be archived. It uses GNU tar format + * extensions for archive entries with path length > 100. + */ + +/** + * The {@link ArchiveBuilder} implements a closable that allows to add new files to the + * {@link ArchiveBuilder}. + */ +public abstract class ArchiveBuilder implements Closeable { + + private final OutputStream stream; + + /** + * Constructs an instance of {@link ArchiveBuilder}. + * + * @param stream + */ + protected ArchiveBuilder(OutputStream stream) { + this.stream = stream; + } + + /** + * Gets the {@link OutputStream}. + */ + protected OutputStream getOutputStream() { + return this.stream; + } + + /** + * Add a file to the {@link Archive} using the directory. + * + * @param directory + * @param pattern + * @param targetPath + */ + protected abstract void addToArchive(File directory, String pattern, String targetPath) throws IOException; + + /** + * Add a file to the {@link ArchiveBuilder} using the directory. + * + * @param location + * @param sourcePath + * @param targetPath + */ + public void addFile(File location, String sourcePath, String targetPath) throws IOException { + addToArchive(location, sourcePath, targetPath); + } + + /** + * Add a file to the {@link ArchiveBuilder} using the directory. + * + * @param directory + * @param file + * @param targetPath + */ + public final void addFile(File directory, File file, String targetPath) throws IOException { + Path path = directory.toPath().relativize(file.toPath()); + addFile(directory, path.toString().replace('\\', '/'), targetPath); + } + + /** + * Archives the files. + * + * @param files + * @param targetPath + */ + public final void addFiles(List files, String targetPath) throws IOException { + for (File file : files) { + addFile(file.getParentFile(), file, targetPath); + } + } + + /** + * Archives the files in the directory + * + * @param directory + */ + public final void addDirectory(File directory) throws IOException { + for (File file : directory.listFiles()) { + addFile(directory, file, null); + } + } + + /** + * Closes this stream and releases any system resources associated with it. + */ + @Override + public final void close() throws IOException { + this.stream.close(); + } +} \ No newline at end of file diff --git a/src/main/java/org/hivevm/util/archive/ArchiveTar.java b/src/main/java/org/hivevm/util/archive/ArchiveTar.java new file mode 100644 index 0000000..3b8609a --- /dev/null +++ b/src/main/java/org/hivevm/util/archive/ArchiveTar.java @@ -0,0 +1,172 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.archive; + + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.tar.TarConstants; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFileAttributes; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.Map; + +/** + * Gzipped Tar archiver which preserves + * + *
  • POSIX file permissions
  • Symbolic links (if the link target points inside the + * archive)
  • Last modification timestamp
+ * + * in the archive as found in the filesystem for files to be archived. It uses GNU tar format + * extensions for archive entries with path length > 100. + */ +class ArchiveTar extends Archive { + + /** + * Creates a .tar.gz file + * + * @param file + * @param name + */ + public ArchiveTar(File file, String name) { + super(file, name); + } + + /** + * Uncompress the provided TAR/GZ-file to the target location + * + * @param target + * @param path + */ + @Override + public LocalDateTime extractTo(File target, Path folder) throws IOException { + LocalDateTime local = null; + Map symLinks = new HashMap<>(); + try (TarArchiveInputStream stream = new TarArchiveInputStream(getInputStream())) { + TarArchiveEntry entry = stream.getNextEntry(); + while (entry != null) { + String entryName = relativize(entry.getName(), folder); + if (entryName == null) { + entry = stream.getNextEntry(); + continue; + } + + long lastModified = entry.getLastModifiedDate().getTime(); + LocalDateTime date = Instant.ofEpochMilli(lastModified).atOffset(ZoneOffset.UTC).toLocalDateTime(); + if ((local == null) || date.isAfter(local)) { + local = date; + } + + File newFile = ArchiveUtil.newFile(target, entry.getName()); + if (entry.isDirectory()) { + newFile.mkdirs(); + } else if (entry.isSymbolicLink()) { + symLinks.put(newFile, entry.getLinkName()); + } else { + newFile.getParentFile().mkdirs(); + ArchiveUtil.streamToFile(newFile, stream); + newFile.setLastModified(entry.getLastModifiedDate().getTime()); + newFile.setExecutable(PosixPerms.isExecuteable(entry.getMode())); + } + entry = stream.getNextEntry(); + } + + for (File file : symLinks.keySet()) { + Files.createSymbolicLink(file.toPath(), Paths.get(symLinks.get(file))); + } + } + return local; + } + + /** + * Archives the files. + * + * @param files + */ + @Override + public final ArchiveBuilder builder() throws IOException { + getFile().getAbsoluteFile().getParentFile().mkdirs(); + + TarArchiveOutputStream stream = new TarArchiveOutputStream(getOutputStream(), "UTF-8"); + stream.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU); + return new TarBuilder(stream); + } + + /** + * The {@link TarBuilder} creates a builder to adding files to a TAR. + */ + private class TarBuilder extends ArchiveBuilder { + + /** + * Constructs an instance of {@link TarBuilder}. + * + * @param stream + */ + private TarBuilder(TarArchiveOutputStream stream) { + super(stream); + } + + /** + * Gets the {@link OutputStream}. + */ + @Override + protected final TarArchiveOutputStream getOutputStream() { + return (TarArchiveOutputStream) super.getOutputStream(); + } + + /** + * Add a file to the {@link Archive} using the directory. + * + * @param directory + * @param pattern + * @param targetPath + */ + @Override + protected final void addToArchive(File directory, String pattern, String targetPath) throws IOException { + for (File file : ArchiveTree.findFiles(directory, pattern)) { + TarArchiveEntry entry = ArchiveTar.createTarEntry(directory, file, targetPath); + PosixFileAttributes attributes = PosixPerms.getAttributes(file); + if (attributes != null) { + entry.setMode(PosixPerms.toOctalFileMode(attributes.permissions())); + } + entry.setModTime(file.lastModified()); + + getOutputStream().putArchiveEntry(entry); + if (file.isFile() && !entry.isSymbolicLink()) { + ArchiveUtil.fileToStream(file, getOutputStream()); + } + getOutputStream().closeArchiveEntry(); + } + } + } + + + /** + * Create a {@link TarArchiveEntry}. + * + * @param root + * @param file + */ + private static TarArchiveEntry createTarEntry(File root, File file, String targetPath) throws IOException { + String path = ArchiveUtil.relativePath(root, file, targetPath); + + // only create symlink entry if link target is inside archive + if (ArchiveUtil.isSymbolicLink(file) && ArchiveUtil.resolvesBelow(file, root)) { + TarArchiveEntry entry = new TarArchiveEntry(path, TarConstants.LF_SYMLINK); + entry.setLinkName(ArchiveUtil.slashify(ArchiveUtil.getRelativeSymLinkTarget(file, file.getParentFile()))); + return entry; + } + return new TarArchiveEntry(file, path); + } +} diff --git a/src/main/java/org/hivevm/util/archive/ArchiveTarGz.java b/src/main/java/org/hivevm/util/archive/ArchiveTarGz.java new file mode 100644 index 0000000..8d50865 --- /dev/null +++ b/src/main/java/org/hivevm/util/archive/ArchiveTarGz.java @@ -0,0 +1,51 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.archive; + + +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Gzipped Tar archiver which preserves + * + *
  • POSIX file permissions
  • Symbolic links (if the link target points inside the + * archive)
  • Last modification timestamp
+ * + * in the archive as found in the filesystem for files to be archived. It uses GNU tar format + * extensions for archive entries with path length > 100. + */ +class ArchiveTarGz extends ArchiveTar { + + /** + * Creates a .tar.gz file + * + * @param file + * @param name + */ + public ArchiveTarGz(File file, String name) { + super(file, name); + } + + /** + * Creates a GZip {@link InputStream}. + */ + @Override + protected final InputStream getInputStream() throws IOException { + return new GzipCompressorInputStream(super.getInputStream()); + } + + /** + * Creates a GZip {@link OutputStream}. + */ + @Override + protected final OutputStream getOutputStream() throws IOException { + return new GzipCompressorOutputStream(super.getOutputStream()); + } +} diff --git a/src/main/java/org/hivevm/util/archive/ArchiveTree.java b/src/main/java/org/hivevm/util/archive/ArchiveTree.java new file mode 100644 index 0000000..c209ef7 --- /dev/null +++ b/src/main/java/org/hivevm/util/archive/ArchiveTree.java @@ -0,0 +1,114 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.archive; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The {@link ArchiveTree} class. + */ +class ArchiveTree extends SimpleFileVisitor { + + private final Path root; + private final Pattern pattern; + + private final List files = new ArrayList<>(); + + + /** + * Constructs an instance of {@link ArchiveTree}. + * + * @param root + * @param pattern + */ + private ArchiveTree(Path root, Pattern pattern) { + this.root = root; + this.pattern = pattern; + } + + /** + * Invoked for a directory before entries in the directory are visited. + * + * @param path + * @param attrs + */ + @Override + public final FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException { + checkPathPattern(path); + return FileVisitResult.CONTINUE; + } + + /** + * Invoked for a file in a directory. + * + * @param path + * @param attrs + */ + @Override + public final FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { + checkPathPattern(path); + return FileVisitResult.SKIP_SUBTREE; + } + + + /** + * Check against the pattern. + * + * @param path + */ + private final void checkPathPattern(Path path) throws IOException { + String input = this.root.relativize(path).toString(); + // Avoid problems on Windows + Matcher matcher = this.pattern.matcher(input.replace('\\', '/')); + if (matcher.find()) { + this.files.add(new File(this.root.toFile(), input)); + } + } + + /** + * The {@link FileComparator} checks that the files are ordered in following order: + * + *
+   * - directory
+   * - regular file
+   * - symbol link
+   * 
+ */ + private static class FileComparator implements Comparator { + + @Override + public int compare(File o1, File o2) { + int t1 = o1.isDirectory() ? 1 : ArchiveUtil.isSymbolicLink(o1) ? 3 : 2; + int t2 = o2.isDirectory() ? 1 : ArchiveUtil.isSymbolicLink(o2) ? 3 : 2; + return (t1 == t2) ? 0 : (t1 < t2 ? -1 : 1); + } + } + + /** + * Copy the file tree using the environment variables. + * + * @param workingDir + * @param pattern + */ + public static List findFiles(File workingDir, String filePattern) throws IOException { + Path workingPath = workingDir.toPath(); + Pattern pattern = Pattern.compile("^" + filePattern.replace(".", "\\.").replace("*", "[^/]*")); + ArchiveTree visitor = new ArchiveTree(workingPath, pattern); + Files.walkFileTree(workingPath, visitor); + Collections.sort(visitor.files, new FileComparator()); + return visitor.files; + } +} diff --git a/src/main/java/org/hivevm/util/archive/ArchiveUtil.java b/src/main/java/org/hivevm/util/archive/ArchiveUtil.java new file mode 100644 index 0000000..0b2b1c4 --- /dev/null +++ b/src/main/java/org/hivevm/util/archive/ArchiveUtil.java @@ -0,0 +1,107 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.archive; + + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.io.IOUtils; + +/** + * + * The {@link ArchiveUtil} class provides global helper functions. + */ +abstract class ArchiveUtil { + + /** + * Copy the file to the {@link OutputStream}. + * + * @param file + * @param stream + */ + public static void fileToStream(File file, OutputStream stream) throws IOException { + try (BufferedInputStream buffer = new BufferedInputStream(new FileInputStream(file))) { + IOUtils.copy(buffer, stream); + } + } + + /** + * Copy the file to the {@link OutputStream}. + * + * @param file + * @param stream + */ + public static void streamToFile(File file, InputStream stream) throws IOException { + try (BufferedOutputStream buffer = new BufferedOutputStream(new FileOutputStream(file))) { + IOUtils.copy(stream, buffer); + } + } + + /** + * Create the new file name. + * + * @param target + * @param name + */ + public static File newFile(File target, String name) throws IOException { + File file = new File(target, name); + String targetPath = target.getCanonicalPath(); + String targetFilePath = file.getCanonicalPath(); + + if (!targetFilePath.startsWith(targetPath + File.separator)) { + throw new IOException("Entry is outside of the target dir: " + name); + } + return file; + } + + /** + * true if the file is a symbolik link. + * + * @param file + */ + public static boolean isSymbolicLink(File file) { + return Files.isSymbolicLink(file.toPath()); + } + + + public static boolean resolvesBelow(File source, File baseDir) throws IOException { + return !ArchiveUtil.getRelativeSymLinkTarget(source, baseDir).startsWith(".."); + } + + public static Path getRelativeSymLinkTarget(File source, File baseDir) throws IOException { + Path sourcePath = source.toPath(); + Path linkTarget = Files.readSymbolicLink(sourcePath); + // link target may be relative, so we resolve it first + Path resolvedLinkTarget = sourcePath.getParent().resolve(linkTarget); + Path relative = baseDir.toPath().relativize(resolvedLinkTarget); + Path normalizedSymLinkPath = relative.normalize(); + System.out.println("Computed symlink target path " + ArchiveUtil.slashify(normalizedSymLinkPath) + " for symlink " + + source + " relative to " + baseDir); + return normalizedSymLinkPath; + } + + public static String slashify(Path path) { + String pathString = path.toString(); + if (File.separatorChar == '/') { + return pathString; + } else { + return pathString.replace(File.separatorChar, '/'); + } + } + + public static String relativePath(File root, File file, String targetPath) { + String path = ArchiveUtil.slashify(root.toPath().relativize(file.toPath())); + return (targetPath == null) ? path : ArchiveUtil.slashify(Paths.get(targetPath, path)); + } +} diff --git a/src/main/java/org/hivevm/util/archive/ArchiveZip.java b/src/main/java/org/hivevm/util/archive/ArchiveZip.java new file mode 100644 index 0000000..ebe9e5d --- /dev/null +++ b/src/main/java/org/hivevm/util/archive/ArchiveZip.java @@ -0,0 +1,139 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.archive; + + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * Gzipped Tar archiver which preserves + * + *
  • POSIX file permissions
  • Symbolic links (if the link target points inside the + * archive)
  • Last modification timestamp
+ * + * in the archive as found in the filesystem for files to be archived. It uses GNU tar format + * extensions for archive entries with path length > 100. + */ +class ArchiveZip extends Archive { + + /** + * Creates a .tar.gz file + * + * @param file + * @param name + */ + public ArchiveZip(File file, String name) { + super(file, name); + } + + /** + * Uncompress the provided ZIP-file to the target location + * + * @param target + * @param folder + */ + @Override + public final LocalDateTime extractTo(File target, Path folder) throws IOException { + LocalDateTime local = null; + try (ZipInputStream stream = new ZipInputStream(getInputStream())) { + ZipEntry entry = stream.getNextEntry(); + while (entry != null) { + String entryName = relativize(entry.getName(), folder); + if (entryName == null) { + entry = stream.getNextEntry(); + continue; + } + + LocalDateTime date = Instant.ofEpochMilli(entry.getTime()).atOffset(ZoneOffset.UTC).toLocalDateTime(); + if ((local == null) || date.isAfter(local)) { + local = date; + } + + File newFile = ArchiveUtil.newFile(target, entryName); + if (entry.isDirectory()) { + newFile.mkdirs(); + } else { + newFile.getParentFile().mkdirs(); + ArchiveUtil.streamToFile(newFile, stream); + newFile.setLastModified(entry.getTime()); + } + stream.closeEntry(); + entry = stream.getNextEntry(); + } + } + return local; + } + + /** + * Archives the files. + * + * @param files + */ + @Override + public final ArchiveBuilder builder() throws IOException { + getFile().getAbsoluteFile().getParentFile().mkdirs(); + return new ZipBuilder(new ZipOutputStream(getOutputStream(), Charset.defaultCharset())); + } + + /** + * The {@link TarBuilder} creates a builder to adding files to a TAR. + */ + private class ZipBuilder extends ArchiveBuilder { + + /** + * Constructs an instance of {@link TarBuilder}. + */ + private ZipBuilder(ZipOutputStream stream) { + super(stream); + } + + /** + * Gets the {@link OutputStream}. + */ + @Override + protected final ZipOutputStream getOutputStream() { + return (ZipOutputStream) super.getOutputStream(); + } + + /** + * Add the files matching the pattern to the {@link Archive}. Optional adds the path as prefix. + * + * @param directory + * @param pattern + * @param targetPath + */ + @Override + protected final void addToArchive(File directory, String pattern, String targetPath) throws IOException { + for (File file : ArchiveTree.findFiles(directory, pattern)) { + if (file.isFile()) { + ZipEntry entry = ArchiveZip.createZipEntry(directory, file, targetPath); + getOutputStream().putNextEntry(entry); + ArchiveUtil.fileToStream(file, getOutputStream()); + } + } + } + } + + + /** + * Create a {@link ZipEntry}. + * + * @param root + * @param file + */ + private static ZipEntry createZipEntry(File root, File file, String targetPath) throws IOException { + return new ZipEntry(ArchiveUtil.relativePath(root, file, targetPath)); + } + +} diff --git a/src/main/java/org/hivevm/util/archive/Assembly.java b/src/main/java/org/hivevm/util/archive/Assembly.java new file mode 100644 index 0000000..b0a1013 --- /dev/null +++ b/src/main/java/org/hivevm/util/archive/Assembly.java @@ -0,0 +1,160 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.archive; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The {@link Assembly} class. + */ +public class Assembly { + + private static final Pattern PATTERN_FILE = Pattern.compile("\\{([^}]+)\\}"); + private static final Pattern PATTERN_PATH = Pattern.compile("^([^\\{=>\\s]*)(?:/([^=>\\s]+))?(?:\\s*=>\\s*(.+))?$"); + + + private final File workingDir; + + + private File archive; + private final List patterns = new ArrayList<>(); + + /** + * Constructs an instance of {@link Assembly}. + * + * @param workingDir + */ + private Assembly(File workingDir) { + this.workingDir = workingDir; + } + + /** + * Get the archive + */ + public final File archive() { + return this.archive; + } + + /** + * Set the archive + * + * @param archive + */ + public final Assembly setArchive(File archive) { + this.archive = archive; + return this; + } + + /** + * Set the archive + * + * @param pattern + */ + public final Assembly setSources(String pattern) { + for (String p : pattern.split("[\\n]")) { + addPattern(p.trim()); + } + return this; + } + + /** + * Set the archive + * + * @param pattern + */ + public final Assembly addPattern(String pattern) { + this.patterns.add(pattern); + return this; + } + + /** + * Build the archive + */ + public final void build(Consumer consumer) throws IOException { + try (ArchiveBuilder builder = Archive.builder(this.archive)) { + for (String input : this.patterns) { + Matcher matcher = Assembly.PATTERN_PATH.matcher(input); + if (matcher.find()) { + File file = new File(this.workingDir, matcher.group(1)); + if (!file.exists()) { + file = new File(matcher.group(1)); + } + if (!file.exists()) { + throw new IOException("File '" + matcher.group(1) + "' does not exist"); + } + + if (matcher.group(2) != null) { + for (String sourcePath : Assembly.parsePatterns(matcher.group(2))) { + builder.addFile(file, sourcePath, matcher.group(3)); + } + } else if (!file.isDirectory()) { + builder.addFile(file.getParentFile(), file, matcher.group(3)); + } else { + builder.addDirectory(file); + } + } else { + throw new IOException("Couldn't find pattern '" + input + "'"); + } + } + } + } + + /** + * Parses all combinations of ile patterns. + * + * @param text + */ + private static List parsePatterns(String text) { + List files = new ArrayList<>(); + Matcher m = Assembly.PATTERN_FILE.matcher(text); + + int offset = 0; + while (m.find()) { + if (m.start() > offset) { + Assembly.appendPatternPart(files, text.substring(offset, m.start())); + } + Assembly.appendPatternPart(files, m.group(1).split(",")); + offset = m.end(); + } + if (offset < text.length()) { + Assembly.appendPatternPart(files, text.substring(offset)); + } + return files; + } + + /** + * Append the parts to the current pattern + * + * @param patterns + * @param parts + */ + private static void appendPatternPart(List patterns, String... parts) { + if (patterns.isEmpty()) { + patterns.addAll(Arrays.asList(parts)); + } else { + List list = new ArrayList<>(); + for (String part : parts) { + patterns.stream().forEach(p -> list.add(p + part)); + } + patterns.clear(); + patterns.addAll(list); + } + } + + /** + * Constructs an instance of {@link Assembly}. + * + * @param workingDir + */ + public static Assembly of(File workingDir) { + return new Assembly(workingDir); + } +} diff --git a/src/main/java/org/hivevm/util/archive/PosixPerms.java b/src/main/java/org/hivevm/util/archive/PosixPerms.java new file mode 100644 index 0000000..c3ca7b4 --- /dev/null +++ b/src/main/java/org/hivevm/util/archive/PosixPerms.java @@ -0,0 +1,92 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.archive; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; + +/** + * The POSIX file permission mask. + */ +class PosixPerms { + + private static final int OWNER_READ = 0400; + private static final int OWNER_WRITE = 0200; + private static final int OWNER_EXEC = 0100; + + private static final int GROUP_READ = 0040; + private static final int GROUP_WRITE = 0020; + private static final int GROUP_EXEC = 0010; + + private static final int OTHERS_READ = 0004; + private static final int OTHERS_WRITE = 0002; + private static final int OTHERS_EXEC = 0001; + + private PosixPerms() {} + + /** + * Converts a set of {@link PosixFilePermission} to chmod-style octal file mode. + */ + public static int toOctalFileMode(Set permissions) { + int result = 0; + for (PosixFilePermission permissionBit : permissions) { + switch (permissionBit) { + case OWNER_READ: + result |= PosixPerms.OWNER_READ; + break; + case OWNER_WRITE: + result |= PosixPerms.OWNER_WRITE; + break; + case OWNER_EXECUTE: + result |= PosixPerms.OWNER_EXEC; + break; + case GROUP_READ: + result |= PosixPerms.GROUP_READ; + break; + case GROUP_WRITE: + result |= PosixPerms.GROUP_WRITE; + break; + case GROUP_EXECUTE: + result |= PosixPerms.GROUP_EXEC; + break; + case OTHERS_READ: + result |= PosixPerms.OTHERS_READ; + break; + case OTHERS_WRITE: + result |= PosixPerms.OTHERS_WRITE; + break; + case OTHERS_EXECUTE: + result |= PosixPerms.OTHERS_EXEC; + break; + } + } + return result; + } + + public static boolean isExecuteable(int mode) { + return ((mode & PosixPerms.OWNER_EXEC) > 0) || ((mode & PosixPerms.GROUP_EXEC) > 0) + || ((mode & PosixPerms.OTHERS_EXEC) > 0); + } + + /** + * Get the {@link PosixFileAttributes} from the {@link File}. + * + * @param file + */ + public static PosixFileAttributes getAttributes(File file) { + PosixFileAttributeView view = + Files.getFileAttributeView(file.toPath(), PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + + try { + return (view == null) ? null : view.readAttributes(); + } catch (IOException e) {} + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/hivevm/util/env/Environment.java b/src/main/java/org/hivevm/util/env/Environment.java new file mode 100644 index 0000000..cdb65c3 --- /dev/null +++ b/src/main/java/org/hivevm/util/env/Environment.java @@ -0,0 +1,66 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.env; + +import java.util.Collections; +import java.util.Map; + +/** + * The {@link Environment} provides the environment variables for an execution context. Depends of + * the {@link Environment} implementation if the variables are available in read-only mode or in + * write mode. + */ +public interface Environment { + + /** + * Return true if the parameter is set. + * + * @param name + */ + boolean isSet(String name); + + /** + * Get a parameter by name. + * + * @param name + */ + String get(String name); + + /** + * Get the environment variables as map. + */ + Map toMap(); + + /** + * Creates a new {@link Environment} adding the {@link Map} for fallback's. + * + * @param map + */ + default Environment map(Map map) { + return new EnvironmentTree(map, this); + } + + /** + * Creates an empty {@link Environment}. + */ + static Environment empty() { + return Environment.of(Collections.emptyMap()); + } + + /** + * Creates an {@link Environment} from the system environment. + */ + static Environment system() { + return Environment.of(System.getenv()); + } + + /** + * Creates an {@link Environment} for the map. + * + * @param map + */ + static Environment of(Map map) { + return new EnvironmentMap(map); + } +} diff --git a/src/main/java/org/hivevm/util/env/EnvironmentMap.java b/src/main/java/org/hivevm/util/env/EnvironmentMap.java new file mode 100644 index 0000000..8ba7e94 --- /dev/null +++ b/src/main/java/org/hivevm/util/env/EnvironmentMap.java @@ -0,0 +1,61 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.env; + +import java.util.HashMap; +import java.util.Map; + +/** + * The {@link EnvironmentMap} provides a simple map of environment variables. The is read-only, it + * is not possible to change the values of the environment variable. + */ +class EnvironmentMap implements Environment { + + private final Map map; + + /** + * Constructs an instance of {@link EnvironmentMap}. + * + * @param map + */ + public EnvironmentMap(Map map) { + this.map = map; + } + + /** + * true if the parameter is set. + * + * @param name + */ + @Override + public boolean isSet(String name) { + return this.map.containsKey(name); + } + + /** + * Get a parameter by name. + * + * @param name + */ + @Override + public String get(String name) { + return this.map.get(name); + } + + /** + * Get the environment variables as map. + */ + @Override + public Map toMap() { + return new HashMap<>(this.map); + } + + /** + * Constructs an instance of {@link Environment}. + */ + @Override + public final String toString() { + return EnvironmentUtilToString.toString(this); + } +} diff --git a/src/main/java/org/hivevm/util/env/EnvironmentTree.java b/src/main/java/org/hivevm/util/env/EnvironmentTree.java new file mode 100644 index 0000000..7b443f6 --- /dev/null +++ b/src/main/java/org/hivevm/util/env/EnvironmentTree.java @@ -0,0 +1,62 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.env; + +import java.util.Map; + +/** + * The {@link EnvironmentTree} class. + */ +class EnvironmentTree extends EnvironmentMap { + + private final Environment parent; + + /** + * Constructs an instance of {@link EnvironmentTree}. + * + * @param map + * @param parent + */ + public EnvironmentTree(Map map, Environment parent) { + super(map); + this.parent = parent; + } + + /** + * Gets the parent {@link Environment}. + */ + protected final Environment getDelegate() { + return this.parent; + } + + /** + * Return true if the parameter is set. + * + * @param name + */ + @Override + public final boolean isSet(String name) { + return super.isSet(name) || getDelegate().isSet(name); + } + + /** + * Get a parameter by name. + * + * @param name + */ + @Override + public String get(String name) { + return super.isSet(name) ? super.get(name) : getDelegate().get(name); + } + + /** + * Get the environment variables as map. + */ + @Override + public Map toMap() { + Map variables = getDelegate().toMap(); + variables.putAll(super.toMap()); + return variables; + } +} diff --git a/src/main/java/org/hivevm/util/env/EnvironmentUtil.java b/src/main/java/org/hivevm/util/env/EnvironmentUtil.java new file mode 100644 index 0000000..0247bab --- /dev/null +++ b/src/main/java/org/hivevm/util/env/EnvironmentUtil.java @@ -0,0 +1,63 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.env; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The {@link EnvironmentUtil} class. + */ +public abstract class EnvironmentUtil { + + private static final Pattern NAMES = Pattern.compile("\\(\\?<([a-z][a-z_0-9]*)>", Pattern.CASE_INSENSITIVE); + private static final Pattern PARAMS = Pattern.compile("\\$([0-9]+|[a-z][a-z_0-9]*)", Pattern.CASE_INSENSITIVE); + + /** + * Constructs an instance of {@link EnvironmentUtil}. + */ + private EnvironmentUtil() {} + + /** + * Parses the group names from the pattern. + * + * @param pattern + */ + public static Set parseGroupNames(String pattern) { + Set names = new HashSet<>(); + Matcher matcher = EnvironmentUtil.NAMES.matcher(pattern); + while (matcher.find()) { + names.add(matcher.group(1)); + } + return names; + } + + /** + * Replaces the indexed or named placeholder's with the the parameter values. + * + * @param pattern + * @param environment + */ + public static String replace(String pattern, Environment environment) { + StringBuffer buffer = new StringBuffer(); + int offset = 0; + + Matcher matcher = EnvironmentUtil.PARAMS.matcher(pattern); + while (matcher.find()) { + String name = matcher.group(1); + String value = environment.get(name); + buffer.append(pattern.substring(offset, matcher.start(1) - 1)); + if (value == null) { + buffer.append("$" + name); + } else { + buffer.append(value); + } + offset = matcher.end(1); + } + buffer.append(pattern.substring(offset, pattern.length())); + return buffer.toString(); + } +} diff --git a/src/main/java/org/hivevm/util/env/EnvironmentUtilToString.java b/src/main/java/org/hivevm/util/env/EnvironmentUtilToString.java new file mode 100644 index 0000000..98918df --- /dev/null +++ b/src/main/java/org/hivevm/util/env/EnvironmentUtilToString.java @@ -0,0 +1,60 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.env; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * The {@link EnvironmentUtilToString} class. + */ +abstract class EnvironmentUtilToString { + + private static final String PATTERN = + String.format("{}%s {}={}\n", OS.isWindows() ? "set" : "export").replaceAll("\\{\\}", "%s"); + + /** + * Constructs an instance of {@link EnvironmentUtilToString}. + */ + private EnvironmentUtilToString() {} + + /** + * Creates the string for the {@link Environment}. + * + * @param environment + */ + public static String toString(Environment environment) { + StringBuffer buffer = new StringBuffer(); + String intent = ""; + List list = new ArrayList<>(); + EnvironmentUtilToString.collect(environment, list); + Map variables = new HashMap<>(); + for (Environment e : list) { + Map values = + e.toMap().entrySet().stream().filter(i -> !variables.containsKey(i.getKey()) && (i.getValue() != null)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + for (String name : values.keySet().stream().sorted().collect(Collectors.toList())) { + buffer.append(String.format(EnvironmentUtilToString.PATTERN, intent, name, e.get(name))); + } + variables.putAll(values); + intent += " "; + } + return buffer.toString(); + } + + /** + * Collects all {@link Environment}'s. + * + * @param list + */ + private static void collect(Environment env, List list) { + if (env instanceof EnvironmentTree) { + EnvironmentUtilToString.collect(((EnvironmentTree) env).getDelegate(), list); + } + list.add(env); + } +} diff --git a/src/main/java/org/hivevm/util/env/EnvironmentVariables.java b/src/main/java/org/hivevm/util/env/EnvironmentVariables.java new file mode 100644 index 0000000..d09fe68 --- /dev/null +++ b/src/main/java/org/hivevm/util/env/EnvironmentVariables.java @@ -0,0 +1,106 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.env; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + + +/** + * The {@link EnvironmentVariables} provides additional environment variables. + */ +public class EnvironmentVariables implements Environment { + + private final Map variables; + private final Environment environment; + + /** + * Constructs an instance of {@link EnvironmentVariables}. + * + * @param variables + * @param environment + */ + public EnvironmentVariables(Map variables, Environment environment) { + this.variables = variables; + this.environment = environment; + } + + /** + * Returns the names of all variables. + */ + public final Set getVariables() { + return this.variables.keySet(); + } + + /** + * Get the local variable by name. + * + * @param name + */ + public final String getVariable(String name) { + return this.variables.get(name); + } + + /** + * Set the local variable by name. + * + * @param name + */ + public final void setVariable(String name, String value) { + this.variables.put(name, value); + this.variables.put(name, value); + } + + /** + * Returns true if the environment contains the named variable. + * + * @param name + */ + @Override + public final boolean isSet(String name) { + return this.variables.containsKey(name) || this.environment.isSet(name); + } + + /** + * Returns the named environment variable, throws an exception otherwise. + * + * @param name + */ + @Override + public final String get(String name) { + return this.variables.containsKey(name) ? this.variables.get(name) : this.environment.get(name); + } + + /** + * Converts the environment variables as {@link Map}. + */ + @Override + public final Map toMap() { + Map map = new HashMap<>(this.environment.toMap()); + map.putAll(this.variables); + return map; + } + + + private Map toVariables() { + Map vars = + (this.environment instanceof EnvironmentVariables) ? ((EnvironmentVariables) this.environment).toVariables() + : new HashMap<>(); + vars.putAll(this.variables); + return this.variables; + } + + /** + * Creates a string of the local defined environment variables. + */ + @Override + public final String toString() { + Map vars = toVariables(); + return vars.isEmpty() ? "" + : "\n" + vars.keySet().stream().sorted().map(k -> String.format(" %s\t= %s", k, vars.get(k))) + .collect(Collectors.joining("\n")); + } +} diff --git a/src/main/java/org/hivevm/util/env/OS.java b/src/main/java/org/hivevm/util/env/OS.java new file mode 100644 index 0000000..b222f14 --- /dev/null +++ b/src/main/java/org/hivevm/util/env/OS.java @@ -0,0 +1,55 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.env; + + +/** + * The {@link OS} defines the available operating systems. + */ +public enum OS { + + MACOS, + LINUX, + WINDOWS; + + private static OS instance = null; + + /** + * Return true if it is windows. + */ + public static boolean isMacOS() { + return OS.current() == OS.MACOS; + } + + /** + * Return true if it is windows. + */ + public static boolean isLinux() { + return OS.current() == OS.LINUX; + } + + /** + * Return true if it is windows. + */ + public static boolean isWindows() { + return OS.current() == OS.WINDOWS; + } + + /** + * Get the current operating system. + */ + public static OS current() { + if (OS.instance == null) { + String name = System.getProperty("os.name").toLowerCase(); + if (name.contains("windows")) { + OS.instance = OS.WINDOWS; + } else if (name.contains("mac")) { + OS.instance = OS.MACOS; + } else { + OS.instance = OS.LINUX; + } + } + return OS.instance; + } +} diff --git a/src/main/java/org/hivevm/util/file/FileMatcher.java b/src/main/java/org/hivevm/util/file/FileMatcher.java new file mode 100644 index 0000000..df713d0 --- /dev/null +++ b/src/main/java/org/hivevm/util/file/FileMatcher.java @@ -0,0 +1,74 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.file; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.hivevm.util.env.Environment; +import org.hivevm.util.env.EnvironmentUtil; + +/** + * The {@link FileMatcher} is an utility that get all files that match the path pattern. The + * returned {@link FileMatcher}'s allow to replace the parameters of the input with the parameters + * found on the matches. + */ +public class FileMatcher { + + private final File file; + private final Environment environment; + + /** + * Constructs an instance of {@link FileMatcher}. + * + * @param file + * @param environment + */ + FileMatcher(File file, Environment environment) { + this.file = file; + this.environment = environment; + } + + /** + * Gets the {@link File}. + */ + public final File getFile() { + return this.file; + } + + /** + * Gets the named parameter. + */ + public final Environment getEnvironment() { + return this.environment; + } + + /** + * Replaces the indexed or named placeholder's with the the parameter values. + * + * @param pattern + */ + public final String map(String pattern) { + return EnvironmentUtil.replace(pattern, this.environment); + } + + /** + * Get the matching {@link File} as string. + */ + @Override + public String toString() { + return getFile().toString(); + } + + /** + * Resolve the input pattern on the working directory, to find all matching files. + * + * @param workingDir + * @param pattern + */ + public static List of(File workingDir, String pattern) throws IOException { + return FilePattern.matches(workingDir, pattern); + } +} \ No newline at end of file diff --git a/src/main/java/org/hivevm/util/file/FilePattern.java b/src/main/java/org/hivevm/util/file/FilePattern.java new file mode 100644 index 0000000..4a1dae3 --- /dev/null +++ b/src/main/java/org/hivevm/util/file/FilePattern.java @@ -0,0 +1,113 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.file; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.hivevm.util.env.Environment; +import org.hivevm.util.env.EnvironmentUtil; + +/** + * The {@link FilePattern} class. + */ +public class FilePattern extends SimpleFileVisitor { + + private final Path workingPath; + + private final Pattern pattern; + private final Set names; + private final List mappers = new ArrayList<>(); + + /** + * Constructs an instance of {@link FilePattern}. + * + * @param workingDir + * @param pattern + */ + private FilePattern(File workingDir, String pattern) { + this.workingPath = workingDir.toPath(); + this.pattern = Pattern.compile("^" + pattern + "$"); + this.names = EnvironmentUtil.parseGroupNames(pattern); + } + + /** + * Gets the list of {@link FileMatcher}. + */ + public final List getMappers() { + return this.mappers; + } + + @Override + public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException { + return visitPath(path, attrs); + } + + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { + return visitPath(path, attrs); + } + + private FileVisitResult visitPath(Path path, BasicFileAttributes attrs) { + String input = this.workingPath.relativize(path).toString(); + Matcher matcher = this.pattern.matcher(input.replace('\\', '/')); // for windows matches + if (matcher.find()) { + Environment e = FilePattern.getParameters(matcher, this.names); + this.mappers.add(new FileMatcher(path.toFile(), e)); + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + /** + * Get the indexed parameters from the matcher. + * + * @param matcher + * @param names + */ + private static Environment getParameters(Matcher matcher, Set names) { + Map params = new HashMap<>(); + params.put(Integer.toString(0), matcher.group(0)); + for (int index = 0; index < matcher.groupCount(); index++) { + params.put(Integer.toString(index + 1), matcher.group(index + 1)); + } + for (String name : names) { + params.put(name, matcher.group(name)); + } + return Environment.of(params); + } + + /** + * Converts the file pattern to a regular expression. + * + * @param pattern + */ + public static String toRegExp(String pattern) { + return pattern.replace(".", "\\.").replace("*", ".*").replace(",", "|").replace("{", "(").replace("}", ")"); + } + + /** + * Copy the file tree using the environment variables. + * + * @param workingDir + * @param pattern + */ + public static List matches(File workingDir, String pattern) throws IOException { + FilePattern visitor = new FilePattern(workingDir, pattern); + Files.walkFileTree(workingDir.toPath(), visitor); + return visitor.getMappers(); + } +} \ No newline at end of file diff --git a/src/main/java/org/hivevm/util/file/FileSystem.java b/src/main/java/org/hivevm/util/file/FileSystem.java new file mode 100644 index 0000000..1516ea3 --- /dev/null +++ b/src/main/java/org/hivevm/util/file/FileSystem.java @@ -0,0 +1,60 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.file; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; + +/** + * The {@link FileSystem} class. + */ +public abstract class FileSystem { + + /** + * Constructs an instance of {@link FileSystem}. + */ + private FileSystem() {} + + /** + * Delete the {@link File} or the whole directory and all its files. + * + * @param file + */ + public static boolean delete(File file) { + if (file.isDirectory()) { + if (!file.exists()) { + return false; + } + + try { + Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + return true; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return file.delete(); + } + + /** + * Get an absolute {@link File}. + * + * @param path + */ + public static File getFile(String path, File workingDir) { + return FileSystem.getFile(new File(path), workingDir); + } + + /** + * Get an absolute {@link File}. + * + * @param file + */ + public static File getFile(File file, File workingDir) { + return file.isAbsolute() ? file : workingDir.toPath().resolve(file.toPath()).toFile(); + } +} diff --git a/src/main/java/org/hivevm/util/file/FileTreeCopying.java b/src/main/java/org/hivevm/util/file/FileTreeCopying.java new file mode 100644 index 0000000..c635d59 --- /dev/null +++ b/src/main/java/org/hivevm/util/file/FileTreeCopying.java @@ -0,0 +1,111 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.file; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +/** + * The {@link FileTreeCopying} copies a directory structure from source to the target path. + */ +public final class FileTreeCopying extends SimpleFileVisitor { + + private final Path source; + private final Path target; + + + private Instant instant; + + /** + * + * Constructs an instance of {@link FileTreeCopying}. + * + * @param source + * @param target + */ + private FileTreeCopying(Path source, Path target) { + this.source = source; + this.target = target; + } + + /** + * Resolves the path. + * + * @param path + */ + private Path toPath(Path path) { + return this.target.resolve(this.source.relativize(path)); + } + + /** + * Resolves the path. + * + * @param path + */ + private Instant toInstant(Path path) { + try { + BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); + return attr.creationTime().toInstant(); + } catch (Throwable ex) {} + return null; + } + + /** + * Visit a directory. + * + * @param path + * @param attrs + */ + @Override + public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException { + Path dir = toPath(path); + if (!Files.exists(dir)) { + Files.createDirectory(dir); + } + return FileVisitResult.CONTINUE; + } + + /** + * Visit a file. + * + * @param path + * @param attrs + */ + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { + Path file = toPath(path); + Files.copy(path, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES, + LinkOption.NOFOLLOW_LINKS); + + Instant current = toInstant(path); + if ((this.instant == null) || current.isAfter(this.instant)) { + this.instant = current; + } + return FileVisitResult.CONTINUE; + } + + + /** + * Copy the file tree using the environment variables. + * + * @param source + * @param target + */ + public static LocalDate copyFileTree(Path source, Path target) throws IOException { + FileTreeCopying visitor = new FileTreeCopying(source, target); + Files.walkFileTree(source, visitor); + return visitor.instant == null ? LocalDate.now() + : LocalDateTime.ofInstant(visitor.instant, ZoneOffset.UTC).toLocalDate(); + } +} \ No newline at end of file diff --git a/src/main/java/org/hivevm/util/file/FileTreeLastModified.java b/src/main/java/org/hivevm/util/file/FileTreeLastModified.java new file mode 100644 index 0000000..a2f3dc6 --- /dev/null +++ b/src/main/java/org/hivevm/util/file/FileTreeLastModified.java @@ -0,0 +1,67 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.file; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +/** + * The {@link FileTreeLastModified} copies a directory structure from source to the target path. + */ +public final class FileTreeLastModified extends SimpleFileVisitor { + + private Instant instant; + + /** + * Visit a directory. + * + * @param path + * @param attrs + */ + @Override + public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + /** + * Visit a file. + * + * @param path + * @param attrs + */ + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { + try { + BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); + Instant current = attr.creationTime().toInstant(); + if ((this.instant == null) || current.isAfter(this.instant)) { + this.instant = current; + } + } catch (Throwable ex) { + throw new IOException(ex); + } + return FileVisitResult.CONTINUE; + } + + + /** + * Copy the file tree using the environment variables. + * + * @param source + */ + public static LocalDate lastModified(Path source) throws IOException { + FileTreeLastModified visitor = new FileTreeLastModified(); + Files.walkFileTree(source, visitor); + return visitor.instant == null ? LocalDate.now() + : LocalDateTime.ofInstant(visitor.instant, ZoneOffset.UTC).toLocalDate(); + } +} \ No newline at end of file diff --git a/src/main/java/org/hivevm/util/git/Repository.java b/src/main/java/org/hivevm/util/git/Repository.java new file mode 100644 index 0000000..c76021d --- /dev/null +++ b/src/main/java/org/hivevm/util/git/Repository.java @@ -0,0 +1,430 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.git; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.eclipse.jgit.api.CheckoutCommand; +import org.eclipse.jgit.api.CommitCommand; +import org.eclipse.jgit.api.CreateBranchCommand; +import org.eclipse.jgit.api.FetchCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeCommand.FastForwardMode; +import org.eclipse.jgit.api.PullCommand; +import org.eclipse.jgit.api.PullResult; +import org.eclipse.jgit.api.PushCommand; +import org.eclipse.jgit.api.StashCreateCommand; +import org.eclipse.jgit.api.TagCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode; +import org.eclipse.jgit.merge.ContentMergeStrategy; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.submodule.SubmoduleWalk; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.TagOpt; + +import org.hivevm.util.Version; + +/** + * The {@link Repository} provides a simple access to a GIT based repository, using the + * https://github.com/centic9/jgit-cookbook. The {@link Repository} hides the implementation details + * of the GIT repository and provides a simplified access. + */ +public class Repository implements AutoCloseable { + + private static final Pattern HASH = Pattern.compile("^[0-9a-fA-F]{40,40}$"); + + + private final Git git; + private final AnyObjectId oid; + private final CredentialsProvider credentials; + + private final List exceptions; + + /** + * Constructs an instance of {@link Repository}. + * + * @param git + * @param credentials + */ + Repository(Git git, CredentialsProvider credentials) { + this.git = git; + this.oid = null; + this.credentials = credentials; + this.exceptions = new ArrayList<>(); + } + + /** + * Constructs an instance of {@link Repository}. + * + * @param git + * @param oid + * @param parent + */ + Repository(Git git, AnyObjectId oid, Repository parent) { + this.git = git; + this.oid = oid; + this.credentials = parent.credentials; + this.exceptions = parent.exceptions; + } + + /** + * Get the location of the {@link Repository}. + */ + public final File getLocation() { + return getGit().getRepository().getWorkTree(); + } + + /** + * Get the {@link Git} handle. + */ + protected final Git getGit() { + return this.git; + } + + /** + * Get the {@link AnyObjectId} identifying the commit of the parent. + */ + protected final AnyObjectId getObjectId() { + return this.oid; + } + + /** + * Get the {@link CredentialsProvider}. + */ + protected final CredentialsProvider getCredentials() { + return this.credentials; + } + + /** + * Get exceptions for the current {@link Repository}. + */ + public final List getExceptions() { + List errors = new ArrayList<>(this.exceptions); + this.exceptions.clear(); + return errors; + } + + /** + * Get exceptions for the current {@link Repository}. + */ + public final Repository catchAndThrow() { + List list = getExceptions(); + if (!list.isEmpty()) { + throw new RuntimeException(list.stream().map(t -> t.getMessage()).collect(Collectors.joining("\n"))); + } + return this; + } + + /** + * Add an {@link Exception}. + * + * @param exception + */ + protected final void handleException(Exception exception) { + this.exceptions.add(exception); + } + + /** + * Create {@link Repository} related information for verbose. + */ + protected final RepositoryVerbose getVerbose() { + return new RepositoryVerbose(this.git, this.credentials); + } + + /** + * Get the current branch. + */ + public final String getBranch() throws IOException { + return getGit().getRepository().getBranch(); + } + + /** + * Get the latest {@link Repository} for a branch. + * + * @param version + */ + public final Revision getRevision(Version version) throws IOException { + return RepositoryVersion.getRevision(getGit(), version); + } + + /** + * Fetch all remote changes to the local repository. + */ + public final void fetch() { + forEach(r -> r.fetch()); + + FetchCommand command = getGit().fetch(); + command.setCredentialsProvider(getCredentials()); + command.setTagOpt(TagOpt.FETCH_TAGS); + command.setRemoveDeletedRefs(true); + command.setCheckFetchedObjects(true); + command.setRecurseSubmodules(FetchRecurseSubmodulesMode.YES); + + try { + command.call(); + } catch (GitAPIException e) { + handleException(e); + } + } + + /** + * Pull all remote changes to the local repository. + */ + public final void pull() { + forEach(r -> r.pull()); + + PullCommand command = getGit().pull(); + command.setCredentialsProvider(getCredentials()); + command.setFastForward(FastForwardMode.FF_ONLY); + command.setContentMergeStrategy(ContentMergeStrategy.OURS); + command.setRecurseSubmodules(FetchRecurseSubmodulesMode.YES); + + try { + PullResult result = command.call(); + if (!result.isSuccessful()) { + handleException(new RepositoryException("Pull aborted")); + } + } catch (GitAPIException e) { + handleException(e); + } + } + + /** + * Push all local changes to the remote repository. + */ + public final void push() { + forEach(r -> r.push()); + + PushCommand command = getGit().push(); + command.setCredentialsProvider(getCredentials()); + command.setForce(true); + + try { + command.call(); + } catch (GitAPIException e) { + handleException(e); + } + + command = getGit().push(); + command.setCredentialsProvider(getCredentials()); + command.setForce(true); + command.setPushTags(); + + try { + command.call(); + } catch (GitAPIException e) { + handleException(e); + } + } + + /** + * Commit all changes with the provided message. + * + * @param message + */ + public final void commit(String message) { + forEach(r -> r.commit(message)); + + CommitCommand command = getGit().commit(); + command.setCredentialsProvider(getCredentials()); + command.setMessage(message); + command.setAll(true); + + try { + if (!getGit().status().call().isClean()) { + command.call(); + } + } catch (GitAPIException e) { + handleException(e); + } + } + + /** + * Checkout a remote branch for all sub-modules. + */ + public final void checkout() { + forEach(r -> { + try { + r.stash(null); + r.checkout(r.getCommit(r.getObjectId())); + } catch (GitAPIException | IOException e) { + handleException(e); + } + }); + } + + /** + * Checkout a remote branch for all sub-modules. + */ + public final void checkoutHard() { + try { + checkout(getBranch()); + } catch (GitAPIException | IOException e) { + handleException(e); + } + } + + /** + * Checkout a remote branch. + * + * @param name + */ + public final RevCommit checkout(String name) throws GitAPIException, IOException { + Ref ref = getGit().getRepository().findRef(name); + + CheckoutCommand command = getGit().checkout(); + command.setCreateBranch(ref == null); + command.setStartPoint("origin/" + name).setName(name); + ref = command.call(); + forEach(r -> { + try { + r.stash(null); + r.checkout(r.getCommit(r.getObjectId()), name); + } catch (GitAPIException | IOException e) { + handleException(e); + } + }); + return ref == null ? null : getCommit(ref.getObjectId()); + } + + /** + * Pull all remote changes to the local repository. + */ + public final void tag(String tagName) { + TagCommand command = getGit().tag(); + command.setName(tagName); + command.setForceUpdate(true); + + try { + command.call(); + } catch (GitAPIException e) { + handleException(e); + } + } + + /** + * Pull all remote changes to the local repository. + */ + public final void stash(String message) { + StashCreateCommand command = getGit().stashCreate(); + command.setIncludeUntracked(false); + command.setIndexMessage(message); + command.setWorkingDirectoryMessage(message); + + try { + command.call(); + } catch (GitAPIException e) { + handleException(e); + } + } + + /** + * Get a commit form the hash, branch- or tag-name. + * + * @param name + */ + public final RevCommit getCommit(String name) throws GitAPIException, IOException { + Matcher matcher = Repository.HASH.matcher(name); + if (matcher.find()) { + return getCommit(ObjectId.fromString(name)); + } + Ref ref = getGit().getRepository().findRef(name); + return (ref == null) ? null : getCommit(ref.getObjectId()); + } + + /** + * Get a commit form the {@link ObjectId}. + * + * @param id + */ + protected final RevCommit getCommit(AnyObjectId id) throws GitAPIException, IOException { + try (RevWalk walk = new RevWalk(getGit().getRepository())) { + return walk.parseCommit(id); + } + } + + /** + * Checkout a commit to the current local brach. + * + * @param commit + */ + protected final void checkout(RevCommit commit) { + try { + String branch = getGit().getRepository().getBranch(); + checkout(commit, branch); + } catch (GitAPIException | IOException e) { + handleException(e); + } + } + + /** + * Checkout a commit to the current local brach. + * + * @param commit + */ + protected final void checkout(RevCommit commit, String branch) throws GitAPIException { + String hash = commit.getId().getName(); + + getGit().branchRename().setNewName(hash).setOldName(branch).call(); + getGit().branchDelete().setBranchNames(branch).setForce(true).call(); + getGit().checkout().setCreateBranch(true).setName(branch).setStartPoint(commit).call(); + getGit().branchDelete().setBranchNames(hash).setForce(true).call(); + } + + /** + * Create a branch at commit and checkout. + * + * @param commit + * @param branch + */ + protected final void branch(RevCommit commit, String branch) throws GitAPIException, IOException { + CreateBranchCommand command = getGit().branchCreate(); + command.setName(branch); + command.setForce(true); + command.setStartPoint(commit); + command.call(); + checkout(commit, branch); + } + + /** + * Iterates of all {@link Repository}'s of the sub modules. + * + * @param consumer + */ + public final void forEach(Consumer consumer) { + try (SubmoduleWalk walk = SubmoduleWalk.forIndex(getGit().getRepository())) { + while (walk.next()) { + if (walk.getRepository() != null) { + Git git = new Git(walk.getRepository()); + try (Repository repo = new Repository(git, walk.getObjectId(), this)) { + consumer.accept(repo); + } + } + } + } catch (IOException e) { + handleException(e); + } + } + + /** + * Closes the GIT {@link Repository}. + */ + @Override + public final void close() { + this.exceptions.forEach(e -> e.printStackTrace()); + this.git.getRepository().close(); + } +} diff --git a/src/main/java/org/hivevm/util/git/RepositoryBuilder.java b/src/main/java/org/hivevm/util/git/RepositoryBuilder.java new file mode 100644 index 0000000..dc6ba60 --- /dev/null +++ b/src/main/java/org/hivevm/util/git/RepositoryBuilder.java @@ -0,0 +1,190 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.git; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.TextProgressMonitor; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.submodule.SubmoduleWalk; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.TagOpt; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; + +import org.hivevm.util.Builder; + + +/** + * The {@link RepositoryBuilder} class. + */ +public class RepositoryBuilder implements Builder { + + private final File location; + + + private String remote; + private String branch; + private String username; + private String password; + + private ProgressMonitor monitor; + private final Set modules = new LinkedHashSet<>(); + + /** + * Constructs an instance of {@link RepositoryBuilder}. + * + * @param location + */ + public RepositoryBuilder(File location) { + this.location = location; + } + + /** + * Sets the remote {@link URI}. + * + * @param remote + */ + public final RepositoryBuilder setRemote(String remote) { + this.remote = remote; + return this; + } + + /** + * Sets the branch {@link URI}. + * + * @param branch + */ + public final RepositoryBuilder setBranch(String branch) { + this.branch = branch; + return this; + } + + /** + * Sets the credentials. + * + * @param username + * @param password + */ + public final RepositoryBuilder setCredentials(String username, String password) { + this.username = username; + this.password = password; + return this; + } + + /** + * Adds a GIT sub modules. + * + * @param modules + */ + public final RepositoryBuilder addSubModules(String... modules) { + this.modules.addAll(Arrays.asList(modules)); + return this; + } + + /** + * Enables a {@link ProgressMonitor}. + */ + public final RepositoryBuilder enableMonitor() { + this.monitor = new TextProgressMonitor(); + return this; + } + + /** + * Return true if the locatione exists. + */ + public final boolean isAvailable() { + return this.location.exists(); + } + + /** + * Get {@link CredentialsProvider}. + */ + protected final CredentialsProvider getCredentials() { + if ((this.username != null) && (this.password != null)) { + return new UsernamePasswordCredentialsProvider(this.username, this.password); + } + return null; + } + + /** + * Creates a {@link CloneCommand}. + * + * @param location + * @param remote + * @param credentials + */ + private CloneCommand createClone(File location, String remote, CredentialsProvider credentials) { + CloneCommand command = Git.cloneRepository(); + command.setDirectory(location); + command.setURI(remote.toString()).setCredentialsProvider(credentials); + command.setTagOption(TagOpt.FETCH_TAGS); + command.setCloneSubmodules(false); + command.setProgressMonitor(this.monitor); + return command; + } + + /** + * Creates the root {@link Git} repository. If not available, it will be checked out. + * + * @param credentials + */ + private Git getRepository(CredentialsProvider credentials) throws GitAPIException, IOException { + if (this.location.exists()) { + FileRepositoryBuilder builder = new FileRepositoryBuilder(); + builder.findGitDir(this.location); + return new Git(builder.build()); + } + + if ((this.remote == null) || (credentials == null)) { + throw new IllegalArgumentException("Remote and credentials are required for a checkout"); + } + + CloneCommand command = createClone(this.location, this.remote, credentials); + return command.setBranch(this.branch).call(); + } + + /** + * Build the {@link Repository} from the configuration in the builder. + */ + @Override + public final Repository build() { + CredentialsProvider credentials = getCredentials(); + try { + Git git = getRepository(credentials); + Repository root = new Repository(git, credentials); + try (SubmoduleWalk walk = SubmoduleWalk.forIndex(git.getRepository())) { + while (walk.next()) { + if (!this.modules.contains(walk.getModulesPath())) { + continue; + } + + if (walk.getRepository() == null) { + File localPath = new File(git.getRepository().getWorkTree(), walk.getPath()); + CloneCommand command = createClone(localPath, walk.getRemoteUrl(), credentials); + try (Repository repo = new Repository(command.call(), walk.getObjectId(), root)) { + RevCommit commit = repo.getCommit(walk.getObjectId()); + repo.branch(commit, this.branch); + } + } + } + } catch (ConfigInvalidException e) { + throw new IllegalArgumentException(e); + } + return root; + } catch (GitAPIException | IOException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/src/main/java/org/hivevm/util/git/RepositoryException.java b/src/main/java/org/hivevm/util/git/RepositoryException.java new file mode 100644 index 0000000..05daac2 --- /dev/null +++ b/src/main/java/org/hivevm/util/git/RepositoryException.java @@ -0,0 +1,24 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.git; + +import org.eclipse.jgit.api.errors.GitAPIException; + + +/** + * The {@link RepositoryException} class. + */ +class RepositoryException extends GitAPIException { + + private static final long serialVersionUID = 1262732979855931184L; + + /** + * Constructs an instance of {@link RepositoryException}. + * + * @param message + */ + public RepositoryException(String message) { + super(message); + } +} diff --git a/src/main/java/org/hivevm/util/git/RepositoryVerbose.java b/src/main/java/org/hivevm/util/git/RepositoryVerbose.java new file mode 100644 index 0000000..5c3291d --- /dev/null +++ b/src/main/java/org/hivevm/util/git/RepositoryVerbose.java @@ -0,0 +1,69 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.git; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ListBranchCommand.ListMode; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.submodule.SubmoduleWalk; +import org.eclipse.jgit.transport.CredentialsProvider; + +import java.io.IOException; +import java.util.Collection; +import java.util.function.Consumer; + +/** + * The {@link RepositoryVerbose} class. + */ +class RepositoryVerbose { + + private final Git repository; + private final CredentialsProvider credentials; + + /** + * Constructs an instance of {@link RepositoryVerbose}. + * + * @param repository + * @param credentials + */ + public RepositoryVerbose(Git repository, CredentialsProvider credentials) { + this.repository = repository; + this.credentials = credentials; + } + + /** + * List all branches. + */ + public final Collection listBranches() throws GitAPIException { + return this.repository.branchList().setListMode(ListMode.ALL).call(); + } + + /** + * List all local tags. + */ + public final Collection listTags() throws GitAPIException { + return this.repository.tagList().call(); + } + + /** + * List all remote tags. + */ + public final Collection listRemoteTags() throws GitAPIException { + return this.repository.lsRemote().setCredentialsProvider(this.credentials).setTags(true).call(); + } + + /** + * Iterates of every sub-module. + * + * @param module + */ + public final void forEach(Consumer module) throws IOException { + try (SubmoduleWalk generator = SubmoduleWalk.forIndex(this.repository.getRepository())) { + while (generator.next()) { + module.accept(generator); + } + } + } +} diff --git a/src/main/java/org/hivevm/util/git/RepositoryVersion.java b/src/main/java/org/hivevm/util/git/RepositoryVersion.java new file mode 100644 index 0000000..0499187 --- /dev/null +++ b/src/main/java/org/hivevm/util/git/RepositoryVersion.java @@ -0,0 +1,168 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.git; + +import java.io.IOException; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.Collection; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.revwalk.RevWalkUtils; + +import org.hivevm.util.Version; + +/** + * The {@link RepositoryVersion} class. + */ +abstract class RepositoryVersion { + + private static final Pattern PATTERN = Pattern.compile( + "(?\\d+)[./](?\\d+)(?:[./](?\\d+))?(?:-(?[a-zA-Z0-9.]+))?(?:\\+(?[a-zA-Z0-9.]+))?"); + + + /** + * Constructs an instance of {@link RepositoryVersion}. + */ + private RepositoryVersion() {} + + /** + * Get the {@link OffsetDateTime} for the {@link RevCommit}. + * + * @param revCommit + */ + public static OffsetDateTime getTime(RevCommit revCommit) { + long instant = revCommit.getAuthorIdent().getWhen().getTime(); + return Instant.ofEpochMilli(instant).atZone(ZoneId.systemDefault()).toOffsetDateTime(); + } + + /** + * Count the number of commits {@link #getCount}. + * + * @param git + */ + public static long getCommitCount(Git git) throws GitAPIException { + return StreamSupport.stream(git.log().call().spliterator(), false).count(); + } + + /** + * Get all reachable tags, ordered by version number. + * + * @param git + * @param version + */ + public static Revision getRevision(Git git, Version version) throws IOException { + String branch = git.getRepository().getBranch(); + ObjectId refId = git.getRepository().resolve("HEAD"); + + try (RevWalk walk = new RevWalk(git.getRepository())) { + RevCommit revCommit = walk.parseCommit(refId); + OffsetDateTime time = RepositoryVersion.getTime(revCommit); + String hash = revCommit.getName().substring(0, 9); + long buildNumber = RepositoryVersion.getCommitCount(git); + + if (Version.NONE.equals(version)) { + Stream stream = RepositoryVersion.getTags(git, revCommit, walk).stream(); + version = stream.map(tag -> tag.getVersion()).findFirst().orElse(Version.of(0, 0)); + } + + return new Revision(hash, time, version.build(buildNumber).preRelease(branch)); + } catch (GitAPIException e) { + throw new IOException("Revision is not available on GIT", e); + } + } + + + /** + * Get all reachable tags, ordered by version number. + * + * @param git + * @param rev + * @param walk + */ + private static Collection getTags(Git git, RevCommit rev, RevWalk walk) throws GitAPIException { + return git.tagList().call().stream().map(tag -> { + try { + RevCommit tagCommit = walk.parseCommit(tag.getObjectId()); + if (walk.isMergedInto(tagCommit, rev)) { + Version version = Version.parse(tag.getName(), RepositoryVersion.PATTERN); + int count = RevWalkUtils.count(walk, rev, tagCommit); + return new TagInfo(tag, count, version); + } + } catch (Exception e) { + System.err.println(e.getMessage()); + } + return new TagInfo(tag, -1, null); + }).filter(i -> i.count != -1).sorted().collect(Collectors.toList()); + } + + + /** + * The {@link TagInfo} class. + */ + public static class TagInfo implements Comparable { + + private final Ref ref; + private final int count; + private final Version version; + + /** + * Constructs an instance of {@link TagInfo}. + * + * @param ref + */ + private TagInfo(Ref ref) { + this(ref, -1, null); + } + + /** + * Constructs an instance of {@link TagInfo}. + * + * @param ref + * @param count + * @param version + */ + private TagInfo(Ref ref, int count, Version version) { + this.ref = ref; + this.count = count; + this.version = version; + } + + /** + * Gets the {@link #ref}. + */ + public final Ref getRef() { + return this.ref; + } + + /** + * Gets the {@link #ref}. + */ + public final String getName() { + return getRef().getName(); + } + + /** + * Gets the {@link #version}. + */ + public final Version getVersion() { + return this.version; + } + + @Override + public int compareTo(TagInfo o) { + return (this.count == o.count) ? this.version.compareTo(o.version) : Integer.compare(this.count, o.count); + } + } +} diff --git a/src/main/java/org/hivevm/util/git/Revision.java b/src/main/java/org/hivevm/util/git/Revision.java new file mode 100644 index 0000000..466b87e --- /dev/null +++ b/src/main/java/org/hivevm/util/git/Revision.java @@ -0,0 +1,95 @@ +// Copyright 2024 HiveVM.ORG. All rights reserved. +// SPDX-License-Identifier: MIT + +package org.hivevm.util.git; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.hivevm.util.Version; + +/** + * The {@link Revision} is a utility to fetch version information from the GIT repository. + */ +public class Revision extends Version { + + private static final Pattern BUILDNUMBER = Pattern.compile("^[^\\d]*(\\d+)$"); + private static final DateTimeFormatter OFFSET_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-mm hh:mm:ss xx"); + + private final String hash; + private final OffsetDateTime time; + + /** + * Constructs an instance of {@link Revision}. + * + * @param hash + * @param time + * @param version + */ + public Revision(String hash, OffsetDateTime time, Version version) { + super(version.getMajor(), version.getMinor(), version.getPatch(), version.getName(), version.getBuild()); + this.hash = hash; + this.time = time; + } + + /** + * Gets the commit hash. + */ + public final String getHash() { + return this.hash; + } + + /** + * Gets the commit {@link OffsetDateTime}. + */ + public final OffsetDateTime getTime() { + return this.time; + } + + /** + * Gets the commit date/time as ISO format. + */ + public final String getISOTime() { + return this.time.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + /** + * Gets the commit date/time as basic ISO format. + */ + public final String getSimpleTime() { + return this.time.format(Revision.OFFSET_DATETIME); + } + + /** + * Extracts the build number from the build text. + */ + public final long getBuildNumber() { + Matcher matcher = Revision.BUILDNUMBER.matcher(getBuild()); + return matcher.find() ? Long.parseLong(matcher.group(1)) : 0; + } + + /** + * Gets the hash of the commit. + */ + public final String getRelease() { + return toString("0.0"); + } + + /** + * Gets the version. + */ + public final String getVersion() { + return toString("0.0.0"); + } + + /** + * Creates a new instance of {@link Revision} using the {@link Version}. + * + * @param version + */ + public final Revision build(Version version) { + return new Revision(getHash(), getTime(), version); + } +} diff --git a/src/test/java/org/hivevm/util/DockerTest.java b/src/test/java/org/hivevm/util/DockerTest.java new file mode 100644 index 0000000..7cbaf4a --- /dev/null +++ b/src/test/java/org/hivevm/util/DockerTest.java @@ -0,0 +1,25 @@ + +package org.hivevm.util; + +import java.util.List; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.Image; +import com.github.dockerjava.core.DockerClientBuilder; + +import org.junit.jupiter.api.Test; + +public class DockerTest { + + @Test + void testDocker() { + DockerClient client = DockerClientBuilder.getInstance().build(); + + List containers = client.listContainersCmd().exec(); + containers.forEach(c -> System.out.printf("Container: %s\n", c.getId())); + + List images = client.listImagesCmd().exec(); + images.forEach(i -> System.out.printf("Image: %s\n", i.getId())); + } +} diff --git a/src/test/java/org/hivevm/util/VersionTest.java b/src/test/java/org/hivevm/util/VersionTest.java new file mode 100644 index 0000000..8a86274 --- /dev/null +++ b/src/test/java/org/hivevm/util/VersionTest.java @@ -0,0 +1,138 @@ + +package org.hivevm.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.text.ParseException; + +public class VersionTest { + + @Test + void testFormat() { + Version version = Version.of(19, 3); + Assertions.assertEquals("19.3", version.toString()); + Assertions.assertEquals("19.03", version.toString("0.00")); + Assertions.assertEquals("19.03.0", version.toString("0.00.0")); + Assertions.assertEquals("19.03", version.toString("0.00-0")); + Assertions.assertEquals("19.03.0", version.toString("0.00.0+0")); + + version = Version.of(19, 3, 5); + Assertions.assertEquals("19.3.5", version.toString()); + Assertions.assertEquals("19.03", version.toString("0.00")); + Assertions.assertEquals("19.03.5", version.toString("0.00.0")); + Assertions.assertEquals("19.03", version.toString("0.00+0")); + Assertions.assertEquals("19.03.5", version.toString("0.00.0+0")); + + version = Version.of(19, 3, null, "build1234"); + Assertions.assertEquals("19.3+build1234", version.toString()); + Assertions.assertEquals("19.03", version.toString("0.00")); + Assertions.assertEquals("19.03.0", version.toString("0.00.0")); + Assertions.assertEquals("19.03+build1234", version.toString("0.00+0")); + Assertions.assertEquals("19.03.0+build1234", version.toString("0.00.0+0")); + + version = Version.of(19, 3, 2, null, "build1234"); + Assertions.assertEquals("19.3.2+build1234", version.toString()); + Assertions.assertEquals("19.03", version.toString("0.00")); + Assertions.assertEquals("19.03.2", version.toString("0.00.0")); + Assertions.assertEquals("19.03+build1234", version.toString("0.00+0")); + Assertions.assertEquals("19.03.2+build1234", version.toString("0.00.0+0")); + + version = Version.of(19, 3, "alpha1", "build1234"); + Assertions.assertEquals("19.3-alpha1+build1234", version.toString()); + Assertions.assertEquals("19.03", version.toString("0.00")); + Assertions.assertEquals("19.03.0", version.toString("0.00.0")); + Assertions.assertEquals("19.03+build1234", version.toString("0.00+0")); + Assertions.assertEquals("19.03.0-alpha1+build1234", version.toString("0.00.0-0+0")); + } + + @Test + void testOf() throws ParseException { + Version version = Version.parse("19.03"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(-1, version.getPatch()); + Assertions.assertNull(version.getName()); + Assertions.assertNull(version.getBuild()); + + version = Version.parse("19.03.1"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(1, version.getPatch()); + Assertions.assertNull(version.getName()); + Assertions.assertNull(version.getBuild()); + + version = Version.parse("19.03+build.1"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(-1, version.getPatch()); + Assertions.assertNull(version.getName()); + Assertions.assertEquals("build.1", version.getBuild()); + + version = Version.parse("19.03-rc1"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(-1, version.getPatch()); + Assertions.assertEquals("rc1", version.getName()); + Assertions.assertNull(version.getBuild()); + + version = Version.parse("19.03.1+build1234"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(1, version.getPatch()); + Assertions.assertNull(version.getName()); + Assertions.assertEquals("build1234", version.getBuild()); + + version = Version.parse("19.03.1-beta+build1234"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(1, version.getPatch()); + Assertions.assertEquals("beta", version.getName()); + Assertions.assertEquals("build1234", version.getBuild()); + } + + @Test + void testParse() throws ParseException { + Version version = Version.parse("Sample v.19.03_some text"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(-1, version.getPatch()); + Assertions.assertNull(version.getName()); + Assertions.assertNull(version.getBuild()); + + version = Version.parse("Sample v.19.03.1_some text"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(1, version.getPatch()); + Assertions.assertNull(version.getName()); + Assertions.assertNull(version.getBuild()); + + version = Version.parse("Sample v.19.03+build.1_some text"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(-1, version.getPatch()); + Assertions.assertNull(version.getName()); + Assertions.assertEquals("build.1", version.getBuild()); + + version = Version.parse("Sample v.19.03-rc1-some text"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(-1, version.getPatch()); + Assertions.assertEquals("rc1", version.getName()); + Assertions.assertNull(version.getBuild()); + + version = Version.parse("Sample v.19.03.1+build1234_some text"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(1, version.getPatch()); + Assertions.assertNull(version.getName()); + Assertions.assertEquals("build1234", version.getBuild()); + + version = Version.parse("Sample v.19.03.1-beta+build1234-some text"); + Assertions.assertEquals(19, version.getMajor()); + Assertions.assertEquals(3, version.getMinor()); + Assertions.assertEquals(1, version.getPatch()); + Assertions.assertEquals("beta", version.getName()); + Assertions.assertEquals("build1234", version.getBuild()); + } +}