diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index e23f81d..e168bbf 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -7,13 +7,12 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v5 + - name: Setup Java + uses: actions/setup-java@v4 with: - go-version: '>=1.24' + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '21' + cache: 'gradle' - name: Build - run: go build . - - - name: Test - run: go test . -v + run: ./gradlew build --no-daemon diff --git a/.gitignore b/.gitignore index a47f9f7..7777057 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,41 @@ -*.db3 -*.db3-* -*.db3.* -*.iml +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### .idea -pcinv +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Old SQLite DB files ### +*.db +*.db3* diff --git a/Dockerfile b/Dockerfile index 752719c..67adab6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,9 @@ -FROM golang:1.24.1-alpine3.21 - +FROM eclipse-temurin:21-jdk-alpine AS builder WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download +COPY . /app +RUN ./gradlew jar --no-daemon -COPY migrations ./migrations -COPY static ./static -COPY templates ./templates -COPY *.go ./ -RUN go build -o /pcinv - -CMD ["/pcinv"] +FROM eclipse-temurin:21-alpine +COPY --from=builder /app/build/libs/pcinv-0.0.1-SNAPSHOT.jar ./ +ENTRYPOINT ["java", "-jar", "pcinv-0.0.1-SNAPSHOT.jar"] +EXPOSE 8088/tcp diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..5497c36 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + java + id("org.springframework.boot") version "3.5.0" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "be.seeseepuff" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom(configurations.annotationProcessor.get()) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-actuator") + compileOnly("org.projectlombok:lombok") + developmentOnly("org.springframework.boot:spring-boot-devtools") + runtimeOnly("org.postgresql:postgresql") + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") + annotationProcessor("org.projectlombok:lombok") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/data.go b/data.go deleted file mode 100644 index 1ff9338..0000000 --- a/data.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "fmt" - "gitea.seeseepuff.be/seeseemelk/mysqlite" -) - -func (a *App) getAllTypes(column, table string) ([]string, error) { - var types []string - var err error - for row := range a.db.Query(fmt.Sprintf("SELECT %s FROM %s GROUP BY %s ORDER BY %s ASC", column, table, column, column)).Range(&err) { - var name string - err := row.Scan(&name) - if err != nil { - return nil, err - } - types = append(types, name) - } - return types, err -} - -func (a *App) GetAllBrands() ([]string, error) { - return a.getAllTypes("brand", "assets") -} - -func (a *App) GetAllRamTypes() ([]string, error) { - return a.getAllTypes("type", "info_ram") -} - -func (a *App) GetAllHddTypes() ([]string, error) { - return a.getAllTypes("type", "info_hdd") -} - -func (a *App) GetAllHddFormFactors() ([]string, error) { - return a.getAllTypes("form_factor", "info_hdd") -} - -func (a *App) GetAllHddConnections() ([]string, error) { - return a.getAllTypes("connection", "info_hdd") -} - -func (a *App) GetAllHddSpeeds() ([]string, error) { - return a.getAllTypes("rpm", "info_hdd") -} - -func (a *App) GetAllGroups(vm *CreateDeviceVM) error { - var err error - vm.AssetBrands, err = a.GetAllBrands() - if err != nil { - return err - } - - vm.RamTypes, err = a.GetAllRamTypes() - if err != nil { - return err - } - - vm.HddTypes, err = a.GetAllHddTypes() - if err != nil { - return err - } - - vm.HddFormFactors, err = a.GetAllHddFormFactors() - if err != nil { - return err - } - - vm.HddFormFactors, err = a.GetAllHddConnections() - if err != nil { - return err - } - - vm.HddRpms, err = a.GetAllHddSpeeds() - if err != nil { - return err - } - return nil -} - -func (a *App) GetAllTypes() ([]string, error) { - var types []string - var err error - for row := range a.db.Query("SELECT type FROM assets GROUP BY type ORDER BY type ASC").Range(&err) { - var name string - err := row.Scan(&name) - if err != nil { - return nil, err - } - types = append(types, name) - } - return types, err -} - -func (a *App) GetAssetCount() (int, error) { - var count int - err := a.db.Query("SELECT COUNT(*) FROM assets").ScanSingle(&count) - return count, err -} - -func (a *App) GetBrandCount() (int, error) { - var count int - err := a.db.Query("SELECT COUNT(DISTINCT brand) FROM assets").ScanSingle(&count) - return count, err -} - -func (a *App) GetTotalRamCapacity() (int, error) { - var capacity int - err := a.db.Query("SELECT SUM(capacity) FROM info_ram").ScanSingle(&capacity) - return capacity, err -} - -func (a *App) DeleteAsset(tx *mysqlite.Tx, qr int) error { - err := tx.Query("DELETE FROM assets WHERE qr=?").Bind(qr).Exec() - if err != nil { - return err - } - - err = tx.Query("DELETE FROM info_ram WHERE asset=?").Bind(qr).Exec() - if err != nil { - return err - } - - return nil -} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ed02886 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + database: + image: postgres:latest + environment: + POSTGRES_USER: pcinv + POSTGRES_PASSWORD: pcinv + POSTGRES_DB: pcinv + ports: + - "5432:5432" diff --git a/go.mod b/go.mod deleted file mode 100644 index 54d933d..0000000 --- a/go.mod +++ /dev/null @@ -1,47 +0,0 @@ -module pcinv - -go 1.24 - -toolchain go1.24.1 - -require ( - gitea.seeseepuff.be/seeseemelk/mysqlite v0.9.0 - github.com/gin-gonic/gin v1.10.0 -) - -require ( - github.com/bytedance/sonic v1.13.1 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/gin-contrib/sse v1.0.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.25.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.15.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.55.3 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.33.1 // indirect - zombiezen.com/go/sqlite v1.4.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 7c65f36..0000000 --- a/go.sum +++ /dev/null @@ -1,126 +0,0 @@ -gitea.seeseepuff.be/seeseemelk/mysqlite v0.9.0 h1:GaU2DSrgDfZEqST3HdnNgfKSI4sNXvMm8SSfeMvBxA4= -gitea.seeseepuff.be/seeseemelk/mysqlite v0.9.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE= -github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= -github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= -github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= -github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= -github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= -golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= -modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= -modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= -modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= -modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= -modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= -modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= -modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= -modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= -modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU= -zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik= diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ca025c8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/main.go b/main.go deleted file mode 100644 index 14526ea..0000000 --- a/main.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "embed" - "html/template" - "log" - "net/http" - "os" - - "gitea.seeseepuff.be/seeseemelk/mysqlite" - "github.com/gin-gonic/gin" -) - -//go:embed migrations/*.sql -var migrations embed.FS - -//go:embed templates/*.gohtml -var templateFS embed.FS - -func main() { - log.Println("Starting...") - - // Get database path from environment variable or use default - dbPath := os.Getenv("PCINV_DB") - if dbPath == "" { - dbPath = "pcinv.db3" - } - - db, err := mysqlite.OpenDb(dbPath) - if err != nil { - log.Fatalf("error opening db: %v", err) - } - defer db.MustClose() - - err = db.MigrateDb(migrations, "migrations") - if err != nil { - log.Fatalf("error migrating db: %v", err) - } - - app := &App{ - db: db, - } - - templates, err := template.New("undefined.gohtml"). - Funcs(template.FuncMap{ - "statusText": http.StatusText, - "createDeviceLink": createDeviceLink, - "formatMemorySize": formatMemorySize, - "formatMemoryPlainSize": formatMemoryPlainSize, - "formatType": formatType, - "isRamType": isRamType, - "createSelectMenu": createSelectMenu, - "createSelectMenuDefault": createSelectMenuDefault, - }). - ParseFS(templateFS, "templates/*.gohtml") - - if err != nil { - log.Fatalf("error parsing templates: %v", err) - } - - gin.DebugPrintFunc = log.Printf - gin.SetMode(gin.ReleaseMode) - r := gin.Default() - r.SetHTMLTemplate(templates) - r.StaticFS("/static", staticFiles) - r.Use(errorHandler) - r.GET("/", app.getIndex) - r.GET("/device", app.getDevice) - r.GET("/create", app.getCreateDevice) - r.POST("/create", app.postCreateDevice) - r.GET("/browse", app.getBrowse) - r.GET("/delete", app.getDelete) - r.POST("/delete", app.postDelete) - err = r.Run() - if err != nil { - log.Fatalf("error serving website: %v", err) - } -} - -type ErrorVM struct { - Errors []*gin.Error - StatusCode int -} - -func errorHandler(c *gin.Context) { - c.Next() - if len(c.Errors) != 0 { - vm := ErrorVM{ - Errors: c.Errors, - StatusCode: c.Writer.Status(), - } - c.HTML(vm.StatusCode, "errors", vm) - } -} diff --git a/migrations/1_initial.sql b/migrations/1_initial.sql deleted file mode 100644 index d6a9938..0000000 --- a/migrations/1_initial.sql +++ /dev/null @@ -1,19 +0,0 @@ -create table assets ( - qr integer unique, - type text not null, - brand text, - name text, - description text -) strict; - -create table worklog ( - asset integer not null, - timestamp integer not null, - action text not null -) strict; - -create table info_ram ( - asset integer not null unique, - capacity integer, - type text -) strict; diff --git a/migrations/2_add_harddrives.sql b/migrations/2_add_harddrives.sql deleted file mode 100644 index d2b0225..0000000 --- a/migrations/2_add_harddrives.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table info_hdd ( - asset integer not null unique, - capacity integer, - type text, - form_factor text, - connection text, - rpm integer -); diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..3134321 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "pcinv" diff --git a/src/main/java/be/seeseepuff/pcinv/PcinvApplication.java b/src/main/java/be/seeseepuff/pcinv/PcinvApplication.java new file mode 100644 index 0000000..1aabf6f --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/PcinvApplication.java @@ -0,0 +1,13 @@ +package be.seeseepuff.pcinv; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PcinvApplication { + + public static void main(String[] args) { + SpringApplication.run(PcinvApplication.class, args); + } + +} diff --git a/src/main/java/be/seeseepuff/pcinv/controllers/StartupController.java b/src/main/java/be/seeseepuff/pcinv/controllers/StartupController.java new file mode 100644 index 0000000..603c8df --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/controllers/StartupController.java @@ -0,0 +1,23 @@ +package be.seeseepuff.pcinv.controllers; + +import be.seeseepuff.pcinv.services.AssetService; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@RequiredArgsConstructor +@Service +public class StartupController { + private final AssetService assetService; + + @PostConstruct + public void init() { + var descriptors = assetService.getAssetDescriptors(); + log.info("Asset descriptors loaded:\n{}", descriptors); + + var assets = assetService.countAssets(); + log.info("The repository contains {} assets.", assets); + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java b/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java new file mode 100644 index 0000000..7ad845b --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java @@ -0,0 +1,227 @@ +package be.seeseepuff.pcinv.controllers; + +import be.seeseepuff.pcinv.services.AssetService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.repository.query.Param; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.HashMap; + +/** + * Controller for handling web requests related to assets. + * Provides endpoints for viewing and creating assets. + */ +@RequiredArgsConstructor +@Controller +public class WebController { + /// The name of the model attribute that holds the global asset descriptors. + private static final String DESCRIPTORS = "descriptors"; + /// The name of the model attribute that holds the asset descriptor for the current view. + private static final String DESCRIPTOR = "descriptor"; + /// The name of the model attribute that holds the list of assets. + private static final String ASSETS = "assets"; + /// The name of the model attribute that holds the asset being viewed or edited. + private static final String ASSET = "asset"; + /// The name of the model attribute that holds a list of all properties of all descriptors. + private static final String PROPERTIES = "properties"; + /// The name of the model attribute that holds the action to be performed. + private static final String ACTION = "action"; + /// The name of the model attribute that holds the current time in milliseconds. + private static final String TIME = "time"; + /// The name of the model attribute that holds the input lists for creating or editing assets. + private static final String INPUT_LIST = "inputLists"; + + private final AssetService assetService; + + /** + * Handles the root URL and returns the index page with asset descriptors and asset count. + */ + @GetMapping("/") + public String index(Model model) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors()); + model.addAttribute("asset_count", assetService.countAssets()); + return "index"; + } + + /** + * Shows a view where a user can select the type of asset to browse. + */ + @GetMapping("/browse") + public String browse(Model model) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors()); + return "browse"; + } + + /** + * Handles the browsing of assets by type. + * Displays the asset descriptor tree and the specific descriptor for the given type. + * + * @param model The model to add attributes to. + * @param type The type of asset to browse. + * @return The view name for browsing assets by type. + */ + @GetMapping("/browse/{type}") + public String browseType(Model model, @PathVariable String type) { + model.addAttribute(TIME, System.currentTimeMillis()); + var tree = assetService.getAssetDescriptorTree(type); + model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type)); + model.addAttribute(DESCRIPTORS, tree); + model.addAttribute(PROPERTIES, tree.stream().flatMap(d -> d.getProperties().stream()).toList()); + model.addAttribute(ASSETS, assetService.getAssetsByType(type)); + return "browse_type"; + } + + /** + * Handles the view of an asset by its QR code. + * If the asset does not exist, it redirects to the index page. + * + * @param qr The QR code of the asset to view. + */ + @GetMapping("/view/{qr}") + public String view(Model model, @PathVariable long qr) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(ACTION, "view"); + return renderView(model, qr); + } + + /** + * Show a page asking if the user wants to delete an asset. + * + * @param qr The QR code of the asset to delete. + */ + @GetMapping("/delete/{qr}") + public String delete(Model model, @PathVariable long qr, @Param("confirm") boolean confirm) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(ACTION, "delete"); + if (confirm) { + var asset = assetService.getAssetByQr(qr); + if (asset == null) { + return "redirect:/"; + } + var type = asset.getAsset().getType(); + assetService.deleteAsset(qr); + return "redirect:/browse/" + type; + } + return renderView(model, qr); + } + + /** + * Renders the view for an asset based on its QR code. + * If the asset does not exist, it redirects to the index page. + * + * @param model The model to add attributes to. + * @param qr The QR code of the asset to view. + * @return The view name for viewing the asset. + */ + private String renderView(Model model, long qr) { + var asset = assetService.getAssetByQr(qr); + if (asset == null) { + return "redirect:/"; + } + model.addAttribute(ASSET, asset); + model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(asset.getAsset().getType())); + model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(asset.getAsset().getType())); + return "view"; + } + + @GetMapping("/search") + public String search(@Param("qr") long qr) { + return "redirect:/view/" + qr; + } + + /** + * Shows a view where the user can create the type of asset to create. + */ + @GetMapping("/create") + public String create(Model model) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors()); + return "create_select"; + } + + /** + * Shows a view where the user can create a specific type of asset. + * + * @param type The type of asset to create. + */ + @GetMapping("/create/{type}") + public String createType(Model model, @PathVariable String type) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(ACTION, "create"); + model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(type)); + model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type)); + model.addAttribute(INPUT_LIST, assetService.getInputList(type)); + return "create_asset"; + } + + /** + * Shows a view where the user can edit an existing asset. + * + * @param qr The QR code of the asset to edit. + */ + @GetMapping("/edit/{qr}") + public String edit(Model model, @PathVariable long qr) { + model.addAttribute(TIME, System.currentTimeMillis()); + var asset = assetService.getAssetByQr(qr); + if (asset == null) { + throw new RuntimeException("Asset not found"); + } + String assetType = asset.getAsset().getType(); + model.addAttribute(ACTION, "edit"); + model.addAttribute(ASSET, asset); + model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(assetType)); + model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(assetType)); + model.addAttribute(INPUT_LIST, assetService.getInputList(assetType)); + return "create_asset"; + } + + /** + * Actually edits an asset based on the form data submitted. + * + * @param qr The QR code of the asset to edit. + */ + @PostMapping("/edit/{qr}") + public String editPost(Model model, @PathVariable long qr, @RequestBody MultiValueMap formData) { + model.addAttribute(TIME, System.currentTimeMillis()); + var formMap = new HashMap(); + formData.forEach((key, values) -> { + if (!values.isEmpty()) { + formMap.put(key, values.getFirst()); + } + }); + var asset = assetService.editAsset(qr, formMap); + return "redirect:/view/" + asset.getQr(); + } + + /** + * Handles the form submission for creating an asset. + * + * @param model The model to add attributes to. + * @param type The type of asset to create. + * @return The view name for creating the asset. + */ + @PostMapping( + value = "/create/{type}", + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE + ) + public String createTypePost(Model model, @PathVariable String type, @RequestBody MultiValueMap formData) { + model.addAttribute(TIME, System.currentTimeMillis()); + var formMap = new HashMap(); + formData.forEach((key, values) -> { + if (!values.isEmpty()) { + formMap.put(key, values.getFirst()); + } + }); + var asset = assetService.createAsset(type, formMap); + return "redirect:/view/" + asset.getQr(); + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java new file mode 100644 index 0000000..ce59f3f --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java @@ -0,0 +1,113 @@ +package be.seeseepuff.pcinv.meta; + +import be.seeseepuff.pcinv.models.Asset; +import lombok.Builder; +import lombok.Getter; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Describes the properties of an asset + */ +@Getter +@Builder +public class AssetDescriptor { + /// The type of property, e.g.: ram, asset, etc... + private final String type; + + /// The displayable name of the property, e.g.: "Random Access Memory" + private final String displayName; + + /// The plural name of the property, e.g.: "Random Access Memories" + private final String pluralName; + + /// Whether the asset is visible in the user interface. + private final boolean visible; + + /// The properties of the asset, such as "brand", "model", etc. + @lombok.Singular + private Collection properties; + + /// A supplier that can be used to create a new instance of the asset type described by this descriptor. + private Supplier instanceProducer; + + /** + * Loads the asset properties from a given asset class. + * + * @param assetType The class of the asset to load properties for. + * @return An AssetProperties instance containing the loaded properties. + */ + public static AssetDescriptor loadFrom(Class assetType) { + var assetInfo = assetType.getAnnotation(AssetInfo.class); + Objects.requireNonNull(assetInfo, "Asset class must be annotated with @AssetInfo"); + var builder = AssetDescriptor.builder() + .type(assetInfo.type()) + .displayName(assetInfo.displayName()) + .pluralName(assetInfo.pluralName()) + .visible(assetInfo.isVisible()) + .properties(Arrays.stream(assetType.getDeclaredFields()) + .map(field -> AssetProperty.loadFrom(field, assetInfo.type())) + .filter(Objects::nonNull) + .toList()); + if (Asset.class.isAssignableFrom(assetType)) { + builder.instanceProducer(() -> { + try { + return (Asset) assetType.getConstructor().newInstance(); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) { + throw new RuntimeException(e); + } + }); + } + return builder.build(); + } + + /** + * Returns a string representation of the asset properties with the specified indentation. + * + * @param indent The indentation to use for formatting. + * @return A formatted string representation of the asset properties. + */ + public String toString(String indent) { + var builder = new StringBuilder(); + builder.append(indent).append("AssetProperties {\n"); + builder.append(indent).append(" type='").append(type).append("',\n"); + builder.append(indent).append(" displayName='").append(displayName).append("',\n"); + if (!visible) { + builder.append(indent).append(" hidden").append(",\n"); + } + builder.append(indent).append(" properties=[\n"); + for (var property : properties) { + builder.append(indent).append(" ").append(property.toString()).append("\n"); + } + builder.append(indent).append(" ]\n"); + builder.append(indent).append('}'); + return builder.toString(); + } + + /** + * Returns a string representation of the asset property in the format "type-propertyName". + * + * @param property The asset property to format. + * @return A string representation of the asset property. + */ + public String asString(AssetProperty property) { + return String.format("%s-%s", type, property.getName()); + } + + /** + * Creates a new instance of the asset type described by this descriptor. + * + * @return A new instance of the asset type. + */ + public Asset newInstance() { + if (instanceProducer == null) { + throw new IllegalStateException("Instance producer is not set for asset descriptor: " + type); + } + return instanceProducer.get(); + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptors.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptors.java new file mode 100644 index 0000000..fce5d91 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptors.java @@ -0,0 +1,35 @@ +package be.seeseepuff.pcinv.meta; + +import lombok.Getter; + +import java.util.Collection; +import java.util.Comparator; +import java.util.TreeSet; + +/** + * Holds the descriptors for all possible asset types. + */ +@Getter +public class AssetDescriptors { + /// A collection of all types of assets. + private final Collection assets = new TreeSet<>(Comparator.comparing(AssetDescriptor::getDisplayName)); + + /** + * Loads the asset properties from a given asset type. + * + * @param assetType The type of the asset to load properties for. + */ + public void loadFrom(Class assetType) { + var property = AssetDescriptor.loadFrom(assetType); + assets.add(property); + } + + @Override + public String toString() { + var builder = new StringBuilder(); + builder.append("AssetDescriptors [\n"); + assets.forEach(assetDescriptor -> builder.append(assetDescriptor.toString(" ")).append('\n')); + builder.append("]"); + return builder.toString(); + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetEnum.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetEnum.java new file mode 100644 index 0000000..b8ef941 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetEnum.java @@ -0,0 +1,15 @@ +package be.seeseepuff.pcinv.meta; + +/** + * An interface that should be implemented by all asset enums. + */ +public interface AssetEnum { + /// Get the internal value of the enum. + String getValue(); + + /// Get the display name of the enum. + String getDisplayName(); + + /// Get the default value of the enum that should be selected when the user creates a device. + boolean isDefaultValue(); +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetInfo.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetInfo.java new file mode 100644 index 0000000..28ad4a9 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetInfo.java @@ -0,0 +1,41 @@ +package be.seeseepuff.pcinv.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Provides metadata about an asset type itself. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface AssetInfo { + /** + * The displayable name of the asset type. + * + * @return the display name of the asset type + */ + String displayName(); + + /** + * The plural name of the asset type, used for display purposes. + * + * @return the plural name of the asset type + */ + String pluralName(); + + /** + * The type of the asset, which can be a string or an integer. + * + * @return the type of the asset + */ + String type(); + + /** + * Indicates whether the asset type should be visible in the UI. + * + * @return true if the asset type is visible, false otherwise + */ + boolean isVisible() default true; +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetOption.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetOption.java new file mode 100644 index 0000000..aa22bf2 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetOption.java @@ -0,0 +1,20 @@ +package be.seeseepuff.pcinv.meta; + +import lombok.Builder; +import lombok.Getter; + +/** + * Represents an option for an asset property enum. + */ +@Getter +@Builder +public class AssetOption { + /// The internal value of the option. + private final String value; + /// The display name of the option. + private final String displayName; + /// The actual enum value associated with this option. + private final Object enumConstant; + /// Whether this option is the default value for the property. + private final boolean isDefaultValue; +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java new file mode 100644 index 0000000..a3c94f5 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java @@ -0,0 +1,222 @@ +package be.seeseepuff.pcinv.meta; + +import be.seeseepuff.pcinv.models.Asset; +import be.seeseepuff.pcinv.models.AssetCondition; +import be.seeseepuff.pcinv.models.GenericAsset; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Represents a property of an asset, such as its name or type. + * This class is used to define the metadata for assets in the system. + */ +@Getter +@Builder +public class AssetProperty { + /// The name of the property, e.g., "brand", "model", etc. + private final String name; + /// The name of the property as it should be displayed, e.g., "Brand", "Model", etc. + private final String displayName; + /// The type of the property, which can be a string or an integer. + private final Type type; + /// Whether the property is required for the asset. + private final boolean required; + /// A set of options for the property, used for enum types. + @Singular + private final List options; + /// Whether the capacity should be displayed in SI units (e.g., GB, MB). + private final boolean capacityAsSI; + /// Whether the capacity should be displayed in IEC units (e.g., GiB, MiB). + private final boolean capacityAsIEC; + /// A setter function that can be used to set the value of the property on an asset. + private final BiConsumer setter; + /// A getter function that can be used to get the value of the property from an asset. + private final Function getter; + /// Whether the property is an input list. + private final boolean inputList; + /// Whether the property should be hidden in the overview. + private final boolean hideInOverview; + /// A description of the property, if any. + private final String description; + + /** + * Enum representing the possible types of asset properties. + */ + @AllArgsConstructor + public enum Type { + STRING(false), + INTEGER(false), + CAPACITY(false), + CONDITION(true), + ; + /// Set to `true` if the type is an enum, `false` otherwise. + public final boolean isEnum; + + /** + * Returns the name of the type, or "enum" if it is an enum type. + * + * @return The name of the type or "enum" if it is an enum. + */ + public String nameOrEnum() { + return isEnum ? "enum" : name(); + } + } + + /** + * Loads an AssetProperty from a given field. + * + * @param property The field representing the property. + * @return An AssetProperty instance with the name, display name, and type determined from the field. + */ + @Nullable + public static AssetProperty loadFrom(Field property, @Nonnull String assetType) { + var annotation = property.getAnnotation(Property.class); + if (annotation == null) { + return null; + } + + var type = determineType(property); + var builder = AssetProperty.builder() + .name(property.getName()) + .displayName(annotation.value()) + .type(type) + .required(annotation.required()) + .inputList(property.isAnnotationPresent(InputList.class)) + .hideInOverview(property.isAnnotationPresent(HideInOverview.class)) + .description(property.isAnnotationPresent(Description.class) ? property.getAnnotation(Description.class).value() : "") + .setter((obj, value) -> { + try { + property.setAccessible(true); + property.set(obj, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }) + .getter(obj -> { + try { + if (assetType.equals(GenericAsset.TYPE) && obj instanceof Asset asset) { + obj = asset.getAsset(); + } + property.setAccessible(true); + return property.get(obj); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + + if (type.isEnum) { + var enumConstants = property.getType().getEnumConstants(); + for (var enumConstant : enumConstants) { + if (!(enumConstant instanceof AssetEnum assetEnum)) { + throw new IllegalArgumentException("Property " + enumConstant.getClass().getName() + " does not implement AssetEnum"); + } + var option = AssetOption.builder() + .value(assetEnum.getValue()) + .displayName(assetEnum.getDisplayName()) + .isDefaultValue(assetEnum.isDefaultValue()) + .enumConstant(enumConstant) + .build(); + builder.option(option); + } + } + if (type == Type.CAPACITY) { + var capacityAnnotation = property.getAnnotation(Capacity.class); + builder.capacityAsSI(capacityAnnotation.si()); + builder.capacityAsIEC(capacityAnnotation.iec()); + } + return builder.build(); + } + + /** + * Determines the type of the property based on its field type. + * + * @param property The field representing the property. + * @return The type of the property. + * @throws IllegalArgumentException if the property type is unsupported. + */ + private static Type determineType(Field property) { + if (property.getType() == String.class) { + return Type.STRING; + } else if (property.isAnnotationPresent(Capacity.class)) { + return Type.CAPACITY; + } else if (property.getType() == Integer.class || property.getType() == int.class || property.getType() == Long.class || property.getType() == long.class) { + return Type.INTEGER; + } else if (property.getType() == AssetCondition.class) { + return Type.CONDITION; + } else { + throw new IllegalArgumentException("Unsupported property type: " + property.getType()); + } + } + + /** + * Sets the value of the property on the given asset. + * + * @param asset The asset to set the property on. + * @param value The value to set for the property. + */ + public void setValue(Object asset, Object value) { + if (value == null && required) { + throw new IllegalArgumentException("Property '" + name + "' is required but received null value."); + } + setter.accept(asset, value); + } + + /** + * Gets the value of the property from the given asset. + * + * @param asset The asset to get the property value from. + * @return The value of the property. + */ + @Nullable + public Object getValue(@Nullable Object asset) { + if (asset == null) { + return null; + } + return getter.apply(asset); + } + + /** + * Renders the value of the property as a string. + * + * @return The rendered value as a string. + */ + @Nullable + public String renderValue(@Nullable Object asset) { + if (asset == null) { + return null; + } + var value = getValue(asset); + if (value == null) { + return "Unknown"; + } else if (type == Type.INTEGER || type == Type.STRING) { + return value.toString(); + } else if (type == Type.CAPACITY) { + return String.format("%s bytes", value); + } else if (type.isEnum) { + if (value instanceof AssetEnum assetEnum) { + return assetEnum.getDisplayName(); + } + throw new IllegalArgumentException("Expected value to be an instance of AssetEnum, but got: " + value.getClass().getName()); + } else { + return value.toString(); + } + } + + @Override + public String toString() { + var enumOptions = ""; + if (type.isEnum) { + enumOptions = " [" + String.join(", ", options.stream().map(AssetOption::getValue).toList()) + "]"; + } + return String.format("%s:%s (%s)%s", name, type.name(), displayName, enumOptions); + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/Capacity.java b/src/main/java/be/seeseepuff/pcinv/meta/Capacity.java new file mode 100644 index 0000000..d5db0bb --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/Capacity.java @@ -0,0 +1,20 @@ +package be.seeseepuff.pcinv.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to mark a property descriptor as representing some sort of capacity. + * It is used to render the capacity in the UI as bytes, kilobytes, megabytes, etc. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Capacity { + /// Set to `true` if the capacity should include SI units (e.g., 1 KB = 1000 bytes). + boolean si() default false; + + /// Set to `true` if the capacity should be displayed in IEC units (e.g., 1 KiB = 1024 bytes). + boolean iec() default true; +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/Description.java b/src/main/java/be/seeseepuff/pcinv/meta/Description.java new file mode 100644 index 0000000..f453271 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/Description.java @@ -0,0 +1,20 @@ +package be.seeseepuff.pcinv.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to provide a description for an asset. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Description { + /** + * The description of the asset. + * + * @return The description text. + */ + String value() default ""; +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/HideInOverview.java b/src/main/java/be/seeseepuff/pcinv/meta/HideInOverview.java new file mode 100644 index 0000000..a25f732 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/HideInOverview.java @@ -0,0 +1,14 @@ +package be.seeseepuff.pcinv.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the field should not be included in the overview of an asset. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface HideInOverview { +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/InputList.java b/src/main/java/be/seeseepuff/pcinv/meta/InputList.java new file mode 100644 index 0000000..333fa7c --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/InputList.java @@ -0,0 +1,15 @@ +package be.seeseepuff.pcinv.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that any form for the given field should be rendered as a dropdown + * list with optional manual text input. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface InputList { +} diff --git a/src/main/java/be/seeseepuff/pcinv/meta/Property.java b/src/main/java/be/seeseepuff/pcinv/meta/Property.java new file mode 100644 index 0000000..0db3629 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/Property.java @@ -0,0 +1,27 @@ +package be.seeseepuff.pcinv.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to mark a property descriptor of an asset. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Property { + /** + * The displayable name of the property. + * + * @return the display name of the property + */ + String value(); + + /** + * Whether the property is required for the asset. + * + * @return true if the property is required, false otherwise + */ + boolean required() default false; +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/Asset.java b/src/main/java/be/seeseepuff/pcinv/models/Asset.java new file mode 100644 index 0000000..0d97e25 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/Asset.java @@ -0,0 +1,13 @@ +package be.seeseepuff.pcinv.models; + +public interface Asset { + long getId(); + + GenericAsset getAsset(); + + default long getQr() { + return getAsset().getQr(); + } + + void setAsset(GenericAsset asset); +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/AssetCondition.java b/src/main/java/be/seeseepuff/pcinv/models/AssetCondition.java new file mode 100644 index 0000000..4392a36 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/AssetCondition.java @@ -0,0 +1,32 @@ +package be.seeseepuff.pcinv.models; + +import be.seeseepuff.pcinv.meta.AssetEnum; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Represents the condition of an asset in the inventory system. + */ +@Getter +@RequiredArgsConstructor +public enum AssetCondition implements AssetEnum +{ + /// The asset is in perfect working order. + HEALTHY("healthy", "Healthy"), + /// The condition of the asset is unknown. E.g.: it is untested. + UNKNOWN("unknown", "Not known"), + /// The asset generally works, but has some known issues. + PARTIAL("partial", "Partially working"), + /// The asset is in need of repair, but is not completely broken. + REPAIR("repair", "Requires repairs"), + /// The asset is completely broken and cannot be used. + BORKED("borked", "Borked"), + ; + private final String value; + private final String displayName; + + @Override + public boolean isDefaultValue() { + return this == UNKNOWN; + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/ChassisAsset.java b/src/main/java/be/seeseepuff/pcinv/models/ChassisAsset.java new file mode 100644 index 0000000..687c6ac --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/ChassisAsset.java @@ -0,0 +1,53 @@ +package be.seeseepuff.pcinv.models; + +import be.seeseepuff.pcinv.meta.AssetInfo; +import be.seeseepuff.pcinv.meta.Description; +import be.seeseepuff.pcinv.meta.HideInOverview; +import be.seeseepuff.pcinv.meta.Property; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@AssetInfo( + displayName = "Chassis", + pluralName = "Chassis", + type = "chassis" +) +@Table(name = "chassis_assets") +public class ChassisAsset implements Asset +{ + @Id + @GeneratedValue + private long id; + + /// The generic asset associated with this RAM. + @OneToOne(orphanRemoval = true) + private GenericAsset asset; + + @Description("The number of 5.25\" drive bays in the chassis.") + @Property("5.25\" Bays") + @HideInOverview + private Long bay5_25Count; + + @Description("The number of 3.5\" drive bays in the chassis.") + @Property("3.5\" Bays") + @HideInOverview + private Long bay3_5Count; + + @Description("The number of internal 3.5\" drive bays in the chassis.") + @Property("Internal 3.5\" Bays") + @HideInOverview + private Long internalBay3_5Count; + + @Description("The number of 2.5\" drive bays in the chassis.") + @Property("2.5\" Bays") + @HideInOverview + private Long bay2_5Count; + + @Description("The number of expansion slots in the chassis.") + @Property("Expansion Slots") + private Long expansionSlotCount; +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/DisplayAdapterAsset.java b/src/main/java/be/seeseepuff/pcinv/models/DisplayAdapterAsset.java new file mode 100644 index 0000000..b74d1c2 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/DisplayAdapterAsset.java @@ -0,0 +1,48 @@ +package be.seeseepuff.pcinv.models; + +import be.seeseepuff.pcinv.meta.*; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@AssetInfo( + displayName = "Display Adapter", + pluralName = "Display Adapters", + type = "gpu" +) +@Table(name = "gpu_assets") +public class DisplayAdapterAsset implements Asset +{ + @Id + @GeneratedValue + private long id; + + /// The generic asset associated with this RAM. + @OneToOne(orphanRemoval = true) + private GenericAsset asset; + + @Property("VRAM Capacity") + @Capacity + private Long vramCapacity; + + @Description("The type of interface. E.g.: AGP, PCIe, ISA-8, etc...") + @Property("Interface") + @HideInOverview + @InputList + private String interfaceType; + + @Description("The highest version of DirectX supported by this display adapter.") + @Property("DirectX Version") + @HideInOverview + @InputList + private String directXVersion; + + @Description("The highest version of OpenGL supported by this display adapter.") + @Property("OpenGL Version") + @HideInOverview + @InputList + private String openGLVersion; +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java b/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java new file mode 100644 index 0000000..a327c46 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java @@ -0,0 +1,68 @@ +package be.seeseepuff.pcinv.models; + +import be.seeseepuff.pcinv.meta.*; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +/** + * Represents a generic asset in the inventory system. + */ +@Getter +@Setter +@Entity +@AssetInfo( + displayName = "Asset", + pluralName = "Assets", + type = "asset", + isVisible = false +) +@Table( + name = "assets", + uniqueConstraints = @UniqueConstraint(columnNames = "qr") +) +public class GenericAsset +{ + public static final String TYPE = "asset"; + + @Id @GeneratedValue + private long id; + + /// The QR code attached to the asset, used for identification. + @Property(value = "QR", required = true) + @Description("The QR code attached to the asset, used for identification.") + private long qr; + + /// The type of asset + private String type; + + @Property("Brand") + @InputList + private String brand; + + @Property("Model") + private String model; + + @Property("Serial Number") + @HideInOverview + private String serialNumber; + + @Property("Description") + @HideInOverview + private String description; + + @Property("Condition") + private AssetCondition condition; + + @Property("Manufacture Date") + @HideInOverview + private String manufactureDate; + + @Property("Purchase Date") + @HideInOverview + private String purchaseDate; + + @Property("Warranty Expiration Date") + @HideInOverview + private String warrantyExpirationDate; +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/HddAsset.java b/src/main/java/be/seeseepuff/pcinv/models/HddAsset.java new file mode 100644 index 0000000..ec79f8d --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/HddAsset.java @@ -0,0 +1,57 @@ +package be.seeseepuff.pcinv.models; + +import be.seeseepuff.pcinv.meta.*; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +/** + * Represents a hard drive or similar device. + */ +@Getter +@Setter +@Entity +@AssetInfo( + displayName = "Hard Drive", + pluralName = "Hard Drives", + type = "hdd" +) +@Table(name = "hdd_assets") +public class HddAsset implements Asset +{ + @Id + @GeneratedValue + private long id; + + @OneToOne(orphanRemoval = true) + private GenericAsset asset; + + @Property("Capacity") + @Capacity(si = true, iec = true) + private Long capacity; + + @Description("The drive's interface type, such as SATA, IDE, ISA-16, ...") + @Property("Interface Type") + @InputList + private String interfaceType; + + @Description("The drive's form factor, such as 2.5\", 3.5\", etc.") + @Property("Form Factor") + @InputList + private String formFactor; + + @Description("The drive's RPM (Revolutions Per Minute) speed, if applicable.") + @Property("RPM Speed") + @HideInOverview + private Long rpmSpeed; + + @Description("The drive's cache size, if applicable.") + @Property("Cache Size") + @HideInOverview + private Long cacheSize; + + @Description("The drive's type, such as HDD, SSD, etc.") + @Property("Drive Type") + @InputList + private String driveType; +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/MotherboardAsset.java b/src/main/java/be/seeseepuff/pcinv/models/MotherboardAsset.java new file mode 100644 index 0000000..d3dddb1 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/MotherboardAsset.java @@ -0,0 +1,108 @@ +package be.seeseepuff.pcinv.models; + +import be.seeseepuff.pcinv.meta.*; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@AssetInfo( + displayName = "Motherboard", + pluralName = "Motherboards", + type = "motherboard" +) +@Table(name = "motherboard_assets") +public class MotherboardAsset implements Asset +{ + @Id + @GeneratedValue + private long id; + + /// The generic asset associated with this RAM. + @OneToOne(orphanRemoval = true) + private GenericAsset asset; + + @Description("The number of 16-bit ISA slots on the motherboard.") + @Property("ISA-16 Slots") + @HideInOverview + private Long isa16Count; + + @Description("The number of 8-bit ISA slots on the motherboard.") + @Property("ISA-8 Slots") + @HideInOverview + private Long isa8Count; + + @Description("The number of PCI slots on the motherboard.") + @Property("PCI Slots") + @HideInOverview + private Long pciCount; + + @Description("The number of PCIe x1 slots on the motherboard.") + @Property("PCIe x1 Slots") + @HideInOverview + private Long pcieX1Count; + + @Description("The number of PCIe x4 slots on the motherboard.") + @Property("PCIe x4 Slots") + @HideInOverview + private Long pcieX4Count; + + @Description("The number of PCIe x8 slots on the motherboard.") + @Property("PCIe x8 Slots") + @HideInOverview + private Long pcieX8Count; + + @Description("The number of PCIe x16 slots on the motherboard.") + @Property("PCIe x16 Slots") + @HideInOverview + private Long pcieX16Count; + + @Description("The number of AGP slots on the motherboard.") + @Property("AGP Slots") + @HideInOverview + private Long agpCount; + + @Description("The number of RAM slots on the motherboard.") + @Property("RAM Slots") + @HideInOverview + private Long ramSlotCount; + + @Description("The maximum amount of RAM supported by the motherboard, in bytes.") + @Property("Max RAM Capacity") + @HideInOverview + @Capacity + private Long maxRamCapacity; + + @Description("The type of memory supported by the motherboard. E.g.: DDR2, SDRAM, etc...") + @Property("Memory Type") + @HideInOverview + @InputList + private String memoryType; + + @Description("The type of CPU socket on the motherboard. E.g.: Socket 370, Socket A, etc...") + @Property("CPU Socket Type") + @InputList + private String cpuSocketType; + + @Description("The chipset used by the motherboard.") + @Property("Chipset") + @InputList + private String chipset; + + @Description("The type of BIOS used by the motherboard. E.g.: AMI, Phoenix, etc...") + @Property("BIOS Type") + @InputList + private String biosType; + + @Description("The version of the BIOS used by the motherboard.") + @Property("BIOS Version") + @HideInOverview + private String biosVersion; + + @Description("The form factor of the motherboard. E.g.: ATX, Micro-ATX, Mini-ITX, etc...") + @Property("Form Factor") + @InputList + private String formFactor; +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/RamAsset.java b/src/main/java/be/seeseepuff/pcinv/models/RamAsset.java new file mode 100644 index 0000000..9182b68 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/RamAsset.java @@ -0,0 +1,41 @@ +package be.seeseepuff.pcinv.models; + +import be.seeseepuff.pcinv.meta.*; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +/** + * Represents a RAM DIMM or similar memory asset in the inventory system. + */ +@Getter +@Setter +@Entity +@AssetInfo( + displayName = "Random Access Memory", + pluralName = "Random Access Memories", + type = "ram" +) +@Table(name = "ram_assets") +public class RamAsset implements Asset +{ + @Id + @GeneratedValue + private long id; + + @OneToOne(orphanRemoval = true) + private GenericAsset asset; + + @Property("Capacity") + @Capacity + private Long capacity; + + @Description("The type of memory. E.g.: DDR2, SDRAM, ISA-8, etc...") + @Property("Type") + @InputList + private String type; + + @Description("The speed of the memory in MHz.") + @Property("Speed") + private Long speed; +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/SoundAdapterAsset.java b/src/main/java/be/seeseepuff/pcinv/models/SoundAdapterAsset.java new file mode 100644 index 0000000..4db9f05 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/SoundAdapterAsset.java @@ -0,0 +1,30 @@ +package be.seeseepuff.pcinv.models; + +import be.seeseepuff.pcinv.meta.*; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@AssetInfo( + displayName = "Sound Adapter", + pluralName = "Sound Adapters", + type = "sound" +) +@Table(name = "sound_assets") +public class SoundAdapterAsset implements Asset +{ + @Id + @GeneratedValue + private long id; + + /// The generic asset associated with this RAM. + @OneToOne(orphanRemoval = true) + private GenericAsset asset; + + @Property("Chipset") + @InputList + private String chipset; +} diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java new file mode 100644 index 0000000..294ac9d --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java @@ -0,0 +1,31 @@ +package be.seeseepuff.pcinv.repositories; + +import be.seeseepuff.pcinv.models.Asset; +import be.seeseepuff.pcinv.models.GenericAsset; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface AssetRepository { + T saveAndFlush(T entity); + + default Asset saveAndFlushAsset(Asset entity) { + if (getAssetType().isInstance(entity)) { + //noinspection unchecked + return saveAndFlush((T) entity); + } else { + throw new ClassCastException("Entity is not of type " + getAssetType().getName()); + } + } + + T findByAsset(GenericAsset asset); + + void deleteByAsset(GenericAsset asset); + + void flush(); + + List findAll(); + + Class getAssetType(); +} diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/ChassisRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/ChassisRepository.java new file mode 100644 index 0000000..35fc809 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/repositories/ChassisRepository.java @@ -0,0 +1,12 @@ +package be.seeseepuff.pcinv.repositories; + +import be.seeseepuff.pcinv.models.ChassisAsset; +import org.springframework.data.jpa.repository.JpaRepository; + +@SuppressWarnings("unused") +public interface ChassisRepository extends JpaRepository, AssetRepository { + @Override + default Class getAssetType() { + return ChassisAsset.class; + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/DisplayAdapterRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/DisplayAdapterRepository.java new file mode 100644 index 0000000..638a85b --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/repositories/DisplayAdapterRepository.java @@ -0,0 +1,12 @@ +package be.seeseepuff.pcinv.repositories; + +import be.seeseepuff.pcinv.models.DisplayAdapterAsset; +import org.springframework.data.jpa.repository.JpaRepository; + +@SuppressWarnings("unused") +public interface DisplayAdapterRepository extends JpaRepository, AssetRepository { + @Override + default Class getAssetType() { + return DisplayAdapterAsset.class; + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/GenericAssetRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/GenericAssetRepository.java new file mode 100644 index 0000000..e672dcc --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/repositories/GenericAssetRepository.java @@ -0,0 +1,9 @@ +package be.seeseepuff.pcinv.repositories; + +import be.seeseepuff.pcinv.models.GenericAsset; +import org.springframework.data.jpa.repository.JpaRepository; + +@SuppressWarnings("unused") +public interface GenericAssetRepository extends JpaRepository { + GenericAsset findByQr(long qr); +} diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/HddRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/HddRepository.java new file mode 100644 index 0000000..464628a --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/repositories/HddRepository.java @@ -0,0 +1,12 @@ +package be.seeseepuff.pcinv.repositories; + +import be.seeseepuff.pcinv.models.HddAsset; +import org.springframework.data.jpa.repository.JpaRepository; + +@SuppressWarnings("unused") +public interface HddRepository extends JpaRepository, AssetRepository { + @Override + default Class getAssetType() { + return HddAsset.class; + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/MotherboardRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/MotherboardRepository.java new file mode 100644 index 0000000..4a161d0 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/repositories/MotherboardRepository.java @@ -0,0 +1,12 @@ +package be.seeseepuff.pcinv.repositories; + +import be.seeseepuff.pcinv.models.MotherboardAsset; +import org.springframework.data.jpa.repository.JpaRepository; + +@SuppressWarnings("unused") +public interface MotherboardRepository extends JpaRepository, AssetRepository { + @Override + default Class getAssetType() { + return MotherboardAsset.class; + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/RamRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/RamRepository.java new file mode 100644 index 0000000..45e7119 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/repositories/RamRepository.java @@ -0,0 +1,12 @@ +package be.seeseepuff.pcinv.repositories; + +import be.seeseepuff.pcinv.models.RamAsset; +import org.springframework.data.jpa.repository.JpaRepository; + +@SuppressWarnings("unused") +public interface RamRepository extends JpaRepository, AssetRepository { + @Override + default Class getAssetType() { + return RamAsset.class; + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/SoundAdapterRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/SoundAdapterRepository.java new file mode 100644 index 0000000..520c72f --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/repositories/SoundAdapterRepository.java @@ -0,0 +1,12 @@ +package be.seeseepuff.pcinv.repositories; + +import be.seeseepuff.pcinv.models.SoundAdapterAsset; +import org.springframework.data.jpa.repository.JpaRepository; + +@SuppressWarnings("unused") +public interface SoundAdapterRepository extends JpaRepository, AssetRepository { + @Override + default Class getAssetType() { + return SoundAdapterAsset.class; + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/services/AssetService.java b/src/main/java/be/seeseepuff/pcinv/services/AssetService.java new file mode 100644 index 0000000..37db028 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/services/AssetService.java @@ -0,0 +1,290 @@ +package be.seeseepuff.pcinv.services; + +import be.seeseepuff.pcinv.meta.AssetDescriptor; +import be.seeseepuff.pcinv.meta.AssetDescriptors; +import be.seeseepuff.pcinv.meta.AssetInfo; +import be.seeseepuff.pcinv.meta.AssetProperty; +import be.seeseepuff.pcinv.models.Asset; +import be.seeseepuff.pcinv.models.GenericAsset; +import be.seeseepuff.pcinv.repositories.AssetRepository; +import be.seeseepuff.pcinv.repositories.GenericAssetRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +/** + * Service for managing assets in the repository. + * Provides methods to interact with the asset repositories. + */ +@Service +@RequiredArgsConstructor +public class AssetService { + private final GenericAssetRepository genericRepository; + private final Collection> repositories; + + /** + * Returns the count of all assets in the repository. + * + * @return the total number of assets + */ + public long countAssets() { + return genericRepository.count(); + } + + /** + * Retrieves an asset by its QR code. + * + * @param qr the QR code of the asset to retrieve + * @return the Asset associated with the given QR code + * @throws IllegalArgumentException if no asset is found with the given QR code + */ + public Asset getAssetByQr(long qr) { + var genericAsset = genericRepository.findByQr(qr); + if (genericAsset == null) { + throw new IllegalArgumentException("No asset found with QR code: " + qr); + } + return getRepositoryFor(genericAsset.getType()).findByAsset(genericAsset); + } + + /** + * Retrieves all assets of a specific type. + * + * @param type the type of asset to retrieve + * @return a list of assets of the specified type + */ + public List getAssetsByType(String type) { + return getRepositoryFor(type).findAll(); + } + + /** + * Retrieves the global asset descriptors for all asset types. + * + * @return the AssetProperties for the specified type + */ + public AssetDescriptors getAssetDescriptors() { + var descriptors = new AssetDescriptors(); + descriptors.loadFrom(GenericAsset.class); + for (var repository : repositories) { + descriptors.loadFrom(repository.getAssetType()); + } + return descriptors; + } + + /** + * Retrieves the asset properties for a specific type. + * + * @param type the type of asset to retrieve properties for + * @return the AssetProperties for the specified type + */ + public AssetDescriptor getAssetDescriptor(String type) { + return getAssetDescriptors().getAssets().stream() + .filter(asset -> asset.getType().equals(type)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown asset type: " + type)); + } + + /** + * Retrieves a tree of asset descriptors for the specified type. + * + * @param type the type of asset to retrieve descriptors for + * @return a list of AssetDescriptors for the specified type + */ + public List getAssetDescriptorTree(String type) { + if (type.equals(GenericAsset.TYPE)) { + return List.of(getAssetDescriptor(GenericAsset.TYPE)); + } + return List.of(getAssetDescriptor(GenericAsset.TYPE), getAssetDescriptor(type)); + } + + /** + * Creates a new asset of the specified type with the provided form data. + * + * @param type The type of asset to create. + * @param formData The form data containing the asset properties. + * @return The created asset. + */ + @Transactional + public Asset createAsset(String type, Map formData) { + var genericDescriptor = getAssetDescriptor(GenericAsset.TYPE); + var assetDescriptor = getAssetDescriptor(type); + + var genericAsset = new GenericAsset(); + genericAsset.setType(type); + fillIn(genericAsset, genericDescriptor, formData); + + var asset = assetDescriptor.newInstance(); + fillIn(asset, assetDescriptor, formData); + + genericAsset = genericRepository.saveAndFlush(genericAsset); + asset.setAsset(genericAsset); + asset = getRepositoryFor(type).saveAndFlushAsset(asset); + return asset; + } + + /** + * Edits an existing asset with the provided form data. + * + * @param qr The QR code of the asset to edit. + * @param formData The form data containing the updated asset properties. + * @return The updated asset. + */ + @Transactional + public Asset editAsset(long qr, Map formData) { + var genericAsset = genericRepository.findByQr(qr); + if (genericAsset == null) { + throw new IllegalArgumentException("No asset found with QR code: " + qr); + } + + var assetType = genericAsset.getType(); + var assetDescriptor = getAssetDescriptor(assetType); + var asset = getRepositoryFor(assetType).findByAsset(genericAsset); + + fillIn(genericAsset, getAssetDescriptor(GenericAsset.TYPE), formData); + fillIn(asset, assetDescriptor, formData); + + genericRepository.saveAndFlush(genericAsset); + return getRepositoryFor(assetType).saveAndFlushAsset(asset); + } + + /** + * Gets the asset repository for the specified type. + * + * @param type the type of asset to get the repository for + * @return the AssetRepository for the specified type + */ + private AssetRepository getRepositoryFor(String type) { + return repositories.stream() + .filter(repo -> repo.getAssetType().getAnnotation(AssetInfo.class).type().equals(type)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No repository found for type: " + type)); + } + + /** + * Fills in the properties of a generic asset from the form data. + * + * @param asset The generic asset to fill in. + * @param assetDescriptor The descriptor containing the properties to fill in. + * @param formData The form data containing the property values. + */ + private void fillIn(Object asset, AssetDescriptor assetDescriptor, Map formData) { + for (var property : assetDescriptor.getProperties()) { + var value = parseValue(assetDescriptor, property, formData); + if (property.isInputList()) { + var selectedItem = formData.get(assetDescriptor.asString(property) + "-list"); + if (selectedItem != null && !selectedItem.isBlank() && !selectedItem.equals("__new__")) { + value = selectedItem; + } + } + if (value == null && property.isRequired()) { + throw new IllegalArgumentException("Property '" + property.getName() + "' is required but not provided."); + } + property.setValue(asset, value); + } + } + + /** + * Parses the string value into the appropriate type based on the asset property. + * + * @param descriptor The asset descriptor containing the property. + * @param property The asset property to determine the type. + * @param values The map of values from the form data. + * @return The parsed value as an Object. + */ + private Object parseValue(AssetDescriptor descriptor, AssetProperty property, Map values) { + if (property.getType() == AssetProperty.Type.CAPACITY) { + var value = values.get(descriptor.asString(property) + "-value"); + var unit = values.get(descriptor.asString(property) + "-unit"); + if (value == null || value.isBlank() || unit == null || unit.isBlank()) { + return null; + } + + try { + return Long.parseLong(value) * Long.parseLong(unit); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid numeric value for property '" + property.getName() + "': " + value, e); + } + } + + var stringValue = values.get(descriptor.asString(property)); + if (stringValue == null || stringValue.isBlank()) { + return null; + } + + if (property.getType() == AssetProperty.Type.INTEGER) { + return Integer.parseInt(stringValue); + } else if (property.getType() == AssetProperty.Type.STRING) { + return stringValue; + } else if (property.getType().isEnum) { + for (var option : property.getOptions()) { + if (option.getValue().equals(stringValue)) { + return option.getEnumConstant(); + } + } + throw new IllegalArgumentException("Invalid value for enum property '" + property.getName() + "': " + stringValue); + } else { + throw new IllegalArgumentException("Unsupported property type: " + property.getType()); + } + } + + @Transactional + public void deleteAsset(long qr) { + var genericAsset = genericRepository.findByQr(qr); + if (genericAsset == null) { + throw new IllegalArgumentException("No asset found with QR code: " + qr); + } + var assetType = genericAsset.getType(); + var assetRepository = getRepositoryFor(assetType); + assetRepository.deleteByAsset(genericAsset); + assetRepository.flush(); + genericRepository.delete(genericAsset); + genericRepository.flush(); + } + + /** + * Retrieves the input list mapping for a specific asset type. + * + * @param type the type of asset to retrieve the input list for + * @return a map of input names to their corresponding list + */ + public Map> getInputList(String type) { + var map = new HashMap>(); + var tree = getAssetDescriptorTree(type); + for (var descriptor : tree) { + for (var property : descriptor.getProperties()) { + if (property.isInputList()) { + var inputList = getInputList(descriptor, property); + map.put(descriptor.asString(property), inputList); + } + } + } + return map; + } + + /** + * Retrieves the input list for a specific asset descriptor and property. + * + * @param descriptor the asset descriptor containing the property + * @param property the asset property to retrieve the input list for + * @return a set of input values for the specified property + */ + private Set getInputList(AssetDescriptor descriptor, AssetProperty property) { + List entries; + if (descriptor.getType().equals(GenericAsset.TYPE)) { + entries = genericRepository.findAll(); + } else { + var repository = getRepositoryFor(descriptor.getType()); + entries = repository.findAll(); + } + + Set inputList = new TreeSet<>(); + for (var entry : entries) { + var value = property.getValue(entry); + if (value != null) { + inputList.add(value.toString()); + } + } + return inputList; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..1b82a3d --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.application.name=pcinv +server.port=8088 +spring.datasource.url=jdbc:postgresql://localhost:5432/pcinv +spring.datasource.username=pcinv +spring.datasource.password=pcinv +spring.jpa.hibernate.ddl-auto=update diff --git a/src/main/resources/templates/browse.html b/src/main/resources/templates/browse.html new file mode 100644 index 0000000..5e6f3a5 --- /dev/null +++ b/src/main/resources/templates/browse.html @@ -0,0 +1,10 @@ + +
+ View device details +
    +
  • + +
  • +
