initial commit

groovy3
Jörg Prante 3 years ago
commit 57dc419fe3

14
.gitignore vendored

@ -0,0 +1,14 @@
data
work
logs
build
target
/.idea
.DS_Store
*.iml
/.settings
/.classpath
/.project
/.gradle
*~
*.orig

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,34 @@
plugins {
id "de.marcphilipp.nexus-publish" version "0.4.0"
id "io.codearte.nexus-staging" version "0.21.1"
}
wrapper {
gradleVersion = "${project.property('gradle.wrapper.version')}"
distributionType = Wrapper.DistributionType.ALL
}
ext {
user = 'jprante'
name = 'groovy-extensions'
description = 'Groovy extensions'
inceptionYear = '2021'
url = 'https://github.com/' + user + '/' + name
scmUrl = 'https://github.com/' + user + '/' + name
scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
scmDeveloperConnection = 'scm:git:ssh://git@github.com:' + user + '/' + name + '.git'
issueManagementSystem = 'Github'
issueManagementUrl = ext.scmUrl + '/issues'
licenseName = 'The Apache License, Version 2.0'
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
subprojects {
apply plugin: 'java-library'
apply from: rootProject.file('gradle/ide/idea.gradle')
apply from: rootProject.file('gradle/compile/java.gradle')
apply from: rootProject.file('gradle/test/junit5.gradle')
apply from: rootProject.file('gradle/repositories/maven.gradle')
apply from: rootProject.file('gradle/publishing/publication.gradle')
}
apply from: rootProject.file('gradle/publishing/sonatype.gradle')

@ -0,0 +1,11 @@
group = org.xbib
name = groovy-extensions
version = 0.0.1
groovy.version = 2.5.12
gradle.wrapper.version = 6.6.1
ftp.version = 2.6.0
mail.version = 1.6.2
sshd.version = 2.6.0.0
log4j.version = 2.14.0
junit4.version = 4.13

@ -0,0 +1,34 @@
apply plugin: 'groovy'
dependencies {
implementation "org.codehaus.groovy:groovy:${project.property('groovy.version')}:indy"
}
compileGroovy {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
compileTestGroovy {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType(GroovyCompile) {
options.compilerArgs
if (!options.compilerArgs.contains("-processor")) {
options.compilerArgs << '-proc:none'
}
groovyOptions.optimizationOptions.indy = true
}
task groovydocJar(type: Jar, dependsOn: 'groovydoc') {
from groovydoc.destinationDir
archiveClassifier.set('javadoc')
}
configurations.all {
resolutionStrategy {
force "org.codehaus.groovy:groovy:${project.property('groovy.version')}:indy"
}
}

@ -0,0 +1,43 @@
apply plugin: 'java-library'
java {
modularity.inferModulePath.set(true)
}
compileJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
compileTestJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
jar {
manifest {
attributes('Implementation-Version': project.version)
}
}
task sourcesJar(type: Jar, dependsOn: classes) {
classifier 'sources'
from sourceSets.main.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier 'javadoc'
}
artifacts {
archives sourcesJar, javadocJar
}
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:all,-fallthrough'
}
javadoc {
options.addStringOption('Xdoclint:none', '-quiet')
}

@ -0,0 +1,55 @@
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
configurations {
asciidoclet
}
dependencies {
asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}"
}
asciidoctor {
backends 'html5'
outputDir = file("${rootProject.projectDir}/docs")
separateOutputDirs = false
attributes 'source-highlighter': 'coderay',
idprefix: '',
idseparator: '-',
toc: 'left',
doctype: 'book',
icons: 'font',
encoding: 'utf-8',
sectlink: true,
sectanchors: true,
linkattrs: true,
imagesdir: 'img',
stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css"
}
/*javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = 'org.asciidoctor.Asciidoclet'
//options.overview = "src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}"
configure(options) {
noTimestamp = true
}
}*/
/*javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = 'org.asciidoctor.Asciidoclet'
options.overview = "${rootProject.projectDir}/src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}"
options.destinationDirectory(file("${projectDir}/docs/javadoc"))
configure(options) {
noTimestamp = true
}
}*/

@ -0,0 +1,13 @@
apply plugin: 'idea'
idea {
module {
outputDir file('build/classes/java/main')
testOutputDir file('build/classes/java/test')
}
}
if (project.convention.findPlugin(JavaPluginConvention)) {
//sourceSets.main.output.classesDirs = file("build/classes/java/main")
//sourceSets.test.output.classesDirs = file("build/classes/java/test")
}

@ -0,0 +1,66 @@
apply plugin: "de.marcphilipp.nexus-publish"
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
pom {
name = project.name
description = rootProject.ext.description
url = rootProject.ext.url
inceptionYear = rootProject.ext.inceptionYear
packaging = 'jar'
organization {
name = 'xbib'
url = 'https://xbib.org'
}
developers {
developer {
id = 'jprante'
name = 'Jörg Prante'
email = 'joergprante@gmail.com'
url = 'https://github.com/jprante'
}
}
scm {
url = rootProject.ext.scmUrl
connection = rootProject.ext.scmConnection
developerConnection = rootProject.ext.scmDeveloperConnection
}
issueManagement {
system = rootProject.ext.issueManagementSystem
url = rootProject.ext.issueManagementUrl
}
licenses {
license {
name = rootProject.ext.licenseName
url = rootProject.ext.licenseUrl
distribution = 'repo'
}
}
}
}
}
}
if (project.hasProperty("signing.keyId")) {
apply plugin: 'signing'
signing {
sign publishing.publications.mavenJava
}
}
if (project.hasProperty("ossrhUsername")) {
nexusPublishing {
repositories {
sonatype {
username = project.property('ossrhUsername')
password = project.property('ossrhPassword')
packageGroup = "org.xbib"
}
}
}
}

@ -0,0 +1,11 @@
if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) {
apply plugin: 'io.codearte.nexus-staging'
nexusStaging {
username = project.property('ossrhUsername')
password = project.property('ossrhPassword')
packageGroup = "org.xbib"
}
}

@ -0,0 +1,4 @@
repositories {
mavenLocal()
mavenCentral()
}

@ -0,0 +1,27 @@
def junitVersion = project.hasProperty('junit.version')?project.property('junit.version'):'5.6.2'
def hamcrestVersion = project.hasProperty('hamcrest.version')?project.property('hamcrest.version'):'2.2'
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
testImplementation "org.hamcrest:hamcrest-library:${hamcrestVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
}
test {
useJUnitPlatform()
failFast = true
testLogging {
events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED'
}
afterSuite { desc, result ->
if (!desc.parent) {
println "\nTest result: ${result.resultType}"
println "Test summary: ${result.testCount} tests, " +
"${result.successfulTestCount} succeeded, " +
"${result.failedTestCount} failed, " +
"${result.skippedTestCount} skipped"
}
}
}

Binary file not shown.

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# 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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# 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
;;
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"
which java >/dev/null 2>&1 || 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
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored

@ -0,0 +1,89 @@
@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
@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=.
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%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
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%"=="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!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

