diff --git a/build/settings.gradle b/build/settings.gradle
index c0216a4822aedef3214cbab94c77c84d7583be67..fb68b5c3bdccc7a68e1eb442b215d16fa19a8e62 100644
--- a/build/settings.gradle
+++ b/build/settings.gradle
@@ -1,6 +1,6 @@
 includeFlat 'lib-commonbase', 'lib-common', 'api-openbis-java', 'lib-openbis-common', 'lib-authentication',
         'lib-dbmigration', 'server-application-server', 'server-original-data-store', 'server-screening',
         'core-plugin-openbis', 'app-openbis-installer', 'lib-image-readers', 'test-ui-core',
-        'test-api-openbis-javascript', 'server-external-data-store', 'ui-admin', 'lib-microservice-server',
+        'test-api-openbis-javascript', 'api-openbis-python3-pybis', 'server-external-data-store', 'ui-admin', 'lib-microservice-server',
         'lib-transactional-file-system', 'server-data-store', 'ui-eln-lims', 'api-openbis-javascript',
-        'lib-json', 'api-data-store-server-java'
\ No newline at end of file
+        'lib-json', 'api-data-store-server-java', 'lib-base'
\ No newline at end of file
diff --git a/lib-base/build.gradle b/lib-base/build.gradle
index 06ab38d7124713b026faf555d1b931aacc4f59be..e2b93329052ad47ea754db991489a2c409125dbb 100644
--- a/lib-base/build.gradle
+++ b/lib-base/build.gradle
@@ -1,12 +1,10 @@
-apply from: './javaproject.gradle'
-
-group='sis'
+apply from: '../build/javaproject.gradle'
 
 dependencies {
-    compile 'apache:commons-io:+',
-            'apache:commons-lang3:+',
-            'testng:testng:+',
-            'rinn:restrictions:+'
+    api 'apache:commons-io:2.6',
+            'apache:commons-lang3:3.11',
+            'testng:testng:6.8-CISD',
+            'rinn:restrictions:1.0.2'
 } 
 
 tasks.withType(Test) {
@@ -40,31 +38,7 @@ sourcesJar {
     archiveName "${project.group}-base-${version}-sources.jar"
 }
 
-task standaloneTestJar(type: Jar, dependsOn: [classes, testClasses]) {
-    archiveName "${project.group}-base-${version}-tests.jar"
-    from files(sourceSets.main.output.classesDir)
-    from files(sourceSets.main.output.resourcesDir)
-    from files(sourceSets.test.output.classesDir)
-    from files("libs")
- 
-    from('targets/dist') {
-        include 'BUILD*INFO'
-    }    
-
-    from {configurations.testRuntime.collect {zipTree(it)}}
- 
-    manifest {
-        attributes 'Main-Class': 'ch.systemsx.cisd.base.AllTests'
-    }
-}
-
-task javadocZip(type: Jar, dependsOn: javadoc) {
-    archiveName "${project.group}-base-${version}-javadoc.jar"
-    baseName "${project.group}-base-javadoc"
-    from javadoc.destinationDir
-}
-
-task distributionZip(type: Zip, dependsOn: [jar, sourcesJar, javadocZip, standaloneTestJar]) {
+task distributionZip(type: Zip, dependsOn: [jar, sourcesJar, javadoc, testJar]) {
     def ver = project.ext.versionNumber.startsWith('SNAPSHOT') ? "${version}" : ""
     def myVersion = project.ext.versionNumber.startsWith('SNAPSHOT') ? "${project.ext.versionNumber}-${project.ext.revisionNumber}" : project.ext.versionNumber
     archiveName "${project.group}-base-${myVersion}.zip"
@@ -76,19 +50,19 @@ task distributionZip(type: Zip, dependsOn: [jar, sourcesJar, javadocZip, standal
         into "${project.group}-base/doc"
     }
 
-    from (jar.archivePath) {
+    from (jar) {
         into "${project.group}-base/lib"
     }
     
-    from (sourcesJar.archivePath) {
+    from (sourcesJar) {
         into "${project.group}-base/src"
     }
     
-    from (standaloneTestJar.archivePath) {
+    from (testJar) {
         into "${project.group}-base/lib"
     }
     
-    from (javadocZip.archivePath) {
+    from (javadoc) {
         into "${project.group}-base/doc"
     }
 }
diff --git a/lib-base/dist/COPYING b/lib-base/dist/COPYING
index 0bae4727c89b5e38a5c28bff7ff8550dc945aaf5..12b210b1347ecac5f1843c380313da7f774bbf48 100644
--- a/lib-base/dist/COPYING
+++ b/lib-base/dist/COPYING
@@ -1,5 +1,5 @@
 /*
- * Copyright 2009 ETH Zuerich, CISD
+ * Copyright ETH 2023 Zürich, Scientific IT Services
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/lib-base/gradle/wrapper/gradle-wrapper.jar b/lib-base/gradle/wrapper/gradle-wrapper.jar
index 644e4cb7af78425d9774939370f5d17bac923d34..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644
Binary files a/lib-base/gradle/wrapper/gradle-wrapper.jar and b/lib-base/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/lib-base/gradle/wrapper/gradle-wrapper.properties b/lib-base/gradle/wrapper/gradle-wrapper.properties
index 8fe999037d0985a6a05b7f9e74bd4731cee14b5e..7745c4ec463c7bb1c4ff6a5e58db239890345152 100644
--- a/lib-base/gradle/wrapper/gradle-wrapper.properties
+++ b/lib-base/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,5 @@
-#Fri Aug 17 11:54:01 CEST 2018
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
+distributionUrl=https\://sissource.ethz.ch/openbis/openbis-public/openbis-ivy/-/raw/main/gradle/distribution/7.4/gradle-7.4-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=http\://svnsis.ethz.ch/repos/cisd/ivy-repository/trunk/gradle/distribution/4.9/gradle-4.9-all.zip
diff --git a/lib-base/gradlew b/lib-base/gradlew
index 4453ccea33d960069d9137ee65f6b21fc65e7e92..83f2acfdc319a24e8766cca78f32474ad7a22dd6 100755
--- a/lib-base/gradlew
+++ b/lib-base/gradlew
@@ -1,5 +1,21 @@
 #!/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
@@ -28,16 +44,16 @@ 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=""
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD="maximum"
 
-warn ( ) {
+warn () {
     echo "$*"
 }
 
-die ( ) {
+die () {
     echo
     echo "$*"
     echo
@@ -109,8 +125,8 @@ if $darwin; then
     GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
 fi
 
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
+# 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"`
@@ -155,7 +171,7 @@ if $cygwin ; then
 fi
 
 # Escape application args
-save ( ) {
+save () {
     for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
     echo " "
 }
diff --git a/lib-base/gradlew.bat b/lib-base/gradlew.bat
index e95643d6a2ca62258464e83c72f5156dc941c609..24467a141f791695fc1009c78d913b2c849d1412 100644
--- a/lib-base/gradlew.bat
+++ b/lib-base/gradlew.bat
@@ -1,3 +1,19 @@
+@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
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
 @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=
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
 
 @rem Find java.exe
 if defined JAVA_HOME goto findJavaFromJavaHome
diff --git a/lib-base/javaproject.gradle b/lib-base/javaproject.gradle
deleted file mode 100644
index 9c6496c7cebff946c97af9717ab9197eddf6379f..0000000000000000000000000000000000000000
--- a/lib-base/javaproject.gradle
+++ /dev/null
@@ -1,314 +0,0 @@
-apply plugin: 'java'
-apply plugin: 'project-report'
-
-evaluationDependsOnChildren()
-
-configurations {
-    tests  { 
-         extendsFrom testRuntime 
-     }
-}
-
-configurations {
-	ecj
-}
-
-configurations.all {
-    resolutionStrategy.cacheDynamicVersionsFor 0, 'hours'
-    resolutionStrategy.cacheChangingModulesFor 0, 'hours'
-}
-
-task wrapper(type: Wrapper) {
-    gradleVersion = '3.5'
-    distributionUrl = "http://svnsis.ethz.ch/repos/cisd/ivy-repository/trunk/gradle/distribution/3.5/gradle-3.5-all.zip"
-}
-
-sourceCompatibility='1.8'
-targetCompatibility='1.8'
-
-sourceSets { 
-    main {
-        java {
-            srcDirs = ['source/java']
-        }
-    }
-    test {
-        java {
-            srcDirs = ['sourceTest/java']
-        }
-        resources {
-            srcDirs = ['sourceTest/java']
-        }
-    }
-    examples {
-        java {
-            srcDirs = ['sourceExamples/java']
-        }
-    }
-}
-
-buildDir = 'targets/gradle'
-
-buildscript {
-    apply from: './repository.gradle'
-    
-    repositories repositoryConfig
-}
-
-repositories repositoryConfig
-
-def execute(command, arguments) {
-    new ByteArrayOutputStream().withStream { os ->
-        print "execute: ${command}"
-        arguments.collect({print " ${it}"})
-        println ''
-        def result = exec {
-            executable = command
-            args = arguments
-            standardOutput = os
-        }
-        return os.toString().split('\n')
-    }
-}
-
-ext.executeFunction = {
-  command, arguments -> execute(command, arguments)
-}
-
-def execute_working_dir(command, arguments, working_dir) {
-    new ByteArrayOutputStream().withStream { os ->
-        print "execute: ${command}"
-        arguments.collect({print " ${it}"})
-        println ''
-        def result = exec {
-            executable = command
-            args = arguments
-            standardOutput = os
-        }
-        return os.toString().split('\n')
-    }
-}
-
-def isGitProject() {
-   return new java.io.File(projectDir, ".git").isDirectory() || new java.io.File(projectDir, "../.git").isDirectory() 
-}
-
-def calculateCleanFlag() {
-    if (isGitProject()) {
-        def output = execute_working_dir('git', ['status', '--porcelain'], '../' + project.name)
-        return (output.length == 0 || (output.length ==1 && output[0].length() == 0)) ? 'clean' : 'dirty'
-    } else {
-    	return 'dirty'
-    }
-}
-
-def findMaximum(lines, key) {
-    return lines.findAll({ it.startsWith(key)}).collect({element -> element.split(':')[1].toInteger()}).max()
-}
-
-def calculateBuildInfo() {
-    if (isGitProject()) {
-        def gitlogoutput = execute_working_dir('git', ['log', '-1', '--format=%at-%H'], '../' + project.name)
-        def rev = gitlogoutput[0].split("-")
-        project.ext.revisionNumber = Integer.parseInt(rev[0])
-        def commitHash = rev[1]
-        def date = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX").format(new Date(revisionNumber * 1000L))
-        project.ext.revisionInfo = "${commitHash} [${date}]"
-        def tag = 'git tag -l --points-at HEAD'.execute().text.trim()
-        if (tag == null || tag.isEmpty()) {
-            project.ext.versionNumber = 'SNAPSHOT'
-        } else if (tag.contains('pybis')) {
-           throw new GradleException("project must contain a readme file")
-        } else {
-           project.ext.versionNumber = tag
-        }
-    } else {
-        project.ext.revisionInfo = '?'
-        project.ext.revisionNumber = 1
-        project.ext.versionNumber = 'SNAPSHOT'
-    }
-
-    version = project.ext.versionNumber.startsWith('SNAPSHOT') ? "${project.ext.revisionNumber}" : project.ext.versionNumber
-    project.ext.revisionForPublication = project.ext.versionNumber.startsWith('SNAPSHOT') ? "${project.ext.revisionNumber}" : project.ext.versionNumber
-    project.ext.cleanFlag = calculateCleanFlag()
-    def buildInfo = "${project.ext.versionNumber}::${project.ext.revisionInfo}::${project.ext.cleanFlag}"
-    println "BUILD INFO for $project: $buildInfo"
-    def targetsDist = 'targets/dist'
-    def distFolder = new File("${project.projectDir}/$targetsDist")
-    distFolder.deleteDir()
-    distFolder.mkdirs()
-    file("${project.projectDir}/$targetsDist/BUILD-${project.name}.INFO") << buildInfo
-}
-
-calculateBuildInfo()
-
-group='cisd'
-
-task checkRestrictions(type: Exec, dependsOn: [classes, testClasses]) {
-    doFirst {
-        def cp = configurations.testCompile.filter({ f -> f.name.startsWith('restrictionchecker') || f.name.startsWith('bcel')}).asPath
-        def cmd = ['java', '-cp', cp, 'ch.rinn.restrictions.RestrictionChecker', '-r', sourceSets.main.output.classesDir]
-        if (sourceSets.test.output.classesDir.exists()) {
-            cmd.add(sourceSets.test.output.classesDir)
-        }
-        cmd.add('-cp')
-        cmd.add(sourceSets.main.output.classesDir)
-        if (sourceSets.test.output.classesDir.exists()) {
-            cmd.add(sourceSets.test.output.classesDir)
-        }
-        cmd.add(configurations.testCompile.asPath)
-        commandLine cmd
-    }
-}
-
-def deleteSymbolicLinksRecursively(file) {
-    def absolutePath = file.getAbsolutePath()
-    def canonicalPath = file.getCanonicalPath()
-    if (absolutePath.equals(canonicalPath) == false) {
-        file.delete();
-    } else if (file.isDirectory()) {
-        File[] files = file.listFiles()
-        for (File child : files) {
-            deleteSymbolicLinksRecursively(child)
-        }
-    }
-}
-
-task deleteSymLinks {
-    doFirst {
-        println "DELETE SYM LINKS in $buildDir"
-        deleteSymbolicLinksRecursively buildDir
-    }
-}
-
-clean.dependsOn deleteSymLinks
-
-test {
-    useTestNG()
-    options.suites('sourceTest/java/tests.xml')    
-
-    systemProperty "ant.project.name", project.name
-
-    maxHeapSize = "8192m"
-    jvmArgs '-XX:MaxPermSize=1024m', '-Duser.timezone=Europe/Zurich'
-    
-    testLogging.showStandardStreams = true
-    ignoreFailures = true
-}
-test.dependsOn checkRestrictions
-
-dependencies {
-    ecj "eclipse:ecj:+"
-}
-
-compileJava {
-    options.encoding = 'utf-8'
-    options.fork = true
-    doFirst {
-        options.forkOptions.with {
-            executable = 'java'
-            jvmArgs = createJvmArgs()
-        }
-    }
-}
-
-def createJvmArgs() {
-    def args = ['-cp', configurations.ecj.asPath, 'org.eclipse.jdt.internal.compiler.batch.Main', '-nowarn']
-    return args
-}
-
-processTestResources {
-    fileMode=0666
-}
-
-apply plugin: 'eclipse'
-
-eclipse {
-    classpath {
-        downloadSources=true
-        defaultOutputDir = file('targets/classes')
-    }
-}
-
-eclipse.classpath.file {
-    whenMerged{ classpath ->
-        def projectRefs = classpath.entries.findAll{entry -> entry.kind =='src' && entry.path.startsWith('/')}
-        classpath.entries.removeAll(projectRefs)
-        classpath.entries.addAll(projectRefs)   
-    }
-}
-
-task testJar(type: Jar, dependsOn: testClasses) {
-    baseName = "test-${project.archivesBaseName}"
-    from sourceSets.test.output
-}
-
-task sourcesJar(type: Jar) {
-    classifier = 'sources'
-    from sourceSets.main.allSource
-}
-
-compileJava.dependsOn sourcesJar
-
-artifacts {
-    tests testJar
-}
-
-artifacts {
-	archives sourcesJar
-}
-
-task compileDependencies(type: Copy) {
-    into "$buildDir/output/compile-dependencies"
-    from configurations.compile
-}
-
-task runtimeDependencies(type: Copy) {
-    into "$buildDir/output/runtime-dependencies"
-    from configurations.runtime
-}
-
-task testCompileDependencies(type: Copy) {
-    into "$buildDir/output/testCompile-dependencies"
-    from configurations.testCompile
-}
-
-task testRuntimeDependencies(type: Copy) {
-    into "$buildDir/output/testRuntime-dependencies"
-    from configurations.testRuntime
-}
-
-task checkDependencies(dependsOn: classes) << {
-	ant.taskdef(name: 'dependencychecker', classname: 'classycle.ant.DependencyCheckingTask', classpath: configurations.testRuntime.asPath)
-    ant.dependencychecker(
-                definitionFile: 'resource/dependency-structure.ddf', 
-                failOnUnwantedDependencies: 'true',
-                mergeInnerClasses: 'true') {
-        fileset(dir: "${buildDir}", includes: "**/*.class")
-    }
-}
-
-apply plugin: 'ivy-publish'
-if (hasProperty('ivyRepository') == false || ''.equals(project.ivyRepository))
-{
-    project.ext.ivyRepository = "${project.projectDir}/../ivy-repository"
-}
-publishing {
-
-    repositories {
-        ivy {
-            ivyPattern "file://${project.ivyRepository}/[organisation]/[module]/[revision]/ivy.xml"
-            artifactPattern "file://${project.ivyRepository}/[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"
-        }
-    }
-}
-
-publish {
-    dependsOn build
-}
-
-if (JavaVersion.current().isJava8Compatible()) {
-    tasks.withType(Javadoc) {
-        options.addStringOption('Xdoclint:none', '-quiet')
-    }
-}
diff --git a/lib-base/repository.gradle b/lib-base/repository.gradle
deleted file mode 100644
index ca7ada3c059ee0c8311659f1ba8911ffac513f34..0000000000000000000000000000000000000000
--- a/lib-base/repository.gradle
+++ /dev/null
@@ -1,10 +0,0 @@
-ext.repositoryConfig = {
-    ivy {
-        ivyPattern "${project.projectDir}/../ivy-repository/[organisation]/[module]/[revision]/ivy.xml"
-        artifactPattern "${project.projectDir}/../ivy-repository/[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"
-    }
-    ivy {
-        ivyPattern "http://svnsis.ethz.ch/repos/cisd/ivy-repository/trunk/[organisation]/[module]/[revision]/ivy.xml"
-        artifactPattern "http://svnsis.ethz.ch/repos/cisd/ivy-repository/trunk/[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"
-    }
-}
diff --git a/lib-base/source/java/ch/systemsx/cisd/base/unix/Posix.java b/lib-base/source/java/ch/systemsx/cisd/base/unix/Posix.java
new file mode 100644
index 0000000000000000000000000000000000000000..80bb928d4668a65d9a69c2d0c1b9e1806eda6d90
--- /dev/null
+++ b/lib-base/source/java/ch/systemsx/cisd/base/unix/Posix.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ * 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.
+ */
+package ch.systemsx.cisd.base.unix;
+
+import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked;
+import com.sun.security.auth.module.UnixSystem;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+public class Posix
+{
+    // Available on unix systems JDK 11 onwards
+    private static UnixSystem unixSystem = new com.sun.security.auth.module.UnixSystem();
+
+    //
+    // User Functions
+    //
+
+    public static int getUid()
+    {
+        return (int) unixSystem.getUid();
+    }
+
+    public static int getGid()
+    {
+        return (int) unixSystem.getGid();
+    }
+
+    //
+    // File functions
+    //
+
+    private static Set<PosixFilePermission> allPermissionsMode = Set.of(PosixFilePermission.OWNER_READ,
+            PosixFilePermission.OWNER_WRITE,
+            PosixFilePermission.OWNER_EXECUTE,
+            PosixFilePermission.GROUP_READ,
+            PosixFilePermission.GROUP_WRITE,
+            PosixFilePermission.GROUP_EXECUTE,
+            PosixFilePermission.OTHERS_READ,
+            PosixFilePermission.OTHERS_WRITE,
+            PosixFilePermission.OTHERS_EXECUTE);
+
+    public static final void setAccessMode(String fileName, short mode) throws IOExceptionUnchecked
+    {
+        if (fileName == null)
+        {
+            throw new NullPointerException("fileName");
+        }
+
+        if (mode != 777) {
+            throw new IOExceptionUnchecked("Unsupported unix file permission mode: " + mode);
+        }
+
+        try
+        {
+            Files.setPosixFilePermissions(Path.of(fileName), allPermissionsMode);
+        } catch (IOException e)
+        {
+            throw new IOExceptionUnchecked(e);
+        }
+    }
+
+    /**
+     * Creates a hard link <var>linkName</var> that points to <var>fileName</var>.
+     *
+     * @throws IOExceptionUnchecked If the underlying system call fails, e.g. because <var>linkName</var> already exists or <var>fileName</var> does
+     *             not exist.
+     */
+    public static final void createHardLink(String fileName, String linkName)
+            throws IOExceptionUnchecked
+    {
+        if (fileName == null)
+        {
+            throw new NullPointerException("fileName");
+        }
+        if (linkName == null)
+        {
+            throw new NullPointerException("linkName");
+        }
+        link(fileName, linkName);
+    }
+
+    /**
+     * Creates a symbolic link <var>linkName</var> that points to <var>fileName</var>.
+     *
+     * @throws IOExceptionUnchecked If the underlying system call fails, e.g. because <var>linkName</var> already exists.
+     */
+    public static final void createSymbolicLink(String fileName, String linkName)
+            throws IOExceptionUnchecked
+    {
+        if (fileName == null)
+        {
+            throw new NullPointerException("fileName");
+        }
+        if (linkName == null)
+        {
+            throw new NullPointerException("linkName");
+        }
+
+        symlink(fileName, linkName);
+    }
+
+    /*
+     * This method manages symbolic link creation using NIO API.
+     */
+    public static final void symlink(String fileName, String linkName) throws IOExceptionUnchecked {
+        try {
+            Path file = Path.of(fileName);
+            Path link = Path.of(linkName);
+            Path relativeFilePath = link.getParent().relativize(file); // Relative path to the file from the link
+            Files.createDirectories(link.getParent()); // Create any missing folder on the directory hierarchy leading to folder that will contain the link
+            Files.createSymbolicLink(link, relativeFilePath); // Creates the link
+        } catch (IOException exception) {
+            throw new IOExceptionUnchecked(exception);
+        }
+    }
+
+    /*
+     * This method manages link creation using NIO API.
+     */
+    public static final void link(String fileName, String linkName) throws IOExceptionUnchecked {
+        try {
+            Path file = Path.of(fileName);
+            Path link = Path.of(linkName);
+            Path relativeFilePath = link.getParent().relativize(file); // Relative path to the file from the link
+            Files.createDirectories(link.getParent()); // Create any missing folder on the directory hierarchy leading to folder that will contain the link
+            Files.createLink(link, relativeFilePath); // Creates the link
+        } catch (IOException exception) {
+            throw new IOExceptionUnchecked(exception);
+        }
+    }
+
+}