+
+ diff --git a/src/main/resources/templates/browse_type.html b/src/main/resources/templates/browse_type.html new file mode 100644 index 0000000..64373d9 --- /dev/null +++ b/src/main/resources/templates/browse_type.html @@ -0,0 +1,21 @@ + +
+ There are in the database. + + + + + + + + + +
Actions
+ + + + View + Edit +
+
+ diff --git a/src/main/resources/templates/create_asset.html b/src/main/resources/templates/create_asset.html new file mode 100644 index 0000000..e900583 --- /dev/null +++ b/src/main/resources/templates/create_asset.html @@ -0,0 +1,49 @@ + +
+ Create a +
+
+

+ + + + + +
+ + + or + + + + + + + + + + Bad input type for +
+
+

+ + +

+
+
+ diff --git a/src/main/resources/templates/create_select.html b/src/main/resources/templates/create_select.html new file mode 100644 index 0000000..d10491d --- /dev/null +++ b/src/main/resources/templates/create_select.html @@ -0,0 +1,8 @@ + +
+ Create a new device +
    +
  • +
+
+ diff --git a/src/main/resources/templates/fragments.html b/src/main/resources/templates/fragments.html new file mode 100644 index 0000000..edde0fb --- /dev/null +++ b/src/main/resources/templates/fragments.html @@ -0,0 +1,21 @@ + + + + PC Inventory + + +