@ -0,0 +1,18 @@
Credits go to Carl Harris (soulwing) who created crpyt4j
http://www.solewing.org/blog/2014/02/unix-compatible-password-encryption-for-java/
and released it under Apache Software License 2.0.
This software was converted to Groovy code, with HMAC methods added.
Credits to Apache Commons UnixCrypt
https://github.com/apache/commons-codec/blob/master/src/main/java/org/apache/commons/codec/digest/UnixCrypt.java
Daniel Dyer (https://maths.uncommons.org/) for the Java port of Mersenne Twister Random
Flake IDs, used for UUID generation, are based on
http://boundary.com/blog/2012/01/12/flake-a-decentralized-k-ordered-unique-id-generator-in-erlang

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,54 @@
= Groovy crypt library
image:https://api.travis-ci.org/xbib/groovy-crypt.svg[title="Build status", link="https://travis-ci.org/xbib/groovy-crypt/"]
image:https://maven-badges.herokuapp.com/maven-central/org.xbib.groovy/groovy-crypt/badge.svg[title="Maven Central", link="http://search.maven.org/#search%7Cga%7C1%7Cxbib%20groovy-crypt"]
image:https://img.shields.io/badge/License-Apache%202.0-blue.svg[title="Apache License 2.0", link="https://opensource.org/licenses/Apache-2.0"]
image:https://img.shields.io/twitter/url/https/twitter.com/xbib.svg?style=social&label=Follow%20%40xbib[title="Twitter", link="https://twitter.com/xbib"]
image:https://sonarqube.com/api/badges/gate?key=org.xbib.groovy:groovy-crypt[title="Quality Gate", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"]
image:https://sonarqube.com/api/badges/measure?key=org.xbib.groovy:groovy-crypt&metric=coverage[title="Coverage", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"]
image:https://sonarqube.com/api/badges/measure?key=org.xbib.groovy:groovy-crypt&metric=vulnerabilities[title="Vulnerabilities", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"]
image:https://sonarqube.com/api/badges/measure?key=org.xbib.groovy:groovy-crypt&metric=bugs[title="Bugs", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"]
image:https://sonarqube.com/api/badges/measure?key=org.xbib.groovy:groovy-crypt&metric=sqale_debt_ratio[title="Technical debt ratio", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"]
This Groovy crypt implementation of the `crypt(3)` function provided in the GNU C library (glibc)
was derived from crypt4j by Carl Harris https://github.com/soulwing/crypt4j
This implementation supports the MD5, SHA, SHA-256, and SHA-512 variants.
Additionally, it supports legacy DES and HMAC.
It is useful for LDAP passwords or secure cookie handling.
= Usage
void testHMAC() {
String s = "Hello World"
String secret = "secret"
String code = CryptUtil.hmac(s, secret, "HmacSHA1")
assertEquals("858da8837b87f04b052c0f6e954c3f7bbe081164", code)
}
void testSHA() {
String plaintext = 'geheim'
String code = CryptUtil.sha(plaintext)
assertEquals('SHA algorithm',
'{sha}kGByAB793z4R5tK1eC9Hd/4Dhzk=', code)
}
void testSSHA256() {
String plaintext = 'geheim'
byte[] salt = "467dd5b71e8d0f9e".decodeHex()
String code = CryptUtil.ssha256(plaintext, salt)
assertEquals('test SSHA-256 method',
'{ssha256}9yT5rYItjXK+mY8sKNBcKsKSnlY6ysTg8wbDVmAguTFGfdW3Ho0Png==', code)
}
void testSSHA512() {
String plaintext = 'geheim'
byte[] salt = "3c68f1f47f41d82f".decodeHex()
String code = CryptUtil.ssha512(plaintext, salt)
assertEquals('test SSHA-512 method',
'{ssha512}jeWuCXRjsvKh/vK548GP9ZCs4q9Sh1u700C8eONyV+EL/P810C8vlx9Eu4vRjHq/TDoGW8FE1l/P2KG3w9lHITxo8fR/Qdgv', code)
}

@ -0,0 +1,684 @@
/*! normalize.css v2.1.2 | MIT License | git.io/normalize */
/* ========================================================================== HTML5 display definitions ========================================================================== */
/** Correct `block` display not defined in IE 8/9. */
article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; }
/** Correct `inline-block` display not defined in IE 8/9. */
audio, canvas, video { display: inline-block; }
/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */
audio:not([controls]) { display: none; height: 0; }
/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */
[hidden], template { display: none; }
script { display: none !important; }
/* ========================================================================== Base ========================================================================== */
/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */
html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ }
/** Remove default margin. */
body { margin: 0; }
/* ========================================================================== Links ========================================================================== */
/** Remove the gray background color from active links in IE 10. */
a { background: transparent; }
/** Address `outline` inconsistency between Chrome and other browsers. */
a:focus { outline: thin dotted; }
/** Improve readability when focused and also mouse hovered in all browsers. */
a:active, a:hover { outline: 0; }
/* ========================================================================== Typography ========================================================================== */
/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */
h1 { font-size: 2em; margin: 0.67em 0; }
/** Address styling not present in IE 8/9, Safari 5, and Chrome. */
abbr[title] { border-bottom: 1px dotted; }
/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */
b, strong { font-weight: bold; }
/** Address styling not present in Safari 5 and Chrome. */
dfn { font-style: italic; }
/** Address differences between Firefox and other browsers. */
hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; }
/** Address styling not present in IE 8/9. */
mark { background: #ff0; color: #000; }
/** Correct font family set oddly in Safari 5 and Chrome. */
code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; }
/** Improve readability of pre-formatted text in all browsers. */
pre { white-space: pre-wrap; }
/** Set consistent quote types. */
q { quotes: "\201C" "\201D" "\2018" "\2019"; }
/** Address inconsistent and variable font size in all browsers. */
small { font-size: 80%; }
/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }
/* ========================================================================== Embedded content ========================================================================== */
/** Remove border when inside `a` element in IE 8/9. */
img { border: 0; }
/** Correct overflow displayed oddly in IE 9. */
svg:not(:root) { overflow: hidden; }
/* ========================================================================== Figures ========================================================================== */
/** Address margin not present in IE 8/9 and Safari 5. */
figure { margin: 0; }
/* ========================================================================== Forms ========================================================================== */
/** Define consistent border, margin, and padding. */
fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; }
/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */
legend { border: 0; /* 1 */ padding: 0; /* 2 */ }
/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */
button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ }
/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */
button, input { line-height: normal; }
/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */
button, select { text-transform: none; }
/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */
button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ }
/** Re-set default cursor for disabled elements. */
button[disabled], html input[disabled] { cursor: default; }
/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ }
/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */
input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; }
/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
/** Remove inner padding and border in Firefox 4+. */
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */
textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ }
/* ========================================================================== Tables ========================================================================== */
/** Remove most spacing between table cells. */
table { border-collapse: collapse; border-spacing: 0; }
meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; }
meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; }
meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; }
*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }
html, body { font-size: 100%; }
body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; }
a:hover { cursor: pointer; }
img, object, embed { max-width: 100%; height: auto; }
object, embed { height: 100%; }
img { -ms-interpolation-mode: bicubic; }
#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; }
.left { float: left !important; }
.right { float: right !important; }
.text-left { text-align: left !important; }
.text-right { text-align: right !important; }
.text-center { text-align: center !important; }
.text-justify { text-align: justify !important; }
.hide { display: none; }
.antialiased { -webkit-font-smoothing: antialiased; }
img { display: inline-block; vertical-align: middle; }
textarea { height: auto; min-height: 50px; }
select { width: 100%; }
object, svg { display: inline-block; vertical-align: middle; }
.center { margin-left: auto; margin-right: auto; }
.spread { width: 100%; }
p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; }
.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; }
/* Typography resets */
div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; }
/* Default Link Styles */
a { color: #2ba6cb; text-decoration: none; line-height: inherit; }
a:hover, a:focus { color: #2795b6; }
a img { border: none; }
/* Default paragraph styles */
p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; }
p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; }
/* Default header styles */
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; }
h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; }
h1 { font-size: 2.125em; }
h2 { font-size: 1.6875em; }
h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; }
h4 { font-size: 1.125em; }
h5 { font-size: 1.125em; }
h6 { font-size: 1em; }
hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; }
/* Helpful Typography Defaults */
em, i { font-style: italic; line-height: inherit; }
strong, b { font-weight: bold; line-height: inherit; }
small { font-size: 60%; line-height: inherit; }
code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; }
/* Lists */
ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; }
ul, ol { margin-left: 1.5em; }
ul.no-bullet, ol.no-bullet { margin-left: 1.5em; }
/* Unordered Lists */
ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ }
ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; }
ul.square { list-style-type: square; }
ul.circle { list-style-type: circle; }
ul.disc { list-style-type: disc; }
ul.no-bullet { list-style: none; }
/* Ordered Lists */
ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; }
/* Definition Lists */
dl dt { margin-bottom: 0.3125em; font-weight: bold; }
dl dd { margin-bottom: 1.25em; }
/* Abbreviations */
abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; }
abbr { text-transform: none; }
/* Blockquotes */
blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; }
blockquote cite { display: block; font-size: 0.8125em; color: #555555; }
blockquote cite:before { content: "\2014 \0020"; }
blockquote cite a, blockquote cite a:visited { color: #555555; }
blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; }
/* Microformats */
.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; }
.vcard li { margin: 0; display: block; }
.vcard .fn { font-weight: bold; font-size: 0.9375em; }
.vevent .summary { font-weight: bold; }
.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; }
@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; }
h1 { font-size: 2.75em; }
h2 { font-size: 2.3125em; }
h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; }
h4 { font-size: 1.4375em; } }
/* Tables */
table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; }
table thead, table tfoot { background: whitesmoke; font-weight: bold; }
table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; }
table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; }
table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; }
table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; }
body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; }
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; }
.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; }
.clearfix:after, .float-group:after { clear: both; }
*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; }
*:not(pre) > code.nobreak { word-wrap: normal; }
*:not(pre) > code.nowrap { white-space: nowrap; }
pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; }
em em { font-style: normal; }
strong strong { font-weight: normal; }
.keyseq { color: #555555; }
kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; }
.keyseq kbd:first-child { margin-left: 0; }
.keyseq kbd:last-child { margin-right: 0; }
.menuseq, .menu { color: #090909; }
b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; }
b.button:before { content: "["; padding: 0 3px 0 2px; }
b.button:after { content: "]"; padding: 0 2px 0 3px; }
#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; }
#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; }
#header:after, #content:after, #footnotes:after, #footer:after { clear: both; }
#content { margin-top: 1.25em; }
#content:before { content: none; }
#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; }
#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; }
#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; }
#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; }
#header .details span:first-child { margin-left: -0.125em; }
#header .details span.email a { color: #6f6f6f; }
#header .details br { display: none; }
#header .details br + span:before { content: "\00a0\2013\00a0"; }
#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; }
#header .details br + span#revremark:before { content: "\00a0|\00a0"; }
#header #revnumber { text-transform: capitalize; }
#header #revnumber:after { content: "\00a0"; }
#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; }
#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; }
#toc > ul { margin-left: 0.125em; }
#toc ul.sectlevel0 > li > a { font-style: italic; }
#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; }
#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; }
#toc li { line-height: 1.3334; margin-top: 0.3334em; }
#toc a { text-decoration: none; }
#toc a:active { text-decoration: underline; }
#toctitle { color: #6f6f6f; font-size: 1.2em; }
@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; }
body.toc2 { padding-left: 15em; padding-right: 0; }
#toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; }
#toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; }
#toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; }
#toc.toc2 ul ul { margin-left: 0; padding-left: 1em; }
#toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; }
body.toc2.toc-right { padding-left: 0; padding-right: 15em; }
body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } }
@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; }
#toc.toc2 { width: 20em; }
#toc.toc2 #toctitle { font-size: 1.375em; }
#toc.toc2 > ul { font-size: 0.95em; }
#toc.toc2 ul ul { padding-left: 1.25em; }
body.toc2.toc-right { padding-left: 0; padding-right: 20em; } }
#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; }
#content #toc > :first-child { margin-top: 0; }
#content #toc > :last-child { margin-bottom: 0; }
#footer { max-width: 100%; background-color: #222222; padding: 1.25em; }
#footer-text { color: #dddddd; line-height: 1.44; }
.sect1 { padding-bottom: 0.625em; }
@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } }
.sect1 + .sect1 { border-top: 1px solid #dddddd; }
#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; }
#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; }
#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; }
#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; }
#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; }
.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; }
.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; }
table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; }
.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; }
table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; }
.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; }
.admonitionblock > table td.icon { text-align: center; width: 80px; }
.admonitionblock > table td.icon img { max-width: initial; }
.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; }
.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; }
.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; }
.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; }
.exampleblock > .content > :first-child { margin-top: 0; }
.exampleblock > .content > :last-child { margin-bottom: 0; }
.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; }
.sidebarblock > :first-child { margin-top: 0; }
.sidebarblock > :last-child { margin-bottom: 0; }
.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; }
.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; }
.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; }
.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; }
.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; }
.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; }
@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } }
@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } }
.literalblock.output pre { color: #eeeeee; background-color: black; }
.listingblock pre.highlightjs { padding: 0; }
.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; }
.listingblock > .content { position: relative; }
.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; }
.listingblock:hover code[data-lang]:before { display: block; }
.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; }
.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; }
table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; }
table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; }
table.pyhltable td.code { padding-left: .75em; padding-right: 0; }
pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; }
pre.pygments .lineno { display: inline-block; margin-right: .25em; }
table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; }
.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; }
.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; }
.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; }
.quoteblock blockquote { margin: 0; padding: 0; border: 0; }
.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); }
.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; }
.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; }
.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; }
.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; }
.quoteblock .quoteblock blockquote:before { display: none; }
.verseblock { margin: 0 1em 1.25em 1em; }
.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; }
.verseblock pre strong { font-weight: 400; }
.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; }
.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; }
.quoteblock .attribution br, .verseblock .attribution br { display: none; }
.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; }
.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; }
.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; }
.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; }
table.tableblock { max-width: 100%; border-collapse: separate; }
table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; }
table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; }
table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; }
table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; }
table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; }
table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; }
table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; }
table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; }
table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; }
table.frame-all { border-width: 1px; }
table.frame-sides { border-width: 0 1px; }
table.frame-topbot { border-width: 1px 0; }
th.halign-left, td.halign-left { text-align: left; }
th.halign-right, td.halign-right { text-align: right; }
th.halign-center, td.halign-center { text-align: center; }
th.valign-top, td.valign-top { vertical-align: top; }
th.valign-bottom, td.valign-bottom { vertical-align: bottom; }
th.valign-middle, td.valign-middle { vertical-align: middle; }
table thead th, table tfoot th { font-weight: bold; }
tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; }
tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; }
p.tableblock > code:only-child { background: none; padding: 0; }
p.tableblock { font-size: 1em; }
td > div.verse { white-space: pre; }
ol { margin-left: 1.75em; }
ul li ol { margin-left: 1.5em; }
dl dd { margin-left: 1.125em; }
dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; }
ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; }
ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; }
ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; }
ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; }
ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; }
ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; }
ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; }
ul.inline > li > * { display: block; }
.unstyled dl dt { font-weight: normal; font-style: normal; }
ol.arabic { list-style-type: decimal; }
ol.decimal { list-style-type: decimal-leading-zero; }
ol.loweralpha { list-style-type: lower-alpha; }
ol.upperalpha { list-style-type: upper-alpha; }
ol.lowerroman { list-style-type: lower-roman; }
ol.upperroman { list-style-type: upper-roman; }
ol.lowergreek { list-style-type: lower-greek; }
.hdlist > table, .colist > table { border: 0; background: none; }
.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; }
td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; }
td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; }
.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; }
.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; }
.colist > table tr > td:first-of-type img { max-width: initial; }
.colist > table tr > td:last-of-type { padding: 0.25em 0; }
.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; }
.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; }
.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; }
.imageblock > .title { margin-bottom: 0; }
.imageblock.thumb, .imageblock.th { border-width: 6px; }
.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; }
.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; }
.image.left { margin-right: 0.625em; }
.image.right { margin-left: 0.625em; }
a.image { text-decoration: none; display: inline-block; }
a.image object { pointer-events: none; }
sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; }
sup.footnote a, sup.footnoteref a { text-decoration: none; }
sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; }
#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; }
#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; }
#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; }
#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; }
#footnotes .footnote:last-of-type { margin-bottom: 0; }
#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; }
.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; }
.gist .file-data > table td.line-data { width: 99%; }
div.unbreakable { page-break-inside: avoid; }
.big { font-size: larger; }
.small { font-size: smaller; }
.underline { text-decoration: underline; }
.overline { text-decoration: overline; }
.line-through { text-decoration: line-through; }
.aqua { color: #00bfbf; }
.aqua-background { background-color: #00fafa; }
.black { color: black; }
.black-background { background-color: black; }
.blue { color: #0000bf; }
.blue-background { background-color: #0000fa; }
.fuchsia { color: #bf00bf; }
.fuchsia-background { background-color: #fa00fa; }
.gray { color: #606060; }
.gray-background { background-color: #7d7d7d; }
.green { color: #006000; }
.green-background { background-color: #007d00; }
.lime { color: #00bf00; }
.lime-background { background-color: #00fa00; }
.maroon { color: #600000; }
.maroon-background { background-color: #7d0000; }
.navy { color: #000060; }
.navy-background { background-color: #00007d; }
.olive { color: #606000; }
.olive-background { background-color: #7d7d00; }
.purple { color: #600060; }
.purple-background { background-color: #7d007d; }
.red { color: #bf0000; }
.red-background { background-color: #fa0000; }
.silver { color: #909090; }
.silver-background { background-color: #bcbcbc; }
.teal { color: #006060; }
.teal-background { background-color: #007d7d; }
.white { color: #bfbfbf; }
.white-background { background-color: #fafafa; }
.yellow { color: #bfbf00; }
.yellow-background { background-color: #fafa00; }
span.icon > .fa { cursor: default; }
.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; }
.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; }
.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; }
.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; }
.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; }
.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; }
.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; }
.conum[data-value] * { color: #fff !important; }
.conum[data-value] + b { display: none; }
.conum[data-value]:after { content: attr(data-value); }
pre .conum[data-value] { position: relative; top: -0.125em; }
b.conum * { color: inherit !important; }
.conum:not([data-value]):empty { display: none; }
.literalblock pre, .listingblock pre { background: #eeeeee; }

@ -0,0 +1,11 @@
= Groovy Crypt library
Jörg Prante
Version 1.0.0
:sectnums:
:toc: preamble
:toclevels: 4
:!toc-title: Content
:experimental:
:description: Crypt library for Groovy
:keywords: Groovy, crypt, des, md5, sha, sha2, sha256, sha512, hmac
:icons: font

@ -0,0 +1,105 @@
package org.xbib.groovy.crypt
import java.lang.reflect.Constructor
import java.lang.reflect.InvocationTargetException
import java.security.NoSuchAlgorithmException
/**
* A utility class that encrypts password strings using algorithms that are
* compatible with {@code crypt(3)} from the GNU C library.
*/
abstract class Crypt {
protected final CryptType type
protected Crypt(CryptType type) {
this.type = type
}
/**
* Encrypts (digests) the given password using the algorithm identified by the
* given salt.
* @param password the password to encrypt
* @param salt algorithm identifier, parameters, and salt text
* @return the encrypted (digested) password
* @throws NoSuchAlgorithmException if the desired algorithm is not supported
* on this platform
* @throws UnsupportedEncodingException if UTF-8 encoding is not available not
* the platform
*
*/
static String crypt(String password, String salt)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
Salt s = new Salt(salt)
newInstance(CryptType.forSalt(s)).doCrypt(password, s)
}
/**
* Constructs a new instance of the specified type.
* @param type crypt type
* @return new crypt object
*/
private static Crypt newInstance(CryptType type) throws NoSuchAlgorithmException {
try {
Constructor<? extends Crypt> constructor = type.providerClass.getConstructor(CryptType)
constructor.newInstance(type)
} catch (NoSuchMethodException ex) {
throw new RuntimeException(ex)
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex)
} catch (InvocationTargetException ex) {
throw new RuntimeException(ex)
} catch (InstantiationException ex) {
throw new RuntimeException(ex)
}
}
/**
* Converts the encrypted password to a crypt output string.
* @param password the encrypted password
* @param salt salt
* @param maxSaltLength maximum allowable length for the salt
* @param params subclass-specific parameters (these will be passed to
* {@link #encodeParameters(Object ...)}
* @return crypt output string
*/
protected String passwordToString(byte[] password, Salt salt, int maxSaltLength, Object... params) {
StringBuilder sb = new StringBuilder()
if (salt.type == 0) {
return encodePassword(password)
}
sb.append('$').append(salt.type).append('$')
if (params != null && params.length != 0) {
String encodedParameters = encodeParameters(params)
if (encodedParameters != null) {
sb.append(encodedParameters).append('$')
}
}
sb.append(salt.getText(maxSaltLength)).append('$')
sb.append(encodePassword(password))
sb.toString()
}
protected abstract String encodeParameters(Object... params)
/**
* Performs the password encryption operation.
* @param password the password to encrypt
* @param salt salt for the encryption
* @return formatted crypt output string
* @throws NoSuchAlgorithmException if the specified encryption type cannot be
* supported on the platform
* @throws UnsupportedEncodingException if the password character encoding
* cannot be supported on the platform
*/
protected abstract String doCrypt(String password, Salt salt)
throws NoSuchAlgorithmException, UnsupportedEncodingException
/**
* Encodes the password for use in the crypt output string.
* @param password the password to encode
* @return string encoding of {@code password}
*/
protected abstract String encodePassword(byte[] password)
}

@ -0,0 +1,69 @@
package org.xbib.groovy.crypt
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
/**
* A enumeration of supported encryption types.
*/
enum CryptType {
DES("0", "DES", DesCrypt),
MD5("1", "MD5", Md5Crypt),
/* Avoid the following:
2 - the original BCrypt, which has been deprecated because of a security issue a long time before BCrypt became popular.
2a - the official BCrypt algorithm and a insecure implementation in crypt_blowfish http://seclists.org/oss-sec/2011/q2/632
2x - suggested for hashes created by the insecure algorithm for compatibility
2y - suggested new marker for the fixed crypt_blowfish
*/
SHA256("5", "SHA-256", Sha256Crypt),
SHA512("6", "SHA-512", Sha512Crypt)
private final Class<? extends Crypt> providerClass
final String type
final String algorithm
private CryptType(String type, String algorithm, Class<? extends Crypt> providerClass) {
this.type = type
this.algorithm = algorithm
this.providerClass = providerClass
}
/**
* Gets the type instance that corresponds to the type specified by the
* given salt.
*
* @param salt the subject salt
* @return type instance
* @throws NoSuchAlgorithmException if no type corresponds to the given salt
*/
static CryptType forSalt(Salt salt) throws NoSuchAlgorithmException {
for (CryptType type : values()) {
if (type.type == salt.type) {
return type
}
}
throw new NoSuchAlgorithmException("unsupported type")
}
/**
* Gets the {@code providerClass} property.
*
* @return
*/
Class<? extends Crypt> getProviderClass() {
providerClass
}
/**
* Creates a new digest for the algorithm specified for this type.
*
* @return message digest
* @throws NoSuchAlgorithmException
*/
MessageDigest newDigest() throws NoSuchAlgorithmException {
return MessageDigest.getInstance(algorithm)
}
}

@ -0,0 +1,113 @@
package org.xbib.groovy.crypt
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
/**
* A utility class for invoking encryption methods and returning LDAP password string,
* using {@link java.security.MessageDigest} and {@link javax.crypto.Mac}.
*/
class CryptUtil {
private static final Random random = new SecureRandom()
static String hexDigest(String plainText, String algo, String prefix) throws NoSuchAlgorithmException {
if (plainText == null) {
return null
}
MessageDigest digest = MessageDigest.getInstance(algo)
digest.update(plainText.getBytes(StandardCharsets.UTF_8))
"{${prefix}}${digest.digest().encodeHex()}"
}
static String base64Digest(String plainText, String algo, String prefix) throws NoSuchAlgorithmException {
if (plainText == null) {
return null
}
MessageDigest digest = MessageDigest.getInstance(algo)
digest.update(plainText.getBytes(StandardCharsets.UTF_8))
"{${prefix}}${digest.digest().encodeBase64()}"
}
static String base64Digest(String plainText, byte[] salt, String algo, String prefix) throws NoSuchAlgorithmException {
if (plainText == null) {
return null
}
MessageDigest digest = MessageDigest.getInstance(algo)
digest.update(plainText.getBytes(StandardCharsets.UTF_8))
digest.update(salt)
byte[] hash = digest.digest()
byte[] code = new byte[salt.length + hash.length]
System.arraycopy(hash, 0, code, 0, hash.length)
System.arraycopy(salt, 0, code, hash.length, salt.length)
"{${prefix}}${code.encodeBase64()}"
}
static String crypt(String plainText, String salt) {
"{crypt}${Crypt.crypt(plainText, salt)}"
}
static String md5(String plainText) {
base64Digest(plainText, 'MD5', 'md5')
}
static String sha(String plainText) {
base64Digest(plainText, 'SHA', 'sha')
}
static String sha256(String plainText) {
base64Digest(plainText, 'SHA-256', 'sha256')
}
static String sha512(String plainText) {
base64Digest(plainText, 'SHA-512', 'sha512')
}
static String ssha(String plainText, byte[] salt) {
base64Digest(plainText, salt, 'SHA1', 'ssha')
}
static String ssha256(String plainText, byte[] salt) {
base64Digest(plainText, salt, 'SHA-256', 'ssha256')
}
static String ssha512(String plainText, byte[] salt) {
base64Digest(plainText, salt, 'SHA-512', 'ssha512')
}
static String hmacSHA1(String plainText, String secret) {
hmac(plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1")
}
static String hmacSHA1(byte[] plainText, String secret) {
hmac(plainText, secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1")
}
static String hmacSHA1(byte[] plainText, byte[] secret) {
hmac(plainText, secret, "HmacSHA1")
}
static String hmac(String plainText, String secret, String algo) {
hmac(plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), algo)
}
static String hmac(byte[] plainText, String secret, String algo) {
hmac(plainText, secret.getBytes(StandardCharsets.UTF_8), algo)
}
static String hmac(byte[] plainText, byte[] secret, String algo) throws NoSuchAlgorithmException {
Mac mac = Mac.getInstance(algo)
mac.init(new SecretKeySpec(secret, algo))
mac.doFinal(plainText).encodeHex()
}
static String random(int length) {
byte[] b = new byte[length]
random.nextBytes(b)
b.encodeHex()
}
}

@ -0,0 +1,390 @@
package org.xbib.groovy.crypt
import java.nio.charset.StandardCharsets
import java.security.NoSuchAlgorithmException
/**
* The DES algorithm.
*/
class DesCrypt extends Crypt {
private static final int[] CON_SALT = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 5, 6,
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
34, 35, 36, 37, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 0, 0, 0, 0, 0]
private static final int[] COV2CHAR = [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70,
71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102,
103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122]
private static
final char[] SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./".toCharArray()
private static final boolean[] SHIFT2 = [false, false, true, true, true, true, true, true, false, true, true,
true, true, true, true, false]
private static final int[][] SKB = [
[0, 16, 0x20000000, 0x20000010, 0x10000, 0x10010, 0x20010000, 0x20010010, 2048, 2064, 0x20000800,
0x20000810, 0x10800, 0x10810, 0x20010800, 0x20010810, 32, 48, 0x20000020, 0x20000030, 0x10020,
0x10030, 0x20010020, 0x20010030, 2080, 2096, 0x20000820, 0x20000830, 0x10820, 0x10830, 0x20010820,
0x20010830, 0x80000, 0x80010, 0x20080000, 0x20080010, 0x90000, 0x90010, 0x20090000, 0x20090010,
0x80800, 0x80810, 0x20080800, 0x20080810, 0x90800, 0x90810, 0x20090800, 0x20090810, 0x80020,
0x80030, 0x20080020, 0x20080030, 0x90020, 0x90030, 0x20090020, 0x20090030, 0x80820, 0x80830,
0x20080820, 0x20080830, 0x90820, 0x90830, 0x20090820, 0x20090830],
[0, 0x2000000, 8192, 0x2002000, 0x200000, 0x2200000, 0x202000, 0x2202000, 4, 0x2000004, 8196, 0x2002004,
0x200004, 0x2200004, 0x202004, 0x2202004, 1024, 0x2000400, 9216, 0x2002400, 0x200400, 0x2200400,
0x202400, 0x2202400, 1028, 0x2000404, 9220, 0x2002404, 0x200404, 0x2200404, 0x202404, 0x2202404,
0x10000000, 0x12000000, 0x10002000, 0x12002000, 0x10200000, 0x12200000, 0x10202000, 0x12202000,
0x10000004, 0x12000004, 0x10002004, 0x12002004, 0x10200004, 0x12200004, 0x10202004, 0x12202004,
0x10000400, 0x12000400, 0x10002400, 0x12002400, 0x10200400, 0x12200400, 0x10202400, 0x12202400,
0x10000404, 0x12000404, 0x10002404, 0x12002404, 0x10200404, 0x12200404, 0x10202404, 0x12202404],
[0, 1, 0x40000, 0x40001, 0x1000000, 0x1000001, 0x1040000, 0x1040001, 2, 3, 0x40002, 0x40003, 0x1000002,
0x1000003, 0x1040002, 0x1040003, 512, 513, 0x40200, 0x40201, 0x1000200, 0x1000201, 0x1040200,
0x1040201, 514, 515, 0x40202, 0x40203, 0x1000202, 0x1000203, 0x1040202, 0x1040203, 0x8000000,
0x8000001, 0x8040000, 0x8040001, 0x9000000, 0x9000001, 0x9040000, 0x9040001, 0x8000002, 0x8000003,
0x8040002, 0x8040003, 0x9000002, 0x9000003, 0x9040002, 0x9040003, 0x8000200, 0x8000201, 0x8040200,
0x8040201, 0x9000200, 0x9000201, 0x9040200, 0x9040201, 0x8000202, 0x8000203, 0x8040202, 0x8040203,
0x9000202, 0x9000203, 0x9040202, 0x9040203],
[0, 0x100000, 256, 0x100100, 8, 0x100008, 264, 0x100108, 4096, 0x101000, 4352, 0x101100, 4104, 0x101008,
4360, 0x101108, 0x4000000, 0x4100000, 0x4000100, 0x4100100, 0x4000008, 0x4100008, 0x4000108,
0x4100108, 0x4001000, 0x4101000, 0x4001100, 0x4101100, 0x4001008, 0x4101008, 0x4001108, 0x4101108,
0x20000, 0x120000, 0x20100, 0x120100, 0x20008, 0x120008, 0x20108, 0x120108, 0x21000, 0x121000,
0x21100, 0x121100, 0x21008, 0x121008, 0x21108, 0x121108, 0x4020000, 0x4120000, 0x4020100,
0x4120100, 0x4020008, 0x4120008, 0x4020108, 0x4120108, 0x4021000, 0x4121000, 0x4021100, 0x4121100,
0x4021008, 0x4121008, 0x4021108, 0x4121108],
[0, 0x10000000, 0x10000, 0x10010000, 4, 0x10000004, 0x10004, 0x10010004, 0x20000000, 0x30000000,
0x20010000, 0x30010000, 0x20000004, 0x30000004, 0x20010004, 0x30010004, 0x100000, 0x10100000,
0x110000, 0x10110000, 0x100004, 0x10100004, 0x110004, 0x10110004, 0x20100000, 0x30100000,
0x20110000, 0x30110000, 0x20100004, 0x30100004, 0x20110004, 0x30110004, 4096, 0x10001000, 0x11000,
0x10011000, 4100, 0x10001004, 0x11004, 0x10011004, 0x20001000, 0x30001000, 0x20011000, 0x30011000,
0x20001004, 0x30001004, 0x20011004, 0x30011004, 0x101000, 0x10101000, 0x111000, 0x10111000,
0x101004, 0x10101004, 0x111004, 0x10111004, 0x20101000, 0x30101000, 0x20111000, 0x30111000,
0x20101004, 0x30101004, 0x20111004, 0x30111004],
[0, 0x8000000, 8, 0x8000008, 1024, 0x8000400, 1032, 0x8000408, 0x20000, 0x8020000, 0x20008, 0x8020008,
0x20400, 0x8020400, 0x20408, 0x8020408, 1, 0x8000001, 9, 0x8000009, 1025, 0x8000401, 1033,
0x8000409, 0x20001, 0x8020001, 0x20009, 0x8020009, 0x20401, 0x8020401, 0x20409, 0x8020409,
0x2000000, 0xa000000, 0x2000008, 0xa000008, 0x2000400, 0xa000400, 0x2000408, 0xa000408, 0x2020000,
0xa020000, 0x2020008, 0xa020008, 0x2020400, 0xa020400, 0x2020408, 0xa020408, 0x2000001, 0xa000001,
0x2000009, 0xa000009, 0x2000401, 0xa000401, 0x2000409, 0xa000409, 0x2020001, 0xa020001, 0x2020009,
0xa020009, 0x2020401, 0xa020401, 0x2020409, 0xa020409],
[0, 256, 0x80000, 0x80100, 0x1000000, 0x1000100, 0x1080000, 0x1080100, 16, 272, 0x80010, 0x80110,
0x1000010, 0x1000110, 0x1080010, 0x1080110, 0x200000, 0x200100, 0x280000, 0x280100, 0x1200000,
0x1200100, 0x1280000, 0x1280100, 0x200010, 0x200110, 0x280010, 0x280110, 0x1200010, 0x1200110,
0x1280010, 0x1280110, 512, 768, 0x80200, 0x80300, 0x1000200, 0x1000300, 0x1080200, 0x1080300, 528,
784, 0x80210, 0x80310, 0x1000210, 0x1000310, 0x1080210, 0x1080310, 0x200200, 0x200300, 0x280200,
0x280300, 0x1200200, 0x1200300, 0x1280200, 0x1280300, 0x200210, 0x200310, 0x280210, 0x280310,
0x1200210, 0x1200310, 0x1280210, 0x1280310],
[0, 0x4000000, 0x40000, 0x4040000, 2, 0x4000002, 0x40002, 0x4040002, 8192, 0x4002000, 0x42000, 0x4042000,
8194, 0x4002002, 0x42002, 0x4042002, 32, 0x4000020, 0x40020, 0x4040020, 34, 0x4000022, 0x40022,
0x4040022, 8224, 0x4002020, 0x42020, 0x4042020, 8226, 0x4002022, 0x42022, 0x4042022, 2048,
0x4000800, 0x40800, 0x4040800, 2050, 0x4000802, 0x40802, 0x4040802, 10240, 0x4002800, 0x42800,
0x4042800, 10242, 0x4002802, 0x42802, 0x4042802, 2080, 0x4000820, 0x40820, 0x4040820, 2082,
0x4000822, 0x40822, 0x4040822, 10272, 0x4002820, 0x42820, 0x4042820, 10274, 0x4002822, 0x42822,
0x4042822]] as int[][]
private static final int[][] SPTRANS = [
[0x820200, 0x20000, 0x80800000, 0x80820200, 0x800000, 0x80020200, 0x80020000, 0x80800000, 0x80020200,
0x820200, 0x820000, 0x80000200, 0x80800200, 0x800000, 0, 0x80020000, 0x20000, 0x80000000,
0x800200, 0x20200, 0x80820200, 0x820000, 0x80000200, 0x800200, 0x80000000, 512, 0x20200,
0x80820000, 512, 0x80800200, 0x80820000, 0, 0, 0x80820200, 0x800200, 0x80020000, 0x820200,
0x20000, 0x80000200, 0x800200, 0x80820000, 512, 0x20200, 0x80800000, 0x80020200, 0x80000000,
0x80800000, 0x820000, 0x80820200, 0x20200, 0x820000, 0x80800200, 0x800000, 0x80000200, 0x80020000,
0, 0x20000, 0x800000, 0x80800200, 0x820200, 0x80000000, 0x80820000, 512, 0x80020200],
[0x10042004, 0, 0x42000, 0x10040000, 0x10000004, 8196, 0x10002000, 0x42000, 8192, 0x10040004, 4,
0x10002000, 0x40004, 0x10042000, 0x10040000, 4, 0x40000, 0x10002004, 0x10040004, 8192, 0x42004,
0x10000000, 0, 0x40004, 0x10002004, 0x42004, 0x10042000, 0x10000004, 0x10000000, 0x40000, 8196,
0x10042004, 0x40004, 0x10042000, 0x10002000, 0x42004, 0x10042004, 0x40004, 0x10000004, 0,
0x10000000, 8196, 0x40000, 0x10040004, 8192, 0x10000000, 0x42004, 0x10002004, 0x10042000, 8192, 0,
0x10000004, 4, 0x10042004, 0x42000, 0x10040000, 0x10040004, 0x40000, 8196, 0x10002000, 0x10002004,
4, 0x10040000, 0x42000],
[0x41000000, 0x1010040, 64, 0x41000040, 0x40010000, 0x1000000, 0x41000040, 0x10040, 0x1000040, 0x10000,
0x1010000, 0x40000000, 0x41010040, 0x40000040, 0x40000000, 0x41010000, 0, 0x40010000, 0x1010040,
64, 0x40000040, 0x41010040, 0x10000, 0x41000000, 0x41010000, 0x1000040, 0x40010040, 0x1010000,
0x10040, 0, 0x1000000, 0x40010040, 0x1010040, 64, 0x40000000, 0x10000, 0x40000040, 0x40010000,
0x1010000, 0x41000040, 0, 0x1010040, 0x10040, 0x41010000, 0x40010000, 0x1000000, 0x41010040,
0x40000000, 0x40010040, 0x41000000, 0x1000000, 0x41010040, 0x10000, 0x1000040, 0x41000040,
0x10040, 0x1000040, 0, 0x41010000, 0x40000040, 0x41000000, 0x40010040, 64, 0x1010000],
[0x100402, 0x4000400, 2, 0x4100402, 0, 0x4100000, 0x4000402, 0x100002, 0x4100400, 0x4000002, 0x4000000,
1026, 0x4000002, 0x100402, 0x100000, 0x4000000, 0x4100002, 0x100400, 1024, 2, 0x100400, 0x4000402,
0x4100000, 1024, 1026, 0, 0x100002, 0x4100400, 0x4000400, 0x4100002, 0x4100402, 0x100000,
0x4100002, 1026, 0x100000, 0x4000002, 0x100400, 0x4000400, 2, 0x4100000, 0x4000402, 0, 1024,
0x100002, 0, 0x4100002, 0x4100400, 1024, 0x4000000, 0x4100402, 0x100402, 0x100000, 0x4100402, 2,
0x4000400, 0x100402, 0x100002, 0x100400, 0x4100000, 0x4000402, 1026, 0x4000000, 0x4000002,
0x4100400],
[0x2000000, 16384, 256, 0x2004108, 0x2004008, 0x2000100, 16648, 0x2004000, 16384, 8, 0x2000008, 16640,
0x2000108, 0x2004008, 0x2004100, 0, 16640, 0x2000000, 16392, 264, 0x2000100, 16648, 0, 0x2000008,
8, 0x2000108, 0x2004108, 16392, 0x2004000, 256, 264, 0x2004100, 0x2004100, 0x2000108, 16392,
0x2004000, 16384, 8, 0x2000008, 0x2000100, 0x2000000, 16640, 0x2004108, 0, 16648, 0x2000000, 256,
16392, 0x2000108, 256, 0, 0x2004108, 0x2004008, 0x2004100, 264, 16384, 16640, 0x2004008,
0x2000100, 264, 8, 16648, 0x2004000, 0x2000008],
[0x20000010, 0x80010, 0, 0x20080800, 0x80010, 2048, 0x20000810, 0x80000, 2064, 0x20080810, 0x80800,
0x20000000, 0x20000800, 0x20000010, 0x20080000, 0x80810, 0x80000, 0x20000810, 0x20080010, 0, 2048,
16, 0x20080800, 0x20080010, 0x20080810, 0x20080000, 0x20000000, 2064, 16, 0x80800, 0x80810,
0x20000800, 2064, 0x20000000, 0x20000800, 0x80810, 0x20080800, 0x80010, 0, 0x20000800, 0x20000000,
2048, 0x20080010, 0x80000, 0x80010, 0x20080810, 0x80800, 16, 0x20080810, 0x80800, 0x80000,
0x20000810, 0x20000010, 0x20080000, 0x80810, 0, 2048, 0x20000010, 0x20000810, 0x20080800,
0x20080000, 2064, 16, 0x20080010],
[4096, 128, 0x400080, 0x400001, 0x401081, 4097, 4224, 0, 0x400000, 0x400081, 129, 0x401000, 1, 0x401080,
0x401000, 129, 0x400081, 4096, 4097, 0x401081, 0, 0x400080, 0x400001, 4224, 0x401001, 4225,
0x401080, 1, 4225, 0x401001, 128, 0x400000, 4225, 0x401000, 0x401001, 129, 4096, 128, 0x400000,
0x401001, 0x400081, 4225, 4224, 0, 128, 0x400001, 1, 0x400080, 0, 0x400081, 0x400080, 4224, 129,
4096, 0x401081, 0x400000, 0x401080, 1, 4097, 0x401081, 0x400001, 0x401080, 0x401000, 4097],
[0x8200020, 0x8208000, 32800, 0, 0x8008000, 0x200020, 0x8200000, 0x8208020, 32, 0x8000000, 0x208000,
32800, 0x208020, 0x8008020, 0x8000020, 0x8200000, 32768, 0x208020, 0x200020, 0x8008000, 0x8208020,
0x8000020, 0, 0x208000, 0x8000000, 0x200000, 0x8008020, 0x8200020, 0x200000, 32768, 0x8208000, 32,
0x200000, 32768, 0x8000020, 0x8208020, 32800, 0x8000000, 0, 0x208000, 0x8200020, 0x8008020,
0x8008000, 0x200020, 0x8208000, 32, 0x200020, 0x8008000, 0x8208020, 0x200000, 0x8200000,
0x8000020, 0x208000, 32800, 0x8008020, 0x8200000, 32, 0x8208000, 0x208020, 0, 0x8000000,
0x8200020, 32768, 0x208020]] as int[][]
static final String B64T = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
DesCrypt(CryptType type) {
super(type)
}
@Override
protected String doCrypt(String password, Salt salt)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
return crypt(password.getBytes(StandardCharsets.UTF_8), salt.getText(2))
}
@Override
protected String encodeParameters(Object... params) {
throw new UnsupportedOperationException()
}
@Override
protected String encodePassword(byte[] password) {
throw new UnsupportedOperationException()
}
static String crypt(byte[] original) {
crypt(original, null)
}
/**
* Generates a crypt(3) compatible hash using the DES algorithm.
* <p>
* Using unspecified characters as salt results incompatible hash values.
*
* @param original
* plaintext password
* @param salt
* a two character string drawn from [a-zA-Z0-9./] or null for a random one
* @return a 13 character string starting with the salt string
* @throws IllegalArgumentException
* if the salt does not match the allowed pattern
*/
static String crypt(byte[] original, String salt) {
if (salt == null) {
Random randomGenerator = new Random()
int numSaltChars = SALT_CHARS.length
salt = "" + SALT_CHARS[randomGenerator.nextInt(numSaltChars)] + SALT_CHARS[randomGenerator.nextInt(numSaltChars)]
} else if (!salt.matches('^[' + B64T + ']{2,}$')) {
throw new IllegalArgumentException('Invalid salt value: ' + salt)
}
StringBuilder sb = new StringBuilder(" ")
char charZero = salt.charAt(0)
char charOne = salt.charAt(1)
sb.setCharAt(0, charZero)
sb.setCharAt(1, charOne)
int eSwap0 = CON_SALT[charZero as int]
int eSwap1 = CON_SALT[charOne as int] << 4
byte[] key = new byte[8]
for (int i = 0; i < key.length; i++) {
key[i] = 0
}
for (int i = 0; i < key.length && i < original.length; i++) {
int iChar = original[i]
key[i] = (byte) (iChar.leftShift(1))
}
int[] schedule = desSetKey(key)
int[] out = body(schedule, eSwap0, eSwap1)
byte[] b = new byte[9]
intToFourBytes(out[0], b, 0)
intToFourBytes(out[1], b, 4)
b[8] = 0
int i = 2
int y = 0
int u = 128
for (; i < 13; i++) {
int j = 0
int c = 0
for (; j < 6; j++) {
c <<= 1
if ((b[y] & u) != 0) {
c |= 0x1
}
u >>>= 1
if (u == 0) {
y++
u = 128
}
sb.setCharAt(i, COV2CHAR[c] as char)
}
}
sb.toString()
}
/**
* Generates a crypt(3) compatible hash using the DES algorithm.
*
* As no salt is given, a random one is used.
*
* @param original
* plaintext password
* @return a 13 character string starting with the salt string
*/
static String crypt(String original) {
crypt(original.getBytes(StandardCharsets.UTF_8))
}
/**
* Generates a crypt(3) compatible hash using the DES algorithm.
*
* @param original
* plaintext password
* @param salt
* a two character string drawn from [a-zA-Z0-9./] or null for a random one
* @return a 13 character string starting with the salt string
* @throws IllegalArgumentException
* if the salt does not match the allowed pattern
*/
static String crypt(String original, String salt) {
crypt(original.getBytes(StandardCharsets.UTF_8), salt)
}
private static int[] body(int[] schedule, int eSwap0, int eSwap1) {
int left = 0
int right = 0
int t
for (int j = 0; j < 25; j++) {
for (int i = 0; i < 32; i += 4) {
left = dEncrypt(left, right, i, eSwap0, eSwap1, schedule)
right = dEncrypt(right, left, i + 2, eSwap0, eSwap1, schedule)
}
t = left
left = right
right = t
}
t = right
right = left >>> 1 | left << 31
left = t >>> 1 | t << 31
int[] results = new int[2]
permOp(right, left, 1, 0x55555555, results)
right = results[0]
left = results[1]
permOp(left, right, 8, 0xff00ff, results)
left = results[0]
right = results[1]
permOp(right, left, 2, 0x33333333, results)
right = results[0]
left = results[1]
permOp(left, right, 16, 65535, results)
left = results[0]
right = results[1]
permOp(right, left, 4, 0xf0f0f0f, results)
right = results[0]
left = results[1]
int[] out = new int[2]
out[0] = left
out[1] = right
return out
}
private static int byteToUnsigned(byte b) {
int value = b
return value < 0 ? value + 256 : value
}
private static int dEncrypt(int el, int r, int s, int e0, int e1, int[] sArr) {
int v = r ^ r >>> 16
int u = v & e0
v &= e1
u = u ^ u << 16 ^ r ^ sArr[s]
int t = v ^ v << 16 ^ r ^ sArr[s + 1]
t = t >>> 4 | t << 28
el ^= SPTRANS[1][t & 0x3f] | SPTRANS[3][t >>> 8 & 0x3f] | SPTRANS[5][t >>> 16 & 0x3f] |
SPTRANS[7][t >>> 24 & 0x3f] | SPTRANS[0][u & 0x3f] | SPTRANS[2][u >>> 8 & 0x3f] |
SPTRANS[4][u >>> 16 & 0x3f] | SPTRANS[6][u >>> 24 & 0x3f]
return el
}
private static int[] desSetKey(byte[] key) {
int[] schedule = new int[32]
int c = fourBytesToInt(key, 0)
int d = fourBytesToInt(key, 4)
int[] results = new int[2]
permOp(d, c, 4, 0xf0f0f0f, results)
d = results[0]
c = results[1]
c = hPermOp(c, -2, 0xcccc0000)
d = hPermOp(d, -2, 0xcccc0000)
permOp(d, c, 1, 0x55555555, results)
d = results[0]
c = results[1]
permOp(c, d, 8, 0xff00ff, results)
c = results[0]
d = results[1]
permOp(d, c, 1, 0x55555555, results)
d = results[0]
c = results[1]
d = ((d & 0xff) << 16) | (d & 0xff00) | ((d & 0xff0000) >>> 16) as int | ((c & 0xf0000000) >>> 4) as int
c &= 0xfffffff
int j = 0
for (int i = 0; i < 16; i++) {
if (SHIFT2[i]) {
c = c >>> 2 | c << 26
d = d >>> 2 | d << 26
} else {
c = c >>> 1 | c << 27
d = d >>> 1 | d << 27
}
c &= 0xfffffff
d &= 0xfffffff
int s = SKB[0][c & 0x3f] | SKB[1][c >>> 6 & 0x3 | c >>> 7 & 0x3c] |
SKB[2][c >>> 13 & 0xf | c >>> 14 & 0x30] |
SKB[3][c >>> 20 & 0x1 | c >>> 21 & 0x6 | c >>> 22 & 0x38]
int t = SKB[4][d & 0x3f] | SKB[5][d >>> 7 & 0x3 | d >>> 8 & 0x3c] | SKB[6][d >>> 15 & 0x3f] |
SKB[7][d >>> 21 & 0xf | d >>> 22 & 0x30]
schedule[j++] = (t << 16 | s & 0xffff)
s = ((s >>> 16) | (t & 0xffff0000)) as int
s = s << 4 | s >>> 28
schedule[j++] = s
}
return schedule
}
private static int fourBytesToInt(byte[] b, int offset) {
int value = byteToUnsigned(b[offset++])
value |= byteToUnsigned(b[offset++]) << 8
value |= byteToUnsigned(b[offset++]) << 16
value |= byteToUnsigned(b[offset]) << 24
return value
}
private static int hPermOp(int a, int n, long m) {
int t = (a << 16 - n ^ a) & (m as int)
a = a ^ t ^ t >>> 16 - n
return a
}
private static void intToFourBytes(int iValue, byte[] b, int offset) {
b[offset++] = (byte) (iValue & 0xff)
b[offset++] = (byte) (iValue >>> 8 & 0xff)
b[offset++] = (byte) (iValue >>> 16 & 0xff)
b[offset] = (byte) (iValue >>> 24 & 0xff)
}
private static void permOp(int a, int b, int n, int m, int[] results) {
int t = (a >>> n ^ b) & m
a ^= t << n
b ^= t
results[0] = a
results[1] = b
}
}

@ -0,0 +1,26 @@
package org.xbib.groovy.crypt
class Encoder {
private static final String BASE64_SET = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
/**
* Encodes a 24-bit value as a character array containing base 64.
* @param b2 high-order 8 bits
* @param b1 middle 8 bits
* @param b0 low-order 8 bits
* @param n number of characters to encode
* @return character array of length {@code n} containing the base 64
* representation of the input value
*/
static char[] base64(byte b2, byte b1, byte b0, int n) {
char[] buf = new char[n];
int i = 0;
int w = (((int) b2 & 0xff) << 16) | (((int) b1 & 0xff) << 8) | ((int) b0 & 0xff)
while (i < n) {
buf[i++] = BASE64_SET.charAt(w & 0x3f)
w >>>= 6
}
return buf
}
}

@ -0,0 +1,107 @@
package org.xbib.groovy.crypt
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
/**
* MD5 crypt implementation.
*/
class Md5Crypt extends Crypt {
private static final String SALT_PREFIX = '$1$'
private static final int MAX_SALT_LENGTH = 8
private static final int ROUNDS = 1000
/**
* Constructs a new instance.
* @param type
*/
Md5Crypt(CryptType type) {
super(type)
}
@Override
protected String doCrypt(String password, Salt salt)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
byte[] encrypted = doCrypt(password.getBytes(StandardCharsets.UTF_8),
salt.getBytes(MAX_SALT_LENGTH, StandardCharsets.UTF_8))
return passwordToString(encrypted, salt, MAX_SALT_LENGTH)
}
protected byte[] doCrypt(byte[] password, byte[] salt)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
final MessageDigest a = type.newDigest()
final int digestLength = a.getDigestLength()
a.update(password)
a.update(SALT_PREFIX.getBytes(StandardCharsets.UTF_8))
a.update(salt)
final MessageDigest b = type.newDigest()
b.update(password)
b.update(salt)
b.update(password)
final byte[] sumB = b.digest()
int max = (password.length / digestLength) as int
for (int i = 0; i < max; i++) {
a.update(sumB)
}
a.update(sumB, 0, password.length % digestLength)
/* The original implementation now does something weird: for every 1
* bit in the key the first 0 is added to the buffer, for every 0 bit
* the first character of the key. This does not seem to be what was
* intended but we have to follow this to be compatible.
*/
final byte[] zero = [0]
for (int length = password.length; length != 0; length >>>= 1) {
if ((length & 1) != 0) {
a.update(zero)
} else {
a.update(password, 0, 1)
}
}
final byte[] sumA = a.digest()
byte[] ac = sumA
for (int i = 0; i < ROUNDS; i++) {
final MessageDigest c = type.newDigest()
if (i % 2 != 0) {
c.update(password)
} else {
c.update(ac)
}
if (i % 3 != 0) {
c.update(salt)
}
if (i % 7 != 0) {
c.update(password)
}
if (i % 2 != 0) {
c.update(ac)
} else {
c.update(password)
}
ac = c.digest()
}
ac
}
@Override
protected String encodeParameters(Object... params) {
throw new UnsupportedOperationException()
}
@Override
protected String encodePassword(byte[] password) {
StringBuilder sb = new StringBuilder()
sb.append(Encoder.base64(password[0], password[6], password[12], 4))
sb.append(Encoder.base64(password[1], password[7], password[13], 4))
sb.append(Encoder.base64(password[2], password[8], password[14], 4))
sb.append(Encoder.base64(password[3], password[9], password[15], 4))
sb.append(Encoder.base64(password[4], password[10], password[5], 4))
sb.append(Encoder.base64((byte) 0, (byte) 0, password[11], 2))
sb.toString()
}
}

@ -0,0 +1,78 @@
package org.xbib.groovy.crypt
import java.nio.charset.Charset
/**
* Salt for encryption.
*/
class Salt {
String type
String params
String text
Salt(String salt) {
if (salt.charAt(0) != '$') {
this.type = "0"
this.params = null
this.text = salt
return
}
int index = 1
int extent = salt.indexOf('$', index)
if (extent == -1) {
throw new IllegalArgumentException("illegal salt format")
}
this.type = salt.substring(index, extent)
index = extent + 1
if (index > salt.length()) {
throw new IllegalArgumentException("illegal salt format")
}
extent = salt.indexOf('$', index)
if (extent == -1 || salt.substring(index, extent).indexOf('=') == -1) {
this.params = null
} else {
this.params = salt.substring(index, extent)
index = extent + 1
if (index > salt.length()) {
throw new IllegalArgumentException("illegal salt format")
}
}
extent = salt.indexOf('$', index)
if (extent == -1) {
extent = salt.length()
}
this.text = salt.substring(index, extent)
}
/**
* Gets the salt text truncated to a given maximum length.
* @param maxLength maximum length
* @return truncated salt text
*/
String getText(int maxLength) {
text.substring(0, Math.min(text.length(), maxLength))
}
/**
* Gets the salt text as an array of bytes of a given character encoding.
* @param charset character set name
* @return byte array
* @throws UnsupportedEncodingException
*/
byte[] getBytes(int maxLength, Charset charset)
throws UnsupportedEncodingException {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()
Writer writer = new OutputStreamWriter(outputStream, charset)
writer.write(text, 0, Math.min(text.length(), maxLength))
writer.close()
return outputStream.toByteArray()
} catch (IOException ex) {
throw new RuntimeException(ex)
}
}
}

@ -0,0 +1,28 @@
package org.xbib.groovy.crypt
/**
* The SHA-256 encryption algorithm.
*/
class Sha256Crypt extends Sha2Crypt {
Sha256Crypt(CryptType type) {
super(type)
}
@Override
protected String encodePassword(byte[] password) {
StringBuilder sb = new StringBuilder()
.append(Encoder.base64(password[0], password[10], password[20], 4))
.append(Encoder.base64(password[21], password[1], password[11], 4))
.append(Encoder.base64(password[12], password[22], password[2], 4))
.append(Encoder.base64(password[3], password[13], password[23], 4))
.append(Encoder.base64(password[24], password[4], password[14], 4))
.append(Encoder.base64(password[15], password[25], password[5], 4))
.append(Encoder.base64(password[6], password[16], password[26], 4))
.append(Encoder.base64(password[27], password[7], password[17], 4))
.append(Encoder.base64(password[18], password[28], password[8], 4))
.append(Encoder.base64(password[9], password[19], password[29], 4))
.append(Encoder.base64((byte) 0, password[31], password[30], 3))
sb.toString()
}
}

@ -0,0 +1,145 @@
package org.xbib.groovy.crypt
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
/**
* An abstract base for password encryption that uses one of the SHA-2
* variants (SHA-256, SHA-512).
*/
abstract class Sha2Crypt extends Crypt {
private static final String ROUNDS_PARAM = "rounds="
private static final int MIN_ROUNDS = 1000
private static final int MAX_ROUNDS = 999999999
private static final int DEFAULT_ROUNDS = 5000
private static final int MAX_SALT_LENGTH = 16
protected Sha2Crypt(CryptType type) {
super(type)
}
@Override
protected String doCrypt(String password, Salt salt)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
Integer rounds = rounds(salt)
byte[] encrypted = doCrypt(password.getBytes(StandardCharsets.UTF_8),
salt.getBytes(MAX_SALT_LENGTH, StandardCharsets.UTF_8),
rounds == null ? DEFAULT_ROUNDS : rounds)
passwordToString(encrypted, salt, MAX_SALT_LENGTH, rounds)
}
@Override
protected String encodeParameters(Object... params) {
params[0] == null ? null : ROUNDS_PARAM + params[0]
}
/**
* Gets the number of rounds explicitly requested in the given salt
* @param salt the subject salt
* @return number of rounds requested or {@code null} if the salt does not
* specify the number of rounds
*/
private static Integer rounds(Salt salt) {
String params = salt.params
if (params == null || !params.startsWith(ROUNDS_PARAM)) {
return null
}
Integer rounds = Integer.valueOf(params.substring(ROUNDS_PARAM.length()))
rounds = Math.max(MIN_ROUNDS, rounds)
rounds = Math.min(rounds, MAX_ROUNDS)
rounds
}
/**
* Encrypts the given password.
* @param password the password to encrypt
* @param salt salt for the encryption
* @param rounds number of rounds requested
* @return byte array with encrypted value.
* @throws NoSuchAlgorithmException
*/
protected byte[] doCrypt(byte[] password, byte[] salt, int rounds)
throws NoSuchAlgorithmException {
final MessageDigest a = type.newDigest()
final int digestLength = a.getDigestLength()
a.update(password)
a.update(salt)
final MessageDigest b = type.newDigest()
b.update(password)
b.update(salt)
b.update(password)
final byte[] sumB = b.digest()
int max = (password.length / digestLength) as int
for (int i = 0; i < max; i++) {
a.update(sumB)
}
a.update(sumB, 0, password.length % digestLength)
for (int length = password.length; length > 0; length >>>= 1) {
if ((length & 1) != 0) {
a.update(sumB)
} else {
a.update(password)
}
}
final byte[] sumA = a.digest()
final MessageDigest dp = type.newDigest()
for (int i = 0; i < password.length; i++) {
dp.update(password)
}
final byte[] sumDP = dp.digest()
final byte[] seqP = makeSequence(sumDP, password.length, digestLength)
final MessageDigest ds = type.newDigest()
max = 16 + ((int) sumA[0] & 0xff)
for (int i = 0; i < max; i++) {
ds.update(salt)
}
final byte[] sumDS = ds.digest()
final byte[] seqS = makeSequence(sumDS, salt.length, digestLength)
byte[] ac = sumA
max = rounds
for (int i = 0; i < max; i++) {
final MessageDigest c = type.newDigest()
if (i % 2 != 0) {
c.update(seqP)
} else {
c.update(ac)
}
if (i % 3 != 0) {
c.update(seqS)
}
if (i % 7 != 0) {
c.update(seqP)
}
if (i % 2 != 0) {
c.update(ac)
} else {
c.update(seqP)
}
ac = c.digest()
}
ac
}
/**
* Makes a sequence as described in steps 16 and 20 of the algorithm.
* @param sum the intermediate sum to place into the sequence
* @param length length of the sequence in bytes
* @param digestLength length of the digest in bytes
* @return sequence
*/
private static byte[] makeSequence(byte[] sum, int length, final int digestLength) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()
int max = (length / digestLength) as int
for (int i = 0; i < max; i++) {
outputStream.write(sum)
}
outputStream.write(sum, 0, length % digestLength)
outputStream.toByteArray()
}
}

@ -0,0 +1,39 @@
package org.xbib.groovy.crypt
/**
* The SHA-512 encryption algorithm.
*/
class Sha512Crypt extends Sha2Crypt {
Sha512Crypt(CryptType type) {
super(type)
}
@Override
protected String encodePassword(byte[] password) {
StringBuilder sb = new StringBuilder()
.append(Encoder.base64(password[0], password[21], password[42], 4))
.append(Encoder.base64(password[22], password[43], password[1], 4))
.append(Encoder.base64(password[44], password[2], password[23], 4))
.append(Encoder.base64(password[3], password[24], password[45], 4))
.append(Encoder.base64(password[25], password[46], password[4], 4))
.append(Encoder.base64(password[47], password[5], password[26], 4))
.append(Encoder.base64(password[6], password[27], password[48], 4))
.append(Encoder.base64(password[28], password[49], password[7], 4))
.append(Encoder.base64(password[50], password[8], password[29], 4))
.append(Encoder.base64(password[9], password[30], password[51], 4))
.append(Encoder.base64(password[31], password[52], password[10], 4))
.append(Encoder.base64(password[53], password[11], password[32], 4))
.append(Encoder.base64(password[12], password[33], password[54], 4))
.append(Encoder.base64(password[34], password[55], password[13], 4))
.append(Encoder.base64(password[56], password[14], password[35], 4))
.append(Encoder.base64(password[15], password[36], password[57], 4))
.append(Encoder.base64(password[37], password[58], password[16], 4))
.append(Encoder.base64(password[59], password[17], password[38], 4))
.append(Encoder.base64(password[18], password[39], password[60], 4))
.append(Encoder.base64(password[40], password[61], password[19], 4))
.append(Encoder.base64(password[62], password[20], password[41], 4))
.append(Encoder.base64((byte) 0, (byte) 0, password[63], 2))
sb.toString()
}
}

@ -0,0 +1,53 @@
package org.xbib.groovy.crypt.random
/**
* RNG seed strategy that gets data from {@literal /dev/random} on systems
* that provide it (e.g. Solaris/Linux). If {@literal /dev/random} does not
* exist or is not accessible, a {@link SeedException} is thrown.
*/
class DevRandomSeedGenerator implements SeedGenerator {
private static final File DEV_RANDOM = new File("/dev/random")
/**
* Generate seed.
* @return The requested number of random bytes, read directly from
* {@literal /dev/random}.
* @throws SeedException If {@literal /dev/random} does not exist or is
* not accessible
*/
@Override
byte[] generateSeed(int length) throws SeedException {
FileInputStream file = null
try {
file = new FileInputStream(DEV_RANDOM)
byte[] randomSeed = new byte[length]
int count = 0
while (count < length) {
int bytesRead = file.read(randomSeed, count, length - count)
if (bytesRead == -1) {
throw new SeedException("end-of-file while reading random data")
}
count += bytesRead
}
return randomSeed
}
catch (IOException ex) {
throw new SeedException("failed reading from " + DEV_RANDOM.getName(), ex)
}
catch (SecurityException ex) {
throw new SeedException("security prevented access to " + DEV_RANDOM.getName(), ex)
}
finally {
if (file != null) {
try {
file.close()
}
catch (IOException ex) {
// ignore
}
}
}
}
}

@ -0,0 +1,147 @@
package org.xbib.groovy.crypt.random
/**
*
*/
class MTRandom extends Random {
private final static int UPPER_MASK = 0x80000000 as int
private final static int LOWER_MASK = 0x7fffffff
private final static int N = 624
private final static int M = 397
private final static int[] MAGIC = [0x0, 0x9908b0df] as int[]
private final static int MAGIC_FACTOR1 = 1812433253
private final static int MAGIC_FACTOR2 = 1664525
private final static int MAGIC_FACTOR3 = 1566083941
private final static int MAGIC_MASK1 = 0x9d2c5680 as int
private final static int MAGIC_MASK2 = 0xefc60000 as int
private final static int MAGIC_SEED = 19650218
private final static int DEFAULT_SEED = 5489L
private transient int[] mt
private transient int mti
private transient boolean compat = false
private transient int[] ibuf
MTRandom() {
}
MTRandom(boolean compatible) {
super(0L)
compat = compatible
long l = compat ? DEFAULT_SEED : System.currentTimeMillis()
setSeed(l)
}
MTRandom(long seed) {
super(seed)
}
MTRandom(byte[] buf) {
super(0L)
setSeed(buf)
}
MTRandom(int[] buf) {
super(0L)
setSeed(buf)
}
void setSeed(int seed) {
if (mt == null) {
mt = new int[N]
}
mt[0] = seed
for (mti = 1; mti < N; mti++) {
mt[mti] = (MAGIC_FACTOR1 * (mt[mti - 1] ^ (mt[mti - 1] >>> 30)) + mti)
}
}
synchronized void setSeed(long seed) {
if (compat) {
setSeed((int) seed)
} else {
if (ibuf == null) {
ibuf = new int[2]
}
ibuf[0] = (int) seed
ibuf[1] = (int) (seed >>> 32)
setSeed(ibuf)
}
}
void setSeed(byte[] buf) {
setSeed(pack(buf))
}
synchronized void setSeed(int[] buf) {
int length = buf.length
if (length == 0) {
throw new IllegalArgumentException("Seed buffer may not be empty")
}
int i = 1
int j = 0
int k = (N > length ? N : length)
setSeed(MAGIC_SEED)
for (; k > 0; k--) {
mt[i] = (mt[i] ^ ((mt[i - 1] ^ (mt[i - 1] >>> 30)) * MAGIC_FACTOR2)) + buf[j] + j
i++
j++
if (i >= N) {
mt[0] = mt[N - 1]
i = 1
}
if (j >= length) {
j = 0
}
}
for (k = N - 1; k > 0; k--) {
mt[i] = (mt[i] ^ ((mt[i - 1] ^ (mt[i - 1] >>> 30)) * MAGIC_FACTOR3)) - i
i++
if (i >= N) {
mt[0] = mt[N - 1]
i = 1
}
}
mt[0] = UPPER_MASK
}
protected synchronized int next(int bits) {
int y
int kk
if (mti >= N) {
for (kk = 0; kk < N - M; kk++) {
y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK)
mt[kk] = mt[kk + M] ^ (y >>> 1) ^ MAGIC[y & 0x1]
}
for (; kk < N - 1; kk++) {
y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK)
mt[kk] = mt[kk + (M - N)] ^ (y >>> 1) ^ MAGIC[y & 0x1]
}
y = (mt[N - 1] & UPPER_MASK) | (mt[0] & LOWER_MASK)
mt[N - 1] = mt[M - 1] ^ (y >>> 1) ^ MAGIC[y & 0x1]
mti = 0
}
y = mt[mti++]
y ^= (y >>> 11)
y ^= (y << 7) & MAGIC_MASK1
y ^= (y << 15) & MAGIC_MASK2
y ^= (y >>> 18)
y >>> (32 - bits)
}
private static int[] pack(byte[] buf) {
int k
int blen = buf.length
int ilen = ((buf.length + 3) >>> 2)
int[] ibuf = new int[ilen]
for (int n = 0; n < ilen; n++) {
int m = (n + 1) << 2
if (m > blen) {
m = blen
}
for (k = buf[--m] & 0xff; (m & 0x3) != 0; k = (k << 8) | buf[--m] & 0xff) {
ibuf[n] = k
}
}
ibuf
}
}

@ -0,0 +1,118 @@
package org.xbib.groovy.crypt.random
import java.util.concurrent.locks.ReentrantLock
class MersenneTwisterRandom extends Random {
private static final int SEED_SIZE_BYTES = 16
private static final int N = 624
private static final int M = 397
private static final int[] MAG01 = [0, 0x9908b0df]
private static final int UPPER_MASK = 0x80000000 as int
private static final int LOWER_MASK = 0x7fffffff as int
private static final int BOOTSTRAP_SEED = 19650218
private static final int BOOTSTRAP_FACTOR = 1812433253
private static final int SEED_FACTOR1 = 1664525
private static final int SEED_FACTOR2 = 1566083941
private static final int GENERATE_MASK1 = 0x9d2c5680 as int
private static final int GENERATE_MASK2 = 0xefc60000 as int
private static final SeedGenerator[] GENERATORS = [new SecureRandomSeedGenerator(), new DevRandomSeedGenerator()]
private static final int BITWISE_BYTE_TO_INT = 0x000000FF
private final ReentrantLock lock = new ReentrantLock()
private final int[] mt = new int[N]
private byte[] seed
private int mtIndex = 0
MersenneTwisterRandom() {
this.seed = generateSeed(SEED_SIZE_BYTES)
if (seed == null || seed.length != SEED_SIZE_BYTES) {
throw new IllegalArgumentException("Mersenne Twister requires a 128-bit seed")
}
int[] seedInts = convertBytesToInts(this.seed)
mt[0] = BOOTSTRAP_SEED
for (mtIndex = 1; mtIndex < N; mtIndex++) {
mt[mtIndex] = BOOTSTRAP_FACTOR * (mt[mtIndex - 1] ^ (mt[mtIndex - 1] >>> 30)) + mtIndex
}
int i = 1
int j = 0
for (int k = Math.max(N, seedInts.length); k > 0; k--) {
mt[i] = (mt[i] ^ ((mt[i - 1] ^ (mt[i - 1] >>> 30)) * SEED_FACTOR1)) + seedInts[j] + j
i++
j++
if (i >= N) {
mt[0] = mt[N - 1]
i = 1
}
if (j >= seedInts.length) {
j = 0
}
}
for (int k = N - 1; k > 0; k--) {
mt[i] = (mt[i] ^ ((mt[i - 1] ^ (mt[i - 1] >>> 30)) * SEED_FACTOR2)) - i
i++
if (i >= N) {
mt[0] = mt[N - 1]
i = 1
}
}
mt[0] = UPPER_MASK
}
@Override
protected final int next(int bits) {
int y = 0
try {
lock.lock()
if (mtIndex >= N) {
int kk
for (kk = 0; kk < N - M; kk++) {
y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK)
mt[kk] = mt[kk + M] ^ (y >>> 1) ^ MAG01[y & 0x1]
}
for (; kk < N - 1; kk++) {
y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK)
mt[kk] = mt[kk + (M - N)] ^ (y >>> 1) ^ MAG01[y & 0x1]
}
y = (mt[N - 1] & UPPER_MASK) | (mt[0] & LOWER_MASK)
mt[N - 1] = mt[M - 1] ^ (y >>> 1) ^ MAG01[y & 0x1]
mtIndex = 0
}
y = mt[mtIndex++]
}
finally {
lock.unlock()
}
y ^= (y >>> 11)
y ^= (y << 7) & GENERATE_MASK1
y ^= (y << 15) & GENERATE_MASK2
y ^= (y >>> 18)
return y >>> (32 - bits)
}
private static byte[] generateSeed(int length) {
for (SeedGenerator generator : GENERATORS) {
try {
return generator.generateSeed(length)
}
catch (SeedException ex) {
// ignore
}
}
throw new IllegalStateException("no seed generator available")
}
private static int[] convertBytesToInts(byte[] bytes) {
if (bytes.length % 4 != 0) {
throw new IllegalArgumentException("number of input bytes must be a multiple of 4")
}
int[] ints = new int[bytes.length / 4]
for (int i = 0; i < ints.length; i++) {
ints[i] = convertBytesToInt(bytes, i * 4)
}
return ints
}
private static int convertBytesToInt(byte[] bytes, int offset) {
(BITWISE_BYTE_TO_INT & bytes[offset + 3]) | ((BITWISE_BYTE_TO_INT & bytes[offset + 2]) << 8) | ((BITWISE_BYTE_TO_INT & bytes[offset + 1]) << 16) | ((BITWISE_BYTE_TO_INT & bytes[offset]) << 24)
}
}

@ -0,0 +1,14 @@
package org.xbib.groovy.crypt.random
import java.security.SecureRandom
class RandomUtil {
static final SecureRandom secureRandom = new SecureRandom()
static String randomString(int len) {
byte[] b = new byte[len]
secureRandom.nextBytes(b)
b.encodeHex().toString()
}
}

@ -0,0 +1,25 @@
package org.xbib.groovy.crypt.random
import java.security.SecureRandom
/**
* <p>{@link SeedGenerator} implementation that uses
* {@link java.security.SecureRandom} to generate random seed data.</p>
*
* <p>The advantage of using SecureRandom for seeding but not as the
* primary random number generator is that we can use it to seed random number generators
* that are much faster than SecureRandom.</p>
*
* <p>This is the only seeding strategy that is guaranteed to work on all
* platforms and therefore is provided as a fall-back option should
* none of the other provided {@link SeedGenerator} implementations be
* useable.</p>
*/
class SecureRandomSeedGenerator implements SeedGenerator {
private static final SecureRandom INSTANCE = new SecureRandom()
byte[] generateSeed(int length) throws SeedException {
return INSTANCE.generateSeed(length)
}
}

@ -0,0 +1,16 @@
package org.xbib.groovy.crypt.random
/**
* Exception thrown by {@link SeedGenerator} implementations when
* they are unable to generate a new seed for a randon number generator.
*/
class SeedException extends Exception {
SeedException(String message) {
super(message);
}
SeedException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,14 @@
package org.xbib.groovy.crypt.random
/**
* Strategy interface for seeding random number generators.
*/
interface SeedGenerator {
/**
* Generate a seed value for a random number generator.
* @param length The length of the seed to generate (in bytes).
* @return A byte array containing the seed data.
* @throws SeedException If a seed cannot be generated for any reason.
*/
byte[] generateSeed(int length) throws SeedException
}

@ -0,0 +1,116 @@
package org.xbib.groovy.crypt.random
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicInteger
/**
* UUID generator. The UUIDs are essentially flake ids
* but we use 6 (not 8) bytes for timestamp and 3 (not 2) bytes for sequence number.
*/
class UUIDGenerator {
private final static byte[] secureAddress = getSecureAddress()
private final AtomicInteger sequenceNumber = new AtomicInteger(RandomUtil.secureRandom.nextInt())
private long lastTimestamp
String getBase16UUID() {
StringBuilder sb = new StringBuilder()
getUUIDBytes().each {
sb.append(Integer.toHexString((int) it & 0xFF))
}
sb.toString()
}
String getBase64UUID() {
byte[] encoded = getBase64UUIDBytes()
new String(encoded, 0, encoded.length, StandardCharsets.US_ASCII)
}
private static void putLong(byte[] array, long l, int pos, int numberOfLongBytes) {
for (int i=0; i<numberOfLongBytes; ++i) {
array[pos + numberOfLongBytes-i-1] = (byte) (l >>> (i*8))
}
}
private static byte[] getSecureAddress() {
byte[] address = null
try {
address = getMacAddress()
} catch (Throwable t) {
}
if (!isValidAddress(address)) {
address = constructDummyMulticastAddress()
}
byte[] bytes = new byte[6]
RandomUtil.secureRandom.nextBytes(bytes)
for (int i = 0; i < 6; ++i) {
bytes[i] = (bytes[i] ^ address[i]) as byte
}
bytes
}
private static byte[] constructDummyMulticastAddress() {
byte[] bytes = new byte[6]
RandomUtil.secureRandom.nextBytes(bytes)
bytes[0] = (bytes[0] | 0x01) as byte
bytes
}
private static byte[] getMacAddress() throws SocketException {
Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces()
if (en != null) {
while (en.hasMoreElements()) {
NetworkInterface nint = en.nextElement()
if (!nint.isLoopback()) {
byte[] address = nint.getHardwareAddress()
if (isValidAddress(address)) {
return address
}
}
}
}
null
}
private static boolean isValidAddress(byte[] address) {
if (address == null || address.length != 6) {
return false
}
for (byte b : address) {
if (b != 0x00 as byte) {
return true
}
}
false
}
private byte[] getUUIDBytes() {
int sequenceId = sequenceNumber.incrementAndGet() & 0xffffff
long timestamp = System.currentTimeMillis()
synchronized (this) {
timestamp = Math.max(lastTimestamp, timestamp)
if (sequenceId == 0) {
timestamp++
}
lastTimestamp = timestamp
}
byte[] uuidBytes = new byte[15]
putLong(uuidBytes, timestamp, 0, 6)
System.arraycopy(secureAddress, 0, uuidBytes, 6, secureAddress.length)
putLong(uuidBytes, sequenceId, 12, 3)
uuidBytes
}
private byte[] getBase64UUIDBytes() {
byte[] uuidBytes = getUUIDBytes()
byte[] encoded
try {
encoded = Base64.encoder.encode(uuidBytes)
} catch (IOException e) {
throw new IllegalStateException("should not be thrown", e)
}
encoded
}
}

@ -0,0 +1,172 @@
package org.xbib.groovy.crypt.test
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.function.Executable
import org.xbib.groovy.crypt.Crypt
import org.xbib.groovy.crypt.CryptUtil
import org.xbib.groovy.crypt.DesCrypt
import java.security.NoSuchAlgorithmException
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertNotSame
import static org.junit.jupiter.api.Assertions.assertTrue
class CryptTest {
@Test
void testWithNonExistentType() throws Exception {
Assertions.assertThrows(NoSuchAlgorithmException.class, new Executable() {
@Override
void execute() throws Throwable {
Crypt.crypt("password", '$99$XX')
}
})
}
@Test
void testUnixCryptStrings() {
assertEquals("xxWAum7tHdIUw", Crypt.crypt("secret", "xx"))
assertEquals("12UFlHxel6uMM", Crypt.crypt("", "12"))
assertEquals("12FJgqDtVOg7Q", Crypt.crypt("secret", "12"))
assertEquals("12FJgqDtVOg7Q", Crypt.crypt("secret", "12345678"))
}
@Test
void testUnixCryptExplicitCall() {
// A call to crypt() with an empty salt would result in a "$6$" hash.
// Using unixCrypt() explicitly results in a random salt.
assertTrue(DesCrypt.crypt("secret".getBytes()).matches('^[a-zA-Z0-9./]{13}$'))
assertTrue(DesCrypt.crypt("secret".getBytes(), null).matches('^[a-zA-Z0-9./]{13}$'))
}
/**
* Single character salts are illegal!
* E.g. with glibc 2.13, crypt("secret", "x") = "xxZREZpkHZpkI" but
* crypt("secret", "xx") = "xxWAum7tHdIUw" which makes it unverifyable.
*/
@Test
void testUnixCryptWithHalfSalt() {
Assertions.assertThrows(IllegalArgumentException.class, new Executable() {
@Override
void execute() throws Throwable {
DesCrypt.crypt("secret", "x")
}
})
}
/**
* Unimplemented "$foo$" salt prefixes would be threated as UnixCrypt salt.
*/
@Test
void testUnicCryptInvalidSalt() {
Assertions.assertThrows(IllegalArgumentException.class, new Executable() {
@Override
void execute() throws Throwable {
DesCrypt.crypt("secret", '$a')
}
})
}
@Test
void testUnixCryptWithEmptySalt() {
Assertions.assertThrows(IllegalArgumentException.class, new Executable() {
@Override
void execute() throws Throwable {
DesCrypt.crypt("secret", "")
}
})
}
@Test
void testUnixCryptWithoutSalt() {
final String hash = DesCrypt.crypt("foo");
assertTrue(hash.matches('^[a-zA-Z0-9./]{13}$'))
final String hash2 = DesCrypt.crypt("foo")
assertNotSame(hash, hash2)
}
@Test
void testCrypt() {
assertEquals('saszt8mUri4AI',
Crypt.crypt('Hello world!', 'saltstring'),
'DES with simple password and salt')
assertEquals('$1$saltstri$YMyguxXMBpd2TEZ.vS/3q1',
Crypt.crypt('Hello world!', '$1$saltstring'),
'MD5 with password and excessive salt')
assertEquals('$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5',
Crypt.crypt('Hello world!', '$5$saltstring'),
'SHA-256 with password < digest and salt < max')
assertEquals('$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2.opqey6IcA',
Crypt.crypt('Hello world!', '$5$rounds=10000$saltstringsaltstring'),
'SHA-256 with excessive salt, explicit rounds')
assertEquals('$5$rounds=5000$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8mGRcvxa5',
Crypt.crypt('This is just a test', '$5$rounds=5000$toolongsaltstring'),
'SHA-256 with excessive salt and explicit default rounds param',)
assertEquals('$5$rounds=1400$anotherlongsalts$Rx.j8H.h8HjEDGomFU8bDkXm3XIUnzyxf12oP84Bnq1',
Crypt.crypt('a very much longer text to encrypt. This one even stretches over morethan one line.', '$5$rounds=1400$anotherlongsaltstring'),
'SHA-256 with password > digest, excessive salt, explicit rounds')
assertEquals('$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/',
Crypt.crypt('we have a short salt string but not a short password', '$5$rounds=77777$short'),
'SHA-256 with short salt, long password, explicit rounds')
assertEquals('$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPyzV/cZKmF/wJvD',
Crypt.crypt('a short string', '$5$rounds=123456$asaltof16chars..'),
'SHA-256 with maximal salt, short password, large explicit round count',)
assertEquals('$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL972bIC',
Crypt.crypt('the minimum number is still observed', '$5$rounds=10$roundstoolow'),
'SHA-256 with rounds below mininum',)
assertEquals('$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1',
Crypt.crypt('Hello world!', '$6$saltstring'),
'SHA-512 with password.length < digest.length and salt < max',)
assertEquals('$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.',
Crypt.crypt('Hello world!', '$6$rounds=10000$saltstringsaltstring'),
'SHA-512 with excessive salt, explicit rounds',)
assertEquals('$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0',
Crypt.crypt('This is just a test', '$6$rounds=5000$toolongsaltstring'),
'SHA-512 with excessive salt and explicit default rounds param',)
assertEquals('$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wPvMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1',
Crypt.crypt('a very much longer text to encrypt. This one even stretches over morethan one line.', '$6$rounds=1400$anotherlongsaltstring'),
'SHA-512 with password > digest, excessive salt, explicit rounds',)
assertEquals('$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0',
Crypt.crypt('we have a short salt string but not a short password', '$6$rounds=77777$short'),
'SHA-512 with short salt, long password, explicit rounds',)
assertEquals('$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1',
Crypt.crypt('a short string', '$6$rounds=123456$asaltof16chars..'),
'SHA-512 with maximal salt, short password, large explicit round count')
assertEquals('$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX.',
Crypt.crypt('the minimum number is still observed', '$6$rounds=10$roundstoolow'),
'SHA-512 with rounds below mininum')
}
@Test
void testHMAC() {
String s = "Hello World"
String secret = "secret"
String code = CryptUtil.hmac(s, secret, "HmacSHA1")
assertEquals("858da8837b87f04b052c0f6e954c3f7bbe081164", code)
}
@Test
void testSHA() {
String plaintext = 'geheim'
String code = CryptUtil.sha(plaintext)
assertEquals('{sha}kGByAB793z4R5tK1eC9Hd/4Dhzk=', code, 'SHA algorithm')
}
@Test
void testSSHA256() {
String plaintext = 'geheim'
byte[] salt = "467dd5b71e8d0f9e".decodeHex()
String code = CryptUtil.ssha256(plaintext, salt)
assertEquals('{ssha256}9yT5rYItjXK+mY8sKNBcKsKSnlY6ysTg8wbDVmAguTFGfdW3Ho0Png==', code, 'test SSHA-256 method')
}
@Test
void testSSHA512() {
String plaintext = 'geheim'
byte[] salt = "3c68f1f47f41d82f".decodeHex()
String code = CryptUtil.ssha512(plaintext, salt)
assertEquals('{ssha512}jeWuCXRjsvKh/vK548GP9ZCs4q9Sh1u700C8eONyV+EL/P810C8vlx9Eu4vRjHq/TDoGW8FE1l/P2KG3w9lHITxo8fR/Qdgv', code,'test SSHA-512 method')
}
}

@ -0,0 +1,19 @@
package org.xbib.groovy.crypt.test
import org.junit.jupiter.api.Test
import org.xbib.groovy.crypt.random.RandomUtil
import static org.junit.jupiter.api.Assertions.assertNotEquals
class RandomTest {
@Test
void testRandom() {
String s1 = RandomUtil.randomString(16)
String s2 = RandomUtil.randomString(16)
String s3 = RandomUtil.randomString(16)
assertNotEquals(s1, s2)
assertNotEquals(s1, s3)
assertNotEquals(s2, s3)
}
}

@ -0,0 +1,7 @@
apply from: rootProject.file('gradle/compile/groovy.gradle')
dependencies {
api "org.xbib:ftp-fs:${project.property('ftp.version')}"
testImplementation "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}"
testImplementation "org.apache.logging.log4j:log4j-jul:${project.property('log4j.version')}"
}

@ -0,0 +1,396 @@
package org.xbib.groovy.ftp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import groovy.lang.Closure;
public class FTP {
private static final int READ_BUFFER_SIZE = 128 * 1024;
private static final int WRITE_BUFFER_SIZE = 128 * 1024;
private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
PosixFilePermissions.fromString("rwxr-xr-x");
private static final Set<PosixFilePermission> DEFAULT_FILE_PERMISSIONS =
PosixFilePermissions.fromString("rw-r--r--");
private final String url;
private final Map<String, ?> env;
private FTP(String url, Map<String, ?> env) {
this.url = url;
this.env = env;
}
public static FTP newInstance() {
return newInstance("ftp://localhost:21");
}
public static FTP newInstance(Map<String, ?> env) {
return newInstance("ftp://localhost:21", env);
}
public static FTP newInstance(String url) {
return newInstance(url, null);
}
public static FTP newInstance(String url, Map<String, ?> env) {
return new FTP(url, env);
}
public Boolean exists(String path) throws Exception {
return performWithContext(ctx -> Files.exists(ctx.fileSystem.getPath(path)));
}
public Boolean isExecutable(String path) throws Exception {
return performWithContext(ctx -> Files.isExecutable(ctx.fileSystem.getPath(path)));
}
public Boolean isDirectory(String path) throws Exception {
return performWithContext(ctx -> Files.isDirectory(ctx.fileSystem.getPath(path)));
}
public Boolean isRegularFile(String path) throws Exception {
return performWithContext(ctx -> Files.isRegularFile(ctx.fileSystem.getPath(path)));
}
public Boolean isHidden(String path) throws Exception {
return performWithContext(ctx -> Files.isHidden(ctx.fileSystem.getPath(path)));
}
public Boolean isSameFile(String path1, String path2) throws Exception {
return performWithContext(ctx -> Files.isSameFile(ctx.fileSystem.getPath(path1), ctx.fileSystem.getPath(path2)));
}
public Boolean isSymbolicLink(String path) throws Exception {
return performWithContext(ctx -> Files.isSymbolicLink(ctx.fileSystem.getPath(path)));
}
public Boolean isReadable(String path) throws Exception {
return performWithContext(ctx -> Files.isReadable(ctx.fileSystem.getPath(path)));
}
public Boolean isWritable(String path) throws Exception {
return performWithContext(ctx -> Files.isWritable(ctx.fileSystem.getPath(path)));
}
public void createFile(String path, FileAttribute<?>... attributes) throws Exception {
performWithContext(ctx -> Files.createFile(ctx.fileSystem.getPath(path), attributes));
}
public void createDirectory(String path, FileAttribute<?>... attributes) throws Exception {
performWithContext(ctx -> Files.createDirectory(ctx.fileSystem.getPath(path), attributes));
}
public void createDirectories(String path, FileAttribute<?>... attributes) throws Exception {
performWithContext(ctx -> Files.createDirectories(ctx.fileSystem.getPath(path), attributes));
}
public void setAttribute(String path, String attribute, Object value) throws Exception {
performWithContext(ctx -> Files.setAttribute(ctx.fileSystem.getPath(path), attribute, value));
}
public Object getAttribute(String path, String attribute) throws Exception {
return performWithContext(ctx -> Files.getAttribute(ctx.fileSystem.getPath(path), attribute));
}
public void setPermissions(String path, Set<PosixFilePermission> permissions) throws Exception {
performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions));
}
public Set<PosixFilePermission> getPermissions(String path) throws Exception {
return performWithContext(ctx -> Files.getPosixFilePermissions(ctx.fileSystem.getPath(path)));
}
public void setLastModifiedTime(String path, FileTime fileTime) throws Exception {
performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime));
}
public FileTime getLastModified(String path) throws Exception{
return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)));
}
public void setOwner(String path, UserPrincipal userPrincipal) throws Exception {
performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal));
}
public UserPrincipal getOwner(String path) throws Exception {
return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)));
}
public void each(String path, Closure<?> closure) throws Exception {
performWithContext(ctx -> {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path))) {
stream.forEach(closure::call);
}
return null;
});
}
public void eachFilter(String path, DirectoryStream.Filter<Path> filter, Closure<?> closure) throws Exception {
performWithContext(ctx -> {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)) {
stream.forEach(closure::call);
}
return null;
});
}
public void upload(Path source, Path target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(Path source, Path target,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE,
dirPerms, filePerms, copyOptions);
return null;
});
}
public void upload(Path source, String target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(Path source, String target,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
dirPerms, filePerms, copyOptions);
return null;
});
}
public void upload(InputStream source, Path target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(InputStream source, Path target,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE,
dirPerms, filePerms, copyOptions);
return null;
});
}
public void upload(InputStream source, String target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(InputStream source, String target,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
dirPerms, filePerms, copyOptions);
return null;
});
}
public void download(Path source, Path target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
download(ctx, source, target, READ_BUFFER_SIZE, copyOptions);
return null;
});
}
public void download(String source, Path target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions);
return null;
});
}
public void download(Path source, OutputStream target) throws Exception {
performWithContext(ctx -> {
download(ctx, source, target, READ_BUFFER_SIZE);
return null;
});
}
public void download(String source, OutputStream target) throws Exception {
performWithContext(ctx -> {
Files.copy(ctx.fileSystem.getPath(source), target);
return null;
});
}
public void copy(String source, String target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
return null;
});
}
public void rename(String source, String target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
return null;
});
}
public void remove(String source) throws Exception {
performWithContext(ctx -> {
Files.deleteIfExists(ctx.fileSystem.getPath(source));
return null;
});
}
private void upload(FTPContext ctx,
ReadableByteChannel source,
Path target,
int bufferSize,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
prepareForWrite(target, dirPerms, filePerms);
transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
}
private void download(FTPContext ctx,
Path source,
OutputStream outputStream,
int bufferSize) throws Exception {
download(ctx, source, Channels.newChannel(outputStream), bufferSize);
}
private void download(FTPContext ctx,
Path source,
WritableByteChannel writableByteChannel,
int bufferSize) throws Exception {
transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel,
bufferSize);
}
private void download(FTPContext ctx,
Path source,
Path target,
int bufferSize,
CopyOption... copyOptions) throws Exception {
prepareForWrite(target);
transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)),
Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
}
private void prepareForWrite(Path path) throws IOException {
if (path == null) {
return;
}
Path parent = path.getParent();
if (parent != null) {
if (!Files.exists(parent)) {
Files.createDirectories(parent);
}
}
if (!Files.exists(path)) {
Files.createFile(path);
}
}
private void prepareForWrite(Path path,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms) throws IOException {
if (path == null) {
return;
}
Path parent = path.getParent();
if (parent != null) {
if (!Files.exists(parent)) {
Files.createDirectories(parent);
}
PosixFileAttributeView posixFileAttributeView =
Files.getFileAttributeView(parent, PosixFileAttributeView.class);
posixFileAttributeView.setPermissions(dirPerms);
}
if (!Files.exists(path)) {
Files.createFile(path);
}
PosixFileAttributeView posixFileAttributeView =
Files.getFileAttributeView(path, PosixFileAttributeView.class);
posixFileAttributeView.setPermissions(filePerms);
}
private Set<? extends OpenOption> prepareReadOptions(CopyOption... copyOptions) {
// ignore user copy options
return EnumSet.of(StandardOpenOption.READ);
}
private Set<? extends OpenOption> prepareWriteOptions(CopyOption... copyOptions) {
Set<? extends OpenOption> options = null;
for (CopyOption copyOption : copyOptions) {
if (copyOption == StandardCopyOption.REPLACE_EXISTING) {
options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
}
}
if (options == null) {
// we can not use CREATE_NEW, file is already there because of prepareForWrite() -> Files.createFile()
options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE);
}
return options;
}
private void transfer(ReadableByteChannel readableByteChannel,
WritableByteChannel writableByteChannel,
int bufferSize) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
int read;
while ((read = readableByteChannel.read(buffer)) > 0) {
buffer.flip();
while (read > 0) {
read -= writableByteChannel.write(buffer);
}
buffer.clear();
}
}
private <T> T performWithContext(WithContext<T> action) throws Exception {
FTPContext ctx = null;
try {
if (url != null) {
ctx = new FTPContext(URI.create(url), env);
return action.perform(ctx);
} else {
return null;
}
} finally {
if (ctx != null) {
ctx.close();
}
}
}
}

