From b59dcfd93c18db4e43983edc27ce6faaefabf032 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Fri, 3 Jun 2022 23:59:59 +0800 Subject: [PATCH] java rewrite --- .gitignore | 5 + build.gradle | 117 +++++ build.sbt | 73 --- gradle.properties | 29 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 +++++++++ gradlew.bat | 89 ++++ jpacrepo-api/build.gradle | 16 + jpacrepo-api/src/main/java/module-info.java | 6 + .../jpacrepo/api/model/CompressionFormat.java | 12 + .../woggioni/jpacrepo/api/model/PkgData.java | 93 ++++ .../woggioni/jpacrepo/api/model/PkgId.java | 30 ++ .../api/service/PacmanServiceLocal.java | 13 + .../api/service/PacmanServiceRemote.java | 9 + jpacrepo-client/build.gradle | 50 ++ .../src/main/java/module-info.java | 8 + .../net/woggioni/jpacrepo/client/Main.java | 58 +++ jpacrepo-client/src/main/resources/log4j2.xml | 14 + jpacrepo-impl/build.gradle | 19 + jpacrepo-impl/src/main/java/module-info.java | 9 + .../impl/model/CompressionFormatImpl.java | 41 ++ .../jpacrepo/impl/model/PkgDataImpl.java | 166 +++++++ nim/src/jpacrepo.nim | 119 +++-- settings.gradle | 26 + .../woggioni/jpacrepo/config/AppConfig.java | 62 +++ .../jpacrepo/factory/BeanFactory.java | 23 + .../factory/PersistenceUnitFactory.java | 22 + .../persistence/InitialSchemaAction.java | 45 ++ .../jpacrepo/persistence/QueryEngine.java | 81 +--- .../jpacrepo/service/PacmanServiceEJB.java | 213 +++++++++ .../jpacrepo/service/PacmanWebService.java | 427 +++++++++++++++++ .../jpacrepo/service/WxRsApplication.java | 17 + .../jpacrepo/service/wire/PkgDataList.java | 31 ++ .../jpacrepo/service/wire/PkgTuple.java | 15 + .../jpacrepo/service/wire/StringList.java | 28 ++ .../jpacrepo/servlet/AbstractFileServlet.java | 225 +++------ .../jpacrepo/servlet/FileServlet.java | 8 +- .../jpacrepo/version/PkgIdComparator.java | 26 +- .../jpacrepo/version/VersionComparator.java | 14 +- src/main/resources/META-INF/persistence.xml | 24 + .../woggioni/jpacrepo/config/AppConfig.scala | 41 -- .../jpacrepo/exception/Exceptions.scala | 5 - .../jpacrepo/factory/BeanFactory.scala | 48 -- .../net/woggioni/jpacrepo/model/Hasher.scala | 46 -- .../jpacrepo/model/MD5InputStream.scala | 11 - .../net/woggioni/jpacrepo/model/Parser.scala | 114 ----- .../jpacrepo/pacbase/CompressionFormat.scala | 32 -- .../woggioni/jpacrepo/pacbase/Ordering.scala | 8 - .../woggioni/jpacrepo/pacbase/PkgData.scala | 78 --- .../net/woggioni/jpacrepo/pacbase/PkgId.scala | 31 -- .../persistence/InitialSchemaAction.scala | 19 - .../jpacrepo/service/PacmanServiceEJB.scala | 174 ------- .../jpacrepo/service/PacmanWebService.scala | 448 ------------------ .../net/woggioni/jpacrepo/utils/Utils.scala | 32 -- src/test/java/ClientTest.java | 105 ++-- src/test/java/ParseTest.java | 6 +- .../jpacrepo/annotation/UnitTesting.java | 4 +- .../gvfs-nfs-1.50.2-1-x86_64.pkg.tar.zst | Bin 0 -> 21719 bytes src/test/resources/log4j2.xml | 3 - .../woggioni/jpacrepo/client/SyncDbTest.scala | 49 -- .../jpacrepo/config/AppConfigTest.scala | 26 - .../jpacrepo/pacbase/MarshalTest.scala | 49 -- .../persistence/TestPersistenceProducer.scala | 11 - .../jpacrepo/sample/ExampleSpec.scala | 38 -- .../service/PacmanServiceEJBTest.scala | 58 --- .../service/PacmanWebServiceTest.scala | 33 -- .../version/VersionComparatorSpec.scala | 16 - 68 files changed, 2218 insertions(+), 1769 deletions(-) create mode 100644 .gitignore create mode 100644 build.gradle delete mode 100644 build.sbt create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 jpacrepo-api/build.gradle create mode 100644 jpacrepo-api/src/main/java/module-info.java create mode 100644 jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/CompressionFormat.java create mode 100644 jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/PkgData.java create mode 100644 jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/PkgId.java create mode 100644 jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceLocal.java create mode 100644 jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceRemote.java create mode 100644 jpacrepo-client/build.gradle create mode 100644 jpacrepo-client/src/main/java/module-info.java create mode 100644 jpacrepo-client/src/main/java/net/woggioni/jpacrepo/client/Main.java create mode 100644 jpacrepo-client/src/main/resources/log4j2.xml create mode 100644 jpacrepo-impl/build.gradle create mode 100644 jpacrepo-impl/src/main/java/module-info.java create mode 100644 jpacrepo-impl/src/main/java/net/woggioni/jpacrepo/impl/model/CompressionFormatImpl.java create mode 100644 jpacrepo-impl/src/main/java/net/woggioni/jpacrepo/impl/model/PkgDataImpl.java create mode 100644 settings.gradle create mode 100644 src/main/java/net/woggioni/jpacrepo/config/AppConfig.java create mode 100644 src/main/java/net/woggioni/jpacrepo/factory/BeanFactory.java create mode 100644 src/main/java/net/woggioni/jpacrepo/factory/PersistenceUnitFactory.java create mode 100644 src/main/java/net/woggioni/jpacrepo/persistence/InitialSchemaAction.java create mode 100644 src/main/java/net/woggioni/jpacrepo/service/PacmanServiceEJB.java create mode 100644 src/main/java/net/woggioni/jpacrepo/service/PacmanWebService.java create mode 100644 src/main/java/net/woggioni/jpacrepo/service/WxRsApplication.java create mode 100644 src/main/java/net/woggioni/jpacrepo/service/wire/PkgDataList.java create mode 100644 src/main/java/net/woggioni/jpacrepo/service/wire/PkgTuple.java create mode 100644 src/main/java/net/woggioni/jpacrepo/service/wire/StringList.java create mode 100644 src/main/resources/META-INF/persistence.xml delete mode 100644 src/main/scala/net/woggioni/jpacrepo/config/AppConfig.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/exception/Exceptions.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/factory/BeanFactory.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/model/Hasher.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/model/MD5InputStream.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/model/Parser.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/pacbase/CompressionFormat.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/pacbase/Ordering.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/pacbase/PkgData.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/pacbase/PkgId.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/persistence/InitialSchemaAction.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/service/PacmanServiceEJB.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/service/PacmanWebService.scala delete mode 100644 src/main/scala/net/woggioni/jpacrepo/utils/Utils.scala create mode 100644 src/test/resources/gvfs-nfs-1.50.2-1-x86_64.pkg.tar.zst delete mode 100644 src/test/scala/net/woggioni/jpacrepo/client/SyncDbTest.scala delete mode 100644 src/test/scala/net/woggioni/jpacrepo/config/AppConfigTest.scala delete mode 100644 src/test/scala/net/woggioni/jpacrepo/pacbase/MarshalTest.scala delete mode 100644 src/test/scala/net/woggioni/jpacrepo/persistence/TestPersistenceProducer.scala delete mode 100644 src/test/scala/net/woggioni/jpacrepo/sample/ExampleSpec.scala delete mode 100644 src/test/scala/net/woggioni/jpacrepo/service/PacmanServiceEJBTest.scala delete mode 100644 src/test/scala/net/woggioni/jpacrepo/service/PacmanWebServiceTest.scala delete mode 100644 src/test/scala/net/woggioni/jpacrepo/version/VersionComparatorSpec.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c76426c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +target +.idea +.gradle + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..04816bc --- /dev/null +++ b/build.gradle @@ -0,0 +1,117 @@ +plugins { + id 'net.woggioni.gradle.lombok' apply false + id 'net.woggioni.gradle.wildfly' + id 'war' +} + +import net.woggioni.gradle.wildfly.Deploy2WildflyTask + +allprojects { + apply plugin: 'net.woggioni.gradle.lombok' + + group = 'net.woggioni' + version = getProperty('jpacrepo.version') + + repositories { + maven { + url = 'https://woggioni.net/mvn/' + content { + includeGroup 'net.woggioni' + } + } + mavenCentral() + } + + lombok { + version = getProperty('lombok.version') + } + + tasks.withType(JavaCompile) { + options.release = 11 + } + + tasks.withType(Test) { + useJUnitPlatform() + } +} + + +dependencies { + implementation project(':jpacrepo-impl') + implementation group: 'net.woggioni', name: 'jwo', version: getProperty('jwo.version') + implementation group: 'org.slf4j', name: 'slf4j-api', version: getProperty('slf4j.version') + + compileOnly group: 'org.hibernate', + name: 'hibernate-jpamodelgen', + version: getProperty('hibernate.version') +// compileOnly group: 'jakarta.persistence', +// name: 'jakarta.persistence-api', version: getProperty('jakarta.persistence.version') +// compileOnly group: 'jakarta.annotation', +// name: 'jakarta.annotation-api', +// version: getProperty('jakarta.annotation.version') +// compileOnly group: 'jakarta.platform', +// name: 'jakarta.jakartaee-web-api', +// version: getProperty('jakarta.ee.version') + compileOnly group: 'jakarta.platform', + name: 'jakarta.jakartaee-api', + version: getProperty('jakarta.ee.version') + implementation group: 'org.apache.commons', + name: 'commons-compress', + version: getProperty('apache.commons.compress.version') + implementation group: 'net.java.dev.jna', name: 'jna', version: getProperty('jna.version') + +// compileOnly group: 'jakarta.inject', +// name: 'jakarta.inject-api', version: getProperty('jakarta.inject.version') +// compileOnly group: 'jakarta.enterprise', name: 'jakarta.enterprise.cdi-api', version: getProperty('jakarta.cdi.version') + + testImplementation group: 'jakarta.platform', + name: 'jakarta.jakartaee-api', + version: getProperty('jakarta.ee.version') + +// testImplementation group: 'jakarta.platform', +// name: 'jakarta.jakartaee-web-api', +// version: getProperty('jakarta.ee.version') + testImplementation group: 'org.jboss', + name: 'jboss-ejb-client', version: getProperty('jboss.ejb.client.version') + testImplementation group: 'org.jboss.weld.se', + name: 'weld-se-core', version: getProperty('weld.version') + testImplementation group: 'com.h2database', + name: 'h2', version: getProperty('h2.version') + testImplementation group: 'org.hibernate', + name: 'hibernate-core', + version: getProperty('hibernate.version') + testImplementation group: 'org.jboss.resteasy', + name: 'resteasy-client', version: getProperty('resteasy.version') + testImplementation group: 'org.jboss.resteasy', + name: 'resteasy-jackson2-provider', version: getProperty('resteasy.version') + testRuntimeOnly group: 'org.slf4j', + name: 'slf4j-simple', version: getProperty('slf4j.version') + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: getProperty('junit.jupiter.version') + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: getProperty('junit.jupiter.version') + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: getProperty('junit.jupiter.version') +} + +wrapper { + distributionType = Wrapper.DistributionType.BIN + gradleVersion = getProperty('gradle.version') +} + +File nimDir = project.file('nim') +File srcDir = new File(nimDir, 'src') +File staticDir = new File(nimDir, 'static') + +Provider nimCompileTaskProvider = tasks.register("compileNim", Exec) { + inputs.files(project.fileTree(srcDir), project.fileTree(staticDir)) + File outputFile = new File(temporaryDir, "jpacrepo.js") + outputs.file(outputFile) + commandLine 'nim', 'js', "-o:$outputFile", 'src/jpacrepo.nim' + workingDir(nimDir) +} + +Provider warTaskProvider = tasks.named('war', War) { + from staticDir + from nimCompileTaskProvider +} + +tasks.named('deploy2Wildfly', Deploy2WildflyTask) { +} \ No newline at end of file diff --git a/build.sbt b/build.sbt deleted file mode 100644 index 575b4a8..0000000 --- a/build.sbt +++ /dev/null @@ -1,73 +0,0 @@ -import net.woggioni.sbt.{NimPlugin, WildflyPlugin} -import net.woggioni.sbt.ConfigurationFile._ - -name := "jpacrepo" - -organization := "net.woggioni" - -version := "2.0" - -resolvers += Resolver.mavenLocal - -scalaVersion := "2.13.2" - -libraryDependencies += "org.tukaani" % "xz" % Versions.xz -libraryDependencies += "org.slf4j" % "slf4j-api" % Versions.slf4j -libraryDependencies += "net.woggioni" % "jzstd" % Versions.jzstd -libraryDependencies += "net.woggioni" % "jwo" % Versions.jwo -libraryDependencies += "org.apache.commons" % "commons-compress" % Versions.`common-compress` - -libraryDependencies += "org.hibernate" % "hibernate-jpamodelgen" % Versions.hibernate % Provided -libraryDependencies += "org.projectlombok" % "lombok" % Versions.lombok % Provided -libraryDependencies += "javax" % "javaee-api" % Versions.javaee % Provided - -libraryDependencies += "org.jboss" % "jboss-ejb-client" % Versions.jbossEjbClient % Test -libraryDependencies += "org.apache.logging.log4j" % "log4j-slf4j-impl" % Versions.log4j % Test - -libraryDependencies += "org.jboss.weld.se" % "weld-se-core" % Versions.weld % Test -libraryDependencies += "com.h2database" % "h2" % Versions.h2 % Test -libraryDependencies += "org.hibernate" % "hibernate-core" % Versions.hibernate % Test - -libraryDependencies += "org.jboss.resteasy" % "resteasy-client" % Versions.restEasy % Test -libraryDependencies += "org.jboss.resteasy" % "resteasy-jackson2-provider" % Versions.restEasy % Test -libraryDependencies += "org.scalatest" %% "scalatest" % Versions.scalatest % Test - -enablePlugins(WarPlugin) -enablePlugins(WildflyPlugin) -enablePlugins(NimPlugin) - -nimCompilerParameters in CompileNim := Seq("-d:release", "-d:serverURL=") -sources in CompileNim := Seq(baseDirectory.value / "nim" / "src" / "jpacrepo.nim") - -webappWebInfClasses := true - - -Test / run / javaOptions += "-Dnet.woggioni.jpacrepo.configuration.file=conf/server.properties" -Test / run / fork := true - -webappPostProcess := { - val baseDir = baseDirectory.value / "nim" / "static" - val nimOutput = (compileNim in CompileNim).value - println(nimOutput) - webappDir: File => { - IO.copyFile(baseDir / "index.html", webappDir / "index.html") - IO.copyFile(baseDir / "jpacrepo.css", webappDir / "jpacrepo.css") - nimOutput.foreach(file => IO.copyFile(file, webappDir / file.getName)) - } -} - - -lazy val datasourceJNDI = SettingKey[String]("datasource-jndi", "JNDI name of the application datasource") -lazy val databaseAction = SettingKey[String]("database-action", - "value of the property \"javax.persistence.schema-generation.database.action\" in the persistence unit") - -datasourceJNDI := "java:/PostgresDS" -databaseAction := "none" - -resourceGenerators in Compile += Def.task({ - Seq(IO.configureFile( - (sourceDirectory in Compile).value / "resources-template" / "persistence.xml", - (resourceManaged in Compile).value / "META-INF" / "persistence.xml", - Map("dataSourceJNDI" -> datasourceJNDI.value, - "dataBaseAction" -> databaseAction.value))) -}).taskValue diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2616bde --- /dev/null +++ b/gradle.properties @@ -0,0 +1,29 @@ +gradle.version=7.4.2 + +net.woggioni.gradle.lombok.version=0.1 +net.woggioni.gradle.wildfly.version=0.1 +net.woggioni.gradle.envelope.version=1.0-SNAPSHOT + +jpacrepo.version=2.0-SNAPSHOT + +lombok.version=1.18.22 +slf4j.version=1.7.32 +xz.version=1.9 +apache.commons.compress.version=1.21 +hibernate.version=5.6.5.Final +jzstd.version=0.1-SNAPSHOT +jwo.version=1.0-SNAPSHOT +jakarta.persistence.version=3.0.0 +jakarta.inject.version=2.0.0 +jakarta.cdi.version=3.0.1 +jakarta.annotation.version=2.0.0 +jakarta.ee.version=8.0.0 +jaxb.api.version=2.3.1 + +jboss.ejb.client.version=4.0.43.Final +log4j.version=2.17.2 +weld.version=4.0.3.Final +h2.version=2.1.210 +resteasy.version=6.0.0.Final +jna.version=5.10.0 +junit.jupiter.version=5.8.2 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b GIT binary patch literal 59821 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..aa991fc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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 "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/jpacrepo-api/build.gradle b/jpacrepo-api/build.gradle new file mode 100644 index 0000000..866de4e --- /dev/null +++ b/jpacrepo-api/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java-library' +} + +dependencies { + compileOnly group: 'jakarta.platform', + name: 'jakarta.jakartaee-api', + version: getProperty('jakarta.ee.version') + compileOnly group: 'javax.xml.bind', name: 'jaxb-api', version: getProperty('jaxb.api.version') +} + +tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { JavaCompile j -> + options.compilerArgs += [ + '--add-reads', 'net.woggioni.jpacrepo.api=ALL-UNNAMED' + ] +} \ No newline at end of file diff --git a/jpacrepo-api/src/main/java/module-info.java b/jpacrepo-api/src/main/java/module-info.java new file mode 100644 index 0000000..df6bd33 --- /dev/null +++ b/jpacrepo-api/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module net.woggioni.jpacrepo.api { + requires static lombok; + requires static java.xml.bind; + exports net.woggioni.jpacrepo.api.model; + exports net.woggioni.jpacrepo.api.service; +} \ No newline at end of file diff --git a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/CompressionFormat.java b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/CompressionFormat.java new file mode 100644 index 0000000..aaa4325 --- /dev/null +++ b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/CompressionFormat.java @@ -0,0 +1,12 @@ +package net.woggioni.jpacrepo.api.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum CompressionFormat { + XZ("xz"), GZIP("gz"), Z_STANDARD("zst"); + + @Getter + private final String value; +} diff --git a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/PkgData.java b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/PkgData.java new file mode 100644 index 0000000..0e6d99f --- /dev/null +++ b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/PkgData.java @@ -0,0 +1,93 @@ +package net.woggioni.jpacrepo.api.model; + +import lombok.Data; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.ElementCollection; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Index; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.PrePersist; +import javax.persistence.PreUpdate; +import javax.persistence.Table; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.time.OffsetDateTime; +import java.util.Set; + +@Data +@Entity +@Access(AccessType.FIELD) +@NamedQueries(value = { + @NamedQuery(name = "searchByFileName", query = "SELECT p FROM PkgData p WHERE p.fileName = :fileName"), + @NamedQuery(name = "searchByName", query = "SELECT p FROM PkgData p WHERE p.id.name = :name"), + @NamedQuery(name = "searchById", query = "SELECT p FROM PkgData p WHERE p.id = :id"), + @NamedQuery(name = "searchByHash", query = "SELECT p FROM PkgData p WHERE p.md5sum = :md5sum") +}) +@Table(indexes = { + @Index(columnList = "md5sum", unique = true), + @Index(columnList = "fileName", unique = true) +}) +@XmlRootElement +@XmlAccessorType(XmlAccessType.FIELD) +public class PkgData { + + @EmbeddedId + private PkgId id; + + private String base; + + private String description; + + private String url; + + private OffsetDateTime buildDate; + + private String packager; + + private long size; + + private String license; + + private String md5sum; + + private String fileName; + + @ElementCollection(fetch = FetchType.EAGER) + private Set replaces; + + @ElementCollection(fetch = FetchType.EAGER) + private Set conflict; + + @ElementCollection(fetch = FetchType.EAGER) + private Set provides; + + @ElementCollection(fetch = FetchType.EAGER) + private Set depend; + + @ElementCollection(fetch = FetchType.EAGER) + private Set optdepend; + + @ElementCollection(fetch = FetchType.EAGER) + private Set makedepend; + + @ElementCollection(fetch = FetchType.EAGER) + private Set makeopkgopt; + + @ElementCollection(fetch = FetchType.EAGER) + private Set backup; + + private OffsetDateTime updTimestamp; + + @PreUpdate + @PrePersist + private void writeTimestamp() { + updTimestamp = OffsetDateTime.now(); + } +} + diff --git a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/PkgId.java b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/PkgId.java new file mode 100644 index 0000000..27992a6 --- /dev/null +++ b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/model/PkgId.java @@ -0,0 +1,30 @@ +package net.woggioni.jpacrepo.api.model; + +import lombok.Data; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Embeddable; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +@Data +@Embeddable +@Access(AccessType.FIELD) +@XmlRootElement +@XmlAccessorType(XmlAccessType.FIELD) +public class PkgId implements Serializable { + + private String name; + + private String version; + + private String arch; + + @Enumerated(EnumType.ORDINAL) + private CompressionFormat compressionFormat; +} diff --git a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceLocal.java b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceLocal.java new file mode 100644 index 0000000..c3a48c6 --- /dev/null +++ b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceLocal.java @@ -0,0 +1,13 @@ +package net.woggioni.jpacrepo.api.service; + + +import net.woggioni.jpacrepo.api.model.PkgData; + +import javax.ejb.Local; +import java.util.List; + +@Local +public interface PacmanServiceLocal extends PacmanServiceRemote { + long countResults(String name, String version, String arch); + List searchPackage(String name, String version, String arch, int page, int pageSize, String fileName); +} \ No newline at end of file diff --git a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceRemote.java b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceRemote.java new file mode 100644 index 0000000..b4b48b4 --- /dev/null +++ b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceRemote.java @@ -0,0 +1,9 @@ +package net.woggioni.jpacrepo.api.service; + +import javax.ejb.Remote; + +@Remote +public interface PacmanServiceRemote { + void syncDB(); + void deletePackage(String filename); +} \ No newline at end of file diff --git a/jpacrepo-client/build.gradle b/jpacrepo-client/build.gradle new file mode 100644 index 0000000..f611353 --- /dev/null +++ b/jpacrepo-client/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'java-library' + id "net.woggioni.gradle.envelope" +} + +import net.woggioni.gradle.envelope.EnvelopeJarTask + +dependencies { + + implementation group: 'org.slf4j', name: 'slf4j-api', version: getProperty('slf4j.version') + implementation group: 'org.apache.logging.log4j', + name: 'log4j-slf4j-impl', + version: getProperty('log4j.version') + + implementation project(':jpacrepo-api') + runtimeOnly group: 'org.jboss', + name: 'jboss-ejb-client', version: getProperty('jboss.ejb.client.version') + +} + +java { + modularity.inferModulePath = true +} + +tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { JavaCompile j -> + options.javaModuleMainClass = 'net.woggioni.jpacrepo.client.Main' +// options.compilerArgs += [ +// '--add-reads', 'javax.ejb=ALL-UNNAMED' +// ] +} + +Provider envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) { + mainClass = 'net.woggioni.jpacrepo.client.Main' +} + +tasks.register('run', JavaExec) { JavaExec t -> + classpath(project.sourceSets.main.output.dirs) + classpath(project.sourceSets.main.runtimeClasspath) + mainModule = 'net.woggioni.jpacrepo.client' + mainClass = 'net.woggioni.jpacrepo.client.Main' +// String modulePath = project.sourceSets.main.runtimeClasspath.files.stream() +// .map(File::toString) +// .collect(Collectors.joining(System.getProperty('path.separator'))) + +// jvmArgs = [ +// '--add-reads', 'net.woggioni.jpacrepo.client=ALL-UNNAMED', +// '--module-path', modulePath +// ] + modularity.inferModulePath = true +} \ No newline at end of file diff --git a/jpacrepo-client/src/main/java/module-info.java b/jpacrepo-client/src/main/java/module-info.java new file mode 100644 index 0000000..88ab7b0 --- /dev/null +++ b/jpacrepo-client/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module net.woggioni.jpacrepo.client { + requires static lombok; + requires jdk.unsupported; + requires java.naming; + requires org.slf4j; + requires org.apache.logging.log4j; + requires net.woggioni.jpacrepo.api; +} \ No newline at end of file diff --git a/jpacrepo-client/src/main/java/net/woggioni/jpacrepo/client/Main.java b/jpacrepo-client/src/main/java/net/woggioni/jpacrepo/client/Main.java new file mode 100644 index 0000000..7b83c8d --- /dev/null +++ b/jpacrepo-client/src/main/java/net/woggioni/jpacrepo/client/Main.java @@ -0,0 +1,58 @@ +package net.woggioni.jpacrepo.client; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.woggioni.jpacrepo.api.service.PacmanServiceRemote; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NameClassPair; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import java.util.Properties; + +@Slf4j +public class Main { + + private static void traverseJndiNode(String nodeName, Context context) { + try { + NamingEnumeration list = context.list(nodeName); + while (list.hasMore()) { + String childName = nodeName + "" + list.next().getName(); + System.out.println(childName); + traverseJndiNode(childName, context); + } + } catch (NamingException ex) { + // We reached a leaf + } + } + + @SneakyThrows + public static void main(String[] args) { + Properties prop = new Properties(); +// InputStream in = Main.class.getClassLoader().getResourceAsStream("jboss-ejb-client.properties"); +// prop.load(in); + prop.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming"); + + prop.put(Context.INITIAL_CONTEXT_FACTORY, "org.wildfly.naming.client.WildFlyInitialContextFactory"); + prop.put(Context.PROVIDER_URL, "http-remoting://localhost:8080"); +// prop.put(Context.PROVIDER_URL, "http-remoting://nuc:8080"); +// prop.put(Context.PROVIDER_URL, "remote://odroid-u3:4447"); + prop.put(Context.SECURITY_PRINCIPAL, "walter"); + prop.put(Context.SECURITY_CREDENTIALS, "27ff5990757d1d"); +// prop.put(Context.SECURITY_PRINCIPAL, "luser"); +// prop.put(Context.SECURITY_CREDENTIALS, "123456"); + + prop.put("jboss.naming.client.ejb.context", true); + Context context = new InitialContext(prop); + Context ctx = new InitialContext(prop); + traverseJndiNode("/", context); +// final PacmanService stateService = (PacmanService) ctx.lookup("/jpacrepo-1.0/remote/PacmanServiceEJB!service.PacmanService"); + final PacmanServiceRemote service = (PacmanServiceRemote) ctx.lookup( + "/jpacrepo-2.0-SNAPSHOT/PacmanServiceEJB!net.woggioni.jpacrepo.api.service.PacmanServiceRemote" + ); +// List pkgs = service.searchPackage("google-earth", null, null, 1, 10); +// System.out.println(new XStream().toXML(pkgs)); + service.syncDB(); + } +} diff --git a/jpacrepo-client/src/main/resources/log4j2.xml b/jpacrepo-client/src/main/resources/log4j2.xml new file mode 100644 index 0000000..a8114ed --- /dev/null +++ b/jpacrepo-client/src/main/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/jpacrepo-impl/build.gradle b/jpacrepo-impl/build.gradle new file mode 100644 index 0000000..67b3b98 --- /dev/null +++ b/jpacrepo-impl/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java-library' +} + +dependencies { + compileOnly group: 'jakarta.platform', + name: 'jakarta.jakartaee-api', + version: getProperty('jakarta.ee.version') + + implementation group: 'org.tukaani', name: 'xz', version: getProperty('xz.version') + implementation group: 'org.slf4j', name: 'slf4j-api', version: getProperty('slf4j.version') + implementation group: 'net.woggioni', name: 'jzstd', version: getProperty('jzstd.version') + implementation group: 'net.woggioni', name: 'jwo', version: getProperty('jwo.version') + implementation group: 'org.apache.commons', + name: 'commons-compress', + version: getProperty('apache.commons.compress.version') + + api project(':jpacrepo-api') +} \ No newline at end of file diff --git a/jpacrepo-impl/src/main/java/module-info.java b/jpacrepo-impl/src/main/java/module-info.java new file mode 100644 index 0000000..7995041 --- /dev/null +++ b/jpacrepo-impl/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module net.woggioni.jpacrepo.impl { + requires static lombok; + requires net.woggioni.jpacrepo.api; + requires net.woggioni.jwo; + requires net.woggioni.jzstd; + requires org.apache.commons.compress; + + exports net.woggioni.jpacrepo.impl.model; +} \ No newline at end of file diff --git a/jpacrepo-impl/src/main/java/net/woggioni/jpacrepo/impl/model/CompressionFormatImpl.java b/jpacrepo-impl/src/main/java/net/woggioni/jpacrepo/impl/model/CompressionFormatImpl.java new file mode 100644 index 0000000..d00b3c2 --- /dev/null +++ b/jpacrepo-impl/src/main/java/net/woggioni/jpacrepo/impl/model/CompressionFormatImpl.java @@ -0,0 +1,41 @@ +package net.woggioni.jpacrepo.impl.model; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import net.woggioni.jpacrepo.api.model.CompressionFormat; +import net.woggioni.jwo.JWO; + +import java.nio.file.Path; +import java.text.ParseException; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CompressionFormatImpl { + private static final Map valueMap; + + static { + Map result = new TreeMap<>(); + for(CompressionFormat format : CompressionFormat.values()) { + result.put(format.getValue(), format); + } + valueMap = Collections.unmodifiableMap(result); + } + + @SneakyThrows + public static CompressionFormat guess(Path file) { + String extension = JWO.splitExtension(file.getFileName()) + .orElseThrow(() -> JWO.newThrowable(ParseException.class, + "Unable to parse file extension for '%s'", file.getFileName())) + .get_2().substring(1); + + CompressionFormat result = valueMap.get(extension); + if(result == null) { + throw JWO.newThrowable(IllegalArgumentException.class, + "Unknown compression format for file extension '%s'", extension); + } + return result; + } +} diff --git a/jpacrepo-impl/src/main/java/net/woggioni/jpacrepo/impl/model/PkgDataImpl.java b/jpacrepo-impl/src/main/java/net/woggioni/jpacrepo/impl/model/PkgDataImpl.java new file mode 100644 index 0000000..5305397 --- /dev/null +++ b/jpacrepo-impl/src/main/java/net/woggioni/jpacrepo/impl/model/PkgDataImpl.java @@ -0,0 +1,166 @@ +package net.woggioni.jpacrepo.impl.model; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import net.woggioni.jpacrepo.api.model.PkgData; +import net.woggioni.jpacrepo.api.model.PkgId; +import net.woggioni.jwo.CollectionUtils; +import net.woggioni.jwo.Fun; +import net.woggioni.jwo.Hash; +import net.woggioni.jwo.JWO; +import net.woggioni.jwo.Tuple2; +import net.woggioni.jwo.UncloseableInputStream; +import net.woggioni.jzstd.ZstdInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.ParseException; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PkgDataImpl { + + @SneakyThrows + public static PkgData parseFile(Path file, net.woggioni.jpacrepo.api.model.CompressionFormat compressionFormat) { + Fun decompressorStreamConstructor; + switch (compressionFormat) { + case XZ: + decompressorStreamConstructor = XZCompressorInputStream::new; + break; + case Z_STANDARD: + decompressorStreamConstructor = ZstdInputStream::from; + break; + case GZIP: + decompressorStreamConstructor = GZIPInputStream::new; + break; + default: + throw JWO.newThrowable(ParseException.class, + "Unsupported compression format '%s'", compressionFormat); + } + try(TarArchiveInputStream is = new TarArchiveInputStream( + decompressorStreamConstructor.apply( + new BufferedInputStream( + Files.newInputStream(file))))) { + var archiveEntry = is.getNextEntry(); + while (archiveEntry != null) { + if (Objects.equals(".PKGINFO", archiveEntry.getName())) { + try(BufferedReader reader = + new BufferedReader( + new InputStreamReader( + new UncloseableInputStream(is)))) { + Map> metadata = reader.lines().map(String::trim) + .filter(Predicate.not(String::isEmpty)) + .filter(line -> !line.startsWith("#")) + .map((Fun>) line -> { + int equals = line.indexOf("="); + if (equals < 0) { + throw JWO.newThrowable(ParseException.class, + "Error parsing .PKGINFO file in '%s'", file); + } else { + return Tuple2.newInstance( + line.substring(0, equals).trim(), + line.substring(equals + 1).trim()); + } + }).collect( + Collectors.groupingBy( + Tuple2::get_1, + TreeMap::new, + Collectors.mapping(Tuple2::get_2, + Collectors.toUnmodifiableList()))); + PkgData data = new PkgData(); + data.setId(new PkgId()); + data.getId().setCompressionFormat(compressionFormat); + + for (Map.Entry> entry : metadata.entrySet()) { + String key = entry.getKey(); + List value = entry.getValue(); + switch (key) { + case "size": + data.setSize(Long.parseLong(value.get(0))); + break; + case "arch": + data.getId().setArch(value.get(0)); + break; + case "replaces": + data.setReplaces(value.stream().collect(CollectionUtils.toUnmodifiableTreeSet())); + break; + case "packager": + data.setPackager(value.get(0)); + break; + case "url": + data.setUrl(value.get(0)); + break; + case "pkgname": + data.getId().setName(value.get(0)); + break; + case "builddate": + data.setBuildDate(OffsetDateTime.ofInstant( + Instant.ofEpochSecond(Long.parseLong(value.get(0))), ZoneOffset.UTC)); + break; + case "license": + data.setLicense(value.get(0)); + break; + case "pkgver": + data.getId().setVersion(value.get(0)); + break; + case "pkgdesc": + data.setDescription(value.get(0)); + break; + case "provides": + data.setProvides(value.stream().collect(CollectionUtils.toUnmodifiableTreeSet())); + break; + case "conflict": + data.setConflict(value.stream().collect(CollectionUtils.toUnmodifiableTreeSet())); + break; + case "backup": + data.setBackup(value.stream().collect(CollectionUtils.toUnmodifiableTreeSet())); + break; + case "optdepend": + data.setOptdepend(value.stream().collect(CollectionUtils.toUnmodifiableTreeSet())); + break; + case "depend": + data.setDepend(value.stream().collect(CollectionUtils.toUnmodifiableTreeSet())); + break; + case "makedepend": + data.setMakedepend(value.stream().collect(CollectionUtils.toUnmodifiableTreeSet())); + break; + case "makepkgopt": + data.setMakeopkgopt(value.stream().collect(CollectionUtils.toUnmodifiableTreeSet())); + break; + case "pkgbase": + data.setBase(value.get(0)); + break; + default: + break; + } + } + try(InputStream fis = Files.newInputStream(file)) { + data.setMd5sum(Hash.hash(Hash.Algorithm.MD5, fis).toString()); + } + data.setFileName(file.getFileName().toString()); + return data; + } + } else { + archiveEntry = is.getNextEntry(); + } + } + throw JWO.newThrowable(ParseException.class, ".PKGINFO file not found in '%s'", file); + } + } +} diff --git a/nim/src/jpacrepo.nim b/nim/src/jpacrepo.nim index ba43783..a3ee3b4 100644 --- a/nim/src/jpacrepo.nim +++ b/nim/src/jpacrepo.nim @@ -9,7 +9,7 @@ from sequtils import map, apply var pkgMap : JsonNode -const serverURL {.strdefine.}: string = "http://woggioni.net/jpacrepo/" +const serverURL {.strdefine.}: string = "" proc last[T](s : seq[T]) : T = s[s.len - 1] proc formatByteSize(size : BiggestInt) : string = size.float64.formatEng(precision=1, siPrefix=true, unit = "B") @@ -73,6 +73,7 @@ proc newDownloadPanel(parent : Element) : DownloadPanel = let tf= document.createElement("input") tf.setAttribute("name", "pkgs") let txt = sequtils.foldl(pkglist, a & " " & b) + echo txt tf.value(txt) form.appendChild(tf) document.body.appendChild(form) @@ -123,16 +124,18 @@ proc addPkg(dp : DownloadPanel, pkgfile : string) = req.send() dp.updateBadge -proc readTableRow(row : Element) : JsonNode = +proc readTableRow(arch : string, row : Element) : JsonNode = let pkgname = $row.querySelector("td:nth-child(2)").textContent let version = $row.querySelector("td:nth-child(3) button").textContent - let arch = $row.querySelector("td:nth-child(4) button").textContent - pkgMap[pkgname][version][arch] + let filename = $row.querySelector("td:nth-child(4) button").textContent + for candidate in pkgMap[arch][pkgname][version]: + if filename == candidate["filename"].getStr: + return candidate proc createDropdown(parent : Element, data :seq[string], onchange : proc(value : string)) = htmlTreeAppend(parent): - "div": + "span": var button : Element classList = ["dropdown"] "button": @@ -158,18 +161,18 @@ proc createDropdown(parent : Element, data :seq[string], onchange : proc(value : type PkgTable = ref object addButton : Element -proc newPkgTable(parent: Element, searchString : string) : PkgTable = +proc newPkgTable(parent: Element, arch: string, searchString : string, addButtonCallback : proc(e : Event)) : PkgTable = var pkgtable = PkgTable() var fragments = newSeq[string]() for fragment in searchString.splitWhitespace(): fragments.add(fragment) var searchResult = newOrderedTable[string, JsonNode]() - for key, value in pkgMap: + for key, value in pkgMap[arch]: for fragment in fragments: if fragment in key: searchResult[key] = value - for table in document.querySelectorAll("table.pkgtable"): - table.parentNode.removeChild(table) + for node in parent.querySelectorAll("table.pkgtable"): + parent.removeChild(node) htmlTreeAppend(parent): "table": classList = ["table", "table-striped","pkgtable"] @@ -190,21 +193,22 @@ proc newPkgTable(parent: Element, searchString : string) : PkgTable = "th": text = "Version" "th": - text = "Arch" + text = "File name" "th": text = "Installed size" "tbody": cb: var i = 0 - for name, versions in searchResult: + for name, versions in searchResult.mpairs: + closureScope: htmlTreeAppend(elem): "tr": var row : Element - var archCell : Element + var fileNameCell : Element var sizeCell : Element let size_change_callback = proc(newValue : string) = - sizeCell.textContent = readTableRow(row)["size"].getInt.formatByteSize + sizeCell.textContent = readTableRow(arch, row)["size"].getInt.formatByteSize "td": "div": @@ -221,36 +225,46 @@ proc newPkgTable(parent: Element, searchString : string) : PkgTable = data.add(version) let vs = versions let change_callback = proc(newValue : string) = - archCell.removeChildren() + fileNameCell.removeChildren() var newdata = newSeq[string]() for arch, pkgname in vs[newValue]: newdata.add(arch) - createDropdown(archCell, newdata, size_change_callback) + createDropdown(fileNameCell, newdata, size_change_callback) size_change_callback(newValue) createDropdown(elem, data, change_callback) "td": cb: - archCell = elem + fileNameCell = elem var data = newSeq[string]() - var arches : JsonNode - for v, a in versions: - arches = a + var files : JsonNode + for v, f in versions: + files = f break - for arch, pkgname in arches: - data.add(arch) + for file in files: + data.add(file["filename"].getStr) createDropdown(elem, data, size_change_callback) "td": cb: sizeCell = elem - for v, arches in versions: - for key, value in arches: - elem.textContent = value["size"].getInt.formatByteSize + for v, files in versions: + for file in files: + elem.textContent = file["size"].getInt.formatByteSize return cb: row = elem - + pkgtable.addButton.addEventListener("click", addButtonCallback) pkgtable +var selectedArch : string +var archButtons : Element +var table : Element +var searchString : string +var add2DownloadList : proc(e : Event) + +proc updateTable(e : Event) = + if pkgMap.len == 0 or searchString.len < 2: return + discard newPkgTable(table, selectedArch, searchString, add2DownloadList) + var dp : DownloadPanel # var pkgTable : PkgTable htmlTreeappend document.body: @@ -264,7 +278,6 @@ htmlTreeappend document.body: # "z-index" : "-10" # } "div": - var table : Element style = {"background-color" : "rgba(255, 255, 255, 0.25)"} classList = ["container"] "div": @@ -291,17 +304,15 @@ htmlTreeappend document.body: classList = ["form-control"] attrs = {"type" : "text"} cb : - proc add2DownloadList(e : Event) = + add2DownloadList = proc(e : Event) = let rows = table.querySelectorAll("tbody tr") for row in rows: let cbox = row.querySelector("td:first-child input") if cbox.checked: - dp.addPkg(readTableRow(row)["filename"].getStr) - proc oninput(e : Event) = - if pkgMap.len == 0 or elem.value.len < 2: return - let pkgtable = newPkgTable(table, $elem.value) - pkgtable.addButton.addEventListener("click", add2DownloadList) - elem.addEventListener("input", oninput) + dp.addPkg(readTableRow(selectedArch, row)["filename"].getStr) + oninput: + searchString = $elem.value + updateTable(event) "div": classList = ["row"] "div": @@ -309,6 +320,11 @@ htmlTreeappend document.body: cb: dp = newDownloadPanel(elem) "div": + "div": + classList = ["btn-group", "btn-group-justified"] + attrs = { "role" : "group" } + cb: + archButtons = elem classList = ["col-sm-9"] cb: table = elem @@ -317,7 +333,42 @@ htmlTreeappend document.body: let r = newXMLHTTPRequest() let load_cb = proc(e : Event) = - pkgMap = parseJson($r.responseText) + pkgMap = parseJson($r.responseText) + var buttons = newSeq[Element]() + var index = 0 + for arch in pkgMap.keys: + closureScope: + htmlTreeAppend(archButtons): + "div": + classList = ["btn-group"] + attrs = { "role" : "group"} + "button": + classList = ["btn", "btn-default"] + attrs = { + "type": "button", + } + text = arch + let thisButtonIndex = index + let thisButtonArch = arch + onclick: + let update = thisButtonArch != selectedArch + selectedArch = thisButtonArch + for i in 0.. getProperty = (String key, String defaultValue) -> { + key = "net.woggioni.jpacrepo." + key; + String result = System.getProperty(key); + if(result == null) { + result = properties.getProperty(key, defaultValue); + } + return result; + }; + return new AppConfig( + Path.of(getProperty.apply("RepoFolder", "/var/cache/pacman/pkg")), + InitialSchemaAction.from(getProperty.apply("InitialSchemaAction", "none")), + getProperty.apply("dataSourceJndi", "java:comp/DefaultDataSource")); + } +} diff --git a/src/main/java/net/woggioni/jpacrepo/factory/BeanFactory.java b/src/main/java/net/woggioni/jpacrepo/factory/BeanFactory.java new file mode 100644 index 0000000..d8e895f --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/factory/BeanFactory.java @@ -0,0 +1,23 @@ +package net.woggioni.jpacrepo.factory; + +import net.woggioni.jpacrepo.config.AppConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.InjectionPoint; + +public class BeanFactory { + + @Produces + public AppConfig newAppConfig() { + return AppConfig.newInstance( + System.getProperty("net.woggioni.jpacrepo.configuration.file", + "/etc/jpacrepo/server.properties")); + } + + @Produces + public Logger createLogger(InjectionPoint injectionPoint) { + return LoggerFactory.getLogger(injectionPoint.getMember().getDeclaringClass().getName()); + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jpacrepo/factory/PersistenceUnitFactory.java b/src/main/java/net/woggioni/jpacrepo/factory/PersistenceUnitFactory.java new file mode 100644 index 0000000..0191b6c --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/factory/PersistenceUnitFactory.java @@ -0,0 +1,22 @@ +package net.woggioni.jpacrepo.factory; + +import net.woggioni.jpacrepo.config.AppConfig; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import java.util.Properties; + +public class PersistenceUnitFactory { + + @Produces + @ApplicationScoped + private EntityManagerFactory createEntityManagerFactory(AppConfig appConfig) { + Properties properties = new Properties(); + properties.put("javax.persistence.schema-generation.database.action", + appConfig.getInitialSchemaAction().getValue()); + properties.put("javax.persistence.jtaDataSource", appConfig.getDataSourceJndi()); + return Persistence.createEntityManagerFactory("jpacrepo_pu", properties); + } +} diff --git a/src/main/java/net/woggioni/jpacrepo/persistence/InitialSchemaAction.java b/src/main/java/net/woggioni/jpacrepo/persistence/InitialSchemaAction.java new file mode 100644 index 0000000..5a5779f --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/persistence/InitialSchemaAction.java @@ -0,0 +1,45 @@ +package net.woggioni.jpacrepo.persistence; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import net.woggioni.jwo.JWO; + +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +@RequiredArgsConstructor +public enum InitialSchemaAction { + NONE("none"), CREATE("create"), DROP("drop"), DROP_AND_CREATE("drop-and-create"); + + @Getter + private final String value; + + + @Override + public String toString() { + return value; + } + + private static final Map valueMap; + + static { + Map result = new TreeMap<>(); + for(InitialSchemaAction initialSchemaAction : values()) { + result.put(initialSchemaAction.value, initialSchemaAction); + } + valueMap = Collections.unmodifiableMap(result); + } + + @SneakyThrows + public static InitialSchemaAction from(String s) { + InitialSchemaAction result = valueMap.get(s); + if(result == null) { + throw JWO.newThrowable(IllegalArgumentException.class, + "Unknown initial schema action '%s'", s); + } + return result; + } +} + diff --git a/src/main/java/net/woggioni/jpacrepo/persistence/QueryEngine.java b/src/main/java/net/woggioni/jpacrepo/persistence/QueryEngine.java index 2fa4695..d95a2cd 100644 --- a/src/main/java/net/woggioni/jpacrepo/persistence/QueryEngine.java +++ b/src/main/java/net/woggioni/jpacrepo/persistence/QueryEngine.java @@ -1,6 +1,7 @@ package net.woggioni.jpacrepo.persistence; -import net.woggioni.jpacrepo.pacbase.PkgData; + +import net.woggioni.jpacrepo.api.model.PkgData; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; @@ -11,66 +12,52 @@ import javax.persistence.criteria.Root; import java.util.ArrayList; import java.util.List; -/** - * Created by walter on 29/03/15. - */ public class QueryEngine { private String entityName; private String query; private List where; - public QueryEngine(Class cls) - { + public QueryEngine(Class cls) { query = String.format("SELECT e FROM %s e", cls.getSimpleName()); this.entityName = cls.getSimpleName(); where = new ArrayList<>(); } - public QueryEngine(String entityName) - { + public QueryEngine(String entityName) { query = String.format("SELECT e FROM %s e", entityName); where = new ArrayList<>(); } - public QueryEngine select(String... fields) - { + public QueryEngine select(String... fields) { String[] strarr = new String[fields.length]; - for (int i = 0; i < fields.length; i++) - { + for (int i = 0; i < fields.length; i++) { strarr[i] = "e." + fields[i]; } query = "SELECT " + String.join(",", strarr) + " FROM " + entityName + " e"; return this; } - public QueryEngine select() - { + public QueryEngine select() { query = String.format("SELECT e FROM %s e", entityName); return this; } - public QueryEngine where(String field, String operator, String value) - { + public QueryEngine where(String field, String operator, String value) { where.add(String.format("e.%s %s '%s'", field, operator, value)); return this; } - public String build() - { - if (where.isEmpty()) - { + public String build() { + if (where.isEmpty()) { return query; - } - else - { + } else { return query + " WHERE " + String.join(" AND ", where); } } public static List searchPackage( - EntityManager em, String name, String version, String arch, int pageNumber, int pageSize, String fileName) - { + EntityManager em, String name, String version, String arch, int pageNumber, int pageSize, String fileName) { CriteriaBuilder builder; CriteriaQuery criteriaQuery; Root entity; @@ -80,49 +67,39 @@ public class QueryEngine entity = criteriaQuery.from(PkgData.class); Predicate finalPredicate = null, p; - if (name != null && !name.isEmpty()) - { + if (name != null && !name.isEmpty()) { p = builder.equal(entity.get("name").get("id"), name); finalPredicate = p; } - if (version != null && !version.isEmpty()) - { + if (version != null && !version.isEmpty()) { p = builder.equal(entity.get("version"), version); finalPredicate = finalPredicate != null ? builder.and(finalPredicate, p) : p; } - if (arch != null && !arch.isEmpty()) - { + if (arch != null && !arch.isEmpty()) { p = builder.equal(entity.get("arch"), arch); finalPredicate = finalPredicate != null ? builder.and(finalPredicate, p) : p; } - if (fileName != null && !fileName.isEmpty()) - { + if (fileName != null && !fileName.isEmpty()) { p = builder.equal(entity.get("fileName"), fileName); finalPredicate = finalPredicate != null ? builder.and(finalPredicate, p) : p; } - if (finalPredicate != null) - { + if (finalPredicate != null) { criteriaQuery.select(entity).where(finalPredicate).orderBy(builder.asc(entity.get("fileName"))); - } - else - { + } else { criteriaQuery.select(entity).orderBy(builder.asc(entity.get("fileName"))); } TypedQuery query = em.createQuery(criteriaQuery); - if (pageNumber >= 0) - { + if (pageNumber >= 0) { query.setFirstResult(pageNumber * pageSize); } - if (pageSize > 0) - { + if (pageSize > 0) { query.setMaxResults(pageSize); } return query.getResultList(); } - public static long countResults(EntityManager em, String name, String version, String arch) - { + public static long countResults(EntityManager em, String name, String version, String arch) { CriteriaBuilder builder; CriteriaQuery criteriaQuery; Root entity; @@ -132,28 +109,22 @@ public class QueryEngine entity = criteriaQuery.from(PkgData.class); Predicate finalPredicate = null, p; - if (name != null && !name.isEmpty()) - { + if (name != null && !name.isEmpty()) { p = builder.equal(entity.get("name").get("id"), name); finalPredicate = p; } - if (version != null && !version.isEmpty()) - { + if (version != null && !version.isEmpty()) { p = builder.equal(entity.get("version"), version); finalPredicate = finalPredicate != null ? builder.and(finalPredicate, p) : p; } - if (arch != null && !arch.isEmpty()) - { + if (arch != null && !arch.isEmpty()) { p = builder.equal(entity.get("arch"), arch); finalPredicate = finalPredicate != null ? builder.and(finalPredicate, p) : p; } - if (finalPredicate != null) - { + if (finalPredicate != null) { criteriaQuery.select(builder.count(entity)).where(finalPredicate); - } - else - { + } else { criteriaQuery.select(builder.count(entity)); } return em.createQuery(criteriaQuery).getSingleResult(); diff --git a/src/main/java/net/woggioni/jpacrepo/service/PacmanServiceEJB.java b/src/main/java/net/woggioni/jpacrepo/service/PacmanServiceEJB.java new file mode 100644 index 0000000..17a2690 --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/service/PacmanServiceEJB.java @@ -0,0 +1,213 @@ +package net.woggioni.jpacrepo.service; + +import lombok.SneakyThrows; +import net.woggioni.jpacrepo.api.model.PkgData; +import net.woggioni.jpacrepo.api.service.PacmanServiceLocal; +import net.woggioni.jpacrepo.api.service.PacmanServiceRemote; +import net.woggioni.jpacrepo.config.AppConfig; +import net.woggioni.jpacrepo.impl.model.CompressionFormatImpl; +import net.woggioni.jpacrepo.impl.model.PkgDataImpl; +import net.woggioni.jpacrepo.persistence.QueryEngine; +import net.woggioni.jwo.Con; +import net.woggioni.jwo.JWO; +import net.woggioni.jwo.Sup; +import org.slf4j.Logger; + +import javax.annotation.Resource; +import javax.ejb.Asynchronous; +import javax.ejb.ConcurrencyManagement; +import javax.ejb.ConcurrencyManagementType; +import javax.ejb.Local; +import javax.ejb.Lock; +import javax.ejb.LockType; +import javax.ejb.Remote; +import javax.ejb.Schedule; +import javax.ejb.Singleton; +import javax.ejb.Startup; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.ejb.TransactionManagement; +import javax.ejb.TransactionManagementType; +import javax.enterprise.concurrent.ManagedExecutorService; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.TypedQuery; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.OffsetDateTime; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Startup +@Singleton +@Lock(LockType.READ) +@TransactionManagement(TransactionManagementType.CONTAINER) +@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) +@Local({PacmanServiceLocal.class}) +@Remote({PacmanServiceRemote.class}) +public class PacmanServiceEJB implements PacmanServiceLocal { + @Inject + private EntityManagerFactory emf; + + @Inject + private AppConfig ctx; + + @Inject + private Logger logger; + + @Resource(name = "DefaultManagedExecutorService") + private ManagedExecutorService executor; + + private final static String nameQuery = "SELECT pname FROM PkgName pname WHERE id = :name"; + private final static String hashQuery = "SELECT pdata FROM PkgData pdata WHERE md5sum = :md5sum"; + private final static String hashQueryCount = "SELECT count(pdata) FROM PkgData pdata WHERE md5sum = :md5sum"; + + private void deletePkgData(EntityManager em, PkgData pkgData) { + em.remove(pkgData); + } + + @Override + @SneakyThrows + @Asynchronous + @TransactionAttribute(TransactionAttributeType.REQUIRED) + @Schedule(hour = "4", minute = "00", persistent = false) + public void syncDB() { + EntityManager em = emf.createEntityManager(); + try { + logger.info("Starting repository cleanup"); + //Removes from DB the packages that have been deleted from filesystem + logger.info("Searching for packages that are no more in the filesystem"); + List resultList = em.createQuery("SELECT p.fileName FROM PkgData p", String.class) + .getResultList(); + logger.info("Got list of filenames from db"); + Set knownPkg = resultList.stream().filter(fileName -> { + Path file = ctx.getFile(fileName); + boolean result = Files.exists(file); + if (!result) { + logger.info("Removing package {} which was not found in filesystem", file.getFileName()); + em.createQuery("SELECT p FROM PkgData p WHERE p.fileName = :fileName", PkgData.class) + .setParameter("fileName", file.getFileName().toString()) + .getResultList().forEach(pkgData -> deletePkgData(em, pkgData)); + } + return result; + }).collect(Collectors.toUnmodifiableSet()); + logger.info("Searching for new packages or packages that were modified after being added to the database"); + CompletionService completionService = new ExecutorCompletionService<>(executor); + final Set> inProgress = new HashSet<>(); + final int maxInProgress = Runtime.getRuntime().availableProcessors() * 5; + Con persistPackages = (Boolean drain) -> { + while ((drain && inProgress.size() > 0) || inProgress.size() > maxInProgress) { + Future future = completionService.poll(1, TimeUnit.SECONDS); + inProgress.remove(future); + PkgData pkgData; + try { + pkgData = future.get(); + } catch (ExecutionException ee) { + throw ee.getCause(); + } + persistPackage(em, pkgData); + } + }; + Files.list(ctx.getRepoFolder()).filter((Path file) -> { + String name = file.getFileName().toString(); + return name.endsWith(".pkg.tar.xz") || name.endsWith(".pkg.tar.zst") || name.endsWith(".pkg.tar.gz"); + }).forEach((Con) file -> { + if (!knownPkg.contains(file.getFileName().toString()) || ((Sup) () -> { + TypedQuery query = em.createQuery("SELECT p.updTimestamp FROM PkgData p WHERE filename = :filename", Date.class); + query.setParameter("filename", file.getFileName().toString()); + Date result = query.getSingleResult(); + return Files.getLastModifiedTime(file).toMillis() > result.getTime(); + }).get()) { + inProgress.add(completionService.submit(() -> { + try { + return PkgDataImpl.parseFile(file, CompressionFormatImpl.guess(file)); + } catch (Exception ex) { + logger.error(String.format("Error parsing '%s'", file.toAbsolutePath()), ex); + throw ex; + } + })); + } + persistPackages.accept(false); + }); + persistPackages.accept(true); + logger.info("Removing obsolete packages"); + deleteOld(em); + logger.info("Repository cleanup completed successfully"); + ctx.getInvalidateCache().set(true); + } finally { + em.close(); + } + } + + private void persistPackage(EntityManager em, PkgData pkgData) { + TypedQuery hquery = em.createQuery(hashQueryCount, Long.class); + hquery.setParameter("md5sum", pkgData.getMd5sum()); + if (hquery.getSingleResult() == 0) { + TypedQuery fquery = + em.createQuery("SELECT p FROM PkgData p WHERE p.fileName = :fileName", PkgData.class); + fquery.setParameter("fileName", pkgData.getFileName()); + fquery.getResultList().forEach(p -> deletePkgData(em, p)); + em.persist(pkgData); + logger.info("Persisting package {}", pkgData.getFileName()); + } + } + + + @Override + @TransactionAttribute(TransactionAttributeType.REQUIRED) + public void deletePackage(String filename) { + EntityManager em = emf.createEntityManager(); + deletePackage(em, filename); + logger.info("Package {} has been deleted", filename); + } + + @SneakyThrows + private void deletePackage(EntityManager em, String filename) { + TypedQuery fquery = em.createQuery("SELECT p FROM PkgData p WHERE fileName = :fileName", PkgData.class); + fquery.setParameter("fileName", filename); + List savedFiles = fquery.getResultList(); + if (savedFiles.size() == 0) { + throw JWO.newThrowable(IllegalArgumentException.class, "Package with name %s not found", filename); + } + PkgData pkg = fquery.getResultList().get(0); + Files.delete(ctx.getFile(pkg)); + em.remove(pkg); + } + + static private final String deleteQuery = + "SELECT p.fileName FROM PkgData p WHERE p.buildDate < :cutoff and p.id.name in \n" + "(SELECT p2.id.name FROM PkgData p2 GROUP BY p2.id.name HAVING count(p2.id.name) > :minVersions\n)"; + + private void deleteOld(EntityManager em) { + TypedQuery query = em.createQuery(deleteQuery, String.class); + Calendar cutoff = Calendar.getInstance(); + cutoff.add(Calendar.YEAR, -2); + query.setParameter("cutoff", OffsetDateTime.now()); + query.setParameter("minVersions", 2L); + List list = query.getResultList(); + list.forEach(this::deletePackage); + } + + @Override + @TransactionAttribute(TransactionAttributeType.SUPPORTS) + public long countResults(String name, String version, String arch) { + EntityManager em = emf.createEntityManager(); + return QueryEngine.countResults(em, name, version, arch); + } + + @Override + @TransactionAttribute(TransactionAttributeType.SUPPORTS) + public List searchPackage(String name, String version, String arch, int pageNumber, int pageSize, String fileName) { + EntityManager em = emf.createEntityManager(); + return QueryEngine.searchPackage(em, name, version, arch, pageNumber, pageSize, null); + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jpacrepo/service/PacmanWebService.java b/src/main/java/net/woggioni/jpacrepo/service/PacmanWebService.java new file mode 100644 index 0000000..20f733f --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/service/PacmanWebService.java @@ -0,0 +1,427 @@ +package net.woggioni.jpacrepo.service; + +import lombok.SneakyThrows; +import lombok.val; +import net.woggioni.jpacrepo.api.model.PkgData; +import net.woggioni.jpacrepo.api.model.PkgId; +import net.woggioni.jpacrepo.api.service.PacmanServiceLocal; +import net.woggioni.jpacrepo.config.AppConfig; +import net.woggioni.jpacrepo.impl.model.CompressionFormatImpl; +import net.woggioni.jpacrepo.impl.model.PkgDataImpl; +import net.woggioni.jpacrepo.service.wire.PkgDataList; +import net.woggioni.jpacrepo.service.wire.PkgTuple; +import net.woggioni.jpacrepo.service.wire.StringList; +import net.woggioni.jpacrepo.version.PkgIdComparator; +import net.woggioni.jpacrepo.version.VersionComparator; +import net.woggioni.jwo.CollectionUtils; +import net.woggioni.jwo.Con; +import net.woggioni.jwo.JWO; +import net.woggioni.jwo.Tuple2; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.slf4j.Logger; + +import javax.ejb.ConcurrencyManagement; +import javax.ejb.ConcurrencyManagementType; +import javax.ejb.Singleton; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.ejb.TransactionManagement; +import javax.ejb.TransactionManagementType; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.NoResultException; +import javax.persistence.NonUniqueResultException; +import javax.persistence.TypedQuery; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.EntityTag; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import javax.ws.rs.core.UriInfo; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +@Singleton +@Path("/pkg") +@ConcurrencyManagement(ConcurrencyManagementType.BEAN) +@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) +@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) +@TransactionManagement(TransactionManagementType.CONTAINER) +public class PacmanWebService { + + private NavigableMap cachedMap; + + @Inject + private EntityManagerFactory emf; + + @Inject + private Logger log; + + @Inject + private PacmanServiceLocal service; + + @Inject + private AppConfig ctx; + + private NavigableMap getCachedMap() { + NavigableMap result = null; + if (!ctx.getInvalidateCache().get()) { + result = cachedMap; + } + if (result == null) { + synchronized(this) { + result = cachedMap; + if (result == null) { + EntityManager em = emf.createEntityManager(); + TypedQuery query = em.createQuery( + "SELECT pkg.id.name, pkg.id.version, pkg.id.arch, pkg.fileName, pkg.size, pkg.md5sum " + + "FROM PkgData pkg ORDER BY pkg.id.name, pkg.id.version, pkg.id.arch", + Object[].class); + cachedMap = query.getResultStream() + .map((Object[] pkg) -> { + String name = (String) pkg[0]; + String version = (String) pkg[1]; + String arch = (String) pkg[2]; + String filename = (String) pkg[3]; + long size = (long) pkg[4]; + String md5sum = (String) pkg[5]; + PkgTuple tuple = new PkgTuple(); + tuple.setFilename(filename); + tuple.setSize(size); + tuple.setMd5sum(md5sum); + PkgId id = new PkgId(); + id.setName(name); + id.setVersion(version); + id.setArch(arch); + return Tuple2.newInstance(id, tuple); + }).collect( + CollectionUtils.toUnmodifiableTreeMap( + Tuple2::get_1, + Tuple2::get_2, + PkgIdComparator.getComparator()) + ); + ctx.getInvalidateCache().set(false); + result = cachedMap; + } + } + } + return result; + } + + private Response manageQueryResult(List list) { + return manageQueryResult(list, false); + } + + private Response manageQueryResult(List list, boolean singleResult) { + if (list.isEmpty()) throw new NotFoundException(); + else if (singleResult) { + if (list.size() == 1) return Response.ok(list.get(0)).build(); + else throw new NonUniqueResultException("The returned list does not contain a single element"); + } else { + return Response.ok(new PkgDataList(list)).build(); + } + } + + @GET + @Path("searchByName/{name}") + public Response searchByName(@PathParam("name") String name) { + val em = emf.createEntityManager(); + if (name == null) throw new WebApplicationException(Response.Status.BAD_REQUEST); + String query = String.format("SELECT pkgId.name FROM PkgId pkgId WHERE LOWER(pkgId.name) LIKE '%%%s%%' ORDER BY pkgId.name", name); + return Response.ok(em.createQuery(query, String.class).getResultList()).build(); + } + + @GET + @Path("searchByHash/{md5sum}") + public Response searchByHash(@PathParam("md5sum") String md5sum) { + return getPackageByHash(md5sum); + } + + @GET + @Path("list/{name}") + public Response getPackage(@PathParam("name") String name) { + EntityManager em = emf.createEntityManager(); + TypedQuery query = em.createQuery("SELECT pkg.id.version FROM PkgData pkg WHERE pkg.id.name = :name ORDER BY pkg.id.version", String.class); + query.setParameter("name", name); + return Response.ok(new StringList(query.getResultList())).build(); + } + + @GET + @Path("list/{name}/{version}") + public Response getPackage(@PathParam("name") String name, @PathParam("version") String version) { + EntityManager em = emf.createEntityManager(); + TypedQuery query = em.createQuery("SELECT pkg.arch FROM PkgData pkg WHERE pkg.id.name = :name AND pkg.id.version = :version ORDER BY pkg.id.arch", String.class); + query.setParameter("name", name); + query.setParameter("version", version); + return Response.ok(new StringList(query.getResultList())).build(); + } + + @GET + @Path("list/{name}/{version}/{arch}") + public Response getPackage(@PathParam("name") String name, @PathParam("version") String version, @PathParam("arch") String arch) { + EntityManager em = emf.createEntityManager(); + TypedQuery query = em.createQuery("SELECT pkg FROM PkgData pkg WHERE " + "pkg.id.name = :name AND " + "pkg.id.version = :version AND " + "pkg.id.arch = :arch " + "ORDER BY pkg.arch", PkgData.class); + query.setParameter("name", name); + query.setParameter("version", version); + query.setParameter("arch", arch); + return Response.ok(query.getSingleResult()).build(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("map") + public Response getPackageMap(@Context Request request) { + CacheControl cc = new CacheControl(); + cc.setMaxAge(86400); + cc.setMustRevalidate(true); + cc.setNoCache(true); + + NavigableMap cachedMap = getCachedMap(); + EntityTag etag = new EntityTag(Integer.toString(cachedMap.hashCode()), false); + Response.ResponseBuilder builder = request.evaluatePreconditions(etag); + if (builder == null) { + TreeMap>>> result = cachedMap.entrySet().stream().collect( + Collectors.groupingBy((Map.Entry entry) -> entry.getKey().getArch(), + TreeMap::new, + Collectors.groupingBy((Map.Entry entry) -> entry.getKey().getName(), + TreeMap::new, + Collectors.groupingBy((Map.Entry entry) -> entry.getKey().getVersion(), + () -> new TreeMap<>(VersionComparator.getInstance().reversed()), + Collectors.mapping( + Map.Entry::getValue, + CollectionUtils.toUnmodifiableTreeSet( + Comparator.comparing(PkgTuple::getFilename) + ) + ) + ) + ) + ) + ); + builder = Response.ok(result); + builder.tag(etag); + } + builder.cacheControl(cc); + return builder.build(); + } + + @GET + @Path("hashes") + public Response getHashes() { + EntityManager em = emf.createEntityManager(); + TypedQuery query = em.createQuery("SELECT p.md5sum FROM PkgData p", String.class); + return Response.ok(new StringList(query.getResultList())).build(); + } + + @GET + @Path("files") + public Response getFiles() { + EntityManager em = emf.createEntityManager(); + TypedQuery query = em.createQuery("SELECT p.fileName FROM PkgData p", String.class); + return Response.ok(new StringList(query.getResultList())).build(); + } + + private Response getPackageByHash(String md5sum) { + EntityManager em = emf.createEntityManager(); + TypedQuery hquery = em.createNamedQuery("searchByHash", PkgData.class); + if (md5sum != null) hquery.setParameter("md5sum", md5sum); + return manageQueryResult(hquery.getResultList(), true); + } + + private Response getPackageByFileName(String file) { + EntityManager em = emf.createEntityManager(); + TypedQuery fnquery = em.createNamedQuery("searchByFileName", PkgData.class); + fnquery.setParameter("fileName", file); + return manageQueryResult(fnquery.getResultList(), true); + } + + @GET + @Path("filesize/{filename}") + @SneakyThrows + public Response getFileSize(@PathParam("filename")String fileName, @Context Request request) { + CacheControl cc = new CacheControl(); + cc.setMaxAge(86400); + cc.setMustRevalidate(true); + cc.setNoCache(true); + EntityTag etag = new EntityTag(Integer.toString(getCachedMap().hashCode())); + Response.ResponseBuilder builder = request.evaluatePreconditions(etag); + if (builder == null) { + java.nio.file.Path res = ctx.getFile(fileName); + if (!Files.exists(res)) throw new NotFoundException(String.format("File '%s' was not found", fileName)); + builder = Response.ok(Files.size(res)); + builder.tag(etag); + } + builder.cacheControl(cc); + return builder.build(); + } + + @SneakyThrows + @GET + @Path("download/{filename}") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + public Response downloadPackage(@PathParam("filename") String fileName) { + EntityManager em = emf.createEntityManager(); + TypedQuery fnquery = em.createNamedQuery("searchByFileName", PkgData.class); + fnquery.setParameter("fileName", fileName); + try { + PkgData pkg = fnquery.getSingleResult(); + StreamingOutput stream = (OutputStream output) -> { + try(InputStream is = Files.newInputStream(ctx.getFile(pkg))) { + JWO.copy(is, output, 0x10000); + } + }; + return Response.ok(stream).header("Content-Length", Files.size(ctx.getFile(pkg))).build(); + } catch(NoResultException nre) { + throw new NotFoundException(); + } + } + + @POST + @Path("/doYouWantAny") + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + public Response doYouWantAny(List filenames) { + EntityManager em = emf.createEntityManager(); + Set result = filenames.stream().collect(CollectionUtils.toTreeSet()); + if (!result.isEmpty()) { + TypedQuery query = em.createQuery("SELECT pkg.fileName from PkgData pkg WHERE pkg.fileName in :filenames", String.class); + query.setParameter("filenames", filenames); + Set toBeRemoved = query.getResultStream().collect(CollectionUtils.toTreeSet()); + result.removeAll(toBeRemoved); + return Response.ok(new StringList(result)).build(); + } + else { + return Response.ok(result.toArray()).build(); + } + } + + @POST + @Path("/upload") + @SneakyThrows + @TransactionAttribute(TransactionAttributeType.REQUIRED) + @Consumes({"application/x-xz", "application/gzip", "application/x-tar", MediaType.APPLICATION_OCTET_STREAM}) + public Response createPackage( + InputStream input, + @MatrixParam("filename") String filename, + @Context UriInfo uriInfo) { + EntityManager em = emf.createEntityManager(); + if (filename == null) throw new BadRequestException(); + java.nio.file.Path file = Files.createTempFile(ctx.getRepoFolder(), filename, null); + TypedQuery fquery = em.createNamedQuery("searchByFileName", PkgData.class); + fquery.setParameter("fileName", filename); + List savedFiles = fquery.getResultList(); + Response result; + if (savedFiles.size() > 0) result = Response.notModified().build(); + else { + try(OutputStream output = Files.newOutputStream(file)) { + JWO.copy(input, output, 0x10000); + PkgData pkg = PkgDataImpl.parseFile(file, + CompressionFormatImpl.guess(Paths.get(filename))); + pkg.setFileName(filename); + Optional.ofNullable(em.find(PkgData.class, pkg.getId())).ifPresent((Con) (pkgData -> { + em.remove(pkgData); + Files.delete(ctx.getRepoFolder().resolve(pkgData.getFileName())); + })); + log.info("Persisting package {}", pkg.getFileName()); + em.persist(pkg); + URI pkgUri = uriInfo.getAbsolutePathBuilder().path(pkg.getFileName()).build(); + Files.move(file, ctx.getRepoFolder().resolve(filename), StandardCopyOption.ATOMIC_MOVE); + ctx.getInvalidateCache().set(true); + cachedMap = null; + result = Response.created(pkgUri).build(); + } catch (Throwable t) { + Files.delete(file); + throw t; + } + } + return result; + } + + @GET + @Path("/search") + public List searchPackage( + @QueryParam("name") String name, + @QueryParam("version") String version, + @QueryParam("arch") String arch, + @QueryParam("page") int pageNumber, + @QueryParam("pageSize") int pageSize, + @QueryParam("fileName") String fileName) { + return service.searchPackage(name, version, arch, pageNumber, pageSize, fileName); + } + + @OPTIONS + @Path("/downloadTar") + @Produces("text/plain; charset=UTF-8") + public Response options() { + return Response.ok("POST, OPTIONS").build(); + } + + @POST + @Path("/downloadTar") + @Produces("application/x-tar") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response downloadTar(@FormParam("pkgs") String formData) { + String[] files = URLDecoder.decode(formData, StandardCharsets.UTF_8).split(" "); + Arrays.stream(files) + .filter(fileName -> !Files.exists(ctx.getFile(fileName))) + .forEach(fileName -> { + throw JWO.newThrowable(NotFoundException.class, "Package file '{}' does not exist", fileName); + }); + StreamingOutput stream = new StreamingOutput() { + private final byte[] buffer = new byte[0x10000]; + + @Override + @SneakyThrows + public void write(OutputStream output) { + try(TarArchiveOutputStream taos = new TarArchiveOutputStream(output)) { + for (String fname : files) { + log.info(fname); + java.nio.file.Path file = ctx.getFile(fname); + try(InputStream input = Files.newInputStream(file)) { + TarArchiveEntry entry = new TarArchiveEntry(fname); + entry.setSize(Files.size(file)); + taos.putArchiveEntry(entry); + JWO.copy(input, taos, buffer); + taos.closeArchiveEntry(); + } + } + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + throw ex; + } + } + }; + return Response.ok(stream).header("Content-Disposition", "attachment; filename=pkgs.tar").build(); + } +} diff --git a/src/main/java/net/woggioni/jpacrepo/service/WxRsApplication.java b/src/main/java/net/woggioni/jpacrepo/service/WxRsApplication.java new file mode 100644 index 0000000..c4bec17 --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/service/WxRsApplication.java @@ -0,0 +1,17 @@ +package net.woggioni.jpacrepo.service; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; +import java.util.HashSet; +import java.util.Set; + +@ApplicationPath("api") +public class WxRsApplication extends Application { + + @Override + public Set> getClasses() { + Set> result = new HashSet<>(); + result.add(PacmanWebService.class); + return result; + } +} diff --git a/src/main/java/net/woggioni/jpacrepo/service/wire/PkgDataList.java b/src/main/java/net/woggioni/jpacrepo/service/wire/PkgDataList.java new file mode 100644 index 0000000..caf36b5 --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/service/wire/PkgDataList.java @@ -0,0 +1,31 @@ +package net.woggioni.jpacrepo.service.wire; + + +import net.woggioni.jpacrepo.api.model.PkgData; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; + +@XmlRootElement +public class PkgDataList extends ArrayList { + + public PkgDataList(List l) { + for (PkgData el : l) add(el); + } + + public PkgDataList(PkgData... elements) { + for (PkgData el : elements) add(el); + } + + @XmlElement(name = "pkgData") + List getItems() { + return this; + } + + void setItems(List pkgs) { + this.clear(); + this.addAll(pkgs); + } +} diff --git a/src/main/java/net/woggioni/jpacrepo/service/wire/PkgTuple.java b/src/main/java/net/woggioni/jpacrepo/service/wire/PkgTuple.java new file mode 100644 index 0000000..6666086 --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/service/wire/PkgTuple.java @@ -0,0 +1,15 @@ +package net.woggioni.jpacrepo.service.wire; + +import lombok.Data; + +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@XmlRootElement +public class PkgTuple { + String md5sum; + + String filename; + + long size; +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jpacrepo/service/wire/StringList.java b/src/main/java/net/woggioni/jpacrepo/service/wire/StringList.java new file mode 100644 index 0000000..c4965b4 --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/service/wire/StringList.java @@ -0,0 +1,28 @@ +package net.woggioni.jpacrepo.service.wire; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; + +@XmlRootElement +public class StringList extends ArrayList { + + public StringList(Iterable l) { + for (String el : l) add(el); + } + + public StringList(String... strings) { + for (String el : strings) add(el); + } + + @XmlElement(name = "pkgData") + List getItems() { + return this; + } + + void setItems(List strings) { + this.clear(); + this.addAll(strings); + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jpacrepo/servlet/AbstractFileServlet.java b/src/main/java/net/woggioni/jpacrepo/servlet/AbstractFileServlet.java index f9d83b9..c315d84 100644 --- a/src/main/java/net/woggioni/jpacrepo/servlet/AbstractFileServlet.java +++ b/src/main/java/net/woggioni/jpacrepo/servlet/AbstractFileServlet.java @@ -1,12 +1,16 @@ package net.woggioni.jpacrepo.servlet; -import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.channels.Channels; @@ -97,8 +101,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; * @author Bauke Scholtz * @since 2.2 */ -public abstract class AbstractFileServlet extends HttpServlet -{ +public abstract class AbstractFileServlet extends HttpServlet { // Constants ------------------------------------------------------------------------------------------------------ @@ -113,16 +116,13 @@ public abstract class AbstractFileServlet extends HttpServlet private static final String CONTENT_DISPOSITION_HEADER = "%s;filename=\"%2$s\"; filename*=UTF-8''%2$s"; private static final String MULTIPART_BOUNDARY = UUID.randomUUID().toString(); - private static long stream(InputStream input, OutputStream output) throws IOException - { + private static long stream(InputStream input, OutputStream output) throws IOException { try (ReadableByteChannel inputChannel = Channels.newChannel(input); - WritableByteChannel outputChannel = Channels.newChannel(output)) - { + WritableByteChannel outputChannel = Channels.newChannel(output)) { ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_STREAM_BUFFER_SIZE); long size = 0; - while (inputChannel.read(buffer) != -1) - { + while (inputChannel.read(buffer) != -1) { buffer.flip(); size += outputChannel.write(buffer); buffer.clear(); @@ -145,32 +145,26 @@ public abstract class AbstractFileServlet extends HttpServlet * @throws IOException When an I/O error occurs. * @since 2.2 */ - public static long stream(File file, OutputStream output, long start, long length) throws IOException - { - if (start == 0 && length >= file.length()) - { + public static long stream(File file, OutputStream output, long start, long length) throws IOException { + if (start == 0 && length >= file.length()) { return stream(new FileInputStream(file), output); } - try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(file.toPath(), StandardOpenOption.READ)) - { + try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(file.toPath(), StandardOpenOption.READ)) { WritableByteChannel outputChannel = Channels.newChannel(output); ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_STREAM_BUFFER_SIZE); long size = 0; - while (fileChannel.read(buffer, start + size) != -1) - { + while (fileChannel.read(buffer, start + size) != -1) { buffer.flip(); - if (size + buffer.limit() > length) - { + if (size + buffer.limit() > length) { buffer.limit((int) (length - size)); } size += outputChannel.write(buffer); - if (size >= length) - { + if (size >= length) { break; } @@ -182,27 +176,20 @@ public abstract class AbstractFileServlet extends HttpServlet } - private static String encodeURL(String string) - { - if (string == null) - { + private static String encodeURL(String string) { + if (string == null) { return null; } - try - { + try { return URLEncoder.encode(string, UTF_8.name()); - } - catch (UnsupportedEncodingException e) - { + } catch (UnsupportedEncodingException e) { throw new UnsupportedOperationException(ERROR_UNSUPPORTED_ENCODING, e); } } - private static String encodeURI(String string) - { - if (string == null) - { + private static String encodeURI(String string) { + if (string == null) { return null; } @@ -218,74 +205,60 @@ public abstract class AbstractFileServlet extends HttpServlet // Actions -------------------------------------------------------------------------------------------------------- @Override - protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException - { + protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doRequest(request, response, true); } @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException - { + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doRequest(request, response, false); } - private void doRequest(HttpServletRequest request, HttpServletResponse response, boolean head) throws IOException - { + private void doRequest(HttpServletRequest request, HttpServletResponse response, boolean head) throws IOException { response.reset(); Resource resource; - try - { + try { resource = new Resource(getFile(request)); - } - catch (IllegalArgumentException e) - { + } catch (IllegalArgumentException e) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } - if (resource.file == null) - { + if (resource.file == null) { handleFileNotFound(request, response); return; } - if (preconditionFailed(request, resource)) - { + if (preconditionFailed(request, resource)) { response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return; } setCacheHeaders(response, resource, getExpireTime(request, resource.file)); - if (notModified(request, resource)) - { + if (notModified(request, resource)) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } List ranges = getRanges(request, resource); - if (ranges == null) - { + if (ranges == null) { response.setHeader("Content-Range", "bytes */" + resource.length); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } - if (!ranges.isEmpty()) - { + if (!ranges.isEmpty()) { response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); - } - else - { + } else { ranges.add(new Range(0, resource.length - 1)); // Full content. } String contentType = setContentHeaders(request, response, resource, ranges); - if (head) - { + if (head) { return; } @@ -315,8 +288,7 @@ public abstract class AbstractFileServlet extends HttpServlet * @throws IOException When something fails at I/O level. * @since 2.3 */ - protected void handleFileNotFound(HttpServletRequest request, HttpServletResponse response) throws IOException - { + protected void handleFileNotFound(HttpServletRequest request, HttpServletResponse response) throws IOException { response.sendError(HttpServletResponse.SC_NOT_FOUND); } @@ -329,8 +301,7 @@ public abstract class AbstractFileServlet extends HttpServlet * @param file The involved file. * @return The client cache expire time in seconds (not milliseconds!). */ - protected long getExpireTime(HttpServletRequest request, File file) - { + protected long getExpireTime(HttpServletRequest request, File file) { return DEFAULT_EXPIRE_TIME_IN_SECONDS; } @@ -344,15 +315,11 @@ public abstract class AbstractFileServlet extends HttpServlet * @param file The involved file. * @return The content type associated with the given HTTP servlet request and file. */ - protected String getContentType(HttpServletRequest request, File file) - { + protected String getContentType(HttpServletRequest request, File file) { String type = request.getServletContext().getMimeType(file.getName()); - if (type != null) - { + if (type != null) { return type; - } - else - { + } else { return "application/octet-stream"; } } @@ -370,8 +337,7 @@ public abstract class AbstractFileServlet extends HttpServlet * @return true if we must force a "Save As" dialog based on the given HTTP servlet request and content * type. */ - protected boolean isAttachment(HttpServletRequest request, String contentType) - { + protected boolean isAttachment(HttpServletRequest request, String contentType) { String accept = request.getHeader("Accept"); return (!contentType.startsWith("text") && !contentType.startsWith("image")) && (accept == null || !accepts(accept, contentType)); } @@ -387,8 +353,7 @@ public abstract class AbstractFileServlet extends HttpServlet * @return The file name to be used in Content-Disposition header. * @since 2.3 */ - protected String getAttachmentName(HttpServletRequest request, File file) - { + protected String getAttachmentName(HttpServletRequest request, File file) { return file.getName(); } @@ -397,8 +362,7 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Returns true if it's a conditional request which must return 412. */ - private boolean preconditionFailed(HttpServletRequest request, Resource resource) - { + private boolean preconditionFailed(HttpServletRequest request, Resource resource) { String match = request.getHeader("If-Match"); long unmodified = request.getDateHeader("If-Unmodified-Since"); return (match != null) ? !matches(match, resource.eTag) : (unmodified != -1 && modified(unmodified, resource.lastModified)); @@ -407,8 +371,7 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Set cache headers. */ - private void setCacheHeaders(HttpServletResponse response, Resource resource, long expires) - { + private void setCacheHeaders(HttpServletResponse response, Resource resource, long expires) { //Servlets.setCacheHeaders(response, expires); response.setHeader("ETag", resource.eTag); response.setDateHeader("Last-Modified", resource.lastModified); @@ -417,8 +380,7 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Returns true if it's a conditional request which must return 304. */ - private boolean notModified(HttpServletRequest request, Resource resource) - { + private boolean notModified(HttpServletRequest request, Resource resource) { String noMatch = request.getHeader("If-None-Match"); long modified = request.getDateHeader("If-Modified-Since"); return (noMatch != null) ? matches(noMatch, resource.eTag) : (modified != -1 && !modified(modified, resource.lastModified)); @@ -427,45 +389,34 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Get requested ranges. If this is null, then we must return 416. If this is empty, then we must return full file. */ - private List getRanges(HttpServletRequest request, Resource resource) - { + private List getRanges(HttpServletRequest request, Resource resource) { List ranges = new ArrayList<>(1); String rangeHeader = request.getHeader("Range"); - if (rangeHeader == null) - { + if (rangeHeader == null) { return ranges; - } - else if (!RANGE_PATTERN.matcher(rangeHeader).matches()) - { + } else if (!RANGE_PATTERN.matcher(rangeHeader).matches()) { return null; // Syntax error. } String ifRange = request.getHeader("If-Range"); - if (ifRange != null && !ifRange.equals(resource.eTag)) - { - try - { + if (ifRange != null && !ifRange.equals(resource.eTag)) { + try { long ifRangeTime = request.getDateHeader("If-Range"); - if (ifRangeTime != -1 && modified(ifRangeTime, resource.lastModified)) - { + if (ifRangeTime != -1 && modified(ifRangeTime, resource.lastModified)) { return ranges; } - } - catch (IllegalArgumentException ifRangeHeaderIsInvalid) - { + } catch (IllegalArgumentException ifRangeHeaderIsInvalid) { return ranges; } } - for (String rangeHeaderPart : rangeHeader.split("=")[1].split(",")) - { + for (String rangeHeaderPart : rangeHeader.split("=")[1].split(",")) { Range range = parseRange(rangeHeaderPart, resource.length); - if (range == null) - { + if (range == null) { return null; // Logic error. } @@ -478,23 +429,18 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Parse range header part. Returns null if there's a logic error (i.e. start after end). */ - private Range parseRange(String range, long length) - { + private Range parseRange(String range, long length) { long start = sublong(range, 0, range.indexOf('-')); long end = sublong(range, range.indexOf('-') + 1, range.length()); - if (start == -1) - { + if (start == -1) { start = length - end; end = length - 1; - } - else if (end == -1 || end > length - 1) - { + } else if (end == -1 || end > length - 1) { end = length - 1; } - if (start > end) - { + if (start > end) { return null; // Logic error. } @@ -504,27 +450,22 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Set content headers. */ - private String setContentHeaders(HttpServletRequest request, HttpServletResponse response, Resource resource, List ranges) - { + private String setContentHeaders(HttpServletRequest request, HttpServletResponse response, Resource resource, List ranges) { String contentType = getContentType(request, resource.file); String disposition = isAttachment(request, contentType) ? "attachment" : "inline"; String filename = encodeURI(getAttachmentName(request, resource.file)); response.setHeader("Content-Disposition", String.format(CONTENT_DISPOSITION_HEADER, disposition, filename)); response.setHeader("Accept-Ranges", "bytes"); - if (ranges.size() == 1) - { + if (ranges.size() == 1) { Range range = ranges.get(0); response.setContentType(contentType); response.setHeader("Content-Length", String.valueOf(range.length)); - if (response.getStatus() == HttpServletResponse.SC_PARTIAL_CONTENT) - { + if (response.getStatus() == HttpServletResponse.SC_PARTIAL_CONTENT) { response.setHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + resource.length); } - } - else - { + } else { response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY); } @@ -534,19 +475,14 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Write given file to response with given content type and ranges. */ - private void writeContent(HttpServletResponse response, Resource resource, List ranges, String contentType) throws IOException - { + private void writeContent(HttpServletResponse response, Resource resource, List ranges, String contentType) throws IOException { ServletOutputStream output = response.getOutputStream(); - if (ranges.size() == 1) - { + if (ranges.size() == 1) { Range range = ranges.get(0); stream(resource.file, output, range.start, range.length); - } - else - { - for (Range range : ranges) - { + } else { + for (Range range : ranges) { output.println(); output.println("--" + MULTIPART_BOUNDARY); output.println("Content-Type: " + contentType); @@ -564,8 +500,7 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Returns true if the given match header matches the given ETag value. */ - private static boolean matches(String matchHeader, String eTag) - { + private static boolean matches(String matchHeader, String eTag) { String[] matchValues = matchHeader.split("\\s*,\\s*"); Arrays.sort(matchValues); return Arrays.binarySearch(matchValues, eTag) > -1 @@ -575,8 +510,7 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Returns true if the given modified header is older than the given last modified value. */ - private static boolean modified(long modifiedHeader, long lastModified) - { + private static boolean modified(long modifiedHeader, long lastModified) { return (modifiedHeader + ONE_SECOND_IN_MILLIS <= lastModified); // That second is because the header is in seconds, not millis. } @@ -584,8 +518,7 @@ public abstract class AbstractFileServlet extends HttpServlet * Returns a substring of the given string value from the given begin index to the given end index as a long. * If the substring is empty, then -1 will be returned. */ - private static long sublong(String value, int beginIndex, int endIndex) - { + private static long sublong(String value, int beginIndex, int endIndex) { String substring = value.substring(beginIndex, endIndex); return substring.isEmpty() ? -1 : Long.parseLong(substring); } @@ -593,8 +526,7 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Returns true if the given accept header accepts the given value. */ - private static boolean accepts(String acceptHeader, String toAccept) - { + private static boolean accepts(String acceptHeader, String toAccept) { String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*"); Arrays.sort(acceptValues); return Arrays.binarySearch(acceptValues, toAccept) > -1 @@ -607,24 +539,19 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Convenience class for a file resource. */ - private static class Resource - { + private static class Resource { private final File file; private final long length; private final long lastModified; private final String eTag; - public Resource(File file) - { - if (file != null && file.isFile()) - { + public Resource(File file) { + if (file != null && file.isFile()) { this.file = file; length = file.length(); lastModified = file.lastModified(); eTag = String.format(ETAG, encodeURL(file.getName()), lastModified); - } - else - { + } else { this.file = null; length = 0; lastModified = 0; @@ -637,14 +564,12 @@ public abstract class AbstractFileServlet extends HttpServlet /** * Convenience class for a byte range. */ - private static class Range - { + private static class Range { private final long start; private final long end; private final long length; - public Range(long start, long end) - { + public Range(long start, long end) { this.start = start; this.end = end; length = end - start + 1; diff --git a/src/main/java/net/woggioni/jpacrepo/servlet/FileServlet.java b/src/main/java/net/woggioni/jpacrepo/servlet/FileServlet.java index 4d5724e..490763b 100644 --- a/src/main/java/net/woggioni/jpacrepo/servlet/FileServlet.java +++ b/src/main/java/net/woggioni/jpacrepo/servlet/FileServlet.java @@ -9,18 +9,16 @@ import java.io.File; import java.nio.file.Path; @WebServlet("/archive/*") -public class FileServlet extends AbstractFileServlet -{ - private Path root; +public class FileServlet extends AbstractFileServlet { + private final Path root; @Inject public FileServlet(AppConfig ctx){ - root = ctx.repoFolder(); + root = ctx.getRepoFolder(); } @Override protected File getFile(HttpServletRequest request) throws IllegalArgumentException { return root.resolve(request.getPathInfo().substring(1)).toFile(); } - } \ No newline at end of file diff --git a/src/main/java/net/woggioni/jpacrepo/version/PkgIdComparator.java b/src/main/java/net/woggioni/jpacrepo/version/PkgIdComparator.java index 1d12d3a..54d1b28 100644 --- a/src/main/java/net/woggioni/jpacrepo/version/PkgIdComparator.java +++ b/src/main/java/net/woggioni/jpacrepo/version/PkgIdComparator.java @@ -1,21 +1,19 @@ package net.woggioni.jpacrepo.version; -import net.woggioni.jpacrepo.pacbase.PkgId; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import net.woggioni.jpacrepo.api.model.PkgId; import java.util.Comparator; -public class PkgIdComparator implements Comparator { - private final Comparator comparator; - - public PkgIdComparator() { - VersionComparator vc = new VersionComparator(); - comparator = Comparator.comparing(PkgId::name) - .thenComparing(PkgId::version, vc) - .thenComparing(PkgId::arch); - } - - @Override - public int compare(PkgId id1, PkgId id2) { - return comparator.compare(id1, id2); +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PkgIdComparator { + @Getter + private static final Comparator comparator; + static { + comparator = Comparator.comparing(PkgId::getName) + .thenComparing(PkgId::getVersion, VersionComparator.getInstance()) + .thenComparing(PkgId::getArch); } } diff --git a/src/main/java/net/woggioni/jpacrepo/version/VersionComparator.java b/src/main/java/net/woggioni/jpacrepo/version/VersionComparator.java index e7b7e34..27efd97 100644 --- a/src/main/java/net/woggioni/jpacrepo/version/VersionComparator.java +++ b/src/main/java/net/woggioni/jpacrepo/version/VersionComparator.java @@ -1,6 +1,10 @@ package net.woggioni.jpacrepo.version; + import com.sun.jna.Native; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.Comparator; @@ -13,10 +17,12 @@ class AlpmLibrary { } } -public class VersionComparator implements Comparator { - @Override - public int compare(String version1, String version2) { - return AlpmLibrary.alpm_pkg_vercmp(version1, version2); +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class VersionComparator { + @Getter + private static final Comparator instance; + static { + instance = AlpmLibrary::alpm_pkg_vercmp; } } diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..87f18df --- /dev/null +++ b/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,24 @@ + + + + + net.woggioni.jpacrepo.api.model.PkgData + net.woggioni.jpacrepo.api.model.PkgId + true + + + + + + + + + + + + diff --git a/src/main/scala/net/woggioni/jpacrepo/config/AppConfig.scala b/src/main/scala/net/woggioni/jpacrepo/config/AppConfig.scala deleted file mode 100644 index e69c31a..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/config/AppConfig.scala +++ /dev/null @@ -1,41 +0,0 @@ -package net.woggioni.jpacrepo.config - -import java.nio.file.{Files, Path, Paths} -import java.util.Properties -import java.util.concurrent.atomic.AtomicBoolean - -import net.woggioni.jpacrepo.pacbase.PkgData -import net.woggioni.jpacrepo.persistence.InitialSchemaAction -import net.woggioni.jpacrepo.utils.Utils._ - -object AppConfig { - - def apply(propertyFile: String): AppConfig = { - val getProperty = new Properties().let { it => - val path = Paths.get(propertyFile) - if (Files.exists(path)) { - Files.newInputStream(path).use { is => - it.load(is) - } - } - (key : String, default : String) => System.getProperty(s"net.woggioni.jpacrepo.$key") match { - case null => it.getProperty(key, default) - case path: String => path - } - } - new AppConfig( - repoFolder = getProperty("RepoFolder", "net.woggioni.jpacrepo.RepoFolder") - .let { it => Paths.get(it) }, - initialSchemaAction = getProperty("InitialSchemaAction", "none").let(InitialSchemaAction(_))) - } -} - -case class AppConfig(val repoFolder: Path, - val initialSchemaAction: InitialSchemaAction) { - - var invalidateCache = new AtomicBoolean(true) - - def getFile(pkg: PkgData) = repoFolder.resolve(pkg.fileName) - - def getFile(fileName: String) = repoFolder.resolve(fileName) -} diff --git a/src/main/scala/net/woggioni/jpacrepo/exception/Exceptions.scala b/src/main/scala/net/woggioni/jpacrepo/exception/Exceptions.scala deleted file mode 100644 index 66abdd0..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/exception/Exceptions.scala +++ /dev/null @@ -1,5 +0,0 @@ -package net.woggioni.jpacrepo.exception - -class ParseException(msg : String, cause : Throwable) extends RuntimeException(msg, cause) { - def this(msg : String) = this(msg, null) -} \ No newline at end of file diff --git a/src/main/scala/net/woggioni/jpacrepo/factory/BeanFactory.scala b/src/main/scala/net/woggioni/jpacrepo/factory/BeanFactory.scala deleted file mode 100644 index 34b310e..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/factory/BeanFactory.scala +++ /dev/null @@ -1,48 +0,0 @@ -package net.woggioni.jpacrepo.factory - -import java.util.Properties - -import javax.annotation.PostConstruct -import javax.enterprise.inject.Produces -import javax.enterprise.inject.spi.InjectionPoint -import javax.faces.bean.ApplicationScoped -import javax.inject.Inject -import javax.persistence.{EntityManagerFactory, Persistence} -import net.woggioni.jpacrepo.config.AppConfig -import org.slf4j.LoggerFactory - -class PersistenceUnitFactory { - - @Inject - private var appConfig : AppConfig = _ - - private var emf : EntityManagerFactory = _ - - @PostConstruct - def init(): Unit = { - val properties = new Properties() - properties.put("javax.persistence.schema-generation.database.action", appConfig.initialSchemaAction.value) - emf = Persistence.createEntityManagerFactory("jpacrepo_pu", properties) - } - - @Produces - private def createEntityManagerFactory = emf -} - -class BeanFactory { - - @Produces - @ApplicationScoped - def produce: AppConfig = { - val ctx = AppConfig( - System.getProperty("net.woggioni.jpacrepo.configuration.file", - "/etc/jpacrepo/server.properties")) - ctx - } - - @Produces - private def createLogger(injectionPoint: InjectionPoint) = - LoggerFactory.getLogger(injectionPoint.getMember.getDeclaringClass.getName) -} - - diff --git a/src/main/scala/net/woggioni/jpacrepo/model/Hasher.scala b/src/main/scala/net/woggioni/jpacrepo/model/Hasher.scala deleted file mode 100644 index 18be36c..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/model/Hasher.scala +++ /dev/null @@ -1,46 +0,0 @@ -package net.woggioni.jpacrepo.model - -import java.io.InputStream -import java.security.MessageDigest - -object Hasher { - - private val md5 = MessageDigest.getInstance("MD5") - - private def getHash(md : MessageDigest, is: InputStream): Array[Byte] = { - val buffer = new Array[Byte](4096) - var read = 0 - while ( { - read = is.read(buffer, 0, buffer.length) - read >= 0 - }) { - md.update(buffer, 0, read) - } - md.digest - } - - def computeMD5(is: InputStream): String = Hasher.bytesToHex(getHash(md5, is)) - - private val hexArray = "0123456789ABCDEF".toCharArray - - def bytesToHex(bytes: Array[Byte]): String = { - val hexChars = new Array[Char](bytes.length * 2) - var j = 0 - while (j < bytes.length) { - val v = bytes(j) & 0xFF - hexChars(j * 2) = hexArray(v >>> 4) - hexChars(j * 2 + 1) = hexArray(v & 0x0F) - j += 1 - } - new String(hexChars) - } -} - -class Hasher(val algorithm: String) { - - private val md = MessageDigest.getInstance(algorithm) - - def getHash(is : InputStream) = Hasher.getHash(md, is) - - def getHashString(is: InputStream): String = Hasher.bytesToHex(getHash(is)) -} \ No newline at end of file diff --git a/src/main/scala/net/woggioni/jpacrepo/model/MD5InputStream.scala b/src/main/scala/net/woggioni/jpacrepo/model/MD5InputStream.scala deleted file mode 100644 index 7d311c3..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/model/MD5InputStream.scala +++ /dev/null @@ -1,11 +0,0 @@ -package net.woggioni.jpacrepo.model - -import java.io.InputStream -import java.security.{DigestInputStream, MessageDigest} - -class MD5InputStream(val is: InputStream) extends DigestInputStream(is, MessageDigest.getInstance("md5")) { - def digest(): String = { - val digest = getMessageDigest.digest - Hasher.bytesToHex(digest) - } -} \ No newline at end of file diff --git a/src/main/scala/net/woggioni/jpacrepo/model/Parser.scala b/src/main/scala/net/woggioni/jpacrepo/model/Parser.scala deleted file mode 100644 index dae55da..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/model/Parser.scala +++ /dev/null @@ -1,114 +0,0 @@ -package net.woggioni.jpacrepo.model - -import java.io.{BufferedInputStream, InputStream} -import java.nio.file.{Files, Path} -import java.util.Date -import java.util.zip.GZIPInputStream - -import net.woggioni.jpacrepo.exception.ParseException -import net.woggioni.jpacrepo.pacbase.{CompressionFormat, PkgData, PkgId} -import net.woggioni.jpacrepo.utils.Utils._ -import net.woggioni.jzstd.ZstdInputStream -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream -import org.apache.commons.compress.compressors.xz.XZCompressorInputStream - -import scala.io.Source -import scala.jdk.CollectionConverters._ - -object Parser { - - def parseFile(file: Path, compressionFormat : CompressionFormat): PkgData = { - val hasher = new Hasher("MD5") - val decompressorStreamConstructor = compressionFormat match { - case CompressionFormat.XZ => arg : InputStream => new XZCompressorInputStream(arg) - case CompressionFormat.Z_STANDARD => is : InputStream => ZstdInputStream.from(is) - case CompressionFormat.GZIP => is : InputStream => new GZIPInputStream(is) - case format => throw new ParseException(s"Unsupported compression format '$format'") - } - - - val is = new TarArchiveInputStream( - decompressorStreamConstructor( - new BufferedInputStream( - Files.newInputStream(file)))) - try { - var archiveEntry = is.getNextEntry - while (archiveEntry != null) { - if (archiveEntry.getName == ".PKGINFO") { - val buffer = new Array[Byte](archiveEntry.getSize.toInt) - is.read(buffer) - val metadata = Source.fromBytes(buffer) - .getLines() - .map(_.trim) - .filter(!_.isEmpty) - .filter(!_.startsWith("#")) - .map(line => { - val equals = line.indexOf("=") - if (equals < 0) { - throw new ParseException(s"Error parsing .PKGINFO file in '${file}'") - } - else { - (line.substring(0, equals).trim, line.substring(equals + 1, line.length).trim) - } - }) - .to(LazyList) - .groupBy(_._1) - .map(pair => pair._1 -> pair._2.map(_._2).toList) - val data = new PkgData - data.id = new PkgId - for (pair <- metadata) { - val (key, value) = pair - key match { - case "size" => - data.size = value.head.toLong - case "arch" => - data.id.arch = value.head - case "replaces" => - data.replaces = value.toSet.asJava - case "packager" => - data.packager = value.head - case "url" => - data.url = value.head - case "pkgname" => - data.id.name = value.head - case "builddate" => - data.buildDate = new Date(value.head.toLong * 1000) - case "license" => - data.license = value.head - case "pkgver" => - data.id.version = value.head - case "pkgdesc" => - data.description = value.head - case "provides" => - data.provides = value.toSet.asJava - case "conflict" => - data.conflict = value.toSet.asJava - case "backup" => - data.backup = value.toSet.asJava - case "optdepend" => - data.optdepend = value.toSet.asJava - case "depend" => - data.depend = value.toSet.asJava - case "makedepend" => - data.makedepend = value.toSet.asJava - case "makepkgopt" => - data.makeopkgopt = value.toSet.asJava - case "pkgbase" => - data.base = value.head - case _ => - } - } - data.md5sum = Files.newInputStream(file).use(hasher.getHashString) - data.fileName = file.getFileName.toString - return data - } - else { - archiveEntry = is.getNextEntry - } - } - throw new ParseException(s".PKGINFO file not found in '$file'") - } finally { - is.close() - } - } -} \ No newline at end of file diff --git a/src/main/scala/net/woggioni/jpacrepo/pacbase/CompressionFormat.scala b/src/main/scala/net/woggioni/jpacrepo/pacbase/CompressionFormat.scala deleted file mode 100644 index a051dbc..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/pacbase/CompressionFormat.scala +++ /dev/null @@ -1,32 +0,0 @@ -package net.woggioni.jpacrepo.pacbase - -import java.nio.file.Path - -import net.woggioni.jpacrepo.exception.ParseException -import net.woggioni.jwo.JWO - -sealed class CompressionFormat(val extension : String) { - override def toString: String = extension -} - -object CompressionFormat { - private val map = LazyList(XZ, GZIP, Z_STANDARD) - .map(v => v.extension -> v).toMap - - def apply(text : String) : Option[CompressionFormat] = map.get(text) - def values() = map.values - - def guess(file : Path) = { - val extension = JWO.splitExtension(file).orElseThrow(() => - new ParseException(s"Unable to parse file extension for '${file.getFileName}'") - )._2 - CompressionFormat(extension.substring(1)) match { - case Some(compressionFormat) => compressionFormat - case None => throw new IllegalArgumentException(s"Unknown compression format for file extension '$extension'") - } - } - - case object XZ extends CompressionFormat("xz") - case object GZIP extends CompressionFormat("gz") - case object Z_STANDARD extends CompressionFormat("zst") -} \ No newline at end of file diff --git a/src/main/scala/net/woggioni/jpacrepo/pacbase/Ordering.scala b/src/main/scala/net/woggioni/jpacrepo/pacbase/Ordering.scala deleted file mode 100644 index a0b6a7f..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/pacbase/Ordering.scala +++ /dev/null @@ -1,8 +0,0 @@ -package net.woggioni.jpacrepo.pacbase - -import net.woggioni.jpacrepo.version.VersionComparator - -class VersionOrdering extends Ordering[String] { - private val comparator = new VersionComparator - override def compare(x: String, y: String): Int = comparator.compare(x, y) -} diff --git a/src/main/scala/net/woggioni/jpacrepo/pacbase/PkgData.scala b/src/main/scala/net/woggioni/jpacrepo/pacbase/PkgData.scala deleted file mode 100644 index 77e2bb9..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/pacbase/PkgData.scala +++ /dev/null @@ -1,78 +0,0 @@ -package net.woggioni.jpacrepo.pacbase - -import java.util -import java.util.Date - -import javax.persistence._ -import javax.xml.bind.annotation.{XmlAccessType, XmlAccessorType, XmlRootElement} - -@Entity -@Access(AccessType.FIELD) -@NamedQueries(value = Array[NamedQuery]( - new NamedQuery(name = "searchByFileName", query = "SELECT p FROM PkgData p WHERE p.fileName = :fileName"), - new NamedQuery(name = "searchByName", query = "SELECT p FROM PkgData p WHERE p.id.name = :name"), - new NamedQuery(name = "searchById", query = "SELECT p FROM PkgData p WHERE p.id = :id"), - new NamedQuery(name = "searchByHash", query = "SELECT p FROM PkgData p WHERE p.md5sum = :md5sum") -)) -@Table(indexes = Array( - new Index(columnList = "md5sum", unique = true), - new Index(columnList = "fileName", unique = true)) -) -@XmlRootElement -@XmlAccessorType(XmlAccessType.FIELD) -class PkgData { - - @EmbeddedId - var id: PkgId = _ - - var base: String = _ - - var description: String = _ - - var url: String = _ - - @Temporal(TemporalType.TIMESTAMP) - var buildDate: Date = _ - - var packager: String = _ - - var size = 0L - - var license: String = _ - - var md5sum: String = _ - - var fileName: String = _ - - @ElementCollection(fetch = FetchType.EAGER) - var replaces: util.Set[String] = _ - - @ElementCollection(fetch = FetchType.EAGER) - var conflict: util.Set[String] = _ - - @ElementCollection(fetch = FetchType.EAGER) - var provides: util.Set[String] = _ - - @ElementCollection(fetch = FetchType.EAGER) - var depend: util.Set[String] = _ - - @ElementCollection(fetch = FetchType.EAGER) - var optdepend: util.Set[String] = _ - - @ElementCollection(fetch = FetchType.EAGER) - var makedepend: util.Set[String] = _ - - @ElementCollection(fetch = FetchType.EAGER) - var makeopkgopt: util.Set[String] = _ - - @ElementCollection(fetch = FetchType.EAGER) - var backup: util.Set[String] = _ - - @Temporal(TemporalType.TIMESTAMP) - var updTimestamp: Date = _ - - @PreUpdate - @PrePersist - private def writeTimestamp(): Unit = updTimestamp = new Date -} - diff --git a/src/main/scala/net/woggioni/jpacrepo/pacbase/PkgId.scala b/src/main/scala/net/woggioni/jpacrepo/pacbase/PkgId.scala deleted file mode 100644 index f2b6ef7..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/pacbase/PkgId.scala +++ /dev/null @@ -1,31 +0,0 @@ -package net.woggioni.jpacrepo.pacbase - -import javax.persistence.{Access, AccessType, Embeddable} -import javax.xml.bind.annotation.{XmlAccessType, XmlAccessorType, XmlRootElement} - -@Embeddable -@Access(AccessType.FIELD) -@XmlRootElement -@XmlAccessorType(XmlAccessType.FIELD) -class PkgId extends Serializable { - var name : String = null - - var version : String = null - - var arch : String = null - - override def equals(obj: Any): Boolean = { - obj match { - case null => false - case pkgId : PkgId => name == pkgId.name && version == pkgId.version && arch == pkgId.arch - } - } - - override def hashCode(): Int = { - var result : Int = 0 - if(name != null) result ^= name.hashCode - if(version != null) result ^= version.hashCode - if(arch != null) result ^= arch.hashCode - result - } -} \ No newline at end of file diff --git a/src/main/scala/net/woggioni/jpacrepo/persistence/InitialSchemaAction.scala b/src/main/scala/net/woggioni/jpacrepo/persistence/InitialSchemaAction.scala deleted file mode 100644 index 0872f70..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/persistence/InitialSchemaAction.scala +++ /dev/null @@ -1,19 +0,0 @@ -package net.woggioni.jpacrepo.persistence - -sealed class InitialSchemaAction(val value : String) { - override def toString: String = value -} - -object InitialSchemaAction { - private val map = LazyList(NONE, CREATE, DROP, DROP_AND_CREATE) - .map(v => v.value -> v).toMap - - def apply(text : String) : InitialSchemaAction = map(text) - def values() = map.values - - case object NONE extends InitialSchemaAction("none") - case object CREATE extends InitialSchemaAction("create") - case object DROP extends InitialSchemaAction("drop") - case object DROP_AND_CREATE extends InitialSchemaAction("drop-and-create") -} - diff --git a/src/main/scala/net/woggioni/jpacrepo/service/PacmanServiceEJB.scala b/src/main/scala/net/woggioni/jpacrepo/service/PacmanServiceEJB.scala deleted file mode 100644 index 7e7147b..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/service/PacmanServiceEJB.scala +++ /dev/null @@ -1,174 +0,0 @@ -package net.woggioni.jpacrepo.service - -import java.nio.file.{Files, Path} -import java.util -import java.util.{Calendar, Date} - -import javax.annotation.PostConstruct -import javax.ejb._ -import javax.inject.Inject -import javax.persistence._ -import net.woggioni.jpacrepo.config.AppConfig -import net.woggioni.jpacrepo.model.Parser -import net.woggioni.jpacrepo.pacbase.{CompressionFormat, PkgData} -import net.woggioni.jpacrepo.persistence.QueryEngine -import org.slf4j.Logger - -import scala.jdk.CollectionConverters._ - -@Remote -trait PacmanServiceRemote { - - def syncDB(): Unit - - def deletePackage(filename: String): Unit -} - -@Local -trait PacmanServiceView extends PacmanServiceRemote { - - def countResults(name: String, version: String, arch: String): Long - - def searchPackage(name: String, version: String, arch: String, page: Int, pageSize: Int, fileName: String): util.List[PkgData] -} - -@Startup -@Singleton -@Lock(LockType.READ) -@TransactionManagement(TransactionManagementType.CONTAINER) -@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) -@Local(Array(classOf[PacmanServiceView])) -@Remote(Array(classOf[PacmanServiceRemote])) -class PacmanServiceEJB extends PacmanServiceView { - - @Inject - private var emf: EntityManagerFactory = _ - - @Inject - private var ctx: AppConfig = _ - - @Inject - private var logger: Logger = _ - - final private val nameQuery = "SELECT pname FROM PkgName pname WHERE id = :name" - - final private val hashQuery = "SELECT pdata FROM PkgData pdata WHERE md5sum = :md5sum" - final private val hashQueryCount = "SELECT count(pdata) FROM PkgData pdata WHERE md5sum = :md5sum" - - private def deletePkgData(em: EntityManager, pkgData: PkgData): Unit = { - if (pkgData.depend != null) pkgData.depend.asScala.foreach(em.remove) - if (pkgData.replaces != null) pkgData.replaces.asScala.foreach(em.remove) - if (pkgData.conflict != null) pkgData.conflict.asScala.foreach(em.remove) - if (pkgData.provides != null) pkgData.provides.asScala.foreach(em.remove) - if (pkgData.makedepend != null) pkgData.makedepend.asScala.foreach(em.remove) - if (pkgData.makeopkgopt != null) pkgData.makeopkgopt.asScala.foreach(em.remove) - if (pkgData.backup != null) pkgData.backup.asScala.foreach(em.remove) - em.remove(pkgData) - } - - @Asynchronous - @TransactionAttribute(TransactionAttributeType.REQUIRED) - @Schedule(hour = "4", minute = "00", persistent = false) - override def syncDB() = { - val em = emf.createEntityManager - logger.info("Starting repository cleanup") - //Elimina i pacchetti sul DB che non esistono più nel filesystem - logger.info("Searching for packages that are no more in the filesystem") - val listaDB = em.createQuery("SELECT p.fileName FROM PkgData p", classOf[String]).getResultList - logger.info("Got list of filenames from db") - val knownPkg = listaDB - .asScala - .filter(fileName => { - val file = ctx.getFile(fileName) - val result = Files.exists(file) - if (!result) { - logger.info(s"Removing package ${file.getFileName} which was not found in filesystem") - em.createQuery("SELECT p FROM PkgData p WHERE p.fileName = :fileName", classOf[PkgData]) - .setParameter("fileName", file.getFileName) - .getResultList.asScala.foreach((pkgData: PkgData) => deletePkgData(em, pkgData)) - } - result - }).toSet - logger.info("Searching for new packages or packages that were modified after being added to the database") - Files.list(ctx.repoFolder).iterator().asScala.filter(file => { - val name = file.getFileName.toString - name.endsWith(".pkg.tar.xz") || name.endsWith(".pkg.tar.zst") - }).foreach(file => { - if (!knownPkg.contains(file.getFileName.toString) || { - val query = em.createQuery("SELECT p.updTimestamp FROM PkgData p WHERE filename = :filename", classOf[Date]) - query.setParameter("filename", file.getFileName.toString) - val result = query.getSingleResult - Files.getLastModifiedTime(file).toMillis > result.getTime - }) { - try { - parseFile(em, file) - } catch { - case e: Exception => - logger.error(s"Error parsing '${file.toAbsolutePath}'", e) - if (em.getTransaction.getRollbackOnly) throw e - else Files.delete(file) - } - } - }) - logger.info("Removing obsolete packages") - deleteOld(em) - logger.info("Repository cleanup completed successfully") - ctx.invalidateCache.set(true) - } - - private def parseFile(em: EntityManager, file: Path) = { - val hquery = em.createQuery(hashQueryCount, classOf[java.lang.Long]) - val data = Parser.parseFile(file, CompressionFormat.guess(file)) - hquery.setParameter("md5sum", data.md5sum) - if (hquery.getSingleResult == 0) { - val fquery = em.createQuery("SELECT p FROM PkgData p WHERE p.fileName = :fileName", classOf[PkgData]) - fquery.setParameter("fileName", file.getFileName.toString) - fquery.getResultList.forEach(deletePkgData(em, _)) - em.persist(data) - logger.info(s"Persisting package ${file.getFileName}") - } - } - - @TransactionAttribute(TransactionAttributeType.REQUIRED) - override def deletePackage(filename: String): Unit = { - val em = emf.createEntityManager - deletePackage(em, filename) - logger.info(s"Package $filename has been deleted") - } - - private def deletePackage(em: EntityManager, filename: String): Unit = { - val fquery = em.createQuery("SELECT p FROM PkgData p WHERE fileName = :fileName", classOf[PkgData]) - fquery.setParameter("fileName", filename) - val savedFiles = fquery.getResultList - if (savedFiles.size == 0) { - throw new RuntimeException(String.format("Package with name %s not found", filename)) - } - val pkg = fquery.getResultList.get(0) - Files.delete(ctx.getFile(pkg)) - em.remove(pkg) - } - - final private val deleteQuery = "SELECT p.fileName FROM PkgData p WHERE p.buildDate < :cutoff and p.id.name in \n" + "(SELECT p2.id.name FROM PkgData p2 GROUP BY p2.id.name HAVING count(p2.id.name) > :minVersions\n)" - - private def deleteOld(em: EntityManager): Unit = { - val query = em.createQuery(deleteQuery, classOf[String]) - val cutoff = Calendar.getInstance - cutoff.add(Calendar.YEAR, -2) - query.setParameter("cutoff", cutoff.getTime) - query.setParameter("minVersions", 2.toLong) - val list = query.getResultList - list.forEach(deletePackage(_)) - } - - @TransactionAttribute(TransactionAttributeType.SUPPORTS) - override def countResults(name: String, version: String, arch: String): Long = { - val em = emf.createEntityManager - QueryEngine.countResults(em, name, version, arch) - } - - @TransactionAttribute(TransactionAttributeType.SUPPORTS) - override def searchPackage(name: String, version: String, arch: String, pageNumber: Int, pageSize: Int, fileName: String) = { - val em = emf.createEntityManager - QueryEngine.searchPackage(em, name, version, arch, pageNumber, pageSize, null) - } -} diff --git a/src/main/scala/net/woggioni/jpacrepo/service/PacmanWebService.scala b/src/main/scala/net/woggioni/jpacrepo/service/PacmanWebService.scala deleted file mode 100644 index 52c4f7a..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/service/PacmanWebService.scala +++ /dev/null @@ -1,448 +0,0 @@ -package net.woggioni.jpacrepo.service - -import java.io._ -import java.net.URI -import java.nio.file.{Files, Paths} -import java.nio.file.StandardCopyOption.ATOMIC_MOVE -import java.util - -import javax.ejb._ -import javax.inject.Inject -import javax.persistence._ -import javax.ws.rs._ -import javax.ws.rs.core._ -import javax.xml.bind.annotation.{XmlElement, XmlRootElement} -import net.woggioni.jpacrepo.config.AppConfig -import net.woggioni.jpacrepo.model.Parser -import net.woggioni.jpacrepo.pacbase.{CompressionFormat, PkgData, PkgId, VersionOrdering} -import net.woggioni.jpacrepo.utils.Utils._ -import net.woggioni.jpacrepo.version.{PkgIdComparator, VersionComparator} -import org.apache.commons.compress.archivers.tar.{TarArchiveEntry, TarArchiveOutputStream} -import org.slf4j.Logger - -import scala.beans.BeanProperty -import scala.collection.SortedMap -import scala.collection.immutable.TreeMap -import scala.jdk.CollectionConverters._ - -@ApplicationPath("api") -class ApplicationConfig() extends Application { - - val classes: Set[Class[_]] = Set(classOf[PacmanWebService]) - - override def getClasses = classes.asJava -} - -object PacmanWebService { - val pkgIdOrdering: Ordering[PkgId] = Ordering.comparatorToOrdering(new PkgIdComparator) - val versionOrdering: Ordering[String] = Ordering.comparatorToOrdering(new VersionComparator) -} - - -@XmlRootElement -class PkgDataList extends util.ArrayList[PkgData] { - - def this(l: util.List[PkgData]) { - this() - l.forEach(el => add(el)) - } - - def this(elements: PkgData*) { - this() - elements.foreach(el => add(el)) - } - - @XmlElement(name = "pkgData") - def getItems: util.List[PkgData] = this - - def setItems(pkgs: util.List[PkgData]): Unit = { - this.clear() - this.addAll(pkgs) - } -} - -@XmlRootElement -class StringList extends util.ArrayList[String] { - - def this(l: util.List[String]) { - this() - l.forEach(el => add(el)) - } - - def this(elements: String*) { - this() - elements.foreach(el => add(el)) - } - - def this(elements: Iterable[String]) { - this() - elements.foreach(el => add(el)) - } - - @XmlElement(name = "string") - def getItems: util.List[String] = this - - def setItems(pkgs: util.List[String]): Unit = { - this.clear() - this.addAll(pkgs) - } -} - -@XmlRootElement -class PkgTuple { - @BeanProperty - var md5sum: String = _ - - @BeanProperty - var filename: String = _ - - @BeanProperty - var size: Long = _ -} - -@Singleton -@Path("/pkg") -@ConcurrencyManagement(ConcurrencyManagementType.BEAN) -@Produces(Array(MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON)) -@Consumes(Array(MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON)) -@TransactionManagement(TransactionManagementType.CONTAINER) -class PacmanWebService { - - private var cachedMap: SortedMap[PkgId, PkgTuple] = _ - - @Inject - private var emf: EntityManagerFactory = _ - - @Inject - private var log: Logger = _ - - @Inject - private var service: PacmanServiceView = _ - - @Inject - private var ctx: AppConfig = _ - - private def getCachedMap: SortedMap[PkgId, PkgTuple] = { - var result: SortedMap[PkgId, PkgTuple] = null - if (!ctx.invalidateCache.get()) { - result = cachedMap - } - if (result == null) { - synchronized { - result = cachedMap - if (result == null) { - val em = emf.createEntityManager() - val query = em.createQuery( - "SELECT pkg.id.name, pkg.id.version, pkg.id.arch, pkg.fileName, pkg.size, pkg.md5sum " + - "FROM PkgData pkg ORDER BY pkg.id.name, pkg.id.version, pkg.id.arch", - classOf[Array[AnyRef]]) - val stream = query.getResultList - .asScala - .to(LazyList) - .map(pkg => { - val name: String = pkg(0).asInstanceOf[String] - val version: String = pkg(1).asInstanceOf[String] - val arch: String = pkg(2).asInstanceOf[String] - val filename: String = pkg(3).asInstanceOf[String] - val size: Long = pkg(4).asInstanceOf[Long] - val md5sum: String = pkg(5).asInstanceOf[String] - val tuple: PkgTuple = new PkgTuple - tuple.filename = filename - tuple.size = size - tuple.md5sum = md5sum - val id = new PkgId() - id.name = name - id.version = version - id.arch = arch - id -> tuple - }) - cachedMap = TreeMap.from(stream)(PacmanWebService.pkgIdOrdering) - ctx.invalidateCache.set(false) - result = cachedMap - } - } - } - result - } - - @GET - @Path("searchByName/{name}") - def searchByName(@PathParam("name") name: String): Response = { - val em = emf.createEntityManager() - if (name == null) throw new WebApplicationException(Response.Status.BAD_REQUEST) - val query: String = String.format("SELECT pkgId.name FROM PkgId pkgId WHERE LOWER(pkgId.name) LIKE '%%%s%%' ORDER BY pkgId.name", name) - Response.ok(em.createQuery(query, classOf[String]).getResultList).build - } - - @GET - @Path("searchByHash/{md5sum}") - def searchByHash(@PathParam("md5sum") md5sum: String): Response = getPackageByHash(md5sum) - - @GET - @Path("list/{name}") - def getPackage(@PathParam("name") name: String): Response = { - val em = emf.createEntityManager() - val query = em.createQuery("SELECT pkg.id.version FROM PkgData pkg WHERE pkg.id.name = :name ORDER BY pkg.id.version", classOf[String]) - query.setParameter("name", name) - Response.ok(new StringList(query.getResultList)).build - } - - @GET - @Path("list/{name}/{version}") - def getPackage(@PathParam("name") name: String, @PathParam("version") version: String): Response = { - val em = emf.createEntityManager() - val query = em.createQuery("SELECT pkg.arch FROM PkgData pkg WHERE pkg.id.name = :name AND pkg.id.version = :version ORDER BY pkg.id.arch", classOf[String]) - query.setParameter("name", name) - query.setParameter("version", version) - Response.ok(new StringList(query.getResultList)).build - } - - @GET - @Path("list/{name}/{version}/{arch}") - def getPackage(@PathParam("name") name: String, @PathParam("version") version: String, @PathParam("arch") arch: String): Response = { - val em = emf.createEntityManager() - val query: TypedQuery[PkgData] = em.createQuery("SELECT pkg FROM PkgData pkg WHERE " + "pkg.id.name = :name AND " + "pkg.id.version = :version AND " + "pkg.id.arch = :arch " + "ORDER BY pkg.arch", classOf[PkgData]) - query.setParameter("name", name) - query.setParameter("version", version) - query.setParameter("arch", arch) - Response.ok(query.getSingleResult).build - } - - @GET - @Produces(Array(MediaType.APPLICATION_JSON)) - @Path("map") - def getPackageMap(@Context request: Request): Response = { - val cc: CacheControl = new CacheControl - cc.setMaxAge(86400) - cc.setMustRevalidate(true) - cc.setNoCache(true) - - val cachedMap = getCachedMap - val etag: EntityTag = new EntityTag(Integer.toString(cachedMap.hashCode)) - var builder: Response.ResponseBuilder = request.evaluatePreconditions(etag) - if (builder == null) { - val result: util.Map[String, util.Map[String, util.Map[String, PkgTuple]]] = cachedMap.to(LazyList) - .groupBy(_._1.name).view.mapValues(t => - SortedMap.from(t.groupBy(_._1.version).view.mapValues( - _.map(pair => pair._1.arch -> pair._2).to(SortedMap).asJava - ))(PacmanWebService.versionOrdering.reverse).asJava - ).toMap.asJava - builder = Response.ok(result) - builder.tag(etag) - } - builder.cacheControl(cc) - builder.build - } - - @GET - @Path("hashes") - def getHashes: Response = { - val em = emf.createEntityManager() - val query = em.createQuery("SELECT p.md5sum FROM PkgData p", classOf[String]) - Response.ok(new StringList(query.getResultList)).build - } - - @GET - @Path("files") - def getFiles: Response = { - val em = emf.createEntityManager() - val query = em.createQuery("SELECT p.fileName FROM PkgData p", classOf[String]) - Response.ok(new StringList(query.getResultList)).build - } - - private def getPackageByHash(md5sum: String): Response = { - val em = emf.createEntityManager() - val hquery = em.createNamedQuery("searchByHash", classOf[PkgData]) - if (md5sum != null) hquery.setParameter("md5sum", md5sum) - manageQueryResult(hquery.getResultList, true) - } - - private def getPackageByFileName(file: String): Response = { - val em = emf.createEntityManager() - val fnquery: TypedQuery[PkgData] = em.createNamedQuery("searchByFileName", classOf[PkgData]) - fnquery.setParameter("fileName", file) - manageQueryResult(fnquery.getResultList, true) - } - - @GET - @Path("filesize/{filename}") - def getFileSize(@PathParam("filename") fileName: String, @Context request: Request): Response = { - val cc: CacheControl = new CacheControl - cc.setMaxAge(86400) - cc.setMustRevalidate(true) - cc.setNoCache(true) - val etag: EntityTag = new EntityTag(Integer.toString(getCachedMap.hashCode)) - var builder: Response.ResponseBuilder = request.evaluatePreconditions(etag) - if (builder == null) { - val res = ctx.getFile(fileName) - if (!Files.exists(res)) throw new NotFoundException(String.format("File '%s' was not found", fileName)) - builder = Response.ok(Files.size(res)) - builder.tag(etag) - } - builder.cacheControl(cc) - builder.build - } - - @GET - @Path("download/{filename}") - @Produces(Array(MediaType.APPLICATION_OCTET_STREAM)) - def downloadPackage(@PathParam("filename") fileName: String): Response = { - val em = emf.createEntityManager() - val fnquery: TypedQuery[PkgData] = em.createNamedQuery("searchByFileName", classOf[PkgData]) - fnquery.setParameter("fileName", fileName) - try { - val pkg: PkgData = fnquery.getSingleResult - val stream: StreamingOutput = (output: OutputStream) => { - Files.newInputStream(ctx.getFile(pkg)).use { is => - val bytes: Array[Byte] = new Array[Byte](1024) - var read = 0 - while ( { - read = is.read(bytes) - read >= 0 - }) { - output.write(bytes, 0, read) - } - } - () - } - Response.ok(stream).header("Content-Length", Files.size(ctx.getFile(pkg))).build - } catch { - case _: NoResultException => - throw new NotFoundException - } - } - - @POST - @Path("/doYouWantAny") - @Produces(Array(MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON)) - def doYouWantAny(filenames: util.List[String]): Response = { - val em = emf.createEntityManager() - val result = Set.from(filenames.asScala) - if (result.nonEmpty) { - val query = em.createQuery("SELECT pkg.fileName from PkgData pkg WHERE pkg.fileName in :filenames", classOf[String]) - query.setParameter("filenames", filenames) - val toBeRemoved = Set.from(query.getResultList.asScala) - Response.ok(new StringList(result -- toBeRemoved)).build - } - else { - Response.ok(result.toArray).build - } - } - - @POST - @Path("/upload") - @TransactionAttribute(TransactionAttributeType.REQUIRED) - @Consumes(Array("application/x-xz", "application/gzip", "application/x-tar", MediaType.APPLICATION_OCTET_STREAM)) - def createPackage(input: InputStream, - @MatrixParam("filename") filename: String, - @Context uriInfo: UriInfo): Response = { - val em = emf.createEntityManager() - if (filename == null) throw new BadRequestException - val file = Files.createTempFile(ctx.repoFolder, filename, null) - val fquery: TypedQuery[PkgData] = em.createNamedQuery("searchByFileName", classOf[PkgData]) - fquery.setParameter("fileName", filename) - val savedFiles: util.List[PkgData] = fquery.getResultList - if (savedFiles.size > 0) Response.notModified.build - else { - Files.newOutputStream(file).useCatch { os => - val buffer: Array[Byte] = new Array[Byte](0x1000) - var read = 0 - while ( { - read = input.read(buffer) - read >= 0 - }) { - os.write(buffer, 0, read) - } - val pkg = Parser.parseFile(file, CompressionFormat.guess(Paths.get(filename))) - pkg.fileName = filename - Option(em.find(classOf[PkgData], pkg.id)) match { - case Some(pkgData) => { - em.remove(pkgData) - Files.delete(ctx.repoFolder.resolve(pkgData.fileName)) - } - case None => - - } - log.info(s"Persisting package ${pkg.fileName}") - em.persist(pkg) - val pkgUri: URI = uriInfo.getAbsolutePathBuilder.path(pkg.fileName).build() - Files.move(file, ctx.repoFolder.resolve(filename), ATOMIC_MOVE) - ctx.invalidateCache.set(true) - cachedMap = null - Response.created(pkgUri).build - } { t: Throwable => - Files.delete(file) - throw t - } - } - } - - @GET - @Path("/search") - def searchPackage( - @QueryParam("name") name: String, - @QueryParam("version") version: String, - @QueryParam("arch") arch: String, - @QueryParam("page") pageNumber: Int, - @QueryParam("pageSize") pageSize: Int, - @QueryParam("fileName") fileName: String): util.List[PkgData] = { - service.searchPackage(name, version, arch, pageNumber, pageSize, fileName) - } - - @OPTIONS - @Path("/downloadTar") - @Produces(Array("text/plain; charset=UTF-8")) - def options: Response = Response.ok("POST, OPTIONS").build - - @POST - @Path("/downloadTar") - @Produces(Array("application/x-tar")) - @Consumes(Array(MediaType.APPLICATION_FORM_URLENCODED)) - def downloadTar(@FormParam("pkgs") formData: String): Response = { - val files: Array[String] = formData.split(" ") - files.find(fileName => !Files.exists(ctx.getFile(fileName))) match { - case Some(fileName) => throw new NotFoundException(s"Package file '$fileName' does not exist") - case None => - } - val stream = new StreamingOutput() { - override def write(output: OutputStream): Unit = { - val taos: TarArchiveOutputStream = new TarArchiveOutputStream(output) - try { - for (fname <- files) { - val file = ctx.getFile(fname) - Files.newInputStream(file).use { input => - val entry: TarArchiveEntry = new TarArchiveEntry(fname) - entry.setSize(Files.size(file)) - taos.putArchiveEntry(entry) - val bytes: Array[Byte] = new Array[Byte](1024) - var read = 0 - while ( { - read = input.read(bytes) - read >= 0 - }) { - taos.write(bytes, 0, read) - } - taos.closeArchiveEntry() - } - } - } finally { - taos.close() - } - } - } - Response.ok(stream).header("Content-Disposition", "attachment; filename=pkgs.tar").build - } - - private def manageQueryResult(list: util.List[PkgData]): Response = manageQueryResult(list, false) - - private def manageQueryResult(list: util.List[PkgData], singleResult: Boolean): Response = { - - if (list.isEmpty) throw new NotFoundException - else if (singleResult) { - if (list.size == 1) Response.ok(list.get(0)).build - else throw new NonUniqueResultException("The returned list does not contain a single element") - } else { - Response.ok(new PkgDataList(list)).build - } - } -} \ No newline at end of file diff --git a/src/main/scala/net/woggioni/jpacrepo/utils/Utils.scala b/src/main/scala/net/woggioni/jpacrepo/utils/Utils.scala deleted file mode 100644 index af6e19b..0000000 --- a/src/main/scala/net/woggioni/jpacrepo/utils/Utils.scala +++ /dev/null @@ -1,32 +0,0 @@ -package net.woggioni.jpacrepo.utils - -object Utils { - - implicit class Use[CLOSEABLE <: AutoCloseable, RESULT](closeable: CLOSEABLE) { - def useCatch(cb: CLOSEABLE => RESULT) (exceptionally : Throwable => RESULT): RESULT = { - try { - cb(closeable) - } catch { - case t : Throwable => exceptionally(t) - } finally { - closeable.close() - } - } - - def use(cb: CLOSEABLE => RESULT): RESULT = { - try { - cb(closeable) - } finally { - closeable.close() - } - } - } - - implicit class ContextFunctions[IN, OUT](in: IN) { - def let(cb: (IN) => OUT): OUT = cb(in) - def also(cb: (IN) => Unit) : IN = { - cb(in) - in - } - } -} diff --git a/src/test/java/ClientTest.java b/src/test/java/ClientTest.java index ebbbf8a..03c7096 100644 --- a/src/test/java/ClientTest.java +++ b/src/test/java/ClientTest.java @@ -1,30 +1,39 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; import lombok.SneakyThrows; -import net.woggioni.jpacrepo.model.Hasher; -import net.woggioni.jpacrepo.model.MD5InputStream; -import net.woggioni.jpacrepo.model.Parser; -import net.woggioni.jpacrepo.pacbase.CompressionFormat; -import net.woggioni.jpacrepo.pacbase.PkgData; -import net.woggioni.jpacrepo.service.PacmanServiceRemote; +import net.woggioni.jpacrepo.api.model.PkgData; +import net.woggioni.jpacrepo.api.service.PacmanServiceRemote; +import net.woggioni.jpacrepo.impl.model.CompressionFormatImpl; +import net.woggioni.jpacrepo.impl.model.PkgDataImpl; +import net.woggioni.jwo.Con; +import net.woggioni.jwo.Fun; +import net.woggioni.jwo.Hash; import net.woggioni.jwo.JWO; import org.jboss.resteasy.plugins.providers.RegisterBuiltin; import org.jboss.resteasy.spi.ResteasyProviderFactory; import javax.naming.*; -import javax.ws.rs.client.*; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; + +import jakarta.ws.rs.client.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + import java.io.File; import java.io.FileInputStream; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestInputStream; import java.security.MessageDigest; import java.util.Properties; +import java.util.stream.Stream; public class ClientTest { @@ -59,8 +68,7 @@ public class ClientTest { } } - public void testPUT() throws Exception - { + public void testPUT() throws Exception { ResteasyProviderFactory instance = ResteasyProviderFactory.getInstance(); RegisterBuiltin.register(instance); Client client = ClientBuilder.newClient(); @@ -77,60 +85,39 @@ public class ClientTest { assert Response.Status.CREATED.getStatusCode() == response.getStatus(); } - public void hashTest() throws Exception - { - String[] files = new String[]{"/var/cache/pacman/pkg/mesa-10.4.5-1-x86_64.pkg.tar.xz", "/var/cache/pacman/pkg/mesa-10.5.3-1-x86_64.pkg.tar.xz"}; - - for (String file : files) - { - MessageDigest md = MessageDigest.getInstance("MD5"); - try (InputStream is = Files.newInputStream(Paths.get(file))) - { - DigestInputStream dis = new DigestInputStream(is, md); - dis.on(true); -// is.read(); - long a = new File(file).length(); - byte[] out = new byte[(int)a]; - dis.read(out, 0, (int) a); + @Test + public void hashTest(@TempDir Path testDir) { + ClassLoader cl = getClass().getClassLoader(); + Stream.of("gvfs-nfs-1.50.2-1-x86_64.pkg.tar.zst") + .forEach((Con) resourceName -> { + Path tmpFile = testDir.resolve(resourceName); + try(InputStream is = cl.getResourceAsStream(resourceName)) { + Files.copy(is, tmpFile); } - System.out.println(Hasher.bytesToHex(md.digest())); - - System.out.println(Hasher.computeMD5(new FileInputStream(file))); - - InputStream fis = new FileInputStream(file); - MD5InputStream h = new MD5InputStream(fis); - long a = new File(file).length(); - byte[] out = new byte[(int)a]; - h.read(out, 0, (int) a); - System.out.println(h.digest()); - - Path path = Paths.get(file); - PkgData p = Parser.parseFile(path, CompressionFormat.guess(path)); - System.out.println(p.md5sum()); - - } - + Hash hash; + try(InputStream is = Files.newInputStream(tmpFile)) { + hash = Hash.md5(is); + } + PkgData p = PkgDataImpl.parseFile(tmpFile, CompressionFormatImpl.guess(tmpFile)); + Assertions.assertEquals(JWO.bytesToHex(hash.getBytes()), p.getMd5sum()); + }); } - private static void traverseJndiNode(String nodeName, Context context) - { - try - { + private static void traverseJndiNode(String nodeName, Context context) { + try { NamingEnumeration list = context.list(nodeName); - while (list.hasMore()) - { + while (list.hasMore()) { String childName = nodeName + "" + list.next().getName(); System.out.println(childName); traverseJndiNode(childName, context); } - } catch (NamingException ex) - { + } catch (NamingException ex) { // We reached a leaf } } - public void invokeStatelessBean() throws Exception - { + @Test + public void invokeStatelessBean() throws Exception { Properties prop = new Properties(); InputStream in = getClass().getClassLoader().getResourceAsStream("jboss-ejb-client.properties"); prop.load(in); @@ -140,10 +127,10 @@ public class ClientTest { prop.put(Context.PROVIDER_URL, "http-remoting://localhost:8080"); // prop.put(Context.PROVIDER_URL, "http-remoting://nuc:8080"); // prop.put(Context.PROVIDER_URL, "remote://odroid-u3:4447"); - prop.put(Context.SECURITY_PRINCIPAL, "walter"); - prop.put(Context.SECURITY_CREDENTIALS, "27ff5990757d1d"); -// prop.put(Context.SECURITY_PRINCIPAL, "admin"); -// prop.put(Context.SECURITY_CREDENTIALS, "123456"); +// prop.put(Context.SECURITY_PRINCIPAL, "walter"); +// prop.put(Context.SECURITY_CREDENTIALS, "27ff5990757d1d"); + prop.put(Context.SECURITY_PRINCIPAL, "luser"); + prop.put(Context.SECURITY_CREDENTIALS, "123456"); prop.put("jboss.naming.client.ejb.context", true); Context context = new InitialContext(prop); @@ -151,7 +138,7 @@ public class ClientTest { traverseJndiNode("/", context); // final PacmanService stateService = (PacmanService) ctx.lookup("/jpacrepo-1.0/remote/PacmanServiceEJB!service.PacmanService"); final PacmanServiceRemote service = (PacmanServiceRemote) ctx.lookup( - "/jpacrepo_2.12-2.0/PacmanServiceEJB!net.woggioni.jpacrepo.service.PacmanServiceRemote" + "/jpacrepo-2.0-SNAPSHOT/PacmanServiceEJB!net.woggioni.jpacrepo.service.PacmanServiceRemote" ); // List pkgs = service.searchPackage("google-earth", null, null, 1, 10); // System.out.println(new XStream().toXML(pkgs)); diff --git a/src/test/java/ParseTest.java b/src/test/java/ParseTest.java index 2bf7b42..0176cbd 100644 --- a/src/test/java/ParseTest.java +++ b/src/test/java/ParseTest.java @@ -1,9 +1,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; import lombok.SneakyThrows; -import net.woggioni.jpacrepo.model.Parser; +import net.woggioni.jpacrepo.api.model.PkgData; +import net.woggioni.jpacrepo.impl.model.PkgDataImpl; import net.woggioni.jpacrepo.pacbase.CompressionFormat; -import net.woggioni.jpacrepo.pacbase.PkgData; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -26,7 +26,7 @@ public class ParseTest { Files.list(Paths.get("/var/cache/pacman/pkg")) .filter(Files::isRegularFile) .filter(p -> pattern.matcher(p.getFileName().toString()).matches()) - .map(path -> Parser.parseFile(path, CompressionFormat.guess(path))) + .map(path -> PkgDataImpl.parseFile(path, CompressionFormat.guess(path))) .limit(10) .map(new Function() { @Override diff --git a/src/test/java/net/woggioni/jpacrepo/annotation/UnitTesting.java b/src/test/java/net/woggioni/jpacrepo/annotation/UnitTesting.java index 28ed0ea..3df91b7 100644 --- a/src/test/java/net/woggioni/jpacrepo/annotation/UnitTesting.java +++ b/src/test/java/net/woggioni/jpacrepo/annotation/UnitTesting.java @@ -1,7 +1,7 @@ package net.woggioni.jpacrepo.annotation; -import javax.enterprise.inject.Alternative; -import javax.enterprise.inject.Stereotype; +import jakarta.enterprise.inject.Alternative; +import jakarta.enterprise.inject.Stereotype; import java.lang.annotation.Retention; import java.lang.annotation.Target; diff --git a/src/test/resources/gvfs-nfs-1.50.2-1-x86_64.pkg.tar.zst b/src/test/resources/gvfs-nfs-1.50.2-1-x86_64.pkg.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..2e23337a3b1fe4a8db7e3c4779f0b7a587e311ca GIT binary patch literal 21719 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml index 45ef430..a8114ed 100644 --- a/src/test/resources/log4j2.xml +++ b/src/test/resources/log4j2.xml @@ -1,8 +1,5 @@ - - $${env:APOLLO_ENVIRONMENT_ROOT:-build}/var/logs - diff --git a/src/test/scala/net/woggioni/jpacrepo/client/SyncDbTest.scala b/src/test/scala/net/woggioni/jpacrepo/client/SyncDbTest.scala deleted file mode 100644 index b7c256f..0000000 --- a/src/test/scala/net/woggioni/jpacrepo/client/SyncDbTest.scala +++ /dev/null @@ -1,49 +0,0 @@ -package net.woggioni.jpacrepo.client - -import java.io.InputStream -import java.util.Properties - -import javax.naming.{Context, InitialContext, NameClassPair, NamingEnumeration, NamingException} -import net.woggioni.jpacrepo.service.PacmanServiceRemote -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class SyncDbTest extends AnyFlatSpec { - - private def traverseJndiNode(nodeName: String, context: Context) { - try { - val list = context.list(nodeName) - while (list.hasMore) { - val childName = nodeName + list.next.getName - System.out.println(childName) - traverseJndiNode(childName, context) - } - } catch { - case _ : NamingException => - } - } - - "it" should "be possible to invoke syncDB remotely" in { - val prop = new Properties - val in = getClass.getClassLoader.getResourceAsStream("jboss-ejb-client.properties") - prop.load(in) - prop.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming") - prop.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.naming.remote.client.InitialContextFactory") - prop.put(Context.PROVIDER_URL, "http-remoting://localhost:5080") - // prop.put(Context.PROVIDER_URL, "http-remoting://nuc:8080"); - // prop.put(Context.PROVIDER_URL, "remote://odroid-u3:4447"); - prop.put(Context.SECURITY_PRINCIPAL, "walter") - prop.put(Context.SECURITY_CREDENTIALS, "27ff5990757d1d") - // prop.put(Context.SECURITY_PRINCIPAL, "admin"); - // prop.put(Context.SECURITY_CREDENTIALS, "123456"); - prop.put("jboss.naming.client.ejb.context", true) - val context = new InitialContext(prop) - val ctx = new InitialContext(prop) - traverseJndiNode("/", context) - // final PacmanService stateService = (PacmanService) ctx.lookup("/jpacrepo-1.0/remote/PacmanServiceEJB!service.PacmanService"); - val service = ctx.lookup("/jpacrepo_2.13-2.0/PacmanServiceEJB!net.woggioni.jpacrepo.service.PacmanServiceRemote").asInstanceOf[PacmanServiceRemote] - // List pkgs = service.searchPackage("google-earth", null, null, 1, 10); - // System.out.println(new XStream().toXML(pkgs)); - service.syncDB() - } -} diff --git a/src/test/scala/net/woggioni/jpacrepo/config/AppConfigTest.scala b/src/test/scala/net/woggioni/jpacrepo/config/AppConfigTest.scala deleted file mode 100644 index 955ca46..0000000 --- a/src/test/scala/net/woggioni/jpacrepo/config/AppConfigTest.scala +++ /dev/null @@ -1,26 +0,0 @@ -package net.woggioni.jpacrepo.config - -import net.woggioni.jpacrepo.factory.BeanFactory -import org.jboss.weld.environment.se.Weld -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.must.Matchers - -class AppConfigTest extends AnyFlatSpec with Matchers { - - private val weld = new Weld - weld.disableDiscovery() - // weld.alternatives(classOf[TestPersistenceProducer]) - weld.beanClasses( - // classOf[PacmanServiceEJB], - // classOf[PacmanWebService], - // classOf[AppConfig], - classOf[BeanFactory], - ) - val container = weld.initialize() - "test" should "pass" in { - val appConfig = container.select(classOf[AppConfig]).get() - println(appConfig.repoFolder) - - } - -} diff --git a/src/test/scala/net/woggioni/jpacrepo/pacbase/MarshalTest.scala b/src/test/scala/net/woggioni/jpacrepo/pacbase/MarshalTest.scala deleted file mode 100644 index 4f4054f..0000000 --- a/src/test/scala/net/woggioni/jpacrepo/pacbase/MarshalTest.scala +++ /dev/null @@ -1,49 +0,0 @@ -package net.woggioni.jpacrepo.pacbase -import java.nio.file.{Files, Path, Paths} -import java.util -import java.util.regex.Pattern -import java.util.stream.Collectors - -import javax.xml.bind.annotation.{XmlAccessType, XmlAccessorType, XmlElement, XmlRootElement} -import javax.xml.bind.{JAXBContext, Marshaller} -import net.woggioni.jpacrepo.model.Parser -import net.woggioni.jpacrepo.service.PkgDataList -import org.scalatest.flatspec.AnyFlatSpec - -import scala.jdk.CollectionConverters._ -import scala.annotation.meta.field - -@XmlRootElement(name = "List") -@XmlAccessorType(XmlAccessType.FIELD) -class JaxbList2[T] private (@(XmlElement @field)(name = "Item") private val list : util.List[T]) { - def this(s : Seq[T]) { - this(s.toList.asJava) - } - - def this() { - this(new util.ArrayList[T]()) - } -} - -class MarshalTest extends AnyFlatSpec { - - "asdfs" should "sdfsd" in { - val context = JAXBContext.newInstance(classOf[PkgId], classOf[PkgDataList]) - val mar = context.createMarshaller - mar.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) - val pkgId2 = new PkgId - pkgId2.name = "linux" - pkgId2.version = "4.6.1" - pkgId2.arch = "x86_64" - val pattern = Pattern.compile(".*\\.pkg\\.tar\\.(zst)") - val pkgDatas = Files.list(Paths.get("/var/cache/pacman/pkg")) - .filter(Files.isRegularFile(_)) - .filter((p: Path) => pattern.matcher(p.getFileName.toString).matches) - .limit(10) - .map(Parser.parseFile(_, CompressionFormat.Z_STANDARD)) - .collect(Collectors.toList[PkgData]) - val list = new PkgDataList(pkgDatas) - mar.marshal(list, System.out) - } - -} diff --git a/src/test/scala/net/woggioni/jpacrepo/persistence/TestPersistenceProducer.scala b/src/test/scala/net/woggioni/jpacrepo/persistence/TestPersistenceProducer.scala deleted file mode 100644 index b92ef6e..0000000 --- a/src/test/scala/net/woggioni/jpacrepo/persistence/TestPersistenceProducer.scala +++ /dev/null @@ -1,11 +0,0 @@ -package net.woggioni.jpacrepo.persistence - -import javax.enterprise.inject.Produces -import javax.persistence.Persistence -import net.woggioni.jpacrepo.annotation.UnitTesting - -class TestPersistenceProducer { - @Produces - @UnitTesting - def createEntityManagerFactory = Persistence.createEntityManagerFactory("test") -} \ No newline at end of file diff --git a/src/test/scala/net/woggioni/jpacrepo/sample/ExampleSpec.scala b/src/test/scala/net/woggioni/jpacrepo/sample/ExampleSpec.scala deleted file mode 100644 index 1e5f040..0000000 --- a/src/test/scala/net/woggioni/jpacrepo/sample/ExampleSpec.scala +++ /dev/null @@ -1,38 +0,0 @@ -package net.woggioni.jpacrepo.sample - -import net.woggioni.jpacrepo.persistence.InitialSchemaAction -import net.woggioni.jpacrepo.version.VersionComparator -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.collection.mutable - -class ExampleSpec extends AnyFlatSpec with Matchers { - - def foo(a : Int)(b : String = "walter"): Unit = { - println(s"a: $a, b: $b") - } - - def foo(c : Int): Unit = { - foo(a=c)("adfsda") - } - -// "A Stack" should "pop values in last-in-first-out order" in { -// val stack = new mutable.Stack[Int] -// stack.push(1) -// stack.push(2) -// stack.pop() should be (2) -// stack.pop() should be (1) -// } -// -// it should "throw NoSuchElementException if an empty stack is popped" in { -// val emptyStack = new mutable.Stack[Int] -// a [NoSuchElementException] should be thrownBy { -// emptyStack.pop() -// } -// } - - "sdfgf" should "dfgfd" in { - foo(a=5)() - } -} \ No newline at end of file diff --git a/src/test/scala/net/woggioni/jpacrepo/service/PacmanServiceEJBTest.scala b/src/test/scala/net/woggioni/jpacrepo/service/PacmanServiceEJBTest.scala deleted file mode 100644 index 31970fa..0000000 --- a/src/test/scala/net/woggioni/jpacrepo/service/PacmanServiceEJBTest.scala +++ /dev/null @@ -1,58 +0,0 @@ -package net.woggioni.jpacrepo.service - -import javax.enterprise.util.TypeLiteral -import net.woggioni.jpacrepo.factory.BeanFactory -import net.woggioni.jpacrepo.persistence.TestPersistenceProducer -import org.jboss.weld.environment.se.Weld - -//object WeldContainer { -// private val weld = new Weld -// weld.disableDiscovery() -// -// weld.beanClasses( -// classOf[TestPersistenceProducer], -// classOf[PacmanServiceEJB], -// classOf[ApplicationContext], -// classOf[BeanFactory], -// ) -// -// private val count = new AtomicInteger(0) -//} - -//class WeldContainer extends AutoCloseable { -// -// val container = WeldContainer.weld.initialize() -// WeldContainer.count.incrementAndGet() -// -// override def close(): Unit = { -// container.close() -// if(WeldContainer.count.decrementAndGet() == 0) { -//// WeldContainer.weld.shutdown() -// } -// } -//} - -class PacmanServiceEJBTest { - - private val weld = new Weld -// weld.disableDiscovery() - weld.alternatives(classOf[TestPersistenceProducer]) -//// - weld.beanClasses( - classOf[PacmanServiceEJB], - classOf[PacmanWebService], -// classOf[ApplicationContext], - classOf[BeanFactory], - ) - - def test = { - val container = weld.initialize() - try { - val s = getClass.getResourceAsStream("/log4j2.xml") - val service = container.select(new TypeLiteral[PacmanServiceView] {}).get() - service.syncDB() - } finally { - container.close() - } - } -} diff --git a/src/test/scala/net/woggioni/jpacrepo/service/PacmanWebServiceTest.scala b/src/test/scala/net/woggioni/jpacrepo/service/PacmanWebServiceTest.scala deleted file mode 100644 index 10b5867..0000000 --- a/src/test/scala/net/woggioni/jpacrepo/service/PacmanWebServiceTest.scala +++ /dev/null @@ -1,33 +0,0 @@ -package net.woggioni.jpacrepo.service - -class PacmanWebServiceTest { - -// val server = { -// val server = new UndertowJaxrsServer() -// server.start() -// import org.jboss.resteasy.spi.ResteasyDeployment -// val deployment = new ResteasyDeployment -// deployment.setInjectorFactoryClass("org.jboss.resteasy.cdi.CdiInjectorFactory") -// deployment.setApplicationClass(classOf[ApplicationConfig].getName) -// val di: DeploymentInfo = server.undertowDeployment(deployment) -// di.setClassLoader(classOf[ApplicationConfig].getClassLoader) -// di.setContextPath("/jpacrepo") -// di.setDeploymentName("jpacrepo") -// di.addListeners(Servlets.listener(classOf[Listener])) -// server.deploy(di) -// server -// } - -// @Test -// def foo { -// val client = ClientBuilder.newClient() -// val webTarget = client.target(TestPortProvider.generateURL("/jpacrepo/rest/pkg/map")) -// val response = webTarget.request().get() -// val res = response.getEntity -// response.getStatus -// } - - def boh = { - println(System.getProperty("net.woggioni.jpacrepo.configuration.file")) - } -} diff --git a/src/test/scala/net/woggioni/jpacrepo/version/VersionComparatorSpec.scala b/src/test/scala/net/woggioni/jpacrepo/version/VersionComparatorSpec.scala deleted file mode 100644 index 8d2a2a1..0000000 --- a/src/test/scala/net/woggioni/jpacrepo/version/VersionComparatorSpec.scala +++ /dev/null @@ -1,16 +0,0 @@ -package net.woggioni.jpacrepo.version - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.util.Random - -class VersionComparatorSpec extends AnyFlatSpec with Matchers { - "Version sorting" should "work as expected" in { - val vc = new VersionComparator - val originalList = List("1.0", "2.0", "3.0", "5.6.7.arch1-1", "5.6.7.arch3-1", "5.6.7.arch3-2", "20200421.78c0348-1") - val shuffledList = new Random(101325).shuffle(originalList) - val sortedList = shuffledList.sorted(Ordering.comparatorToOrdering(vc)) - sortedList should be (originalList) - } -}