PC Inventory - +

+
+ Home + Browse + Create +
+
+
+
+

+ Rendered in 25ms on . +

+ + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..e170565 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,12 @@ +
+
+

This system holds 5 assets.

+
+ + +
+
+
diff --git a/src/main/resources/templates/view.html b/src/main/resources/templates/view.html new file mode 100644 index 0000000..a6075eb --- /dev/null +++ b/src/main/resources/templates/view.html @@ -0,0 +1,26 @@ + +
+

Details of Hard Disk Drive 21

+

Are you sure you want to delete Hard Disk Drive 21

+
+

+ + + + + +
+
+

+

+ +

+
+ diff --git a/src/test/java/be/seeseepuff/pcinv/PcinvApplicationTests.java b/src/test/java/be/seeseepuff/pcinv/PcinvApplicationTests.java new file mode 100644 index 0000000..396ca1c --- /dev/null +++ b/src/test/java/be/seeseepuff/pcinv/PcinvApplicationTests.java @@ -0,0 +1,13 @@ +package be.seeseepuff.pcinv; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class PcinvApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/static/scripts.js b/static/scripts.js deleted file mode 100644 index 34e9308..0000000 --- a/static/scripts.js +++ /dev/null @@ -1,16 +0,0 @@ -function newOption(elementId, name) { - var el = document.getElementById(elementId) - if (el.value !== "New...") { - return - } - - var newValue = window.prompt("Enter " + name + " Name") - if (newValue === null) { - return; - } - var child = document.createElement("option") - child.value = newValue - child.innerText = newValue - el.prepend(child) - el.value = newValue -} diff --git a/template_debug.go b/template_debug.go deleted file mode 100644 index f73461b..0000000 --- a/template_debug.go +++ /dev/null @@ -1,8 +0,0 @@ -// / +build !release -package main - -import ( - "net/http" -) - -const staticFiles = http.Dir("./static") diff --git a/template_funcs.go b/template_funcs.go deleted file mode 100644 index c7d4481..0000000 --- a/template_funcs.go +++ /dev/null @@ -1,121 +0,0 @@ -package main - -import ( - "fmt" -) - -func createDeviceLink(deviceType, name string, qr *int) CreateDeviceLink { - return CreateDeviceLink{ - Type: deviceType, - Name: name, - Qr: qr, - } -} - -type CreateDeviceLink struct { - Type string - Name string - Qr *int -} - -func formatMemoryUnit(size int) string { - const ( - KB = 1024 - MB = KB * 1024 - GB = MB * 1024 - ) - - switch { - case size >= GB: - return "GB" - case size >= MB: - return "MB" - case size >= KB: - return "KB" - default: - return "B" - } -} - -func formatMemorySize(size int) string { - const ( - KB = 1024 - MB = KB * 1024 - GB = MB * 1024 - ) - - switch formatMemoryUnit(size) { - case "GB": - return fmt.Sprintf("%.2f GB", float64(size)/GB) - case "MB": - return fmt.Sprintf("%.2f MB", float64(size)/MB) - case "KB": - return fmt.Sprintf("%.2f KB", float64(size)/KB) - case "B": - return fmt.Sprintf("%d B", size) - default: - panic("invalid memory size") - } -} - -func formatMemoryPlainSize(size int) int { - const ( - KB = 1024 - MB = KB * 1024 - GB = MB * 1024 - ) - - switch formatMemoryUnit(size) { - case "GB": - return size / GB - case "MB": - return size / MB - case "KB": - return size / KB - case "B": - return size - default: - panic("invalid memory size") - } -} - -func isRamType(size int, unit string) bool { - if size == 0 && unit == "MB" { - return true - } - actualUnit := formatMemoryUnit(size) - return unit == actualUnit -} - -func formatType(t string) string { - switch t { - case "ram": - return "Random Access Memory" - case "hdd": - return "Hard Disk Drive" - default: - return t - } -} - -type SelectMenu struct { - Name string - Label string - Selected string - Options []string - DefaultValue string -} - -func createSelectMenu(name, label, selected string, options []string) SelectMenu { - return createSelectMenuDefault(name, label, selected, options, "Unknown") -} - -func createSelectMenuDefault(name, label, selected string, options []string, defaultValue string) SelectMenu { - return SelectMenu{ - Name: name, - Label: label, - Selected: selected, - Options: options, - DefaultValue: defaultValue, - } -} diff --git a/templates/browse.gohtml b/templates/browse.gohtml deleted file mode 100644 index c24bd45..0000000 --- a/templates/browse.gohtml +++ /dev/null @@ -1,31 +0,0 @@ -{{- /*gotype: main.BrowseVM */}} -{{define "browse"}} -{{template "header" "Search Results"}} - - - - - - - - {{if .HasRam}} - - - {{end}} - - {{range .Assets}} - - - - - - - {{if $.HasRam}} - - - {{end}} - - {{end}} -
QRTypeNameBrandDescriptionRAM TypeRAM Capacity
{{.Qr}}{{.Type | formatType}}{{.Name}}{{.Brand}}{{.Description}}{{.RamType}}{{.RamCapacity | formatMemorySize}}
-{{template "footer"}} -{{end}} diff --git a/templates/create_device.gohtml b/templates/create_device.gohtml deleted file mode 100644 index 3825755..0000000 --- a/templates/create_device.gohtml +++ /dev/null @@ -1,8 +0,0 @@ -{{- /*gotype: main.CreateDeviceVM */}} -{{define "create_device"}} - {{if .Type}} - {{template "create_device_step2" .}} - {{else}} - {{template "create_device_step1" .}} - {{end}} -{{end}} diff --git a/templates/create_device_step1.gohtml b/templates/create_device_step1.gohtml deleted file mode 100644 index e4369e8..0000000 --- a/templates/create_device_step1.gohtml +++ /dev/null @@ -1,18 +0,0 @@ -{{- /*gotype: main.CreateDeviceVM */}} -{{define "create_device_step1"}} -{{template "header" "Create Device - Choose Device Type"}} -
    - {{template "create_device_link" createDeviceLink "ram" "Random Access Memory" .Qr}} -