@ -0,0 +1,27 @@
package org.xbib.groovy.ftp;
import org.xbib.io.ftp.fs.FTPEnvironment;
import org.xbib.io.ftp.fs.FTPFileSystemProvider;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.util.Map;
/**
*/
class FTPContext {
final FTPFileSystemProvider provider;
final FileSystem fileSystem;
FTPContext(URI uri, Map<String, ?> env) throws IOException {
this.provider = new FTPFileSystemProvider();
this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPEnvironment());
}
void close() throws IOException {
fileSystem.close();
}
}

@ -0,0 +1,9 @@
package org.xbib.groovy.ftp;
/**
*
* @param <T> the context parameter
*/
public interface WithContext<T> {
T perform(FTPContext ctx) throws Exception;
}

@ -0,0 +1,22 @@
package org.xbib.groovy.ftp
import groovy.util.logging.Log4j2
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import java.nio.file.Files
import java.nio.file.Path
@Log4j2
class FTPTest {
@Disabled
@Test
void testFTP() {
FTP ftp = FTP.newInstance("ftp://demo.wftpserver.com:21", [username: 'demo', password: 'demo'.toCharArray()])
log.info ftp.exists('/')
ftp.each('/') { Path path ->
log.info "{} {} {}", path, Files.isDirectory(path), Files.getLastModifiedTime(path)
}
}
}

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{ISO8601}][%-5p][%-25c][%t] %m%n"/>
</Console>
</appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</configuration>

@ -0,0 +1,7 @@
apply from: rootProject.file('gradle/compile/groovy.gradle')
dependencies {
api "org.xbib:ftp-fs:${project.property('ftp.version')}"
testImplementation "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}"
testImplementation "org.apache.logging.log4j:log4j-jul:${project.property('log4j.version')}"
}

@ -0,0 +1,395 @@
package org.xbib.groovy.ftps;
import groovy.lang.Closure;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
public class FTPS {
private static final int READ_BUFFER_SIZE = 128 * 1024;
private static final int WRITE_BUFFER_SIZE = 128 * 1024;
private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
PosixFilePermissions.fromString("rwxr-xr-x");
private static final Set<PosixFilePermission> DEFAULT_FILE_PERMISSIONS =
PosixFilePermissions.fromString("rw-r--r--");
private final String url;
private final Map<String, ?> env;
private FTPS(String url, Map<String, ?> env) {
this.url = url;
this.env = env;
}
public static FTPS newInstance() {
return newInstance("ftps://localhost:21");
}
public static FTPS newInstance(Map<String, ?> env) {
return newInstance("ftps://localhost:21", env);
}
public static FTPS newInstance(String url) {
return newInstance(url, null);
}
public static FTPS newInstance(String url, Map<String, ?> env) {
return new FTPS(url, env);
}
public Boolean exists(String path) throws Exception {
return performWithContext(ctx -> Files.exists(ctx.fileSystem.getPath(path)));
}
public Boolean isExecutable(String path) throws Exception {
return performWithContext(ctx -> Files.isExecutable(ctx.fileSystem.getPath(path)));
}
public Boolean isDirectory(String path) throws Exception {
return performWithContext(ctx -> Files.isDirectory(ctx.fileSystem.getPath(path)));
}
public Boolean isRegularFile(String path) throws Exception {
return performWithContext(ctx -> Files.isRegularFile(ctx.fileSystem.getPath(path)));
}
public Boolean isHidden(String path) throws Exception {
return performWithContext(ctx -> Files.isHidden(ctx.fileSystem.getPath(path)));
}
public Boolean isSameFile(String path1, String path2) throws Exception {
return performWithContext(ctx -> Files.isSameFile(ctx.fileSystem.getPath(path1), ctx.fileSystem.getPath(path2)));
}
public Boolean isSymbolicLink(String path) throws Exception {
return performWithContext(ctx -> Files.isSymbolicLink(ctx.fileSystem.getPath(path)));
}
public Boolean isReadable(String path) throws Exception {
return performWithContext(ctx -> Files.isReadable(ctx.fileSystem.getPath(path)));
}
public Boolean isWritable(String path) throws Exception {
return performWithContext(ctx -> Files.isWritable(ctx.fileSystem.getPath(path)));
}
public void createFile(String path, FileAttribute<?>... attributes) throws Exception {
performWithContext(ctx -> Files.createFile(ctx.fileSystem.getPath(path), attributes));
}
public void createDirectory(String path, FileAttribute<?>... attributes) throws Exception {
performWithContext(ctx -> Files.createDirectory(ctx.fileSystem.getPath(path), attributes));
}
public void createDirectories(String path, FileAttribute<?>... attributes) throws Exception {
performWithContext(ctx -> Files.createDirectories(ctx.fileSystem.getPath(path), attributes));
}
public void setAttribute(String path, String attribute, Object value) throws Exception {
performWithContext(ctx -> Files.setAttribute(ctx.fileSystem.getPath(path), attribute, value));
}
public Object getAttribute(String path, String attribute) throws Exception {
return performWithContext(ctx -> Files.getAttribute(ctx.fileSystem.getPath(path), attribute));
}
public void setPermissions(String path, Set<PosixFilePermission> permissions) throws Exception {
performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions));
}
public Set<PosixFilePermission> getPermissions(String path) throws Exception {
return performWithContext(ctx -> Files.getPosixFilePermissions(ctx.fileSystem.getPath(path)));
}
public void setLastModifiedTime(String path, FileTime fileTime) throws Exception {
performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime));
}
public FileTime getLastModified(String path) throws Exception{
return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)));
}
public void setOwner(String path, UserPrincipal userPrincipal) throws Exception {
performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal));
}
public UserPrincipal getOwner(String path) throws Exception {
return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)));
}
public void each(String path, Closure<?> closure) throws Exception {
performWithContext(ctx -> {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path))) {
stream.forEach(closure::call);
}
return null;
});
}
public void eachFilter(String path, DirectoryStream.Filter<Path> filter, Closure<?> closure) throws Exception {
performWithContext(ctx -> {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)) {
stream.forEach(closure::call);
}
return null;
});
}
public void upload(Path source, Path target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(Path source, Path target,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE,
dirPerms, filePerms, copyOptions);
return null;
});
}
public void upload(Path source, String target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(Path source, String target,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
dirPerms, filePerms, copyOptions);
return null;
});
}
public void upload(InputStream source, Path target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(InputStream source, Path target,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE,
dirPerms, filePerms, copyOptions);
return null;
});
}
public void upload(InputStream source, String target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(InputStream source, String target,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
dirPerms, filePerms, copyOptions);
return null;
});
}
public void download(Path source, Path target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
download(ctx, source, target, READ_BUFFER_SIZE, copyOptions);
return null;
});
}
public void download(String source, Path target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions);
return null;
});
}
public void download(Path source, OutputStream target) throws Exception {
performWithContext(ctx -> {
download(ctx, source, target, READ_BUFFER_SIZE);
return null;
});
}
public void download(String source, OutputStream target) throws Exception {
performWithContext(ctx -> {
download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE);
return null;
});
}
public void copy(String source, String target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
return null;
});
}
public void rename(String source, String target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
return null;
});
}
public void remove(String source) throws Exception {
performWithContext(ctx -> {
Files.deleteIfExists(ctx.fileSystem.getPath(source));
return null;
});
}
private void upload(FTPSContext ctx,
ReadableByteChannel source,
Path target,
int bufferSize,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
prepareForWrite(target, dirPerms, filePerms);
transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
}
private void download(FTPSContext ctx,
Path source,
OutputStream outputStream,
int bufferSize) throws Exception {
download(ctx, source, Channels.newChannel(outputStream), bufferSize);
}
private void download(FTPSContext ctx,
Path source,
WritableByteChannel writableByteChannel,
int bufferSize) throws Exception {
transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel,
bufferSize);
}
private void download(FTPSContext ctx,
Path source,
Path target,
int bufferSize,
CopyOption... copyOptions) throws Exception {
prepareForWrite(target);
transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)),
Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
}
private void prepareForWrite(Path path) throws IOException {
if (path == null) {
return;
}
Path parent = path.getParent();
if (parent != null) {
if (!Files.exists(parent)) {
Files.createDirectories(parent);
}
}
if (!Files.exists(path)) {
Files.createFile(path);
}
}
private void prepareForWrite(Path path,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms) throws IOException {
if (path == null) {
return;
}
Path parent = path.getParent();
if (parent != null) {
if (!Files.exists(parent)) {
Files.createDirectories(parent);
}
PosixFileAttributeView posixFileAttributeView =
Files.getFileAttributeView(parent, PosixFileAttributeView.class);
posixFileAttributeView.setPermissions(dirPerms);
}
if (!Files.exists(path)) {
Files.createFile(path);
}
PosixFileAttributeView posixFileAttributeView =
Files.getFileAttributeView(path, PosixFileAttributeView.class);
posixFileAttributeView.setPermissions(filePerms);
}
private Set<? extends OpenOption> prepareReadOptions(CopyOption... copyOptions) {
// ignore user copy options
return EnumSet.of(StandardOpenOption.READ);
}
private Set<? extends OpenOption> prepareWriteOptions(CopyOption... copyOptions) {
Set<? extends OpenOption> options = null;
for (CopyOption copyOption : copyOptions) {
if (copyOption == StandardCopyOption.REPLACE_EXISTING) {
options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
}
}
if (options == null) {
// we can not use CREATE_NEW, file is already there because of prepareForWrite() -> Files.createFile()
options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE);
}
return options;
}
private void transfer(ReadableByteChannel readableByteChannel,
WritableByteChannel writableByteChannel,
int bufferSize) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
int read;
while ((read = readableByteChannel.read(buffer)) > 0) {
buffer.flip();
while (read > 0) {
read -= writableByteChannel.write(buffer);
}
buffer.clear();
}
}
private <T> T performWithContext(WithContext<T> action) throws Exception {
FTPSContext ctx = null;
try {
if (url != null) {
ctx = new FTPSContext(URI.create(url), env);
return action.perform(ctx);
} else {
return null;
}
} finally {
if (ctx != null) {
ctx.close();
}
}
}
}