-{{template "footer"}} -{{end}} - -{{define "create_device_link"}} - {{if .Qr}} -
  • {{"ram" | formatType}}
  • -
  • {{"hdd" | formatType}}
  • - {{- else}} -
  • {{"ram" | formatType}}
  • -
  • {{"hdd" | formatType}}
  • - {{- end}} -{{end}} diff --git a/templates/create_device_step2.gohtml b/templates/create_device_step2.gohtml deleted file mode 100644 index 006210d..0000000 --- a/templates/create_device_step2.gohtml +++ /dev/null @@ -1,130 +0,0 @@ -{{- /*gotype: pcinv.CreateDeviceVM*/}} -{{define "create_device_step2"}} -{{template "header" "Create Device - Enter Device Data"}} -
    -

    General Information

    - - - - {{if .Qr}} - - {{else}} - - {{end}} - - - - - - - - - - - - - - - - - -
    - -
    - - {{if eq .Type "ram"}} -

    Memory Information

    - - - - - - - - - -
    - -
    - - -
    - {{end}} - - {{if eq .Type "hdd"}} -

    Hard Drive Information

    - - - - - - - - - - - - - - - - - - - - - -
    - - -
    {{template "create_device_select" createSelectMenu "hdd_type" "HDD Type" .HddType .HddTypes}}
    {{template "create_device_select" createSelectMenu "hdd_form_factor" "HDD Form Factor" .HddFormFactor .HddFormFactors}}
    {{template "create_device_select" createSelectMenu "hdd_connection" "HDD Connection" .HddConnection .HddConnections}}
    {{template "create_device_select" createSelectMenuDefault "hdd_rpm" "HDD RPM" .HddRpm .HddRpms "Not Applicable"}}
    - {{end}} - - {{if .IsEdit}} - - {{else}} - - {{end}} -
    -{{template "footer"}} -{{end}} - -{{define "create_device_select"}} - -{{end}} diff --git a/templates/delete.gohtml b/templates/delete.gohtml deleted file mode 100644 index 2f955c5..0000000 --- a/templates/delete.gohtml +++ /dev/null @@ -1,9 +0,0 @@ -{{- /*gotype: pcinv.DeleteVM*/ -}} -{{define "delete"}} -{{template "header" "Delete Device"}} -

    Are you sure you want to delete this device?

    -
    - - -{{template "footer"}} -{{end}} diff --git a/templates/device.gohtml b/templates/device.gohtml deleted file mode 100644 index f47de45..0000000 --- a/templates/device.gohtml +++ /dev/null @@ -1,57 +0,0 @@ -{{- /*gotype: main.DeviceVM */}} -{{define "device"}} -{{template "header" "Device Details"}} - - - - - - - - - - - - - - - - - - {{if eq .Type "ram"}} - - - - - - - - - {{end}} - {{if eq .Type "hdd"}} - - - - - - - - - - - - - - - - - - - - - {{end}} -
    Name:{{.Name}}
    Brand:{{.Brand}}
    Type:{{.Type}}
    Description:{{.Description}}
    RAM Type:{{.RamType}}
    RAM Capacity:{{.RamCapacity | formatMemorySize}}
    HDD Capacity:{{.HddCapacity}}
    HDD Type:{{.HddType}}
    Form Factor:{{.HddFormFactor}}
    Connection:{{.HddConnection}}
    RPM:{{.HddRpm}}
    - - -{{template "footer"}} -{{end}} diff --git a/templates/errors.gohtml b/templates/errors.gohtml deleted file mode 100644 index 45e7b65..0000000 --- a/templates/errors.gohtml +++ /dev/null @@ -1,12 +0,0 @@ -{{- /*gotype: main.ErrorVM*/}} -{{define "errors"}} -{{template "header" .StatusCode | statusText}} - -
      - {{range .Errors}} -
    • {{.}}
    • - {{end}} -
    - -{{template "footer"}} -{{end}} diff --git a/templates/footer.gohtml b/templates/footer.gohtml deleted file mode 100644 index 634a10f..0000000 --- a/templates/footer.gohtml +++ /dev/null @@ -1,4 +0,0 @@ -{{define "footer"}} - - -{{end}} diff --git a/templates/header.gohtml b/templates/header.gohtml deleted file mode 100644 index ba9c862..0000000 --- a/templates/header.gohtml +++ /dev/null @@ -1,16 +0,0 @@ -{{define "header"}} - - - PC Inventory{{if .}} - {{.}}{{end}} - - - -

    PC Inventory{{if .}} - {{.}}{{end}}

    -
    - - - - Create Device -
    -
    -{{end}} diff --git a/templates/index.gohtml b/templates/index.gohtml deleted file mode 100644 index c6782a3..0000000 --- a/templates/index.gohtml +++ /dev/null @@ -1,34 +0,0 @@ -{{- /*gotype: main.IndexVM*/}} -{{define "index"}} -{{template "header"}} -

    Statistics

    - The inventory contains: -
      -
    • {{.AssetCount}} assets in total.
    • -
    • {{.BrandCount}} unique brands.
    • -
    • a combined {{.TotalRamCapacity | formatMemorySize}} of RAM.
    • -
    - -

    Filter Devices

    -
    -

    Select Brands:

    - {{range .Brands}} -
    - {{end}} - -

    Select Types:

    - {{range .Types}} -
    - {{end}} - - -
    - -{{template "footer"}} -{{end}} diff --git a/views.go b/views.go deleted file mode 100644 index 2f14ff3..0000000 --- a/views.go +++ /dev/null @@ -1,430 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "net/http" - "strconv" - - "gitea.seeseepuff.be/seeseemelk/mysqlite" - "github.com/gin-gonic/gin" -) - -type App struct { - db *mysqlite.Db -} - -type IndexVM struct { - AssetCount int - BrandCount int - TotalRamCapacity int - Brands []string - Types []string -} - -func (a *App) getIndex(c *gin.Context) { - vm := &IndexVM{} - var err error - vm.AssetCount, err = a.GetAssetCount() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - vm.BrandCount, err = a.GetBrandCount() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - vm.TotalRamCapacity, err = a.GetTotalRamCapacity() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - vm.Brands, err = a.GetAllBrands() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - vm.Types, err = a.GetAllTypes() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - c.HTML(http.StatusOK, "index", vm) -} - -type DeviceVM struct { - Qr int - Name string - Brand string - Type string - Description string - RamType string - RamCapacity int - HddCapacity int - HddType string - HddFormFactor string - HddConnection string - HddRpm int -} - -func (a *App) getDevice(c *gin.Context) { - qr, err := strconv.Atoi(c.Query("id")) - if err != nil { - c.AbortWithError(http.StatusBadRequest, err) - return - } - - var count int - err = a.db.Query("SELECT COUNT(*) FROM assets WHERE qr = ?"). - Bind(qr). - ScanSingle(&count) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - if count == 0 { - c.Redirect(http.StatusTemporaryRedirect, "/create?id="+strconv.Itoa(qr)) - return - } - - vm := &DeviceVM{Qr: qr} - err = a.db.Query("SELECT name, brand, type, description FROM assets WHERE qr = ?"). - Bind(qr). - ScanSingle(&vm.Name, &vm.Brand, &vm.Type, &vm.Description) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - if vm.Type == "ram" { - err = a.db.Query("SELECT type, capacity FROM info_ram WHERE asset = ?"). - Bind(qr). - ScanSingle(&vm.RamType, &vm.RamCapacity) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - } else if vm.Type == "hdd" { - err = a.db.Query("SELECT capacity, type, form_factor, connection, rpm FROM info_hdd WHERE asset = ?"). - Bind(qr). - ScanSingle(&vm.HddCapacity, &vm.HddType, &vm.HddFormFactor, &vm.HddConnection, &vm.HddRpm) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - } - - c.HTML(http.StatusOK, "device", vm) -} - -type CreateDeviceVM struct { - IsEdit bool - - // Assets - Qr *int - Type string - - AssetBrand string - AssetBrands []string - - AssetName string - AssetDescription string - - // RAM - RamType string - RamTypes []string - - RamCapacity int - - // HDD - HddCapacity int - - HddType string - HddTypes []string - HddFormFactor string - HddFormFactors []string - HddConnection string - HddConnections []string - HddRpm string - HddRpms []string -} - -func (a *App) getCreateDevice(c *gin.Context) { - var err error - vm := &CreateDeviceVM{} - vm.Type = c.Query("type") - - qr := c.Query("id") - if qr != "" { - qrInt, err := strconv.Atoi(qr) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("invalid qr: %v", err)) - return - } - vm.Qr = &qrInt - } - - err = a.GetAllGroups(vm) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - if c.Query("edit") != "" && vm.Qr != nil { - vm.IsEdit = true - err = a.db.Query("SELECT name, type, brand, description FROM assets WHERE qr = ?"). - Bind(*vm.Qr). - ScanSingle(&vm.AssetName, &vm.Type, &vm.AssetBrand, &vm.AssetDescription) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - if vm.Type == "ram" { - err = a.db.Query("SELECT type, capacity FROM info_ram WHERE asset = ?"). - Bind(*vm.Qr). - ScanSingle(&vm.RamType, &vm.RamCapacity) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - } else if vm.Type == "hdd" { - err = a.db.Query("SELECT capacity, type, form_factor, connection, rpm FROM info_hdd WHERE asset = ?"). - Bind(*vm.Qr). - ScanSingle(&vm.HddCapacity, &vm.HddType, &vm.HddFormFactor, &vm.HddConnection, &vm.HddRpm) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - } - } - - c.HTML(http.StatusOK, "create_device", vm) -} - -func (a *App) postCreateDevice(c *gin.Context) { - qr, err := strconv.Atoi(c.PostForm("qr")) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("invalid qr: %v", err)) - } - assetType := c.PostForm("asset_type") - - tx, err := a.db.Begin() - defer tx.MustRollback() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("error beginning tx: %v", err)) - return - } - - err = a.DeleteAsset(tx, qr) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("error removing assets: %v", err)) - return - } - err = tx.Query("INSERT INTO assets (qr, type, brand, name, description) VALUES (?, ?, ?, ?, ?)"). - Bind(qr, c.PostForm("asset_type"), c.PostForm("asset_brand"), c.PostForm("asset_name"), c.PostForm("asset_description")). - Exec() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("error inserting assets: %v", err)) - return - } - - if assetType == "ram" { - err = a.postCreateDeviceRam(c, qr, tx) - } else if assetType == "hdd" { - err = a.postCreateDeviceHdd(c, qr, tx) - } else { - c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("invalid type: %s", assetType)) - return - } - - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - err = tx.Commit() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - c.Redirect(http.StatusSeeOther, "/") -} - -func (a *App) postCreateDeviceRam(c *gin.Context, qr int, tx *mysqlite.Tx) error { - var err error - capacity := 0 - capacityString := c.PostForm("ram_capacity") - if capacityString != "" { - capacity, err = strconv.Atoi(c.PostForm("ram_capacity")) - if err != nil { - return err - } - } - switch c.PostForm("ram_capacity_unit") { - case "B": - case "KB": - capacity *= 1024 - case "MB": - capacity *= 1024 * 1024 - case "GB": - capacity *= 1024 * 1024 * 1024 - default: - return errors.New("invalid ram_capacity_unit") - } - - err = tx.Query("INSERT INTO info_ram (asset, type, capacity) VALUES (?, ?, ?)"). - Bind(qr, c.PostForm("ram_type"), capacity).Exec() - return err -} - -func (a *App) postCreateDeviceHdd(c *gin.Context, qr int, tx *mysqlite.Tx) error { - var err error - capacity := 0 - capacityString := c.PostForm("hdd_capacity") - if capacityString != "" { - capacity, err = strconv.Atoi(c.PostForm("hdd_capacity")) - if err != nil { - return err - } - } - switch c.PostForm("hdd_capacity_unit") { - case "B": - case "KB": - capacity *= 1024 - case "MB": - capacity *= 1024 * 1024 - case "GB": - capacity *= 1024 * 1024 * 1024 - default: - return errors.New("invalid hdd_capacity_unit") - } - - err = tx.Query("INSERT INTO info_hdd (asset, capacity, type, form_factor, connection, rpm) VALUES (?, ?, ?, ?, ?, ?)"). - Bind(qr, capacity, c.PostForm("hdd_type"), c.PostForm("hdd_form_factor"), c.PostForm("hdd_connection"), c.PostForm("hdd_rpm")). - Exec() - return err -} - -type BrowseVM struct { - Assets []Asset - HasRam bool -} - -type Asset struct { - Qr int - Name string - Brand string - Type string - Description string - RamType string - RamCapacity int - HddCapacity int - HddType string - HddFormFactor string - HddConnection string - HddRpm int -} - -func (a *App) getBrowse(c *gin.Context) { - brands := c.QueryArray("brand") - types := c.QueryArray("type") - - query := `SELECT assets.qr, assets.name, assets.brand, assets.type, assets.description, - info_ram.type, info_ram.capacity, - info_hdd.capacity, info_hdd.type, info_hdd.form_factor, info_hdd.connection, info_hdd.rpm - FROM assets - LEFT JOIN info_ram ON info_ram.asset = assets.qr - LEFT JOIN info_hdd ON info_hdd.asset = assets.qr - WHERE 1=1` - if len(brands) > 0 { - query += " AND assets.brand IN (" + placeholders(len(brands)) + ")" - } - if len(types) > 0 { - query += " AND assets.type IN (" + placeholders(len(types)) + ")" - } - query += "ORDER BY assets.type, assets.brand, assets.name, assets.qr" - - vm := &BrowseVM{} - - var err error - q := a.db.Query(query).Bind(brands, types) - for row := range q.Range(&err) { - var asset Asset - err := row.Scan(&asset.Qr, &asset.Name, &asset.Brand, &asset.Type, &asset.Description, - &asset.RamType, &asset.RamCapacity, - &asset.HddCapacity, &asset.HddType, &asset.HddFormFactor, &asset.HddConnection, &asset.HddRpm) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - vm.Assets = append(vm.Assets, asset) - if asset.Type == "ram" { - vm.HasRam = true - } - } - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - c.HTML(http.StatusOK, "browse", vm) -} - -type DeleteVM struct { - Qr int -} - -func (a *App) getDelete(c *gin.Context) { - qr, err := strconv.Atoi(c.Query("id")) - if err != nil { - c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid qr: %v", err)) - return - } - vm := &DeleteVM{Qr: qr} - c.HTML(http.StatusOK, "delete", vm) -} - -func (a *App) postDelete(c *gin.Context) { - qr, err := strconv.Atoi(c.Query("id")) - if err != nil { - c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid qr: %v", err)) - return - } - tx, err := a.db.Begin() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - err = a.DeleteAsset(tx, qr) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - err = tx.Commit() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - c.Redirect(http.StatusSeeOther, "/") -} - -func placeholders(count int) string { - if count == 0 { - return "" - } - placeholder := "?" - for count > 1 { - placeholder += ", ?" - count-- - } - return placeholder -}