@ -0,0 +1,24 @@
package org.xbib.groovy.ftps;
import org.xbib.io.ftp.fs.FTPSEnvironment;
import org.xbib.io.ftp.fs.FTPSFileSystemProvider;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.util.Map;
public class FTPSContext {
final FTPSFileSystemProvider provider;
final FileSystem fileSystem;
FTPSContext(URI uri, Map<String, ?> env) throws IOException {
this.provider = new FTPSFileSystemProvider();
this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPSEnvironment());
}
void close() throws IOException {
fileSystem.close();
}
}

@ -0,0 +1,6 @@
package org.xbib.groovy.ftps;
public interface WithContext<T> {
T perform(FTPSContext ctx) throws Exception;
}

@ -0,0 +1,39 @@
package org.xbib.groovy.ftps
import groovy.util.logging.Log4j2
import org.junit.Test
import org.xbib.io.ftp.fs.SecurityMode
import java.nio.file.Files
import java.nio.file.Path
@Log4j2
class FTPSTest {
@Test
void testExplicitFTPS() {
Map env = [
username: 'demo',
password: 'password'.toCharArray()
]
FTPS ftps = FTPS.newInstance("ftps://test.rebex.net:21", env)
log.info ftps.exists('/')
ftps.each('/') { Path path ->
log.info "{} {} {}", path, Files.isDirectory(path), Files.getLastModifiedTime(path)
}
}
@Test
void testImplicitFTPS() {
Map env = [
username: 'demo',
password: 'password'.toCharArray(),
securityMode: SecurityMode.IMPLICIT
]
FTPS ftps = FTPS.newInstance("ftps://test.rebex.net:990", env)
log.info ftps.exists('/')
ftps.each('/') { Path path ->
log.info "{} {} {}", path, Files.isDirectory(path), Files.getLastModifiedTime(path)
}
}
}

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{ISO8601}][%-5p][%-25c][%t] %m%n"/>
</Console>
</appenders>
<Loggers>
<Root level="all">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</configuration>

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,2 @@
This is a Java 8 / Gradle version of https://svn.apache.org/repos/asf/directory/sandbox/szoerner/groovyldap/
with some modifications.

@ -0,0 +1,6 @@
apply from: rootProject.file('gradle/compile/groovy.gradle')
dependencies {
testImplementation "org.codehaus.groovy:groovy:${project.property('groovy.version')}"
testImplementation "junit:junit:${project.property('junit4.version')}"
}

@ -0,0 +1,427 @@
package org.xbib.groovy.ldap;
import groovy.lang.Closure;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A wrapper class which provides LDAP functionality to Groovy.
*/
public class LDAP {
private static final Logger logger = Logger.getLogger(LDAP.class.getName());
private static final String DEFAULT_URL = "ldap://localhost:389/";
private final String url;
private final String bindUser;
private final String bindPassword;
private LDAP(String url, String bindUser, String bindPassword) {
this.url = url;
this.bindUser = bindUser;
this.bindPassword = bindPassword;
}
public static LDAP newInstance() {
return new LDAP(DEFAULT_URL, null, null);
}
public static LDAP newInstance(String url) {
return new LDAP(url, null, null);
}
public static LDAP newInstance(String url, String bindUser, String bindPassword) {
return new LDAP(url, bindUser, bindPassword);
}
/**
* LDAP add operation. Adds a new entry to the directory. The attributes have to be provided as a map.
*
* @param dn DN of the entry
* @param attributes attributes of the entry
* @throws NamingException if DN can not be resolved
*/
public void add(final String dn, final Map<String, Object> attributes) throws NamingException {
WithContext<Object> action = ctx -> {
BasicAttributes attrs = new BasicAttributes();
for (Map.Entry<String,Object> entry : attributes.entrySet()) {
logger.log(Level.FINE, MessageFormat.format("entry {0} {1}", entry, entry.getValue().getClass()));
Attribute attr = createAttribute(entry.getKey(), entry.getValue());
logger.log(Level.FINE, MessageFormat.format("attr {0} {1}", attr, attr.get().getClass()));
attrs.put(attr);
}
ctx.createSubcontext(dn, attrs);
return null;
};
performWithContext(action);
}
/**
* LDAP delete operation. Deletes an entry from the directory.
*
* @param dn DN of the entry
* @throws NamingException if DN can not be resolved
*/
public void delete(final String dn) throws NamingException {
if (!exists(dn)) {
throw new NameNotFoundException("Entry " + dn + " does not exist!");
}
WithContext<Object> action = ctx -> {
ctx.destroySubcontext(dn);
return null;
};
performWithContext(action);
}
/**
* Reads an entry by its DN.
* @param dn distinguished name
* @return object
* @throws NamingException if DN can not be resolved
*/
public Object read(final String dn) throws NamingException {
return performWithContext(ctx -> ctx.lookup(dn));
}
/**
* Check whether an entry with the given DN exists. The method performs a search to check this, which is not so
* efficient than just reading the entry.
* @param dn distinguished name
* @return true if exists
* @throws NamingException if DN can not be resolved
*/
public Boolean exists(final String dn) throws NamingException {
WithContext<Boolean> action = ctx -> {
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.OBJECT_SCOPE);
searchControls.setReturningAttributes(new String[0]);
searchControls.setReturningObjFlag(false);
try {
ctx.search(dn, "(objectClass=*)", searchControls);
return true;
} catch (NameNotFoundException e) {
logger.log(Level.FINEST, e.getMessage(), e);
}
return false;
};
return performWithContext(action);
}
/**
* LDAP compare operation.
*
* @param dn Distinguished name of the entry.
* @param assertion attribute assertion.
* @return true is comparison matches
* @throws NamingException if DN can not be resolved
*/
public boolean compare(final String dn, final Map<String, Object> assertion) throws NamingException {
if (assertion.size() != 1) {
throw new IllegalArgumentException("Assertion may only include one attribute");
}
WithContext<Boolean> action = ctx -> {
SearchControls searchControls = new SearchControls();
searchControls.setReturningAttributes(new String[0]);
searchControls.setSearchScope(SearchControls.OBJECT_SCOPE);
searchControls.setReturningObjFlag(false);
String attrName = assertion.keySet().iterator().next();
String filter = "(" + attrName + "={0})";
Object value = assertion.get(attrName);
NamingEnumeration<SearchResult> enumeration = ctx.search(dn, filter, new Object[]{value}, searchControls);
return enumeration.hasMore();
};
return performWithContext(action);
}
/**
* LDAP modify DN operation.
*
* @param dn Distinguished name of the entry.
* @param newRDN new realtive distinguished name of the entry.
* @param deleteOldRDN if old relative distinguished name should be deleted
* @param newSuperior new superior DN
* @throws NamingException if DN can not be resolved
*/
public void modifyDn(final String dn, final String newRDN, final boolean deleteOldRDN, final String newSuperior)
throws NamingException {
WithContext<Object> action = ctx -> {
LdapName source = new LdapName(dn);
LdapName target = new LdapName(newSuperior);
target.add(newRDN);
ctx.addToEnvironment("java.naming.ldap.deleteRDN", Boolean.toString(deleteOldRDN));
ctx.rename(source, target);
return null;
};
performWithContext(action);
}
public void eachEntry(String filter, String base, SearchScope scope, Closure<?> closure) throws NamingException {
eachEntry(new Search(base, scope, filter), closure);
}
public void eachEntry(Map<String, Object> searchParams, Closure<?> closure) throws NamingException {
eachEntry(new Search(searchParams), closure);
}
public void eachEntry(String filter, Closure<?> closure) throws NamingException {
eachEntry(filter, "", SearchScope.SUB, closure);
}
public void eachEntry(Search search, Closure<?> closure) throws NamingException {
WithContext<Object> action = ctx -> {
SearchControls ctls = new SearchControls();
ctls.setSearchScope(search.getScope().getValue());
ctls.setReturningAttributes(search.getAttrs());
ctls.setReturningObjFlag(true);
NamingEnumeration<SearchResult> results = ctx.search(search.getBase(), search.getFilter(), search
.getFilterArgs(), ctls);
while (results != null && results.hasMore()) {
SearchResult sr = results.next();
String dn = sr.getNameInNamespace();
Attributes attrs = sr.getAttributes();
NamingEnumeration<? extends Attribute> en = attrs.getAll();
Map<String, Object> map = new LinkedHashMap<>();
map.put("dn", dn);
while (en.hasMore()) {
Attribute attr = en.next();
String key = attr.getID();
map.put(key, attr.get(0).toString());
}
closure.call(map);
}
return null;
};
performWithContext(action);
}
public void modify(String dn, String modType, Map<String, Object> attributes) throws NamingException {
modify(dn, ModificationType.valueOf(modType), attributes);
}
public void modify(String dn, ModificationType modType, Map<String, Object> attributes) throws NamingException {
List<ModificationItem> mods = new ArrayList<>();
for (String key : attributes.keySet()) {
Attribute attr = createAttribute(key, attributes.get(key));
ModificationItem item = new ModificationItem(modType.getValue(), attr);
mods.add(item);
}
ModificationItem[] modItems = mods.toArray(new ModificationItem[mods.size()]);
WithContext<Object> action = ctx -> {
ctx.modifyAttributes(dn, modItems);
return null;
};
performWithContext(action);
}
public void modify(String dn, List<List<Object>> modificationItem) throws NamingException {
List<ModificationItem> mods = new ArrayList<>();
for (List<Object> pair : modificationItem) {
if (pair.size() != 2) {
throw new IllegalArgumentException("parameter 2 is not a list of pairs");
}
Object oModType = pair.get(0);
ModificationType modType;
if (oModType instanceof ModificationType) {
modType = (ModificationType) oModType;
} else if (oModType instanceof String) {
modType = ModificationType.valueOf((String) oModType);
} else {
throw new IllegalArgumentException("parameter is not o valid ModificationType: " + oModType);
}
if (pair.get(1) instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> attributes = (Map<String, Object>) pair.get(1);
for (String key : attributes.keySet()) {
Attribute attr = createAttribute(key, attributes.get(key));
ModificationItem item = new ModificationItem(modType.getValue(), attr);
mods.add(item);
}
}
}
ModificationItem[] modItems = mods.toArray(new ModificationItem[mods.size()]);
WithContext<Object> action = ctx -> {
ctx.modifyAttributes(dn, modItems);
return null;
};
performWithContext(action);
}
public List<Map<String, Object>> search(String filter) throws NamingException {
return search(new Search("", SearchScope.SUB, filter));
}
public List<Map<String, Object>> search(String base, SearchScope scope, String filter) throws NamingException {
return search(new Search(base, scope, filter));
}
public List<Map<String, Object>> search(Map<String, Object> searchParams) throws NamingException {
return search(new Search(searchParams));
}
public List<Map<String, Object>> search(Search search) throws NamingException {
List<Map<String, Object>> result = new ArrayList<>();
WithContext<Object> action = ctx -> {
NamingEnumeration<SearchResult> results =
ctx.search(search.getBase(), search.getFilter(), search.getFilterArgs(), search.getSearchControls());
while (results != null && results.hasMore()) {
SearchResult sr = results.next();
String dn = sr.getNameInNamespace();
Attributes attrs = sr.getAttributes();
NamingEnumeration<? extends Attribute> en = attrs.getAll();
Map<String, Object> map = new LinkedHashMap<>();
map.put("dn", dn);
while (en.hasMore()) {
Attribute attr = en.next();
String key = attr.getID();
if (attr.size() == 1) {
map.put(key, attr.get());
} else {
List<Object> l = new ArrayList<>();
for (int i = 0; i < attr.size(); ++i) {
l.add(attr.get(i));
}
map.put(key, l);
}
}
result.add(map);
}
return null;
};
performWithContext(action);
return result;
}
public void bind(String bindUser, String bindPassword) throws NamingException {
LdapContext ctx = null;
try {
ctx = new InitialLdapContext(createEnvironment(url, bindUser, bindPassword), null);
} finally {
try {
if (ctx != null) {
ctx.close();
}
} catch (NamingException e) {
logger.log(Level.FINEST, e.getMessage(), e);
}
}
}
/**
* Open an LDAP context and perform a given task within this context.
*
* @param <T> parameter type
* @param action action
* @return an action result
* @throws NamingException naming exception
*/
private <T> T performWithContext(WithContext<T> action) throws NamingException {
LdapContext ctx = null;
try {
if (url != null) {
ctx = new InitialLdapContext(createEnvironment(url, bindUser, bindPassword), null);
return action.perform(ctx);
} else {
return null;
}
} finally {
try {
if (ctx != null) {
ctx.close();
}
} catch (NamingException e) {
logger.log(Level.FINEST, e.getMessage(), e);
}
}
}
private static Properties createEnvironment(String url, String bindUser, String bindPassword) {
Properties env = new Properties();
env.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.setProperty(Context.PROVIDER_URL, url);
if (bindUser != null) {
env.setProperty(Context.SECURITY_PRINCIPAL, bindUser);
env.setProperty(Context.SECURITY_CREDENTIALS, bindPassword);
}
return env;
}
private static Attribute createAttribute(String name, Object value) {
Attribute attr = new BasicAttribute(name);
if (value instanceof Collection) {
Collection<?> values = (Collection<?>) value;
for (Object val : values) {
attr.add(val);
}
} else {
attr.add(value);
}
return attr;
}
public static String escapeValue(String filter) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filter.length(); i++) {
switch (filter.charAt(i)) {
case '\\':
sb.append("\\5c");
break;
case '!':
sb.append("\\21");
break;
case '&':
sb.append("\\26");
break;
case '*':
sb.append("\\2a");
break;
case ':':
sb.append("\\3a");
break;
case '(':
sb.append("\\28");
break;
case ')':
sb.append("\\29");
break;
case '|':
sb.append("\\7c");
break;
case '~':
sb.append("\\7e");
break;
case '\u0000':
sb.append("\\00");
break;
default:
sb.append(filter.charAt(i));
}
}
return sb.toString();
}
}

@ -0,0 +1,23 @@
package org.xbib.groovy.ldap;
import javax.naming.directory.DirContext;
/**
* Modification types for LDAP attributes.
*/
public enum ModificationType {
ADD(DirContext.ADD_ATTRIBUTE),
DELETE(DirContext.REMOVE_ATTRIBUTE),
REPLACE(DirContext.REPLACE_ATTRIBUTE);
private final int value;
ModificationType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}

@ -0,0 +1,117 @@
package org.xbib.groovy.ldap;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Map;
import javax.naming.directory.SearchControls;
/**
* Contains all parameters for an LDAP search.
*/
public class Search {
public static final int DEFAULT_TIME_LIMIT = 5000;
public static final int DEFAULT_COUNT_LIMIT = 20000;
private final SearchControls searchControls;
private final String base;
private final SearchScope scope;
private final String filter;
private final Object[] filterArgs;
private final String[] attrs;
private final int timeLimit;
private final int countLimit;
public Search() {
this("", SearchScope.SUB, "(objectClass=*)", DEFAULT_TIME_LIMIT, DEFAULT_COUNT_LIMIT);
}
public Search(String base, SearchScope scope, String filter) {
this(base, scope, filter, DEFAULT_TIME_LIMIT, DEFAULT_COUNT_LIMIT);
}
public Search(String base, SearchScope scope, String filter, int timeLimit, int countLimit) {
this.base = base;
this.scope = scope;
this.filter = filter;
this.filterArgs = null;
this.attrs = null;
this.timeLimit = timeLimit;
this.countLimit = countLimit;
this.searchControls = getSearchControls(scope, null, timeLimit, countLimit);
}
public Search(Map<String, Object> map) {
this.base = map.containsKey("base") ? map.get("base").toString() : "";
this.scope = map.containsKey("scope") ? SearchScope.valueOf(map.get("scope").toString()) : SearchScope.SUB;
this.filter = map.containsKey("filter") ? map.get("filter").toString() : "(objectClass=*)";
this.filterArgs = map.containsKey("filterArgs") ? toArray(Object.class, map.get("filterArgs")) : null;
this.attrs = map.containsKey("attrs") ? toArray(String.class, map.get("attrs")) : null;
this.timeLimit = map.containsKey("timeLimit") ?
Integer.parseInt((String) map.get("timeLimit")) : DEFAULT_TIME_LIMIT;
this.countLimit = map.containsKey("countLimit") ?
Integer.parseInt((String) map.get("countLimit")) : DEFAULT_COUNT_LIMIT;
this.searchControls = getSearchControls(scope, attrs, timeLimit, countLimit);
}
@SuppressWarnings("unchecked")
private static <T> T[] toArray(Class<T> target, Object value) {
T[] values = null;
if (value.getClass().isArray()) {
values = (T[]) value;
} else if (value instanceof Collection) {
Collection<T> c = (Collection<T>) value;
values = c.toArray((T[]) Array.newInstance(target, c.size()));
} else {
values = (T[]) Array.newInstance(target, 1);
values[0] = (T) value;
}
return values;
}
public String[] getAttrs() {
return attrs;
}
public String getBase() {
return base;
}
public String getFilter() {
return filter;
}
public Object[] getFilterArgs() {
return filterArgs;
}
public SearchScope getScope() {
return scope;
}
public SearchControls getSearchControls() {
return searchControls;
}
private static SearchControls getSearchControls(SearchScope searchScope,
String[] attrs,
int timeLimit,
int countLimit) {
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(searchScope.getValue());
searchControls.setReturningAttributes(attrs);
searchControls.setReturningObjFlag(true);
searchControls.setTimeLimit(timeLimit);
searchControls.setCountLimit(countLimit);
return searchControls;
}
}

@ -0,0 +1,23 @@
package org.xbib.groovy.ldap;
import javax.naming.directory.SearchControls;
/**
* Enumeration for the search scope options. To be used in LDAP search operations.
*/
public enum SearchScope {
BASE(SearchControls.OBJECT_SCOPE),
ONE(SearchControls.ONELEVEL_SCOPE),
SUB(SearchControls.SUBTREE_SCOPE);
private final int value;
SearchScope(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}

@ -0,0 +1,12 @@
package org.xbib.groovy.ldap;
import javax.naming.NamingException;
import javax.naming.ldap.LdapContext;
/**
*
* @param <T>
*/
public interface WithContext<T> {
T perform(LdapContext ctx) throws NamingException;
}

@ -0,0 +1,14 @@
package org.xbib.groovy.ldap
ldap = LDAP.newInstance('ldap://localhost:389', 'uid=admin,ou=system' ,'secret')
assert ! ldap.exists('cn=Joe Doe,dc=example,dc=com')
attrs = [
objectclass: ['top', 'person'],
sn: 'Doe',
cn: 'Joe DOe'
]
ldap.add('cn=Joe Doe,dc=example,dc=com', attrs)
assert ldap.exists('cn=Joe Doe,dc=example,dc=com')

@ -0,0 +1,19 @@
package org.xbib.groovy.ldap
ldap = LDAP.newInstance('ldap://localhost:389', 'uid=admin,ou=system' ,'secret')
assert ! ldap.exists('cn=Joe Doe,dc=example,dc=com')
attrs = [
objectclass: ['top', 'person'],
sn: 'Doe',
cn: 'Joe Doe',
userPassword: 'secret'
]
ldap.add('cn=Joe Doe,dc=example,dc=com', attrs)
assert ldap.exists('cn=Joe Doe,dc=example,dc=com')
assert ldap.compare('cn=Joe Doe,dc=example,dc=com', [cn: 'Joe Doe'] )
assert ldap.compare('cn=Joe Doe,dc=example,dc=com', [cn: 'JOE DOE'] )
assert ldap.compare('cn=Joe Doe,dc=example,dc=com', [userPassword: 'secret'] )
assert ! ldap.compare('cn=Joe Doe,dc=example,dc=com', [userPassword: 'SECRET'] )
ldap.delete('cn=Joe Doe,dc=example,dc=com')

@ -0,0 +1,6 @@
package org.xbib.groovy.ldap
ldap = LDAP.newInstance('ldap://localhost:389', 'uid=admin,ou=system' ,'secret')
ldap.delete('cn=Joe Doe,dc=example,dc=com')
assert !ldap.exists('cn=Joe Doe,dc=example,dc=com')

@ -0,0 +1,22 @@
package org.xbib.groovy.ldap
ldap = LDAP.newInstance('ldap://zanzibar:10389', 'uid=admin,ou=system' ,'secret')
superior='dc=example,dc=com'
dn = 'cn=Myra Ellen Amos,dc=example,dc=com'
newRdn = 'cn=Tori Amos'
newDn = 'cn=Tori Amos,dc=example,dc=com'
assert !ldap.exists(dn)
attrs = [
objectclass: ['top', 'person'],
sn: 'Amos',
cn: ['Tori Amos', 'Myra Ellen Amos'],
]
ldap.add(dn, attrs)
assert ldap.exists(dn)
ldap.modifyDn(dn, newRdn, true, superior)
assert ldap.exists(newDn)
tori = ldap.read(newDn)
assert tori.cn == 'Tori Amos'
ldap.delete(newDn)

@ -0,0 +1,18 @@
package org.xbib.groovy.ldap
ldap = LDAP.newInstance('ldap://zanzibar:10389', 'uid=admin,ou=system' ,'secret')
dn = 'cn=Heather Nova,dc=example,dc=com'
// Adding a single attribute
//
descr = [ description: 'a singer-songwriter' ]
ldap.modify(dn, 'ADD', descr)
// performing two operations atomically
//
mods = [
[ 'REPLACE', [description: 'a singer-songwriter, born in Bermuda'] ],
[ 'ADD', [userPassword: 'secret'] ]
]
ldap.modify(dn, mods)

@ -0,0 +1,12 @@
package org.xbib.groovy.ldap
ldap = LDAP.newInstance("ldap://zanzibar:10389")
// Simple entry lookup via dn
heather = ldap.read('cn=Heather Nova,dc=example,dc=com')
print """
DN: ${heather.dn}
Common name: ${heather.cn}
Object classes: ${heather.objectclass}
"""

@ -0,0 +1,7 @@
package org.xbib.groovy.ldap
ldap = LDAP.newInstance('ldap://zanzibar:10389', 'uid=admin,ou=system' ,'secret')
ldap.eachEntry(base:'dc=example,dc=com', filter:'(objectClass=person)', scope:'ONE') { entry ->
ldap.delete(entry.dn)
}

@ -0,0 +1,8 @@
package org.xbib.groovy.ldap
ldap = LDAP.newInstance('ldap://zanzibar:10389/dc=example,dc=com')
ldap.eachEntry ('(objectClass=person)') { person ->
println "${person.cn} (${person.dn})"
}

@ -0,0 +1,30 @@
package org.xbib.groovy.ldap
ldap = LDAP.newInstance('ldap://zanzibar:10389/')
results = ldap.search('dc=example,dc=com', SearchScope.ONE, '(objectClass=person)')
println " ${results.size} entries found ".center(40,'-')
for (entry in results) {
println entry.dn
}
println ""
results = ldap.search(filter: '(objectClass=person)', base: 'dc=example,dc=com', scope: 'ONE')
println " ${results.size} entries found ".center(40,'-')
for (entry in results) {
println entry.dn
}
println ""
def params = new JavaSearchTest()
params.filter='(objectClass=person)'
params.base='dc=example,dc=com'
params.scope=SearchScope.ONE
results = ldap.search(params)
println " ${results.size} entries found ".center(40,'-')
for (entry in results) {
println entry.dn
}

@ -0,0 +1,14 @@
package org.xbib.groovy.ldap;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class JavaSearchTest {
@Test
public void defaultConstructor() {
Search search = new Search();
assertEquals(SearchScope.SUB, search.getScope());
}
}

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,5 @@
apply from: rootProject.file('gradle/compile/groovy.gradle')
dependencies {
api "com.sun.mail:javax.mail:${project.property('mail.version')}"
}

@ -0,0 +1,211 @@
package org.xbib.groovy.imap;
import groovy.lang.Closure;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.search.FlagTerm;
import javax.mail.search.SearchTerm;
import java.net.URI;
import java.util.Properties;
/**
* A wrapper class for IMAP functionality to Groovy.
*/
public class IMAP {
private static final String DEFAULT_URL = "imap://localhost:143/";
private final String url;
private final String username;
private final String password;
private IMAP(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
public static IMAP newInstance() {
return new IMAP(DEFAULT_URL, null, null);
}
public static IMAP newInstance(String url) {
return new IMAP(url, null, null);
}
public static IMAP newInstance(String url, String username, String password) {
return new IMAP(url, username, password);
}
public Boolean exist(String folderName) throws Exception {
WithContext<Boolean> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
return folder.exists();
};
return performWithContext(action);
}
public void eachFolder(String folderName, String folderPattern, Closure<?> closure) throws Exception {
WithContext<Object> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
if (folder.exists()) {
folder.open(Folder.READ_ONLY);
Folder[] folders = folder.list(folderPattern);
for (Folder f : folders) {
closure.call(f);
}
folder.close(false);
}
return null;
};
performWithContext(action);
}
public void create(String folderName) throws Exception {
WithContext<Object> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
if (!folder.exists()) {
folder.create(Folder.HOLDS_MESSAGES | Folder.READ_WRITE);
}
folder.close(false);
return null;
};
performWithContext(action);
}
public void delete(String folderName) throws Exception {
WithContext<Object> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
if (folder.exists()) {
folder.delete(true);
}
folder.close(true);
return null;
};
performWithContext(action);
}
public void expunge(String folderName) throws Exception {
WithContext<Object> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
if (folder.exists()) {
folder.expunge();
}
folder.close(false);
return null;
};
performWithContext(action);
}
public Integer messageCount(String folderName) throws Exception {
WithContext<Integer> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
if (folder.exists()) {
return folder.getMessageCount();
}
return null;
};
return performWithContext(action);
}
public void eachMessage(String folderName, Closure<?> closure) throws Exception {
WithContext<Object> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
if (folder.exists()) {
folder.open(Folder.READ_ONLY);
Message[] messages = folder.getMessages();
for (Message message : messages) {
closure.call(message);
}
folder.close(false);
}
return null;
};
performWithContext(action);
}
public void eachMessage(String folderName, int start, int end, Closure<?> closure) throws Exception {
WithContext<Object> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
if (folder.exists()) {
folder.open(Folder.READ_ONLY);
Message[] messages = folder.getMessages(start, end);
for (Message message : messages) {
closure.call(message);
}
folder.close(false);
}
return null;
};
performWithContext(action);
}
public void eachSearchedMessage(String folderName, Flags flags, Closure<?> closure) throws Exception {
WithContext<Object> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
if (folder.exists()) {
folder.open(Folder.READ_ONLY);
FlagTerm flagTerm = new FlagTerm(flags, false);
Message[] messages = folder.search(flagTerm);
for (Message message : messages) {
closure.call(message);
}
folder.close(false);
}
return null;
};
performWithContext(action);
}
public void eachSearchedMessage(String folderName, SearchTerm searchTerm, Closure<?> closure) throws Exception {
WithContext<Object> action = ctx -> {
Folder folder = ctx.store.getFolder(folderName);
if (folder.exists()) {
folder.open(Folder.READ_ONLY);
Message[] messages = folder.search(searchTerm);
for (Message message : messages) {
closure.call(message);
}
folder.close(false);
}
return null;
};
performWithContext(action);
}
private <T> T performWithContext(WithContext<T> action) throws Exception {
ImapContext ctx = null;
try {
if (url != null) {
ctx = new ImapContext();
ctx.properties = createEnvironment(url);
ctx.session = Session.getDefaultInstance(ctx.properties, null);
ctx.store = ctx.session.getStore("imap");
ctx.store.connect(username, password);
return action.perform(ctx);
} else {
return null;
}
} finally {
if (ctx != null) {
ctx.close();
}
}
}
private static Properties createEnvironment(String urlSpec) {
URI uri = URI.create(urlSpec);
Properties env = new Properties();
env.setProperty("mail.store.protocol", "imap");
env.setProperty("mail.imap.host", uri.getHost());
env.setProperty("mail.imap.port", Integer.toString(uri.getPort()));
boolean secure = uri.getScheme().equals("imaps") || 993 == uri.getPort();
env.setProperty("mail.imap.ssl.enable", secure ? "true" : "false");
return env;
}
}

@ -0,0 +1,21 @@
package org.xbib.groovy.imap;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import java.util.Properties;
public class ImapContext {
Properties properties;
Session session;
Store store;
void close() throws MessagingException {
if (store != null) {
store.close();
}
}
}

@ -0,0 +1,10 @@
package org.xbib.groovy.imap;
/**
*
* @param <T>
*/
public interface WithContext<T> {
T perform(ImapContext ctx) throws Exception;
}

@ -0,0 +1,4 @@
/**
* Groovy IMAP support.
*/
package org.xbib.groovy.imap;

@ -0,0 +1,137 @@
package org.xbib.groovy.smtp;
import javax.mail.Address;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import java.net.URI;
import java.util.Date;
import java.util.Properties;
/**
*/
public class SMTP {
private static final String DEFAULT_URL = "smtp://localhost:25/";
private final String url;
private final String username;
private final String password;
private SMTP(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
public static SMTP newInstance() {
return new SMTP(DEFAULT_URL, null, null);
}
public static SMTP newInstance(String url) {
return new SMTP(url, null, null);
}
public static SMTP newInstance(String url, String username, String password) {
return new SMTP(url, username, password);
}
public String getURL() {
return url;
}
public void send(String subject, String from, String to, String text) throws Exception {
Address[] toAddr = { new InternetAddress(to) };
send(subject, new InternetAddress(from), null, toAddr, null, null, text);
}
public void send(String subject, Address from, Address[] to, String text) throws Exception {
send(subject, from, null, to, null, null, text);
}
public void send(String subject,
Address from, Address[] replyTo,
Address[] to, Address[] cc, Address[] bcc,
String text) throws Exception {
Multipart multipart = new MimeMultipart("mixed");
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeBodyPart.setHeader("Content-Transfer-Encoding", "base64");
mimeBodyPart.setText(text);
multipart.addBodyPart(mimeBodyPart);
send(subject, from, replyTo, to, cc, bcc, multipart);
}
public void send(String subject,
Address from, Address[] replyTo,
Address[] to, Address[] cc, Address[] bcc,
Multipart multipart) throws Exception {
WithContext<Object> action = ctx -> {
Message message = new MimeMessage(ctx.session);
message.setSentDate(new Date());
message.setFrom(from);
message.setSubject(subject);
if (replyTo != null) {
message.setReplyTo(replyTo);
}
message.setRecipients(Message.RecipientType.TO, to);
if (cc != null) {
message.setRecipients(Message.RecipientType.CC, cc);
}
if (bcc != null) {
message.setRecipients(Message.RecipientType.BCC, bcc);
}
message.setContent(multipart);
Transport.send(message);
return null;
};
performWithContext(action);
}
private <T> T performWithContext(WithContext<T> action) throws Exception {
SmtpContext ctx = null;
try {
if (url != null) {
ctx = new SmtpContext();
ctx.properties = createEnvironment(url);
ctx.session = username != null ?
Session.getDefaultInstance(ctx.properties, new SMTPAuthenticator()) :
Session.getDefaultInstance(ctx.properties);
return action.perform(ctx);
} else {
return null;
}
} finally {
if (ctx != null) {
ctx.close();
}
}
}
private static Properties createEnvironment(String urlSpec) {
URI uri = URI.create(urlSpec);
Properties env = new Properties();
env.setProperty("mail.smtp.auth", "false");
env.setProperty("mail.smtp.host", uri.getHost());
env.setProperty("mail.smtp.port", Integer.toString(uri.getPort()));
boolean secure = uri.getScheme().equals("smtps") || 995 == uri.getPort();
env.setProperty("mail.smtp.ssl.enable", secure ? "true" : "false");
env.setProperty("mail.debug", "true");
return env;
}
private class SMTPAuthenticator extends Authenticator {
@Override
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
}
}

@ -0,0 +1,15 @@
package org.xbib.groovy.smtp;
import javax.mail.MessagingException;
import javax.mail.Session;
import java.util.Properties;
public class SmtpContext {
Properties properties;
Session session;
void close() throws MessagingException {
}
}

@ -0,0 +1,11 @@
package org.xbib.groovy.smtp;
/**
* The Context for {@link SMTP}.
*
* @param <T> the type parameter
*/
public interface WithContext<T> {
T perform(SmtpContext ctx) throws Exception;
}

@ -0,0 +1,4 @@
/**
* Groovy SMTP support.
*/
package org.xbib.groovy.smtp;

@ -0,0 +1,6 @@
apply from: rootProject.file('gradle/compile/groovy.gradle')
dependencies {
api "org.xbib:sshd-fs:${project.property('sshd.version')}"
testImplementation "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}"
}

@ -0,0 +1,388 @@
package org.xbib.groovy.sshd;
import groovy.lang.Closure;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
public class SFTP {
private static final int READ_BUFFER_SIZE = 128 * 1024;
private static final int WRITE_BUFFER_SIZE = 128 * 1024;
private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
PosixFilePermissions.fromString("rwxr-xr-x");
private static final Set<PosixFilePermission> DEFAULT_FILE_PERMISSIONS =
PosixFilePermissions.fromString("rw-r--r--");
private final String url;
private final Map<String, ?> env;
private SFTP(String url, Map<String, ?> env) {
this.url = url;
this.env = env;
}
public static SFTP newInstance() {
return newInstance("sftp://localhost:22");
}
public static SFTP newInstance(Map<String, ?> env) {
return newInstance("sftp://localhost:22", env);
}
public static SFTP newInstance(String url) {
return newInstance(url, Collections.emptyMap());
}
public static SFTP newInstance(String url, Map<String, ?> env) {
return new SFTP(url, env);
}
public Boolean exists(String path) throws Exception {
return performWithContext(ctx -> Files.exists(ctx.fileSystem.getPath(path)));
}
public Boolean isExecutable(String path) throws Exception {
return performWithContext(ctx -> Files.isExecutable(ctx.fileSystem.getPath(path)));
}
public Boolean isDirectory(String path) throws Exception {
return performWithContext(ctx -> Files.isDirectory(ctx.fileSystem.getPath(path)));
}
public Boolean isRegularFile(String path) throws Exception {
return performWithContext(ctx -> Files.isRegularFile(ctx.fileSystem.getPath(path)));
}
public Boolean isHidden(String path) throws Exception {
return performWithContext(ctx -> Files.isHidden(ctx.fileSystem.getPath(path)));
}
public Boolean isSameFile(String path1, String path2) throws Exception {
return performWithContext(ctx -> Files.isSameFile(ctx.fileSystem.getPath(path1), ctx.fileSystem.getPath(path2)));
}
public Boolean isSymbolicLink(String path) throws Exception {
return performWithContext(ctx -> Files.isSymbolicLink(ctx.fileSystem.getPath(path)));
}
public Boolean isReadable(String path) throws Exception {
return performWithContext(ctx -> Files.isReadable(ctx.fileSystem.getPath(path)));
}
public Boolean isWritable(String path) throws Exception {
return performWithContext(ctx -> Files.isWritable(ctx.fileSystem.getPath(path)));
}
public void createFile(String path, FileAttribute<?>... attributes) throws Exception {
performWithContext(ctx -> Files.createFile(ctx.fileSystem.getPath(path), attributes));
}
public void createDirectory(String path, FileAttribute<?>... attributes) throws Exception {
performWithContext(ctx -> Files.createDirectory(ctx.fileSystem.getPath(path), attributes));
}
public void createDirectories(String path, FileAttribute<?>... attributes) throws Exception {
performWithContext(ctx -> Files.createDirectories(ctx.fileSystem.getPath(path), attributes));
}
public void setAttribute(String path, String attribute, Object value) throws Exception {
performWithContext(ctx -> Files.setAttribute(ctx.fileSystem.getPath(path), attribute, value));
}
public Object getAttribute(String path, String attribute) throws Exception {
return performWithContext(ctx -> Files.getAttribute(ctx.fileSystem.getPath(path), attribute));
}
public void setPermissions(String path, Set<PosixFilePermission> permissions) throws Exception {
performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions));
}
public Set<PosixFilePermission> getPermissions(String path) throws Exception {
return performWithContext(ctx -> Files.getPosixFilePermissions(ctx.fileSystem.getPath(path)));
}
public void setLastModifiedTime(String path, FileTime fileTime) throws Exception {
performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime));
}
public FileTime getLastModified(String path) throws Exception{
return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)));
}
public void setOwner(String path, UserPrincipal userPrincipal) throws Exception {
performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal));
}
public UserPrincipal getOwner(String path) throws Exception {
return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)));
}
public void each(String path, Closure<?> closure) throws Exception {
performWithContext(ctx -> {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path))) {
stream.forEach(closure::call);
}
return null;
});
}
public void eachFilter(String path, DirectoryStream.Filter<Path> filter, Closure<?> closure) throws Exception {
performWithContext(ctx -> {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)) {
stream.forEach(closure::call);
}
return null;
});
}
public void upload(Path source, Path target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(Path source, Path target,
Set<PosixFilePermission> dirPermissions,
Set<PosixFilePermission> filePermissions,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE,
dirPermissions, filePermissions, copyOptions);
return null;
});
}
public void upload(Path source, String target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(Path source, String target,
Set<PosixFilePermission> dirPermissions,
Set<PosixFilePermission> filePermissions,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
dirPermissions, filePermissions, copyOptions);
return null;
});
}
public void upload(InputStream source, Path target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(InputStream source, Path target,
Set<PosixFilePermission> dirPermissions,
Set<PosixFilePermission> filePermissions,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE,
dirPermissions, filePermissions, copyOptions);
return null;
});
}
public void upload(InputStream source, String target, CopyOption... copyOptions) throws Exception {
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
}
public void upload(InputStream source, String target,
Set<PosixFilePermission> dirPermissions,
Set<PosixFilePermission> filePermissions,
CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
dirPermissions, filePermissions, copyOptions);
return null;
});
}
public void download(Path source, Path target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
download(ctx, source, target, READ_BUFFER_SIZE, copyOptions);
return null;
});
}
public void download(String source, Path target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> {
download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions);
return null;
});
}
public void download(Path source, OutputStream target) throws Exception {
performWithContext(ctx -> {
download(ctx, source, target, READ_BUFFER_SIZE);
return null;
});
}
public void download(String source, OutputStream target) throws Exception {
performWithContext(ctx -> {
download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE);
return null;
});
}
public void copy(String source, String target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions));
}
public void rename(String source, String target, CopyOption... copyOptions) throws Exception {
performWithContext(ctx -> Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions));
}
public void remove(String source) throws Exception {
performWithContext(ctx -> Files.deleteIfExists(ctx.fileSystem.getPath(source)));
}
private void upload(SFTPContext ctx,
ReadableByteChannel source,
Path target,
int bufferSize,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws Exception {
prepareForWrite(target, dirPerms, filePerms);
transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
}
private void download(SFTPContext ctx,
Path source,
OutputStream outputStream,
int bufferSize) throws Exception {
download(ctx, source, Channels.newChannel(outputStream), bufferSize);
}
private void download(SFTPContext ctx,
Path source,
WritableByteChannel writableByteChannel,
int bufferSize) throws Exception {
transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel,
bufferSize);
}
private void download(SFTPContext ctx,
Path source,
Path target,
int bufferSize,
CopyOption... copyOptions) throws Exception {
prepareForRead(target);
transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)),
Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
}
private void prepareForRead(Path path) throws IOException {
if (path == null) {
return;
}
Path parent = path.getParent();
if (parent != null) {
if (!Files.exists(parent)) {
Files.createDirectories(parent);
}
}
if (!Files.exists(path)) {
Files.createFile(path);
}
}
private void prepareForWrite(Path path,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms) throws IOException {
if (path == null) {
return;
}
Path parent = path.getParent();
if (parent != null) {
if (!Files.exists(parent)) {
Files.createDirectories(parent);
}
PosixFileAttributeView posixFileAttributeView =
Files.getFileAttributeView(parent, PosixFileAttributeView.class);
posixFileAttributeView.setPermissions(dirPerms);
}
if (!Files.exists(path)) {
Files.createFile(path);
}
PosixFileAttributeView posixFileAttributeView =
Files.getFileAttributeView(path, PosixFileAttributeView.class);
posixFileAttributeView.setPermissions(filePerms);
}
private Set<? extends OpenOption> prepareReadOptions(CopyOption... copyOptions) {
// ignore user copy options
return EnumSet.of(StandardOpenOption.READ);
}
private Set<? extends OpenOption> prepareWriteOptions(CopyOption... copyOptions) {
Set<? extends OpenOption> options = null;
for (CopyOption copyOption : copyOptions) {
if (copyOption == StandardCopyOption.REPLACE_EXISTING) {
options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
}
}
if (options == null) {
// we can not use CREATE_NEW, file is already there because of prepareForWrite() -> Files.createFile()
options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE);
}
return options;
}
private void transfer(ReadableByteChannel readableByteChannel,
WritableByteChannel writableByteChannel,
int bufferSize) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
int read;
while ((read = readableByteChannel.read(buffer)) > 0) {
buffer.flip();
while (read > 0) {
read -= writableByteChannel.write(buffer);
}
buffer.clear();
}
}
private <T> T performWithContext(WithContext<T> action) throws Exception {
SFTPContext ctx = null;
try {
if (url != null) {
ctx = new SFTPContext(URI.create(url), env);
return action.perform(ctx);
} else {
return null;
}
} finally {
if (ctx != null) {
ctx.close();
}
}
}
}

@ -0,0 +1,42 @@
package org.xbib.groovy.sshd;
import org.apache.sshd.client.ClientBuilder;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.fs.SftpFileSystem;
import org.apache.sshd.fs.SftpFileSystemProvider;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
/**
*/
class SFTPContext {
private final SshClient sshClient;
final SftpFileSystemProvider provider;
final SftpFileSystem fileSystem;
SFTPContext(URI uri, Map<String, ?> env) throws IOException {
this.sshClient = ClientBuilder.builder().build();
Object object = env.get("workers");
if (object instanceof Integer) {
sshClient.setNioWorkers((Integer) object);
} else if (object instanceof String) {
sshClient.setNioWorkers(Integer.parseInt((String) object));
} else {
// we do not require a vast pool of threads
sshClient.setNioWorkers(1);
}
sshClient.start();
this.provider = new SftpFileSystemProvider(sshClient);
this.fileSystem = provider.newFileSystem(uri, env);
}
void close() throws IOException {
sshClient.stop();
fileSystem.close();
}
}

@ -0,0 +1,9 @@
package org.xbib.groovy.sshd;
/**
*
* @param <T> the context parameter
*/
public interface WithContext<T> {
T perform(SFTPContext ctx) throws Exception;
}

@ -0,0 +1,4 @@
/**
* Groovy SSH/SFTP support.
*/
package org.xbib.groovy.sshd;

@ -0,0 +1,29 @@
package org.xbib.groovy.sshd
import groovy.util.logging.Log4j2
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.xbib.io.sshd.eddsa.EdDSASecurityProvider
import java.nio.file.Files
import java.nio.file.Path
import java.security.Security
@Log4j2
class SFTPTest {
static {
Security.addProvider(new EdDSASecurityProvider());
}
@Disabled
@Test
void testSFTP() {
SFTP sftp = SFTP.newInstance("sftp://demo.wftpserver.com:2222",[username: 'demo', password: 'demo'.toCharArray()])
log.info sftp.exists('/')
sftp.each('/') { Path path ->
log.info "{} {} {}", path, Files.isDirectory(path), Files.getLastModifiedTime(path)
}
}
}

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{ISO8601}][%-5p][%-25c][%t] %m%n"/>
</Console>
</appenders>
<Loggers>
<Root level="ALL">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</configuration>

@ -0,0 +1,7 @@
include 'groovy-ldap'
include 'groovy-crypt'
include 'groovy-mail'
include 'groovy-ftp'
include 'groovy-ftps'
include 'groovy-sshd'
Loading…
Cancel
Save